Skip to content

14. البنية الطبقية والبرمجة القائمة على الواجهة

14.1. مقدمة

نقترح كتابة تطبيق يعرض درجات طلاب المدارس الإعدادية. يمكن أن يكون لهذا التطبيق بنية متعددة الطبقات:

Image

  • طبقة [ui] (واجهة المستخدم) هي الطبقة التي تتفاعل مع مستخدم التطبيق؛
  • طبقة [business] تنفذ قواعد العمل الخاصة بالتطبيق، مثل حساب الراتب أو الفاتورة. تستخدم هذه الطبقة البيانات الواردة من المستخدم عبر طبقة [presentation] ومن نظام إدارة قواعد البيانات (DBMS) عبر طبقة [DAO]؛
  • طبقة [DAO] (كائنات الوصول إلى البيانات) تدير الوصول إلى البيانات في نظام إدارة قواعد البيانات (DBMS).

هذه هي البنية التي تم استخدامها في |دورة Python 2|. يمكن أيضًا إدخال نسخة معدلة:

Image

فيما يلي الاختلافات عن البنية الطبقية السابقة:

  • يقوم البرنامج النصي الرئيسي المسمى [main] أعلاه بتنظيم إنشاء مثيلات الطبقات؛
  • لم تعد طبقات [ui، business، dao] تتواصل بالضرورة مع بعضها البعض. وإذا دعت الحاجة إلى ذلك، فإن البرنامج النصي [main] يزودها بالمراجع الخاصة بالطبقات التي تحتاجها؛

يتم تنظيم الكود هنا في مناطق وظيفية مع منسق مركزي:

  • المنسق هو البرنامج النصي الرئيسي [main]؛
  • طبقات [ui] و [dao] و [business] هي مراكز الخبرة؛

يمكننا تسمية هذه البنية بتنظيم أوركسترالي.

14.2. مثال 1

سنوضح البنية الطبقية باستخدام تطبيق وحدة تحكم بسيط:

  • لن تكون هناك قاعدة بيانات؛
  • ستدير طبقة [DAO] كيانات الطالب والصف والموضوع والدرجة للتعامل مع درجات الطلاب؛
  • ستقوم طبقة [business] بحساب المقاييس بناءً على درجات طالب معين؛
  • ستكون طبقة [ui] عبارة عن تطبيق وحدة تحكم يعرض نتائج الطلاب؛

مشروع PyCharm للتطبيق هو كما يلي:

ملاحظة: المجلدات باللون الأزرق هي جزء من [Root Sources] لمشروع PyCharm.

14.2.1. كيانات التطبيق

سنشير إلى الفئات التي يتمثل دورها الوحيد في تغليف البيانات باسم الكيانات. يمكن استخدام القواميس لهذا الغرض. وتتمثل ميزة الفئة في أنها تسمح لنا باختبار صحة البيانات المخزنة في الكائن وتوفير طريقة تعرض هوية الكائن كسلسلة.

14.2.1.1. كيان [Class]

يمثل كيان [Class] (Class.py) فصلًا دراسيًا في المرحلة الإعدادية:

#  imports
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Classe(BaseEntity):
    #  attributes excluded from class state
    excluded_keys = []

    #  class properties
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: class identifier
        #  name: class name
        return BaseEntity.get_allowed_keys() + ["nom"]

    #  getter
    @property
    def nom(self: object) -> str:
        return self.__nom

    #   setters
    @nom.setter
    def nom(self: object, nom: str):
        #  name must be a non-empty string
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(11, f"Le nom de la classe {self.id} doit être une chaîne de caractères non vide")

ملاحظات

  • السطر 7: الكيان [Class] مشتق من الكيان [BaseEntity] الذي تمت مناقشته في القسم |فئة BaseEntity
  • الأسطر 11–16: يتم تعريف الفئة بواسطة معرف واسم (السطر 16). يتم توفير الخاصية [id] بواسطة فئة [BaseEntity] والاسم بواسطة فئة [Class]؛
  • الأسطر 18–30: أداة الحصول/التعيين لخاصية [name]؛

14.2.1.2. كيان [Subject]

فئة [Subject] (subject.py) هي كما يلي:

#  imports
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Matière(BaseEntity):
    #  attributes excluded from class state
    excluded_keys = []

    #  class properties
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: material identifier
        #  name: material name
        #  coefficient: subject coefficient
        return BaseEntity.get_allowed_keys() + ["nom", "coefficient"]

    #  getter
    @property
    def nom(self: object) -> str:
        return self.__nom

    @property
    def coefficient(self: object) -> float:
        return self.__coefficient

    #   setters
    @nom.setter
    def nom(self: object, nom: str):
        #  name must be a non-empty string
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(21, f"Le nom de la matière {self.id} doit être une chaîne de caractères non vide")

    @coefficient.setter
    def coefficient(self, coefficient: float):
        #  the coefficient must be a real number >=0
        erreur = False
        if isinstance(coefficient, (int, float)):
            if coefficient >= 0:
                self.__coefficient = coefficient
            else:
                erreur = True
        else:
            erreur = True
        #  mistake?
        if erreur:
            raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")

ملاحظات

  • السطر 7: الفئة [Class] مشتقة من الفئة [BaseEntity]؛
  • الأسطر 11–17: يتم تعريف الموضوع من خلال معرّفه [id] واسمه [name] ووزنه [coefficient]؛
  • الأسطر 19–50: متغيرات الحصول/التعيين لسمات الفئة؛

14.2.1.3. كيان [Student]

فئة [Student] (student.py) هي كما يلي:

#  imports
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException

from Utils import Utils


class Elève(BaseEntity):
    #  attributes excluded from class state
    excluded_keys = []

    #  class properties
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: student identifier
        #  name: student's name
        #  first name: student's first name
        #  class: student's class
        return BaseEntity.get_allowed_keys() + ["nom", "prénom", "classe"]

    #  getters
    @property
    def nom(self: object) -> str:
        return self.__nom

    @property
    def prénom(self: object) -> str:
        return self.__prénom

    @property
    def classe(self: object) -> Classe:
        return self.__classe

    #   setters
    @nom.setter
    def nom(self: object, nom: str) -> str:
        #  name must be a non-empty string
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(41, f"Le nom de l'élève {self.id} doit être une chaîne de caractères non vide")

    @prénom.setter
    def prénom(self: object, prénom: str) -> str:
        #  first name must be a non-empty string
        if Utils.is_string_ok(prénom):
            self.__prénom = prénom
        else:
            raise MyException(42, f"Le prénom de l'élève {self.id} doit être une chaîne de caractères non vide")

    @classe.setter
    def classe(self: object, value):
        try:
            #  we expect a Class type
            if isinstance(value, Classe):
                self.__classe = value
            #  or a type dict
            elif isinstance(value,dict):
                self.__classe=Classe().fromdict(value)
            #  or a json type
            elif isinstance(value,str):
                self.__classe = Classe().fromjson(value)
        except BaseException as erreur:
            raise MyException(43, f"L'attribut [{value}] de l'élève {self.id} doit être de type Classe ou dict ou json. Erreur : {erreur}")

ملاحظات

  • السطر 9: الفئة [Student] مشتقة من الفئة [BaseEntity]؛
  • الأسطر 13–20: يتميز الطالب برقمه التعريفي [id]، واسم عائلته [lastName]، واسمه الأول [firstName]، وفصله الدراسي [class]. المعلمة الأخيرة هي مرجع إلى كائن [Class]؛
  • الأسطر 22–65: متغيرات الحصول/التعيين لسمات الفئة؛

14.2.1.4. كيان [Note]

فئة [Note] (note.py) هي كما يلي:

#  imports
from BaseEntity import BaseEntity
from Elève import Elève
from Matière import Matière
from MyException import MyException


class Note(BaseEntity):
    #  attributes excluded from class state
    excluded_keys = []

    #  class properties
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: note identifier
        #  value: the note itself
        #  student: student (of type Student) concerned by the note
        #  subject: subject (of type Subject) concerned by the grade
        #  the Note object is therefore a student's grade in a subject
        return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]

    #  getters
    @property
    def valeur(self: object) -> float:
        return self.__valeur

    @property
    def élève(self: object) -> Elève:
        return self.__élève

    @property
    def matière(self: object) -> Matière:
        return self.__matière

    #  getters
    @valeur.setter
    def valeur(self: object, valeur: float):
        #  the score must be a real number between 0 and 20
        if isinstance(valeur, (int, float)) and 0 <= valeur <= 20:
            self.__valeur = valeur
        else:
            raise MyException(31,
                              f"L'attribut {valeur} de la note {self.id} doit être un nombre dans l'intervalle [0,20]")

    @élève.setter
    def élève(self: object, value):
        try:
            #  we expect a Student type
            if isinstance(value, Elève):
                self.__élève = value
            #  or a type dict
            elif isinstance(value, dict):
                self.__élève = Elève().fromdict(value)
            #  or a json type
            elif isinstance(value, str):
                self.__élève = Elève().fromjson(value)
        except BaseException as erreur:
            raise MyException(32,
                              f"L'attribut [{value}] de la note {self.id} doit être de type Elève ou dict ou json. Erreur : {erreur}")

    @matière.setter
    def matière(self: object, value):
        try:
            #  we expect a Material type
            if isinstance(value, Matière):
                self.__matière = value
            #  or a type dict
            elif isinstance(value, dict):
                self.__matière = Matière().fromdict(value)
            #  or a json type
            elif isinstance(value, str):
                self.__matière = Matière().fromjson(value)
        except BaseException as erreur:
            raise MyException(33,
                              f"L'attribut [{value}] de la note {self.id} doit être de type Matière ou dict ou json. Erreur : {erreur}")

ملاحظات

  • السطر 8: فئة [Note] مشتقة من فئة [BaseEntity]؛
  • الأسطر 12–20: يتميز كائن [Note] بمعرفه [id]، وقيمة الدرجة [value]، وإشارة [student] إلى الطالب الذي حصل على هذه الدرجة، وإشارة إلى المادة [subject] المرتبطة بالدرجة؛
  • الأسطر 22–75: متغيرات الحصول/التعيين لسمات الفئة؛

14.2.2. تكوين التطبيق

يقوم ملف [config.py] بتهيئة البيئة للنص البرمجي الرئيسي [main] (1) وكذلك للاختبارات (2). تحتوي جميع هذه البرامج النصية على عبارة [import config] في بداية الكود. لاحظ أن الدليل الذي يحتوي على البرنامج النصي المستهدف بواسطة الأمر [python script] يصبح تلقائيًا جزءًا من مسار Python. لذلك، إذا كان [config] موجودًا في نفس الدليل الذي يحتوي على البرامج النصية التي تحتوي على عبارة [import config]، فسيتم العثور عليه. الملفان [1] و[2] متطابقان هنا. قد لا يكون هذا هو الحال دائمًا.

ملف [config.sys] هو كما يلي:

def configure():
    import os

    #  absolute path of this script's folder
    script_dir = os.path.dirname(os.path.abspath(__file__))
    #  root_dir
    root_dir="C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"
    #  absolute dependencies
    absolute_dependencies=[
        #  local folders containing classes and interfaces
        f"{root_dir}/02/entities",
        f"{script_dir}/../entities",
        f"{script_dir}/../interfaces",
        f"{script_dir}/../services",
    ]

    #  update syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  return the config
    return {}
  • الأسطر 11–14: الدلائل التي يجب أن تكون جزءًا من مسار Python (sys.path
  • يتيح الدليل [f"{root_dir}/02/entities"] الوصول إلى الفئات [BaseEntity] و [MyException]؛
  • يوفر المجلد [f"{script_dir}/../entities"] الوصول إلى الفئات [Student] و [Class] و [Subject] و [Grade]؛
  • يوفر المجلد [f"{script_dir}/../interfaces"] الوصول إلى واجهات التطبيق؛
  • يوفر المجلد [f"{script_dir}/../services"] الوصول إلى الفئات التي تنفذ الواجهات؛

14.2.3. اختبار الكيانات

هنا، سنكتب اختبارات يتم تنفيذها بواسطة أداة تسمى [unittest]. يأتي PyCharm مزودًا بعدة أطر عمل للاختبار. يمكنك اختيار أحدها في إعدادات PyCharm:

Image

  • في [4]، تتوفر عدة أطر عمل للاختبار:

Image

14.2.3.1. فئة الاختبار [TestBaseEntity]

سيكون نص الاختبار [TestBaseEntity] كما يلي:

import unittest

#  configure the application
import config

config = config.configure()


class TestBaseEntity (unittest.TestCase):

    def test_note1(self):
        #  imports
        from Note import Note
        from Elève import Elève
        from Classe import Classe
        from Matière import Matière
        #  construction of a note from a jSON string
        note = Note().fromjson(
            '{"id": 8, "valeur": 12, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}')
        #  checks
        self.assertIsInstance(note, Note)
        self.assertIsInstance(note.élève, Elève)
        self.assertIsInstance(note.élève.classe, Classe)
        self.assertIsInstance(note.matière, Matière)


    def test_note2(self):
        #  imports
        from Note import Note
        from Elève import Elève
        from Classe import Classe
        from Matière import Matière
        #  building a note from a dictionary
        note = Note().fromdict(
            {"id": 8, "valeur": 12, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4",
                                              "classe": {"id": 2, "nom": "classe2"}},
             "matière": {"id": 2, "nom": "matière2", "coefficient": 2}})
        #  checks
        self.assertIsInstance(note, Note)
        self.assertIsInstance(note.élève, Elève)
        self.assertIsInstance(note.élève.classe, Classe)
        self.assertIsInstance(note.matière, Matière)

if __name__ == '__main__':
    unittest.main()

ملاحظات

  • السطر 1: نستورد الوحدة النمطية [unittest]، التي توفر طرق الاختبار المختلفة؛
  • الأسطر 3–6: نقوم بتكوين التطبيق بحيث يمكن العثور على الفئات اللازمة للاختبار؛
  • السطر 9: يجب أن تمتد فئة اختبار [unittest] إلى فئة [unittest.TestCase]؛
  • السطران 11 و27: يجب أن يبدأ اسم وظائف الاختبار بـ [testوإلا فلن يتم التعرف عليها؛
  • الأسطر 13-16: نستورد الفئات التي نحتاجها؛
  • في فئة الاختبار هذه، نريد التحقق من سلوك الطريقتين [BaseEntity.fromdict] (السطر 34) و [BaseEntity.fromjson] (السطر 18). تحتوي فئة [Note] على خصائص تشير إلى فئات أخرى. نريد التحقق من أن الطريقتين السابقتين تنشئان كائنات [Note] صالحة؛
  • السطر 18: نقوم بإنشاء كائن [Note] من كائن JSON؛
  • السطر 21: نتحقق من أن الكائن الذي تم إنشاؤه هو بالفعل من النوع [Note]. الطريقة [assertIsInstance] هي طريقة تابعة لفئة [unittest.TestCase]، وهي الفئة الأم لفئة [TestBaseEntity]؛
  • السطر 22: نتحقق من أن [note.student] هو بالفعل من النوع [Student]؛
  • السطر 23: نتحقق من أن [note.student.class] هو بالفعل من النوع [Class]؛
  • السطر 24: نتحقق من أن [note.subject] هو بالفعل من النوع [Subject]؛
  • الأسطر 33–42: نقوم بنفس الشيء مع طريقة [BaseEntity.fromdict]؛

هناك عدة طرق لتشغيل الاختبارات:

  • في [1-2]، نقوم بتشغيل [TestBaseEntity] باستخدام إطار عمل [UnitTest]؛
  • في [3-5]، تفشل الاختبارات. تشير [UnitTests] إلى أنها لم تعثر على أي اختبارات لتشغيلها؛

تفشل الاختبارات بسبب بنية كود [TestBaseEntity]:

1
2
3
4
5
6
7
8
9
import unittest

#  configure the application
import config

config = config.configure()


class TestBaseEntity(unittest.TestCase):

ما يسبب مشاكل لإطار عمل [UnitTest] هو وجود كود قابل للتنفيذ (الأسطر 3–6) قبل تعريف فئة الاختبار (السطر 9).

لذلك، نعيد تنظيم الكود على النحو التالي:

import unittest


class TestBaseEntity(unittest.TestCase):

    def setUp(self):
        #  configure the application
        import config

        config.configure()

    def test_note1(self):
        

    def test_note2(self):
        


if __name__ == '__main__':
    unittest.main()
  • الأسطر 6–10: نُعرّف دالة [setUp]. لهذه الدالة دور محدد: يتم تنفيذها قبل كل دالة اختبار (test_note1، test_note2

بمجرد الانتهاء من ذلك، يؤدي تنفيذ فئة [TestBaseEntity] إلى النتائج التالية:

هذه المرة، تم تنفيذ كلتا طريقتي الاختبار ونجحت الاختبارات.

لنرى ماذا يحدث عندما يفشل الاختبار. لنعدل الكود في [test_note1] على النحو التالي:

1
2
3
4
5
6
    def test_note1(self):
        #  deliberate error - check that 1==2
        self.assertEqual(1,2)
        #  imports
        from Note import Note

  • السطر 2: نتحقق من أن 1==2؛

نتائج التنفيذ هي كما يلي:

يمكنك معرفة سبب الخطأ بالنقر على الاختبار الفاشل [2]:

  • في [7-8]، سبب الخطأ؛

هناك طريقة أخرى لتشغيل فئة الاختبار وهي تشغيلها في محطة طرفية:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests>python -m unittest TestBaseEntity.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.026s
 
OK

تشير السطر 6 إلى نجاح كلا الاختبارين (لقد أزلنا الخطأ 1==2)؛

أخيرًا، هناك طريقة ثالثة لتشغيل فئة الاختبار [TestBaseEntity]، لا تزال في محطة طرفية، وهي كما يلي. ننهي فئة الاختبار بالسطور 6-7 التالية؛



        self.assertIsInstance(note.élève.classe, Classe)
        self.assertIsInstance(note.matière, Matière)
 
 
if __name__ == '__main__':
    unittest.main()
  • السطر 6: المتغير [__name__] هو الاسم الممنوح للبرنامج النصي الذي يتم تنفيذه. عندما يكون البرنامج النصي هو الذي تم تشغيله بواسطة الأمر [python script.py]، يكون المتغير [__name__] هو [__main__] (شرطتان سفليتان قبل وبعد المعرف). وبالتالي، يتم تنفيذ السطر 7 فقط عندما يتم تشغيل البرنامج النصي [TestBaseEntity] بواسطة الأمر [python TestBaseEntity.py]. تعمل العبارة [unittest.main()] على تشغيل البرنامج النصي عبر إطار عمل [UnitTest]. فيما يلي مثال على ذلك:

(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests>python TestBaseEntity.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.013s
 
OK

14.2.3.2. فئة الاختبار [TestEntities]

فئة الاختبار [TestEntities] هي كما يلي:

import unittest


class TestEntités(unittest.TestCase):
    def setUp(self):
        #  configure the application
        import config

        config.configure()

    def test_code1a(self):
        #  imports
        from Elève import Elève
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid id
            Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 1)

    def test_code41(self):
        #  imports
        from Elève import Elève
        from MyException import MyException
        #  error code
        code = None

        try:
            #  invalid name
            Elève().fromdict({"id": 1, "nom": "", "prénom": "z", "classe": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 41)

    def test_code42(self):
        #  imports
        from Elève import Elève
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid first name
            Elève().fromdict({"id": 1, "nom": "y", "prénom": "", "classe": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 42)

    def test_code43(self):
        #  imports
        from Elève import Elève
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid class
            Elève().fromdict({"id": 1, "nom": "y", "prénom": "z", "classe": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 43)

    def test_code1b(self):
        #  imports
        from Classe import Classe
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid identifier
            Classe().fromdict({"id": "x", "nom": "y"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 1)

    def test_code11(self):
        #  imports
        from Classe import Classe
        from MyException import MyException

        #  error code
        code = None
        try:
            #  invalid name
            Classe().fromdict({"id": 1, "nom": ""})
        except MyException as ex:
            code = ex.code
        #  check
        self.assertEqual(code, 11)

    def test_code1c(self):
        #  imports
        from Matière import Matière
        from MyException import MyException

        #  error code
        code = None
        try:
            #  invalid identifier
            Matière().fromdict({"id": "x", "nom": "y", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 1)

    def test_code21(self):
        #  imports
        from Matière import Matière
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid name
            Matière().fromdict({"id": "1", "nom": "", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 21)

    def test_code22(self):
        #  imports
        from Matière import Matière
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid coefficient
            Matière().fromdict({"id": 1, "nom": "y", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 22)

    def test_code1d(self):
        #  imports
        from Note import Note
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid identifier
            Note().fromdict({"id": "x", "valeur": "x", "élève": "y", "matière": "z"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 1)

    def test_code31(self):
        #  imports
        from Note import Note
        from MyException import MyException

        #  error code
        code = None
        try:
            #  invalid value
            Note().fromdict({"id": 1, "valeur": "x", "élève": "y", "matière": "z"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 31)

    def test_code32(self):
        #  imports
        from Note import Note
        from MyException import MyException

        #  error code
        code = None
        try:
            #  disabled student
            Note().fromdict({"id": 1, "valeur": 10, "élève": "y", "matière": "z"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 32)

    def test_code33(self):
        #  imports
        from Elève import Elève
        from Note import Note
        from Classe import Classe
        from MyException import MyException

        #  error code
        code = None
        try:
            #  invalid material
            classe = Classe().fromdict({"id": 1, "nom": "x"})
            élève = Elève().fromdict({"id": 1, "nom": "a", "prénom": "b", "classe": classe})
            Note().fromdict({"id": 1, "valeur": 10, "élève": élève, "matière": "z"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 33)

    def test_exception(self):
        #  imports
        from Elève import Elève
        #  the test must launch type [MyException] to succeed
        from MyException import MyException
        with self.assertRaises(MyException):
            #  the test
            Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})


if __name__ == '__main__':
    unittest.main()
  • الغرض من البرنامج النصي للاختبار هو اختبار مُعيّنات الفئة: للتحقق من أنه لا يمكن تعيين قيم غير صحيحة لسمات الكيانات المختلفة؛
  • الأسطر 11–24: نختبر عدم إمكانية تعيين معرّف غير صالح لطالب. وبما أننا نمرر القيمة 'x' في السطر 16 كمعرّف للطالب، فإننا نتوقع حدوث استثناء. لذا يجب أن ننتقل إلى الأسطر 20–22؛
  • السطر 21: عرض رسالة الخطأ؛
  • السطر 22: استرداد رمز الخطأ (انظر القسم |كيان MyException|)؛
  • السطر 24: نتحقق (نؤكد) من أن رمز الخطأ هو 1. هنا، نتحقق من أمرين:
    • أن خطأً قد حدث بالفعل؛
    • أن رمز الخطأ هو 1؛
  • تتكرر هذه العملية مع الدوال في الأسطر 24–213؛
  • الأسطر 215–222: نختبر ما إذا كان الإجراء يرمي استثناءً من نوع معين؛
  • السطر 220: نشير إلى أن الاختبار ناجح إذا ألقى استثناءً من النوع [MyException]؛

النتائج

نقوم بتشغيل البرنامج النصي للاختبار:

النتائج التي تم الحصول عليها هي كما يلي:


Testing started at 09:39 ...
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py" --path C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestEntités.py
Launching unittests with arguments python -m unittest C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestEntités.py in C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests
 
 
code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Elève.Elève'> doit être un entier >=0]
 
code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Classe.Classe'> doit être un entier >=0]
 
code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Matière.Matière'> doit être un entier >=0]
 
code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Note.Note'> doit être un entier >=0]
 
code erreur=21, message=MyException[21, Le nom de la matière 1 doit être une chaîne de caractères non vide]
 
code erreur=22, message=MyException[22, Le coefficient de la matière y doit être un réel >=0]
 
code erreur=31, message=MyException[31, L'attribut x de la note 1 doit être un nombre dans l'intervalle [0,20]]
 
code erreur=32, message=MyException[32, L'attribut [y] de la note 1 doit être de type Elève ou dict ou json. Erreur : Expecting value: line 1 column 1 (char 0)]
 
code erreur=33, message=MyException[33, L'attribut [z] de la note 1 doit être de type Matière ou dict ou json. Erreur : Expecting value: line 1 column 1 (char 0)]
 
code erreur=41, message=MyException[41, Le nom de l'élève 1 doit être une chaîne de caractères non vide]
 
code erreur=42, message=MyException[42, Le prénom de l'élève 1 doit être une chaîne de caractères non vide]
 
code erreur=43, message=MyException[43, L'attribut [t] de l'élève 1 doit être de type Classe ou dict ou json. Erreur : Expecting value: line 1 column 1 (char 0)]
 
 
Ran 14 tests in 0.040s
 
OK
 
Process finished with exit code 0

هنا، تم اجتياز جميع الاختبارات

14.2.4. طبقة [dao]

Image

تنفذ طبقة [dao] واجهة [InterfaceDao] [1]. ويتم تنفيذ ذلك بواسطة فئة [Dao] (2). ويختبر البرنامج النصي [tests_dao] (3) أساليب طبقة [dao].

14.2.4.1. واجهة [InterfaceDao]

الواجهة هي عقد بين الكود المستدعي والكود المستدعى. والكود المستدعى هو الذي يوفر الواجهة:

  • لا يعرف الكود المستدعي [1] كيفية تنفيذ الكود المستدعى [3]. إنه يعرف فقط كيفية استدعائه. وتخبره الواجهة [2] بكيفية القيام بذلك. تحدد هذه الواجهة مجموعة من الأساليب/الوظائف التي سيتم استخدامها للتفاعل مع الكود المستدعى. تُعرف هذه الواجهة أيضًا باسم API (واجهة برمجة التطبيقات)؛

ستوفر طبقة [dao] الواجهة التالية:

  • [get_classes] تعرض قائمة الفصول الدراسية في المدرسة الإعدادية؛
  • [get_subjects] تعرض قائمة المواد التي تُدرس في المدرسة الإعدادية؛
  • [get_students] تعرض قائمة الطلاب في المدرسة الإعدادية؛
  • [get_grades] تعرض قائمة بدرجات جميع الطلاب؛
  • [get_grades_for_student_by_id] تعرض درجات طالب معين؛
  • [get_student_by_id] تعرض الطالب المحدد برقمه التعريفي؛

سيستخدم كود الاستدعاء هذه الطرق فقط. ولا يحتاج إلى معرفة كيفية تنفيذها. ويمكن أن تأتي البيانات من مصادر مختلفة (مبرمجة بشكل ثابت، من قاعدة بيانات، من ملفات نصية، إلخ) دون التأثير على كود الاستدعاء. وهذا ما يُسمى بالبرمجة القائمة على الواجهة.

يحتوي Python 3 على مفهوم مشابه لمفهوم الواجهة: الفئة المجردة. سنستخدمها. سنقوم بتجميع واجهات هذا المثال في مجلد [interfaces].

نحدد فئة مجردة [InterfaceDao] (InterfaceDao.py) لطبقة [dao]:

#  imports
from abc import ABC, abstractmethod

#  dao interface
from Elève import Elève


class InterfaceDao(ABC):
    #  list of classes
    @abstractmethod
    def get_classes(self: object) -> list:
        pass

    #  list of students
    @abstractmethod
    def get_élèves(self: object) -> list:
        pass

    #  list of materials
    @abstractmethod
    def get_matières(self: object) -> list:
        pass

    #  lIST OF NOTES
    @abstractmethod
    def get_notes(self: object) -> list:
        pass

    #  list of student grades
    @abstractmethod
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        pass

    #  search for a student by id
    @abstractmethod
    def get_élève_by_id(self, élève_id: int) -> Elève:
        pass

ملاحظات:

  • السطر 2: ABC = الفئة الأساسية المجردة. نقوم باستيراد فئة ABC من الوحدة النمطية [abc]، بالإضافة إلى الزخرفة [abstractmethod] المستخدمة في الأسطر 10 و15 و20 و25 و30 و35؛
  • السطر 8: تسمى الفئة المجردة [InterfaceDao] وتشتق من فئة [ABC]؛
  • يتم تزيين أساليب الفئة المجردة بزخرفة [@abstractmethod]، مما يجعل الأسلوب المزخرف أسلوبًا مجردًا: لا يتم تعريف كوده. ومع ذلك، نقوم بتضمين كود هناك: عبارة [pass]، التي لا تفعل شيئًا؛
  • لا يمكن إنشاء مثيل للفئة المجردة [InterfaceDao]. ولا يمكن إنشاء مثيلات إلا للفئات المشتقة من [InterfaceDao] التي قامت بتنفيذ جميع أساليب [InterfaceDao]. لذلك، إذا أنشأنا فئتين [Dao1] و [Dao2] مشتقتين من الفئة [InterfaceDao]، فستقوم كلتاهما بتنفيذ الطرق المجردة لـ [InterfaceDao]. وبالتالي يمكننا القول إنهما تنفذان الواجهة [InterfaceDao]؛
  • اللغات التي تدعم كل من الواجهات والفئات المجردة تخصص دورًا مختلفًا للواجهة عن الفئة المجردة. لا تحتوي الواجهة على سمات ولا يمكن إنشاء مثيل لها. يمكن للفئة تنفيذ واجهة عن طريق تعريف جميع أساليبها؛

14.2.4.2. تنفيذ [Dao]

تنفذ فئة [Dao] (dao.py) واجهة [InterfaceDao] على النحو التالي:

#  import entities and interfaces
from Classe import Classe
from Elève import Elève
from InterfaceDao import InterfaceDao
from Matière import Matière
from MyException import MyException
from Note import Note


#  dao] layer implements InterfaceDao interface
class Dao(InterfaceDao):
    #  manufacturer
    #  we build hard lists
    def __init__(self):
        #  classes are instantiated
        classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
        classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
        self.classes = [classe1, classe2]
        #  materials
        matière1 = Matière().fromdict({"id": 1, "nom": "matière1", "coefficient": 1})
        matière2 = Matière().fromdict({"id": 2, "nom": "matière2", "coefficient": 2})
        self.matières = [matière1, matière2]
        #  students
        élève11 = Elève().fromdict({"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": classe1})
        élève21 = Elève().fromdict({"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": classe1})
        élève32 = Elève().fromdict({"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": classe2})
        élève42 = Elève().fromdict({"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": classe2})
        self.élèves = [élève11, élève21, élève32, élève42]
        #  student grades in various subjects
        note1 = Note().fromdict({"id": 1, "valeur": 10, "élève": élève11, "matière": matière1})
        note2 = Note().fromdict({"id": 2, "valeur": 12, "élève": élève21, "matière": matière1})
        note3 = Note().fromdict({"id": 3, "valeur": 14, "élève": élève32, "matière": matière1})
        note4 = Note().fromdict({"id": 4, "valeur": 16, "élève": élève42, "matière": matière1})
        note5 = Note().fromdict({"id": 5, "valeur": 6, "élève": élève11, "matière": matière2})
        note6 = Note().fromdict({"id": 6, "valeur": 8, "élève": élève21, "matière": matière2})
        note7 = Note().fromdict({"id": 7, "valeur": 10, "élève": élève32, "matière": matière2})
        note8 = Note().fromdict({"id": 8, "valeur": 12, "élève": élève42, "matière": matière2})
        self.notes = [note1, note2, note3, note4, note5, note6, note7, note8]

    # -----------
    #  interface IDao
    # -----------

ملاحظات:

  • الأسطر 1-7: نقوم باستيراد الكيانات وواجهة [InterfaceDao]؛
  • السطر 11: الفئة [Dao] مشتقة من الفئة المجردة [InterfaceDao]. نقول إنها تنفذ واجهة [InterfaceDao]؛
  • السطر 14: لا تحتوي المنشئة على معلمات. وهي تضمّن أربع قوائم:
    • الأسطر 15–18: قائمة الفئات؛
    • الأسطر 19-22: قائمة المواد؛
    • الأسطر 23-28: قائمة الطلاب؛
    • الأسطر 29–38: قائمة الدرجات؛
  • الأسطر 40–44: تنفيذ أساليب [واجهة Dao]. هنا، لا نقوم بتعريفها لكي نرى رسالة الخطأ التي تصدرها لغة Python؛

قد يبدو برنامج الاختبار كما يلي [tests-dao.py]:

#  configure the application
import config

config = config.configure()

#  layer instantiation [dao]
from Dao import Dao

daoImpl = Dao()

#  class list
for classe in daoImpl.get_classes():
    print(classe)

#  list of materials
for matière in daoImpl.get_matières():
    print(matière)

#  class list
for élève in daoImpl.get_élèves():
    print(élève)

#  lIST OF NOTES
for note in daoImpl.get_notes():
    print(note)

ملاحظة: البرنامج النصي [tests-dao.py] ليس [unittest] لأنه لا يحتوي على أي طرق تبدأ أسماؤها بـ [test_].

التعليقات واضحة بذاتها. تستخدم الأسطر 11–25 واجهة طبقة [dao]. لا توجد هنا أي افتراضات حول التنفيذ الفعلي للطبقة. في السطر 9، نقوم بإنشاء مثيل لطبقة [dao].

نتائج تشغيل هذا البرنامج النصي هي كما يلي:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/tests_dao.py
Traceback (most recent call last):
  File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/tests_dao.py", line 9, in <module>
    daoImpl = Dao()
TypeError: Can't instantiate abstract class Dao with abstract methods get_classes, get_matières, get_notes, get_notes_for_élève_by_id, get_élève_by_id, get_élèves
 
Process finished with exit code 1

نلاحظ حدوث خطأ فور إنشاء مثيل للفئة [Dao] (السطر 3 أعلاه). يُخبرنا مترجم Python 3 أنه لا يمكنه إنشاء مثيل للفئة لأننا لم نُعرّف الطرق المجردة [get_classes، get_subjects، get_grades، get_grades_for_student_by_id، get_student_by_id، get_students].

يدعم PyCharm أيضًا الفئات المجردة ويقدم تعريفًا لأساليبها:

  • في [1]، انقر بزر الماوس الأيمن على الكود؛
  • في [2-3]، حدد [إنشاء / تنفيذ الطرق] لتنفيذ الطرق المفقودة لفئة [Dao]؛
  • في [4]، حدد الطرق المراد تنفيذها — في هذه الحالة، جميعها؛

بمجرد الانتهاء من ذلك، يكمل PyCharm فئة [Dao] على النحو التالي:

    # -----------
    #  interface IDao
    # -----------

    def get_classes(self: object) -> list:
        pass

    def get_élèves(self: object) -> list:
        pass

    def get_matières(self: object) -> list:
        pass

    def get_notes(self: object) -> list:
        pass

    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        pass

    def get_élève_by_id(self, élève_id: int) -> Elève:
        pass

نكمل فئة [Dao] على النحو التالي:

    # -----------
    #  interface IDao
    # -----------

    #  class list
    def get_classes(self) -> list:
        return self.classes

    #  list of materials
    def get_matières(self) -> list:
        return self.matières

    #  list of students
    def get_élèves(self) -> list:
        return self.élèves

    #  lIST OF NOTES
    def get_notes(self) -> list:
        return self.notes

    def get_notes_for_élève_by_id(self, élève_id: int) -> dict:
        #  we're looking for the student
        élève = self.get_élève_by_id(élève_id)
        #  get your notes back
        notes = list(filter(lambda n: n.élève.id == élève_id, self.get_notes()))
        #  we return the result
        return {"élève": élève, "notes": notes}

    def get_élève_by_id(self, élève_id: int) -> Elève:
        #  filtering students
        élèves = list(filter(lambda e: e.id == élève_id, self.get_élèves()))
        #  found?
        if not élèves:
            raise MyException(10, f"L'élève d'identifiant {élève_id} n'existe pas")
        #  result
        return élèves[0]
  • الأسطر 5–19 واضحة؛
  • الأسطر 29–36: الطريقة التي تُرجع الطالب الذي تم تمرير معرّفه. إذا لم يكن الطالب موجودًا، يتم إثارة استثناء؛
  • السطر 31: تسمح لك الدالة [filter] بتصفية قائمة:
    • المعلمة الأولى هي معيار التصفية؛
    • المعلمة الثانية هي القائمة المراد تصفيتها، وهي في هذه الحالة قائمة الطلاب؛
  • السطر 31: يتم تنفيذ معيار التصفية للقائمة باستخدام دالة [f(e:Student) -> bool]. يتم تطبيق هذا على كل عنصر من عناصر القائمة المراد تصفيتها. إذا كان العنصر يفي بمعيار التصفية، يتم الاحتفاظ به في القائمة المصفّاة؛ وإلا، يتم استبعاده. هنا، يمكننا إما:
    • تحديد اسم الدالة f وتنفيذها في مكان آخر. عندئذ يصبح استدعاء الدالة [filter] هو [filter(f, self.get_students)]؛
    • تقديم تعريف الدالة f. عندئذٍ يصبح استدعاء الدالة [filter] هو [filter(f(e :Student){…}, self.get_students())]، حيث يمثل [e] عنصرًا من القائمة التي تم تصفيتها، أي طالبًا. وهذا ما تم فعله هنا. سيكون تعريف الدالة f هنا هو [f(e :Student){return e.id == student_id)]: يتم اختيار الطالب فقط إذا كان رقم هويته [id] مطابقًا للرقم الذي يتم البحث عنه. يمكن استبدال هذه الدالة بما يُسمى دالة لامدا: [lambda e: e.id == student_id]:
      • e: يمثل معلمة الدالة f، وهي هنا طالب. يمكنك استخدام أي اسم تريده؛
      • e.id==student_id هو معيار التصفية: يتم اختيار الطالب [e] فقط إذا كان رقم هويته [id] مطابقًا للرقم الذي يتم البحث عنه؛
  • السطر 31: تُرجع الدالة [filter] القائمة المُصفاة كنوع لا ينتمي إلى نوع [listلكن يمكن تحويله إلى نوع [list]. وهذا ما نقوم به هنا باستخدام التعبير [list(filtered_list)]؛
  • السطران 33-34: إذا كانت القائمة المفلترة فارغة، فهذا يعني أن الطالب الذي يتم البحث عنه غير موجود. عندئذ يتم إثارة استثناء؛
  • السطر 36: إذا وصلنا إلى هذه النقطة، فهذا يعني أنه لم يتم إثارة أي استثناء. عندئذ نعلم أننا استرجعنا قائمة تحتوي على عنصر واحد (لا يوجد طالبان يحملان رقم [id] نفسه). لذلك نُرجع العنصر الأول من القائمة؛
  • الأسطر 21–27: يجب أن تُرجع الطريقة [get_notes_for_élève_by_id] الدرجات الخاصة بالطالب الذي تم تمرير رقم [id] الخاص به إليها؛
  • السطور 22–23: نبدأ بالبحث عن الطالب الذي يحمل الرقم التعريفي [student_id] باستخدام طريقة [get_student_by_id]، التي قمنا للتو بتعليقها. قد تحدث استثناء إذا كان الطالب الذي نبحث عنه غير موجود. وبما أنه لا يوجد كتلة try/catch حول العبارة في السطر 23، فسيتم تمرير الاستثناء إلى الكود المستدعي. وهذا هو السلوك المطلوب؛
  • السطران 24-25: بمجرد استرداد الطالب، نسترد جميع درجاته. نقوم بذلك مرة أخرى باستخدام مرشح:
    • المرشح هو [filter(criterion, self_getnotes()]. وبالتالي، فإن القائمة المراد تصفيةها هي قائمة جميع الدرجات لجميع الطلاب في المدرسة؛
    • يتم التعبير عن معيار التصفية باستخدام دالة [lambda]: lambda n: n.student.id == student_id. المعلمة n هي عنصر من القائمة المراد تصفية، أي درجة. يحتوي النوع [Note] على خاصية [student] تمثل الطالب صاحب الدرجة. لذلك، يجب أن يكون [n.student.idالذي يمثل معرف ذلك الطالب، مساوياً لمعرف الطالب الذي نبحث عنه؛

ثم نقوم بتشغيل البرنامج النصي [tests-dao.py].

#  configure the application
import config

config = config.configure()

#  layer instantiation [dao]
from Dao import Dao

daoImpl = Dao()

#  class list
for classe in daoImpl.get_classes():
    print(classe)

#  list of materials
for matière in daoImpl.get_matières():
    print(matière)

#  class list
for élève in daoImpl.get_élèves():
    print(élève)

#  lIST OF NOTES
for note in daoImpl.get_notes():
    print(note)

#  a special student
print(daoImpl.get_élève_by_id(11))

#  a list of his notes
dict1 = daoImpl.get_notes_for_élève_by_id(11)
print(f"élève n° 11 = {dict1['élève']}")
for note in dict1["notes"]:
    print(f"note de l'élève n° 11 = {note}")

ثم نحصل على النتائج التالية:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/tests_dao.py
{"id": 1, "nom": "classe1"}
{"id": 2, "nom": "classe2"}
{"id": 1, "nom": "matière1", "coefficient": 1}
{"id": 2, "nom": "matière2", "coefficient": 2}
{"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
{"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom": "classe1"}}
{"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom": "classe2"}}
{"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom": "classe2"}}
{"id": 1, "valeur": 10, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 2, "valeur": 12, "élève": {"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 3, "valeur": 14, "élève": {"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 4, "valeur": 16, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 5, "valeur": 6, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 6, "valeur": 8, "élève": {"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 7, "valeur": 10, "élève": {"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 8, "valeur": 12, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
élève n° 11 = {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
note de l'élève n° 11 = {"id": 1, "valeur": 10, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
note de l'élève n° 11 = {"id": 5, "valeur": 6, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
 
Process finished with exit code 0

لاحظ أنه عند عرض الدرجة (العملية مشابهة بالنسبة للكائنات الأخرى)، لدينا أيضًا:

  • الطالب المرتبط بالدرجة؛
  • المادة المشار إليها بالدرجة؛

يتم إنتاج هذه النتيجة بواسطة الدالة [BaseEntity.asdict] (انظر قسم "الرابط").

14.2.5. طبقة [business]

  • [InterfaceMétier] هي واجهة طبقة [business]؛
  • [Business] هي فئة التنفيذ لطبقة [business]؛
  • [TestBusiness] هي فئة [UnitTest] لاختبار فئة [Business]؛

14.2.5.1. واجهة [BusinessInterface]

ستقوم طبقة [business] بتنفيذ واجهة [BusinessInterface] التالية (BusinessInterface.py):

#  imports
from abc import ABC, abstractmethod

from StatsForElève import StatsForElève


#  business interface
class InterfaceMétier(ABC):
    #  calculating statistics for a student
    @abstractmethod
    def get_stats_for_élève(self, idElève: int) -> StatsForElève:
        pass
  • تُرجع [get_stats_for_student] الدرجات الخاصة بالطالب idStudent مع معلومات عنه: المتوسط المرجح، وأدنى درجة، وأعلى درجة. يتم تغليف هذه المعلومات في كائن من النوع [StudentStats]؛

14.2.5.2. كيان [StatsForStudent]

نوع [StatsForStudent] (StatsForStudent.py)، الذي يغلف إحصائيات الطالب (الدرجات، الحد الأدنى، الحد الأقصى، المتوسط المرجح)، هو كما يلي:

#  imports
from BaseEntity import BaseEntity


#  individual student statistics


class StatsForElève(BaseEntity):
    #  attributes excluded from class state
    excluded_keys = []

    #  class properties
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: note identifier
        #  pupil: the pupil concerned
        #  notes: his notes
        #  moyennePondérée: average weighted by subject coefficients
        #  min: its minimum score
        #  max: its maximum rating

        return BaseEntity.get_allowed_keys() + ["élève", "notes", "moyenne_pondérée", "min", "max"]

    #  toString
    def __str__(self) -> str:
        #  students without grades
        if len(self.notes) == 0:
            return f"Elève={self.élève}, notes=[]"
        #  student with grades
        str = ""
        for note in self.notes:
            str += f"{note.valeur} "
        return f"Elève={self.élève}, notes=[{str.strip()}], max={self.max}, min={self.min}, " \
               f"moyenne pondérée={self.moyenne_pondérée:4.2f}"

ملاحظات:

  • السطر 8: الفئة [StatsForStudent] مشتقة من الفئة [BaseEntity]؛
  • الأسطر 13–22: خصائص الفئة؛
    • معرف [id] من [BaseEntity]؛
    • الطالب [student] الذي تم تغليف إحصاءاته؛
    • درجاته [grades]؛
    • المتوسط المرجح [weighted_average]؛
    • أدنى درجة حصل عليها [min]؛
    • أعلى درجة له [max]؛
  • نحن لا نحدد متغيرات الحصول/التعيين لهذه السمات. نفترض أن طبقة [business] تنشئ كائنات من هذا النوع وأنها لا تنشئ كائنات غير صالحة؛
  • الأسطر 23–33: تُرجع الدالة [__str__] سلسلة تحتوي على خصائص الكائن؛

14.2.5.3. تنفيذ [Business]

سيكون تنفيذ [Business] (Metier.py) لواجهة [BusinessInterface] كما يلي:

#  imports
from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier
from StatsForElève import StatsForElève


class Métier(InterfaceMétier):

    #  manufacturer
    def __init__(self, dao: InterfaceDao):
        #  the parameter
        self.__dao = dao

    # -----------
    #  interface
    # -----------

    #  indicators on a particular student's grades
    def get_stats_for_élève(self, id_élève: int) -> StatsForElève:
        #  Stats for student no. idEleve
        #  id_élève : pupil number

        #  retrieve notes with the [dao] layer
        notes_élève = self.__dao.get_notes_for_élève_by_id(id_élève)
        élève = notes_élève["élève"]
        notes = notes_élève["notes"]

        #  we stop if there are no notes
        if len(notes) == 0:
            #  we return the result
            return StatsForElève().fromdict({"élève": élève, "notes": []})

        #  use of student notes
        somme_pondérée = 0
        somme_coeff = 0
        max = -1
        min = 21
        for note in notes:
            #  nOTE VALUE
            valeur = note.valeur
            #  material coefficient
            coeff = note.matière.coefficient
            #  sum of coefficients
            somme_coeff += coeff
            #  weighted sum
            somme_pondérée += valeur * coeff
            #  search for min
            if valeur < min:
                min = valeur
            #  search for the max
            if valeur > max:
                max = valeur
        #  calculation of missing indicators
        moyenne_pondérée = float(somme_pondérée) / somme_coeff

        #  the result is returned as type [StatsForElève]
        return StatsForElève(). \
            fromdict({"élève": élève, "notes": notes,
                      "moyenne_pondérée": moyenne_pondérée,
                      "min": min, "max": max})

ملاحظات

  • السطر 7: الفئة [Métier] مشتقة من الفئة [InterfaceMétier]. ومن المعتاد القول إنها تنفذ واجهة [InterfaceMétier]؛
  • الأسطر 9–12: يأخذ المنشئ معلمة واحدة، وهي مرجع إلى طبقة [dao]. في السطر 10، لاحظ أننا قمنا بتعيين النوع [InterfaceDao] للمعلمة [dao]. لا نتوقع تنفيذًا محددًا، بل مجرد تنفيذ يحترم واجهة [DaoInterface]. هنا، لا يهم ذلك لأن Python لن تأخذ هذا النوع في الاعتبار، ولكن من الأفضل العمل مع الواجهات بدلاً من التنفيذات المحددة. يصبح تعديل الكود أسهل عندئذٍ؛
  • الأسطر 19-60: تنفيذ طريقة [get_stats_for_élève]؛
  • السطر 19: تتلقى الطريقة معلمة واحدة، وهي [idElève] للطالب الذي نريد إحصائياته؛
  • السطر 24: نطلب درجات الطالب من طبقة [dao]. ينتج عن هذا الطلب استثناء إذا كان الطالب غير موجود. لا يتم التعامل مع هذا الاستثناء (لا يوجد try/catch) وبالتالي يتم تمريره مرة أخرى إلى الكود المستدعي؛
  • السطر 25: نصل إلى هذه النقطة إذا لم تحدث أي استثناءات. [student_grades] هو إذن قاموس يحتوي على مفتاحين [student, grade]:
    • السطر 25: نسترد معلومات عن الطالب (اسمه، فصله، إلخ)؛
    • السطر 26: نسترد درجاته؛
  • الأسطر 28–31: نتحقق مما إذا كان لدى الطالب أي درجات. إذا لم يكن لديه، فلا توجد إحصائيات لحسابها؛
  • السطر 31: نُرجع كائن [StatsForStudent] تم إنشاؤه من قاموس باستخدام الطريقة [BaseEntity.fromdict]؛
  • الأسطر 33–54: نستخدم درجات الطالب لحساب الإحصائيات المطلوبة. يجب أن تكون تعليقات الكود كافية للفهم؛
  • الأسطر 56-60: نُرجع كائن [StatsForStudent] تم إنشاؤه من قاموس باستخدام طريقة [BaseEntity.fromdict]؛

14.2.5.4. اختبار طبقة [business]

قد يبدو نص [UnitTest] الخاص بطبقة [business] كما يلي (TestMétier.py):

#  imports
import unittest


class Testmétier(unittest.TestCase):
    def setUp(self):
        #  configure the application
        import config
        config.configure()

    def test_statsForEleve11(self):
        #  imports
        from Dao import Dao
        from Métier import Métier
        #  student indicators are tested 11
        dao = Dao()
        stats_for_élève = Métier(dao).get_stats_for_élève(11)
        #  display
        print(f"\nstats={stats_for_élève}")
        #  checks
        self.assertEqual(stats_for_élève.min, 6)
        self.assertEqual(stats_for_élève.max, 10)
        self.assertAlmostEqual(stats_for_élève.moyenne_pondérée, 7.333, delta=1e-3)


if __name__ == '__main__':
    unittest.main()

ملاحظات

  • الأسطر 6–9: تُستخدم دالة [setUp] هنا لتكوين مسار Python الخاص بالاختبار؛
  • السطر 16: نقوم بإنشاء مثيل لطبقة [dao]؛
  • السطر 17: نقوم بإنشاء مثيل لطبقة [business] ونستخدم طريقة [get_stats_for_student] الخاصة بها لحساب الإحصائيات الخاصة بالطالب رقم 11؛
  • السطر 19: يتم عرض [StatsForStudent] الناتج. نظرًا لأن [StatsForStudent] مشتق من [BaseEntity]، يتم عرض سلسلة JSON الخاصة بـ [StatsForStudent] هنا؛
  • السطر 21: نتحقق من الدرجة الدنيا للطالب؛
  • السطر 22: نتحقق من أعلى درجة حصل عليها؛
  • السطر 23: نختبر أن المتوسط المرجح هو 7.333، بدقة تصل إلى 10⁻³. بشكل عام، لا يمكن مقارنة الأعداد الحقيقية بدقة لأنها عادةً ما يتم تمثيلها داخليًا على أنها تقريبية فقط؛

نتائج الاختبار هي كما يلي:


Testing started at 18:17 ...
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py" --path C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestMétier.py
Launching unittests with arguments python -m unittest C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestMétier.py in C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests
 
 
 
Ran 1 test in 0.015s
 
OK
 
stats=Elève={"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, notes=[10 6], max=10, min=6, moyenne pondérée=7.33
 
Process finished with exit code 0

14.2.6. طبقة [ui]

Image

  • في [1]، واجهة طبقة [ui]؛
  • في [2]، تنفيذ هذه الواجهة؛
  • في [3]، البرنامج النصي الرئيسي للتطبيق؛

14.2.6.1. الواجهة [InterfaceUi]

ستكون واجهة طبقة [UI] كما يلي:

#  imports
from abc import ABC, abstractmethod


#  interface UI
class InterfaceUi(ABC):
    #  execute UI layer
    @abstractmethod
    def run(self: object):
        pass

ملاحظات

  • السطران 9-10: ستحتوي طبقة [UI] على طريقة واحدة فقط، وهي [run]؛

14.2.6.2. تنفيذ [Console]

يتم تنفيذ طبقة [console] بواسطة البرنامج النصي [Console.py] التالي:

#  layer imports

from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier
from InterfaceUi import InterfaceUi

#  other dependencies
from MyException import MyException


class Console(InterfaceUi):
    #  manufacturer
    def __init__(self: object, métier: InterfaceMétier):
        #  business: the [business] layer

        #  attributes are memorized
        self.métier = métier


        # -----------
        #  interface
        # -----------

    def run(self):
        #  user dialog
        fini = False
        while not fini:
            #  question/answer
            réponse = input("Numéro de l'élève (>=1 et * pour arrêter) : ").strip()
            #  finished?
            if réponse == "*":
                break
            #  is the input correct?
            ok = False
            try:
                id_élève = int(réponse, 10)
                ok = id_élève >= 1
            except ValueError as erreur:
                pass
            #  correct data?
            if not ok:
                print("Saisie incorrecte. Recommencez...")
                continue
            #  calculation of statistics for the selected student
            try:
                print(self.métier.get_stats_for_élève(id_élève))
            except MyException as erreur:
                print(f"L'erreur suivante s'est produite : {erreur}")
  • الأسطر 3-5: استيراد جميع الواجهات؛
  • السطر 11: تنفذ فئة [Console] واجهة [InterfaceUi]؛
  • الأسطر 12-17: يتلقى منشئ فئة [Console] مرجعًا إلى طبقة [business] كمعلمة. لاحظ أننا أعطينا هذه المعلمة النوع [BusinessInterface] للتأكيد على أننا نعمل مع واجهات بدلاً من تطبيقات محددة؛
  • السطر 24: تنفيذ طريقة [run] للواجهة؛
  • السطر 27: حلقة تتوقف عند استيفاء الشرط الموجود في السطر 31؛
  • السطر 29: إدخال البيانات المكتوبة على لوحة المفاتيح. تتلقى الدالة [input] معلمة اختيارية: الرسالة التي سيتم عرضها على الشاشة لطلب الإدخال. يتم استرداد هذا الإدخال دائمًا كسلسلة. تزيل الدالة [strip] أي مسافات بيضاء في بداية أو نهاية السلسلة؛
  • الأسطر 34–39: نتحقق من صحة الإدخال، وهو رقم هوية الطالب. يجب أن يكون عددًا صحيحًا >= 1. تذكر أن الإدخال تم إدخاله كسلسلة؛
  • السطر 36: نحاول تحويل المدخلات إلى عدد صحيح في النظام العشري. تثير الدالة [int] استثناءً إذا لم يكن ذلك ممكنًا؛
  • السطر 37: نصل إلى هذه النقطة فقط إذا لم تحدث أي استثناءات. نتحقق من أن العدد الصحيح الذي تم استرداده هو بالفعل >=1؛
  • السطور 38–39: نتعامل مع الاستثناء. إذا حدث استثناء، تظل المتغير [ok] من السطر 34 مضبوطًا على [False]؛
  • الأسطر 41-43: إذا كان الإدخال غير صحيح، يتم عرض رسالة خطأ وإعادة تشغيل الحلقة (السطر 43)؛
  • الأسطر 45–48: نحسب الإحصائيات الخاصة بالطالب الذي تم إدخال معرّفه؛
  • السطر 46: يتم استخدام طريقة [get_stats_for_student] من طبقة [business]. ترمي هذه الطريقة استثناءً إذا كان الطالب غير موجود. يتم التعامل مع هذا الاستثناء في السطرين 47 و48. ونحن نعلم أن طبقتي [DAO] و[business] ترميان استثناء [MyException]؛

14.3. البرنامج النصي الرئيسي [main]

النص البرمجي الرئيسي [main] هو كما يلي (main.py):

#  configure the application
import config

config = config.configure()

#  syspath is configured - imports can be made
from Console import Console
from Dao import Dao
from Métier import Métier

#  ----------- layer [console]
try:
    #  layer instantiation [dao]
    dao = Dao()
    #  instantiation layer [business]
    métier = Métier(dao)
    #  instantiation layer [ui]
    console = Console(métier)
    #  layer execution [console]
    console.run()
except BaseException as ex:
    #  error is displayed
    print(f"L'erreur suivante s'est produite : {ex}")
finally:
    pass
  • الأسطر 1–4: تكوين مسار Python للتطبيق؛
  • الأسطر 6-9: استيراد الفئات والواجهات المطلوبة؛
  • السطر 14: إنشاء مثيل لطبقة [DAO]؛
  • السطر 16: إنشاء مثيل لطبقة [business]؛
  • السطر 18: إنشاء مثيل لطبقة [ui]؛
  • السطر 20: بدء واجهة المستخدم؛
  • الأسطر 13–20: عادةً، لا يتم إلقاء أي استثناءات من هذه الأسطر. يتم التقاط أي استثناءات تنتشر من طبقات [DAO] و[business] بواسطة طبقة [Console]. تعتبر معالجة الاستثناءات فنًا صعبًا عندما لا تفهم تمامًا الطبقات المستخدمة (وهذا ليس هو الحال هنا). في حالة الشك، يمكن إضافة كود لالتقاط أي نوع من الاستثناءات التي قد يتم إلقاءها بواسطة الكود المنفذ. هذا ما يتم هنا، الأسطر 21–23. نلتقط أي استثناء مشتق من [BaseException]، أي جميع الاستثناءات؛
  • الأسطر 24-25: لا تقوم جملة [finally] بأي شيء هنا. إنها موجودة فقط للسماح بتعليق الأسطر 21-23. في الواقع، في وضع التصحيح، لا يُنصح بالتقاط الاستثناءات. في هذه الحالة، يقوم مترجم Python بالتقاطها ثم الإبلاغ عن رقم السطر الذي حدث فيه الاستثناء. هذه معلومات أساسية. عندما يتم تعليق الأسطر 21–23، يضمن وجود الأسطر 24–25 وجود كتلة try/catch صحيحة من الناحية النحوية. بدونها، يثير Python خطأً؛

فيما يلي مثال على التنفيذ:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/main/main.py
Numéro de l'élève (>=1 et * pour arrêter) : 11
Elève={"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, notes=[10 6], max=10, min=6, moyenne pondérée=7.33
Numéro de l'élève (>=1 et * pour arrêter) : 1
L'erreur suivante s'est produite : MyException[10, L'élève d'identifiant 1 n'existe pas]
Numéro de l'élève (>=1 et * pour arrêter) : *
 
Process finished with exit code 0

14.4. المثال 2

يهدف هذا المثال الجديد للبنى ذات الطبقات إلى توضيح مزايا البرمجة القائمة على الواجهات. يسهل هذا النهج صيانة التطبيق واختباره. سنستخدم مرة أخرى بنية من ثلاث طبقات:

Image

سيتم تنفيذ كل طبقة بطريقتين مختلفتين. نريد أن نظهر أن تنفيذ طبقة ما يمكن تغييره بسهولة مع تأثير ضئيل على الطبقات الأخرى.

14.4.1. طبقة [dao]

Image

تبدو واجهة [InterfaceDao] كما يلي:

#  imports
from abc import ABC, abstractmethod


#  dao interface
class InterfaceDao(ABC):
    #  a single method
    @abstractmethod
    def do_something_in_dao_layer(self, x: int, y: int) -> int:
        pass
  • الأسطر 8–10: الطريقة [do_something_in_dao_layer] هي الطريقة الوحيدة للواجهة؛

تقوم فئة [DaoImpl1] بتنفيذ واجهة [InterfaceDao] على النحو التالي:

1
2
3
4
5
6
7
from InterfaceDao import InterfaceDao


class DaoImpl1(InterfaceDao):
    #  implementation InterfaceDao
    def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
        return x + y

تقوم فئة [DaoImpl2] بتنفيذ واجهة [InterfaceDao] على النحو التالي:

1
2
3
4
5
6
7
from InterfaceDao import InterfaceDao


class DaoImpl2(InterfaceDao):
    #  implementation InterfaceDao
    def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
        return x - y

14.4.2. طبقة [الأعمال]

Image

واجهة [BusinessInterface] هي كما يلي:

#  imports
from abc import ABC, abstractmethod


#  business interface
class InterfaceMétier(ABC):
    #  a single method
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • الأسطر 8–10: الطريقة [do_something_in_business_layer] هي الطريقة الوحيدة في الواجهة؛

تنفذ فئة [AbstractBaseMétier] واجهة [InterfaceMétier] على النحو التالي:

#  imports
from abc import ABC, abstractmethod

from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier


class AbstractBaseMétier(InterfaceMétier, ABC):
    #  properties
    #  __dao is a reference to the [dao] layer
    @property
    def dao(self) -> InterfaceDao:
        return self.__dao

    @dao.setter
    def dao(self, dao: InterfaceDao):
        self.__dao = dao

    #  interface implementation [InterfaceMétier]
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • السطر 8: تنبثق عن فئة [AbstractBaseMétier] فئتان:
    • [BusinessInterface]: تنفذ الفئة [AbstractBusinessBase] هذه الواجهة في الأسطر 19–22. في الواقع، نرى أنها لم تنفذ الطريقة [do_something_in_business_layerالتي أعلنت عنها على أنها مجردة (السطر 20). وسيكون الأمر متروكًا للفئات المشتقة لتنفيذ الطريقة؛
    • [ABC] للوصول إلى تعليقات [@abstractmethod]؛
    • الترتيب مهم: إذا عكسناه هنا، فإن Python تثير خطأً في وقت التشغيل؛

هذه هي المرة الأولى التي نستخدم فيها الوراثة المتعددة (الوراثة من فئات متعددة). ترث فئة [AbstractBaseMétier] الخصائص من كل من فئتي [InterfaceMétier] و [ABC].

  • الأسطر 9–17: نُعرّف الخاصية [dao]، والتي ستكون مرجعًا لطبقة [dao]؛

الواجهة مصممة ليتم تنفيذها. عندما تتشارك تطبيقات مختلفة في خصائص، من المفيد وضع هذه الخصائص في فئة أصلية لتجنب التكرار. وهذا هو الحال هنا مع الخاصية [dao]. عادةً ما تكون الفئة الأصلية مجردة دائمًا لأنها لا تنفذ جميع أساليب الواجهة.

تنفذ فئة [BusinessImpl1] واجهة [BusinessInterface] على النحو التالي:

1
2
3
4
5
6
7
8
9
from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl1(AbstractBaseMétier):
    #  interface implementation [InterfaceMétier]
    def do_something_in_métier_layer(self:AbstractBaseMétier, x: int, y: int) -> int:
        x += 1
        y += 1
        return self.dao.do_something_in_dao_layer(x, y)
  • السطر 4: الفئة [BusinessImpl1] مشتقة من الفئة [AbstractBusinessBase]. وبالتالي، فإنها ترث الخاصية [dao] من هذه الفئة؛
  • الأسطر 6-9: تنفيذ واجهة [BusinessInterface] التي لم تنفذها الفئة الأم [AbstractBusinessBase]؛
  • السطر 9: يتم استخدام طبقة [dao]؛

تنفذ الفئة [BusinessImpl2] الواجهة [BusinessInterface] بطريقة مماثلة:

1
2
3
4
5
6
7
8
9
from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl2(AbstractBaseMétier):
    #  interface implementation [InterfaceMétier]
    def do_something_in_métier_layer(self:AbstractBaseMétier, x: int, y: int) -> int:
        x -= 1
        y -= 1
        return self.dao.do_something_in_dao_layer(x, y)

14.4.3. طبقة [ui]

Image

واجهة [InterfaceUi] هي كما يلي:

#  imports
from abc import ABC, abstractmethod


#  ui interface
class InterfaceUi(ABC):
    #  a single method
    @abstractmethod
    def do_something_in_ui_layer(self, x: int, y: int) -> int:
        pass
  • الأسطر 8–10: الطريقة الوحيدة للواجهة؛

تقوم فئة [AbstractBaseUi] بتنفيذ واجهة [InterfaceUi] على النحو التالي:

#  imports
from abc import ABC, abstractmethod

from InterfaceMétier import InterfaceMétier
from InterfaceUi import InterfaceUi


class AbstractBaseUi(InterfaceUi, ABC):
    #  properties
    #  business is a reference to the [business] layer
    @property
    def métier(self) -> InterfaceMétier:
        return self.__métier

    @métier.setter
    def métier(self, métier: InterfaceMétier):
        self.__métier = métier

    #  interface implementation [InterfaceUI]
    @abstractmethod
    def do_something_in_ui_layer(self: InterfaceUi, x: int, y: int) -> int:
        pass
  • فئة [AbstractBaseUi] هي فئة مجردة (السطر 20). يجب أن تُشتق منها لتنفيذ واجهة [InterfaceUi]؛
  • الأسطر 9–17: تحتوي فئة [AbstractBaseUi] على مرجع إلى طبقة [business]؛

فئة التنفيذ [UiImpl1] هي كما يلي:

1
2
3
4
5
6
7
8
9
from AbstractBaseUi import AbstractBaseUi


class UiImpl1(AbstractBaseUi):
    #  interface implementation [InterfaceUi]
    def do_something_in_ui_layer(self: AbstractBaseUi, x: int, y: int) -> int:
        x += 1
        y += 1
        return self.métier.do_something_in_métier_layer(x, y)
  • السطر 4: الفئة [UiImpl1] مشتقة من الفئة [AbstractBaseUi] وبالتالي ترث خاصية [business] الخاصة بها. يتم استخدام هذه الخاصية في السطر 9؛

فئة التنفيذ [UiImpl2] مشابهة:

1
2
3
4
5
6
7
8
9
from AbstractBaseUi import AbstractBaseUi


class UiImpl2(AbstractBaseUi):
    #  interface implementation [InterfaceUi]
    def do_something_in_ui_layer(self: AbstractBaseUi, x: int, y: int) -> int:
        x -= 1
        y -= 1
        return self.métier.do_something_in_métier_layer(x, y)
  • السطر 4: الفئة [UiImpl2] مشتقة من الفئة [AbstractBaseUi] وبالتالي ترث خاصية [business] الخاصة بها. يتم استخدام هذه الخاصية في السطر 9؛

14.4.4. ملفات التكوين

Image

  • تقوم ملفات [config1، config2] بتكوين التطبيق بطريقتين مختلفتين؛
  • ملف [main] هو البرنامج النصي الرئيسي للتطبيق؛

ملف [config1] هو كما يلي:

def configure():
    #  step 1 ------
    #  absolute path of this script's folder
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    #  dependencies
    absolute_dependencies = [
        #  local Python Path folders
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    #  configure the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application layer configuration
    from DaoImpl1 import DaoImpl1
    from MétierImpl1 import MétierImpl1
    from UiImpl1 import UiImpl1
    #  layer instantiation
    #  dao
    dao = DaoImpl1()
    #  business
    métier = MétierImpl1()
    métier.dao = dao
    #  ui
    ui = UiImpl1()
    ui.métier = métier

    #  put the layer instances in the config
    #  only the ui layer is required here
    config = {"ui": ui}

    #  return the config
    return config
  • الأسطر 2–16: تكوين مسار Python للتطبيق؛
  • الأسطر 18–31: إنشاء مثيلات لطبقات [DAO، business، UI]. لتنفيذ واجهاتها، نختار في كل مرة أول تنفيذ مدمج؛
  • الأسطر 33–35: نضيف مراجع الطبقات إلى التكوين. هنا، لا يحتاج البرنامج النصي الرئيسي سوى إلى طبقة [ui]؛

ملف [config2] مشابه ويقوم بتنفيذ كل واجهة باستخدام التنفيذ الثاني المتاح:

def configure():
    #  step 1 ---
    #  absolute path of this script's folder
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    #  dependencies
    absolute_dependencies = [
        #  local Python Path folders
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    #  configure the syspath
    from myutils import set_syspath

    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application layer configuration
    from DaoImpl2 import DaoImpl2
    from MétierImpl2 import MétierImpl2
    from UiImpl2 import UiImpl2
    #  layer instantiation
    #  dao
    dao = DaoImpl2()
    #  business
    métier = MétierImpl2()
    métier.dao = dao
    #  ui
    ui = UiImpl2()
    ui.métier = métier

    #  put the layer instances in the config
    #  only the ui layer is required here
    config = {"ui": ui}

    #  return the config
    return config

14.4.5. البرنامج النصي الرئيسي [main]

Image

النص البرمجي الرئيسي هو كما يلي:

#  imports
import importlib
import sys

#  hand ---------

#  you need two arguments
nb_args = len(sys.argv)
if nb_args != 2 or (sys.argv[1] != "config1" and sys.argv[1] != "config2"):
    print(f"Syntaxe : {sys.argv[0]} config1 ou config2")
    sys.exit()

#  application configuration
module = importlib.import_module(sys.argv[1])
config = module.configure()

#  execution of the [ui] layer
print(config["ui"].do_something_in_ui_layer(10, 20))

يأخذ هذا البرنامج النصي معلمة واحدة:

  • [config1] لاستخدام التكوين رقم 1؛
  • [config2] لاستخدام التكوين رقم 2؛

يخزن Python المعلمات في قائمة [sys.argv]:

  • sys.argv[0] هو اسم البرنامج النصي، وهنا [main]. هذه المعلمة موجودة دائمًا؛
  • sys.argv[1] هو المعلمة الأولى التي يتم تمريرها إلى البرنامج النصي، و sys.argv[2] هي الثانية، ...
  • السطر 8: نسترد عدد المعلمات؛
  • الأسطر 9–11: نتحقق من وجود حجة بالفعل وأن قيمتها هي إما [config1] أو [config2]. إذا لم يكن الأمر كذلك، يتم عرض رسالة خطأ (السطر 10) ونخرج من البرنامج (السطر 11)؛

بمجرد معرفة التكوين المطلوب، نحتاج إلى تنفيذ هذا التكوين. على سبيل المثال، إذا تم اختيار التكوين 1، نحتاج إلى تنفيذ الكود:

import config1
config1.configure()

المشكلة هنا هي أن التكوين المراد استخدامه مخزن في متغير، وهو [sys.argv[1]. لاستيراد وحدة نمطية اسمها مخزن في متغير، نحتاج إلى استخدام حزمة [importlib] (السطر 2).

  • السطر 14: نستورد الوحدة النمطية التي يوجد اسمها في [sys.argv[1]
  • السطر 15: بمجرد الانتهاء من ذلك، نقوم بتنفيذ دالة [configure] الخاصة بهذا الوحدة النمطية. نسترد قاموس [config] الذي يمثل تكوين التطبيق؛
  • السطر 18: نعلم أن الإشارة إلى طبقة [ui] موجودة في config['ui']. نستخدمها لاستدعاء طريقة [do_something_in_ui_layer]. نعلم أن هذه الطريقة ستستدعي طريقة في طبقة [business]، والتي بدورها ستستدعي طريقة في طبقة [dao]؛

على سبيل المثال، تكون الدالة [do_something_in_ui_layer] كما يلي:

1
2
3
4
5
6
class UiImpl1(AbstractBaseUi):
    #  interface implementation [InterfaceUi]
    def do_something_in_ui_layer(self: AbstractBaseUi, x: int, y: int) -> int:
        x += 1
        y += 1
        return self.métier.do_something_in_métier_layer(x, y)
  • يستخدم السطر 6 أعلاه الخاصية [business] لفئة [UiImpl1]، السطر 1. ومع ذلك، في التكوين [config1]، تمت كتابة ما يلي:

# métier
    métier = MétierImpl1()
    métier.dao = dao
    # ui
    ui = UiImpl1()
    ui.métier = métier
  • السطر 6: الخاصية [business] في [UIImpl1] هي مرجع إلى فئة [BusinessImpl1] (السطر 2). وبالتالي، سيتم تنفيذ الأسلوب [do_something_in_ui_layer] الخاص بفئة [BusinessImpl1]؛

في فئة [MétierUiImpl1]، مكتوب:

1
2
3
4
5
6
class MétierImpl1(AbstractBaseMétier):
    #  interface implementation [InterfaceMétier]
    def do_something_in_métier_layer(self: AbstractBaseMétier, x: int, y: int) -> int:
        x += 1
        y += 1
        return self.dao.do_something_in_dao_layer(x, y)
  • السطر 6: الطريقة التي تستدعيها طبقة [ui] تستدعي بدورها طريقة للخاصية [dao] في فئة [BusinessImpl1]؛

ومع ذلك، في التكوين [config1]، تمت كتابة ما يلي:


# dao
    dao = DaoImpl1()
    # métier
    métier = MétierImpl1()
    métier.dao = dao
  • السطر 5: الخاصية [BusinessImpl1.dao] من النوع [DaoImpl1] (السطر 2)؛

ما نريد إظهاره هنا هو أن البرنامج النصي [main] لا يحتاج إلى الاهتمام بطبقتي [business] و [DAO]. بل يحتاج فقط إلى الاهتمام بطبقة [UI]، حيث تم إنشاء الاتصالات بين هذه الطبقة والطبقات الأخرى من خلال التكوين.

Image

لتمرير المعلمة [config1] أو [config2] إلى البرنامج النصي [main]، اتبع الخطوات التالية:

Image

  • في [1-2]، قم بإنشاء ما يُسمى تكوين وقت التشغيل؛
  • في [3]، قم بتسمية هذا التكوين حتى تتمكن من العثور عليه لاحقًا؛
  • في [4]، حدد البرنامج النصي المراد تشغيله. إذا اتبعت الإجراء الموضح في [1-2]، فسيكون البرنامج النصي الصحيح قد تم تحديده بالفعل؛
  • في [5]، أدخل المعلمات المراد تمريرها إلى البرنامج النصي هنا. هنا، نمرر السلسلة [config1] لإرشاد البرنامج النصي لاستخدام التكوين رقم 1؛
  • في [6]، قم بتأكيد تكوين التنفيذ؛

Image

  • في [1-2]، اعرض سياقات التنفيذ الحالية؛
  • في [3]، حدد سياق التنفيذ الموجود وقم بنسخه [4]؛

Image

  • في [5]، الاسم الممنوح للتكوين الجديد. هذا هو التكوين الذي يقوم بتشغيل البرنامج النصي [main] [6] عن طريق تمرير المعلمة [config2] إليه [7]؛

تتوفر تكوينات التنفيذ في الزاوية العلوية اليمنى من نافذة PyCharm:

Image

ما عليك سوى اختيار [2] أو [3] ثم النقر على [4] لتشغيل البرنامج النصي [main] باستخدام المعلمة [config1] أو [config2].

مع [config1]، يؤدي تشغيل [main] إلى النتائج التالية:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v02/main/main.py config1
34
 
Process finished with exit code 0

مع [config2]، يؤدي تشغيل [main] إلى النتائج التالية:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v02/main/main.py config2
-10
 
Process finished with exit code 0

ندعو القارئ إلى التحقق من هذه النتائج.