Skip to content

14. معماری لایه‌ای و برنامه‌نویسی مبتنی بر رابط

14.1. مقدمه

ما پیشنهاد می‌کنیم برنامه‌ای بنویسیم که نمرات دانش‌آموزان یک دبیرستان را نمایش دهد. این برنامه ممکن است معماری چندلایه داشته باشد:

Image

  • لایه [ui] (رابط کاربری) لایه‌ای است که با کاربر برنامه تعامل دارد؛
  • لایه [métier] قواعد کسب‌وکار برنامه را پیاده‌سازی می‌کند، مانند محاسبه حقوق یا فاکتور. این لایه داده‌ها را از کاربر از طریق لایه [présentation] و از لایه SGBD از طریق لایه [dao] استفاده می‌کند؛
  • لایه [dao] (ابجکت‌های دسترسی به داده) دسترسی به داده‌ها را از لایه SGBD (سیستم مدیریت پایگاه داده) مدیریت می‌کند.

این معماری است که در |دوره پایتون 2| استفاده شد. می‌توان یک واریانت نیز معرفی کرد:

Image

تفاوت‌ها در مقایسه با ساختار لایه‌ای قبلی به شرح زیر است:

  • یک اسکریپت اصلی، که در بالا با نام [main] ذکر شده است، ایجاد اشیاء لایه‌ها را سازماندهی می‌کند؛
  • لایه‌های [ui, métier, dao] دیگر لزوماً با یکدیگر ارتباط برقرار نمی‌کنند. اگر به این کار نیاز داشته باشند، اسکریپت [main] مراجع لایه‌هایی را که به آنها نیاز دارند، در اختیارشان قرار می‌دهد؛

کد در اینجا به واحدهای عملکردی سازماندهی شده است، که هر کدام یک «راهنما» دارند:

  • «رَهنما» اسکریپت اصلی [main] است؛
  • لایه‌های [ui]، [dao] و [métier] مراکز تخصصی هستند؛

این ساختار را می‌توان به‌عنوان یک سازمان «ارکسترال» توصیف کرد.

14.2. مثال ۱

ما معماری لایه‌ای را با استفاده از یک برنامه کنسول ساده نشان خواهیم داد:

  • هیچ پایگاه‌داده وجود نخواهد داشت؛
  • لایه [dao]، اشیاء Elève، Classe، Matière و Note را مدیریت خواهد کرد و امکان مدیریت نمرات دانش‌آموزان را فراهم می‌آورد؛
  • لایه [métier] برای محاسبه معیارها بر اساس نمرات یک شاگرد خاص استفاده خواهد شد؛
  • لایه [ui] یک برنامه کنسول خواهد بود که نتایج دانش‌آموزان را نمایش می‌دهد؛

پروژه PyCharm برای این برنامه به شرح زیر است:

توجه: پوشه‌های آبی بخشی از [Sources Root] در پروژه PyCharm هستند.

14.2.1. اشیاء برنامه

ما کلاس‌هایی را که تنها نقششان دربرگیری داده‌ها است، «اشیاء» می‌نامیم. می‌توان از دیکشنری‌ها برای این منظور استفاده کرد. هدف از این کلاس، امکان‌پذیر ساختن بررسی اعتبار داده‌های ذخیره‌شده در شیء و فراهم کردن متدی است که هویت شیء را به صورت یک رشته بازمی‌گرداند.

14.2.1.1. اِنتیتی [Classe]

اِنتیت [Classe] (Classe.py) یک کلاس مدرسه‌ای را نشان می‌دهد:


#وارداتی
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Classe(BaseEntity):
    # ویژگی‌های حذف‌شده از وضعیت کلاس
    excluded_keys = []

    # ویژگی‌های کلاس
    @staticmethod
    def get_allowed_keys() -> list:
        # id: شناسه‌ی کلاس
        # name: نام کلاس
        return BaseEntity.get_allowed_keys() + ["nom"]

    # گیرنده
    @property
    def nom(self: object) -> str:
        return self.__nom

    # setterها
    @nom.setter
    def nom(self: object, nom: str):
        #نام باید یک رشته غیرخالی باشد
        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")

یادداشت‌ها

  • خط ۷: عنصر [Classe] از عنصر [BaseEntity] که در بخش |کلاس BaseEntity| مورد بحث قرار گرفته است، مشتق شده است؛
  • خطوط ۱۱–۱۶: یک کلاس با یک شماره id و یک nom (خط ۱۶) تعریف می‌شود. ویژگی [id] توسط کلاس [BaseEntity] و نام توسط کلاس [Classe] ارائه می‌شود؛
  • خطوط ۱۸–۳۰: گتر/ستتر برای ویژگی [nom];

14.2.1.2. اِنتیتای [Matière]

کلاس [Matière] (matière.py) به شرح زیر است:


# واردات
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Matière(BaseEntity):
    # ویژگی‌های مستثنی‌شده از وضعیت کلاس
    excluded_keys = []

    # ویژگی‌های کلاس
    @staticmethod
    def get_allowed_keys() -> list:
        # id: شناسه‌ی موضوع
        # name: نام موضوع
        # وزن‌دهی: وزن‌دهی موضوع
        return BaseEntity.get_allowed_keys() + ["nom", "coefficient"]

    # گیرنده
    @property
    def nom(self: object) -> str:
        return self.__nom

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

    # مقداردهی‌کننده‌ها
    @nom.setter
    def nom(self: object, nom: str):
        # نام باید یک رشته غیرخالی باشد
        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):
        #ضریب باید یک عدد حقیقی ≥ 0 باشد
        erreur = False
        if isinstance(coefficient, (int, float)):
            if coefficient >= 0:
                self.__coefficient = coefficient
            else:
                erreur = True
        else:
            erreur = True
        # خطا؟
        if erreur:
            raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")

یادداشت‌ها

  • خط ۷: کلاس [Classe] از کلاس [BaseEntity] مشتق شده است؛
  • خطوط ۱۱–۱۷: یک موضوع با شماره [id]، نام [nom] و وزن‌دهی [coefficient] تعریف می‌شود؛
  • خطوط ۱۹–۵۰: گترها و سترها برای ویژگی‌های کلاس؛

14.2.1.3. اِنتیته [Elève]

کلاس [Elève] (élève.py) به شرح زیر است:


# واردات‌ها
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException

from Utils import Utils


class Elève(BaseEntity):
    # ویژگی‌های مستثنی‌شده از وضعیت کلاس
    excluded_keys = []

    # ویژگی‌های کلاس
    @staticmethod
    def get_allowed_keys() -> list:
        # id: شناسهٔ شاگرد
        # نام خانوادگی: نام خانوادگی شاگرد
        # نام: نام شاگرد
        # کلاس: کلاس شاگرد
        return BaseEntity.get_allowed_keys() + ["nom", "prénom", "classe"]

    # گیرنده‌ها
    @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

    # setورها
    @nom.setter
    def nom(self: object, nom: str) -> str:
        # نام خانوادگی باید یک رشته غیرخالی باشد
        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:
        #نام باید یک رشته غیرخالی باشد
        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:
            #انتظار یک نوع Class را دارد
            if isinstance(value, Classe):
                self.__classe = value
            # یا نوع `dict`
            elif isinstance(value,dict):
                self.__classe=Classe().fromdict(value)
            # یا نوع `json`
            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}")

یادداشت‌ها

  • خط ۹: کلاس [Elève] از کلاس [BaseEntity] مشتق شده است؛
  • خطوط ۱۳–۲۰: یک دانش‌آموز با شماره [id]، نام خانوادگی [nom]، نام [prénom] و کلاس [classe] خود شناسایی می‌شود. این پارامتر آخر، مرجعی به یک شیء [Classe] است؛
  • خطوط ۲۲–۶۵: گترها و سترها برای ویژگی‌های کلاس؛

14.2.1.4. اِنتیت [Note]

کلاس [Note] (note.py) به شرح زیر است:


# واردات‌ها
from BaseEntity import BaseEntity
from Elève import Elève
from Matière import Matière
from MyException import MyException


class Note(BaseEntity):
    # ویژگی‌هایی که از وضعیت کلاس حذف شده‌اند
    excluded_keys = []

    # ویژگی‌های کلاس
    @staticmethod
    def get_allowed_keys() -> list:
        # id: شناسه یادداشت
        # value: خود نمره
        # دانشجو: دانشجو (از نوع Student) که نمره به او مربوط می‌شود
        # موضوع: موضوع (از نوع Subject) که نمره به آن مربوط می‌شود
        # بنابراین شیء «نمره» نمرهٔ یک شاگرد در یک درس است
        return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]

    # گیرنده‌ها
    @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

    # گیرنده‌ها
    @valeur.setter
    def valeur(self: object, valeur: float):
        #نمره باید یک عدد حقیقی بین ۰ و ۲۰ باشد
        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:
            # نوع مورد انتظار 'Student' است
            if isinstance(value, Elève):
                self.__élève = value
            # یا نوع 'dict'
            elif isinstance(value, dict):
                self.__élève = Elève().fromdict(value)
            # یا نوع 'json'
            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:
            # نوع مورد انتظار 'Subject' است
            if isinstance(value, Matière):
                self.__matière = value
            # یا نوع `dict`
            elif isinstance(value, dict):
                self.__matière = Matière().fromdict(value)
            # یا نوع `json`
            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}")

یادداشت‌ها

  • خط ۸: کلاس [Note] از کلاس [BaseEntity] ارث می‌برد؛
  • خطوط ۱۲–۲۰: یک شیء [Note] با شناسه (ID) خود [id] مشخص می‌شود، مقدار نمره [valeur]، یک مرجع [élève] به دانش‌آموزی که این نمره را دارد، و یک مرجع به موضوع [matière] که نمره به آن مربوط می‌شود؛
  • خطوط ۲۲–۷۵: گترها و سترها برای ویژگی‌های کلاس؛

14.2.2. پیکربندی برنامه

فایل [config.py] محیط را برای اسکریپت اصلی [main] (۱) و همچنین برای تست‌ها (۲) پیکربندی می‌کند. تمام این اسکریپت‌ها در ابتدای کد حاوی یک دستور [import config] هستند. توجه داشته باشید که پوشه‌ای که حاوی اسکریپت هدف‌گیری‌شده توسط دستور [python script] است، به طور خودکار بخشی از محیط پایتون Path.Si می‌شود؛ بنابراین، از آنجایی که [config] در همان پوشه‌ای قرار دارد که اسکریپت‌های حاوی دستور [import config] در آن هستند، پیدا خواهد شد. فایل‌های [1] و [2] در اینجا یکسان هستند. این ممکن است همیشه صادق نباشد.

فایل [config.sys] به شرح زیر است:


def configure():
    import os

    #مسیر مطلق پوشهٔ این اسکریپت
    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=[
        # پوشه‌های محلی حاوی کلاس‌ها و رابط‌ها
        f"{root_dir}/02/entities",
        f"{script_dir}/../entities",
        f"{script_dir}/../interfaces",
        f"{script_dir}/../services",
    ]

    # به‌روزرسانی syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # پیکربندی اعمال می‌شود
    return {}
  • خطوط ۱۱–۱۴: دایرکتوری‌هایی که باید در مسیر پایتون (Python Path) گنجانده شوند (sys.path);
  • پوشه [f"{root_dir}/02/entities"] دسترسی به کلاس‌های [BaseEntity] و [MyException] را فراهم می‌کند؛
  • پوشه [f"{script_dir}/../entities"] دسترسی به کلاس‌های [Elève]، [Classe]، [Matière] و [Note] را فراهم می‌کند؛
  • پوشه [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

#پیکربندی برنامه
import config

config = config.configure()


class TestBaseEntity (unittest.TestCase):

    def test_note1(self):
        # واردات‌ها
        from Note import Note
        from Elève import Elève
        from Classe import Classe
        from Matière import Matière
        #ساخت یک علامت از یک رشته jSON
        note = Note().fromjson(
            '{"id": 8, "value": 12, "student": {"id": 42, "surname": "surname4", "first name": "first name4", "class": {"id": 2, "name": "class2"}}, "subject": {"id": 2, "name": "subject2", "weighting": 2}}')
        # بررسی‌ها
        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):
        # واردات
        from Note import Note
        from Elève import Elève
        from Classe import Classe
        from Matière import Matière
        #ساخت یک نشان از یک فرهنگ لغت
        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}})
        # بررسی‌ها
        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()

یادداشت‌ها

  • خط ۱: ماژول [unittest] وارد می‌شود که متدهای تست مختلف را فراهم می‌کند؛
  • خطوط ۳–۶: برنامه به گونه‌ای پیکربندی شده است که کلاس‌های مورد نیاز برای تست قابل دسترس باشند؛
  • خط ۹: یک کلاس تست [unittest] باید از کلاس [unittest.TestCase] ارث ببرد؛
  • خطوط ۱۱ و ۲۷: توابع تست باید نامی داشته باشند که با [test] شروع شود، در غیر این صورت شناسایی نخواهند شد؛
  • خطوط ۱۳–۱۶: کلاس‌های مورد نیاز وارد می‌شوند؛
  • در این کلاس تست، می‌خواهیم رفتار متدهای [BaseEntity.fromdict] (خط ۳۴) و [BaseEntity.fromjson] (خط ۱۸) را بررسی کنیم. کلاس [Note] دارای خواصی است که ارجاع به کلاس‌های دیگر هستند. ما می‌خواهیم تأیید کنیم که دو متد مذکور اشیاء معتبر [Note] را ایجاد می‌کنند؛
  • خط ۱۸: ما یک شیء [Note] را از یک شیء jSON ایجاد می‌کنیم؛
  • خط ۲۱: ما تأیید می‌کنیم که شیء ایجادشده واقعاً از نوع [Note] است. متد [assertIsInstance] یک متد از کلاس [unittest.TestCase] است، کلاس والد کلاس [TestBaseEntity]؛
  • خط ۲۲: بررسی می‌کنیم که [note.élève] واقعاً از نوع [Elève] است؛
  • خط ۲۳: بررسی می‌شود که [note.élève.classe] در واقع از نوع [Classe] است؛
  • خط ۲۴: بررسی می‌کنیم که [note.matière] واقعاً از نوع [Matière] است؛
  • خطوط ۳۳–۴۲: همین کار را برای متد [BaseEntity.fromdict] انجام می‌دهیم؛

چندین روش برای اجرای تست‌ها وجود دارد:

  • در [1-2]، ما [TestBaseEntity] را با استفاده از چارچوب [UnitTest] اجرا می‌کنیم؛
  • در [3-5]، تست‌ها ناموفق هستند. [UnitTests] نشان می‌دهد که هیچ تستی برای اجرا پیدا نکرده است؛

تست‌ها به دلیل ساختار کد در [TestBaseEntity] شکست می‌خورند:


import unittest

# پیکربندی برنامه
import config

config = config.configure()


class TestBaseEntity(unittest.TestCase):

آنچه باعث شکست چارچوب [UnitTest] می‌شود، وجود کد قابل اجرا در خطوط ۳ تا ۶ قبل از تعریف کلاس تست در خط ۹ است.

بنابراین کد را به شکل زیر بازسازی می‌کنیم:


import unittest


class TestBaseEntity(unittest.TestCase):

    def setUp(self):
        # پیکربندی برنامه
        import config

        config.configure()

    def test_note1(self):
        

    def test_note2(self):
        


if __name__ == '__main__':
    unittest.main()
  • خطوط ۶–۱۰: تابع [setUp] تعریف شده است. این تابع نقش مشخصی دارد: قبل از هر تابع آزمون (test_note1, test_note2) اجرا می‌شود؛

پس از انجام این کار، اجرای کلاس [TestBaseEntity] نتایج زیر را تولید می‌کند:

این بار، هر دو روش تست اجرا شدند و تست‌ها با موفقیت به پایان رسیدند.

بیایید ببینیم وقتی یک تست شکست می‌خورد چه اتفاقی می‌افتد. بیایید کد [test_note1] را به شرح زیر تغییر دهیم:


    def test_note1(self):
        # خطای عمدی – بررسی اینکه 1==2
        self.assertEqual(1,2)
        # واردات‌ها
        from Note import Note

  • خط ۲: بررسی می‌کنیم که 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

خط ۶ نشان می‌دهد که هر دو تست با موفقیت انجام شده‌اند (خطای 1==2 حذف شده است)؛

در نهایت، روش سوم برای اجرای کلاس تست [TestBaseEntity]، باز هم در یک ترمینال، به شرح زیر است. ما کلاس تست را با خطوط ۶–۷ زیر به پایان می‌رسانیم؛



        self.assertIsInstance(note.élève.classe, Classe)
        self.assertIsInstance(note.matière, Matière)


if __name__ == '__main__':
    unittest.main()
  • خط ۶: متغیر [__name__] نام اسکریپتی است که اجرا می‌شود. وقتی اسکریپت توسط فرمان [python script.py] اجرا می‌شود، متغیر [__name__] روی [__main__] (با دو خط تیره قبل و بعد از شناسه) تنظیم می‌شود. بنابراین، خط ۷ تنها زمانی اجرا می‌شود که اسکریپت [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. کلاس تست [TestEntités]

کلاس تست [TestEntités] به شرح زیر است:


import unittest


class TestEntités(unittest.TestCase):
    def setUp(self):
        # در حال پیکربندی برنامه
        import config

        config.configure()

    def test_code1a(self):
        # واردات
        from Elève import Elève
        from MyException import MyException
        # کد خطا
        code = None
        try:
            # شناسه نامعتبر
            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
        # تأیید
        self.assertEqual(code, 1)

    def test_code41(self):
        # واردات
        from Elève import Elève
        from MyException import MyException
        # کد خطا
        code = None

        try:
            # نام نامعتبر
            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
        # تأیید
        self.assertEqual(code, 41)

    def test_code42(self):
        # واردات
        from Elève import Elève
        from MyException import MyException
        # کد خطا
        code = None
        try:
            # نام اول نامعتبر
            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
        # تأیید
        self.assertEqual(code, 42)

    def test_code43(self):
        # واردات
        from Elève import Elève
        from MyException import MyException
        # کد خطا
        code = None
        try:
            # کلاس نامعتبر
            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
        # اعتبارسنجی
        self.assertEqual(code, 43)

    def test_code1b(self):
        # واردات
        from Classe import Classe
        from MyException import MyException
        # کد خطا
        code = None
        try:
            # شناسه نامعتبر
            Classe().fromdict({"id": "x", "nom": "y"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # تأیید
        self.assertEqual(code, 1)

    def test_code11(self):
        # واردات
        from Classe import Classe
        from MyException import MyException

        # کد خطا
        code = None
        try:
            # نام نامعتبر
            Classe().fromdict({"id": 1, "nom": ""})
        except MyException as ex:
            code = ex.code
        # تأیید
        self.assertEqual(code, 11)

    def test_code1c(self):
        # واردات
        from Matière import Matière
        from MyException import MyException

        # کد خطا
        code = None
        try:
            # شناسه نامعتبر
            Matière().fromdict({"id": "x", "nom": "y", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # تأیید
        self.assertEqual(code, 1)

    def test_code21(self):
        # واردات
        from Matière import Matière
        from MyException import MyException
        # کد خطا
        code = None
        try:
            # نام نامعتبر
            Matière().fromdict({"id": "1", "nom": "", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # تأیید
        self.assertEqual(code, 21)

    def test_code22(self):
        # واردات
        from Matière import Matière
        from MyException import MyException
        # کد خطا
        code = None
        try:
            #ضریب نامعتبر
            Matière().fromdict({"id": 1, "nom": "y", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # تأیید
        self.assertEqual(code, 22)

    def test_code1d(self):
        # واردات
        from Note import Note
        from MyException import MyException
        # کد خطا
        code = None
        try:
            # شناسه نامعتبر
            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
        # تأیید
        self.assertEqual(code, 1)

    def test_code31(self):
        # واردات
        from Note import Note
        from MyException import MyException

        # کد خطا
        code = None
        try:
            # مقدار نامعتبر
            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
        #اعتبارسنجی
        self.assertEqual(code, 31)

    def test_code32(self):
        # واردات
        from Note import Note
        from MyException import MyException

        # کد خطا
        code = None
        try:
            # دانشجوی نامعتبر
            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
        # تأیید
        self.assertEqual(code, 32)

    def test_code33(self):
        # واردات
        from Elève import Elève
        from Note import Note
        from Classe import Classe
        from MyException import MyException

        # کد خطا
        code = None
        try:
            # موضوع نامعتبر
            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
        # تأیید
        self.assertEqual(code, 33)

    def test_exception(self):
        # واردات‌ها
        from Elève import Elève
        # آزمون باید اجرا شود تا با تایپ [MyException] پاس شود
        from MyException import MyException
        with self.assertRaises(MyException):
            # آزمون
            Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})


if __name__ == '__main__':
    unittest.main()
  • هدف اسکریپت تست، آزمایش متدهای تنظیم‌کننده کلاس‌ها است: برای تأیید اینکه مقادیر نادرست را نمی‌توان به ویژگی‌های موجودیت‌های مختلف اختصاص داد؛
  • خطوط ۱۱–۲۴: ما بررسی می‌کنیم که یک شناسه نامعتبر نمی‌تواند به یک دانش‌آموز اختصاص داده شود. از آنجا که در خط ۱۶ مقدار «x» را به عنوان شناسه دانش‌آموز ارسال می‌کنیم، انتظار داریم یک استثنا رخ دهد. بنابراین باید به خطوط ۲۰–۲۲ برویم؛
  • خط ۲۱: نمایش پیام خطا؛
  • خط ۲۲: کد خطا بازیابی می‌شود (به بخش |وجودیت MyException| مراجعه کنید)؛
  • خط ۲۴: ما بررسی می‌کنیم (تأیید می‌کنیم) که کد خطا ۱ است. در اینجا، ما دو چیز را بررسی می‌کنیم:
    • که واقعاً خطایی رخ داده است؛
    • اینکه کد خطا ۱ است؛
  • این فرآیند با توابع در خطوط 24–213 تکرار می‌شود؛
  • خطوط 215–222: ما بررسی می‌کنیم که یک عمل یک استثنا از نوع خاصی را پرتاب می‌کند؛
  • خط ۲۲۰: نشان می‌دهیم که اگر تست یک استثنا از نوع [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_matières] فهرستی از موضوعات تدریس‌شده در دبیرستان را بازمی‌گرداند؛
  • [get_élèves] فهرستی از دانش‌آموزان دبیرستان را بازمی‌گرداند؛
  • [get_notes] فهرستی از نمرات همهٔ دانش‌آموزان را بازمی‌گرداند؛
  • [get_notes_for_élève_by_id] نمرات یک دانش‌آموز مشخص را بازمی‌گرداند؛
  • [get_élève_by_id] دانش‌آموز را با شمارهٔ او شناسایی می‌کند؛

کد فراخوانی فقط از این متدها استفاده خواهد کرد. نیازی نیست بداند که چگونه پیاده‌سازی شده‌اند. بنابراین، داده‌ها می‌توانند از منابع مختلفی (کد سخت، از پایگاه داده، از فایل‌های متنی و غیره) بدون تأثیر بر کد فراخوانی، تأمین شوند. این روش به برنامه‌نویسی مبتنی بر رابط (interface-based programming) معروف است.

پایتون ۳ مفهومی مشابه با اینترفیس دارد: کلاس انتزاعی. ما از این کلاس استفاده خواهیم کرد. برای این مثال، اینترفیس‌ها را در پوشه [interfaces] گروه‌بندی می‌کنیم.

ما یک کلاس انتزاعی [InterfaceDao] (InterfaceDao.py) برای لایه [dao] تعریف می‌کنیم:


#وارداتی
from abc import ABC, abstractmethod

#رابط Dao
from Elève import Elève


class InterfaceDao(ABC):
    # فهرست کلاس‌ها
    @abstractmethod
    def get_classes(self: object) -> list:
        pass

    # فهرست دانش‌آموزان
    @abstractmethod
    def get_élèves(self: object) -> list:
        pass

    # فهرست دروس
    @abstractmethod
    def get_matières(self: object) -> list:
        pass

    # فهرست نمرات
    @abstractmethod
    def get_notes(self: object) -> list:
        pass

    # فهرست نمرات یک شاگرد
    @abstractmethod
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        pass

    #جستجوی دانش‌آموز بر اساس شناسه
    @abstractmethod
    def get_élève_by_id(self, élève_id: int) -> Elève:
        pass

یادداشت‌ها:

  • خط ۲: ABC = کلاس پایه انتزاعی. کلاس ABC و دکوراتور [abstractmethod] که در خطوط ۱۰، ۱۵، ۲۰، ۲۵، ۳۰ و ۳۵ استفاده شده‌اند، از ماژول [abc] وارد شده‌اند؛
  • خط ۸: کلاس انتزاعی [InterfaceDao] نامیده می‌شود و از کلاس [ABC] ارث می‌برد؛
  • متدهای کلاس انتزاعی با دکوراتور [@abstractmethod] تزئین شده‌اند، که باعث می‌شود متد تزئین‌شده به یک متد انتزاعی تبدیل شود: کد آن تعریف نشده است. با این حال، کدی در آنجا گنجانده شده است: عبارت [pass]، که هیچ کاری انجام نمی‌دهد؛
  • کلاس انتزاعی [InterfaceDao] قابل نمونه‌سازی نیست. تنها کلاس‌هایی که از [InterfaceDao] مشتق شده‌اند و تمام متدهای [InterfaceDao] را پیاده‌سازی کرده‌اند، قابل نمونه‌سازی هستند. بنابراین، اگر دو کلاس [Dao1] و [Dao2] را مشتق از کلاس [InterfaceDao] ایجاد کنیم، هر دو متدهای انتزاعی [InterfaceDao] را پیاده‌سازی خواهند کرد. بنابراین می‌توان گفت که آن‌ها رابط [InterfaceDao] را پیاده‌سازی می‌کنند؛
  • زبان‌هایی که هر دو رابط و کلاس‌های انتزاعی را پیاده‌سازی می‌کنند، نقشی متفاوت برای رابط نسبت به کلاس انتزاعی در نظر می‌گیرند. یک رابط هیچ صفتی ندارد و نمی‌توان آن را نمونه‌سازی کرد. یک کلاس می‌تواند با تعریف تمام متدهای آن، یک رابط را پیاده‌سازی کند؛

14.2.4.2. پیاده‌سازی [Dao]

کلاس [Dao] (dao.py) رابط [InterfaceDao] را به شرح زیر پیاده‌سازی می‌کند:


#وارد کردن اشیاء و رابط‌ها
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] رابط InterfaceDao را پیاده‌سازی می‌کند
class Dao(InterfaceDao):
    # سازنده
    # فهرست‌های کدگذاری‌شدهٔ سخت‌کد ساخته می‌شوند
    def __init__(self):
        #کلاس‌ها نمونه سازی می‌شوند
        classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
        classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
        self.classes = [classe1, classe2]
        # موضوعات
        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]
        # دانش‌آموزان
        é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]
        #نمرات شاگردان در موضوعات مختلف
        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]

    # -----------
    #رابط IDao
    # -----------
    

یادداشت‌ها:

  • خطوط ۱–۷: اشیاء و رابط [InterfaceDao] وارد شده‌اند؛
  • خط ۱۱: کلاس [Dao] از کلاس انتزاعی [InterfaceDao] ارث می‌برد. ما خواهیم گفت که این کلاس رابط [InterfaceDao] را پیاده‌سازی می‌کند؛
  • خط 14: سازنده هیچ پارامتری ندارد. آن چهار لیست را به‌صورت ثابت کد می‌کند:
    • خطوط ۱۵–۱۸: لیست کلاس‌ها؛
    • خطوط ۱۹–۲۲: لیست موضوعات؛
    • خطوط ۲۳–۲۸: فهرست دانش‌آموزان؛
    • خطوط ۲۹–۳۸: لیست نمرات؛
  • خطوط ۴۰–۴۴: پیاده‌سازی متدهای رابط [Interface Dao]. در اینجا، ما آن‌ها را تعریف نمی‌کنیم تا بتوانیم پیام خطای بازگردانده شده توسط پایتون را ببینیم؛

یک برنامهٔ آزمایشی ممکن است به این شکل باشد: [tests-dao.py]:


# راه‌اندازی برنامه
import config

config = config.configure()

# آشکارسازی لایه [dao]
from Dao import Dao

daoImpl = Dao()

# فهرست کلاس‌ها
for classe in daoImpl.get_classes():
    print(classe)

# فهرست موضوعات
for matière in daoImpl.get_matières():
    print(matière)

# فهرست کلاس‌ها
for élève in daoImpl.get_élèves():
    print(élève)

# فهرست نمرات
for note in daoImpl.get_notes():
    print(note)

توجه: اسکریپت [tests-dao.py] یک تست برای [unittest] نیست، زیرا هیچ متدی که نامش با [test_] شروع شود را در خود ندارد.

توضیحات به‌خودی‌خود گویا هستند. خطوط ۱۱ تا ۲۵ از رابط لایه [dao] استفاده می‌کنند. در اینجا هیچ فرضی در مورد پیاده‌سازی واقعی لایه مطرح نشده است. در خط ۹، لایه [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] (خط ۳ بالا)، یک خطا رخ می‌دهد. تفسیرگر پایتون ۳ به ما می‌گوید که نمی‌تواند این کلاس را نمونه‌سازی کند، زیرا ما متدهای انتزاعی [get_classes, get_matières, get_notes, get_notes_for_élève_by_id, get_élève_by_id, get_élèves] را تعریف نکرده‌ایم.

PyCharm همچنین مفهوم کلاس‌های انتزاعی را تشخیص می‌دهد و پیشنهاد می‌کند که متدهای این کلاس را تعریف کنیم:

  • در [1]، روی کد کلیک راست کنید؛
  • در [2-3]، [Generate / Implement Methods] را برای پیاده‌سازی متدهای از دست رفته کلاس [Dao] انتخاب کنید؛
  • در [4]، متدهایی را که باید پیاده‌سازی شوند انتخاب کنید – در این مورد، همه آنها؛

پس از انجام این کار، کلاس [Dao] به شرح زیر با PyCharm تکمیل می‌شود:


    # -----------
    #رابط 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] را به شرح زیر تکمیل می‌کنیم:


    # -----------
    # IDao رابط
    # -----------
    
    # فهرست کلاس‌ها
    def get_classes(self) -> list:
        return self.classes

    # فهرست موضوعات
    def get_matières(self) -> list:
        return self.matières

    # فهرست دانش‌آموزان
    def get_élèves(self) -> list:
        return self.élèves

    # فهرست نمرات
    def get_notes(self) -> list:
        return self.notes

    def get_notes_for_élève_by_id(self, élève_id: int) -> dict:
        #جستجوی دانش‌آموز
        élève = self.get_élève_by_id(élève_id)
        # نمره‌هایشان را بازیابی کنید
        notes = list(filter(lambda n: n.élève.id == élève_id, self.get_notes()))
        # نتیجه را بازگردانید
        return {"élève": élève, "notes": notes}

    def get_élève_by_id(self, élève_id: int) -> Elève:
        # فیلتر کردن دانش‌آموزان
        élèves = list(filter(lambda e: e.id == élève_id, self.get_élèves()))
        # یافت شد؟
        if not élèves:
            raise MyException(10, f"L'élève d'identifiant {élève_id} n'existe pas")
        # نتیجه
        return élèves[0]
  • خطوط ۵ تا ۱۹ هیچ دشواری ندارند؛
  • خطوط ۲۹–۳۶: متدی که دانشجویی را بازمی‌گرداند که شماره‌اش ارسال شده است. اگر دانشجو وجود نداشته باشد، یک استثنا پرتاب می‌شود؛
  • خط ۳۱: تابع [filter] امکان فیلتر کردن یک لیست را فراهم می‌کند:
    • پارامتر اول معیار فیلتر است؛
    • پارامتر دوم فهرستی است که باید فیلتر شود، در این مورد فهرست دانش‌آموزان؛
  • خط ۳۱: معیار فیلتر برای لیست با استفاده از تابع [f(e :Elève)->bool] پیاده‌سازی شده است. این معیار بر روی هر عنصر در لیست مورد نظر اعمال می‌شود. اگر عنصر معیار فیلتر را برآورده کند، در لیست فیلتر شده نگه داشته می‌شود؛ در غیر این صورت، حذف می‌شود. در اینجا، می‌توان یکی از دو کار زیر را انجام داد:
    • نام تابع f را مشخص کرده و آن را در جای دیگری پیاده‌سازی کنید. در این صورت، فراخوانی تابع [filter] به [filter(f,self.get_élèves()] تبدیل می‌شود؛
    • تعریف تابع f را ارائه دهید. در این صورت، فراخوانی تابع [filter] به [filter(f(e :Elève){…},self.get_élèves()] تبدیل می‌شود، که در آن [e] نمایانگر یک عنصر از لیست فیلتر شده، یعنی یک دانش‌آموز است. این کاری است که در اینجا انجام شده است. تعریف تابع f در اینجا [f(e :Elève){return e.id==élève_id)] خواهد بود: یک شاگرد تنها در صورتی انتخاب می‌شود که عدد [id] با عددی که جستجو می‌شود برابر نباشد. چنین تابعی را می‌توان با تابعی به نام لامبدا جایگزین کرد: [lambda e: e.id == élève_id]:
      • e: نشان‌دهنده پارامتر تابع f است، که در این مورد یک شاگرد است. می‌توان از هر نامی استفاده کرد؛
      • e.id==élève_id معیار فیلتر کردن است: یک دانش‌آموز [e] تنها در صورتی انتخاب می‌شود که عدد [id] او با عددی که جستجو می‌شود مطابقت داشته باشد؛
  • خط ۳۱: تابع [filter] لیست فیلتر شده را به عنوان نوعی بازمی‌گرداند که از نوع [list] نیست اما می‌تواند به نوع [list] تبدیل شود. این کاری است که ما در اینجا با عبارت [list(liste filtrée)] انجام می‌دهیم؛
  • خطوط ۳۳–۳۴: اگر لیست فیلتر شده خالی باشد، یعنی دانش‌آموز مورد جستجو وجود ندارد. در این صورت یک استثنا ایجاد می‌شود؛
  • خط ۳۶: اگر به این نقطه برسیم، یعنی هیچ استثنایی پرتاب نشده است. در این صورت می‌دانیم که یک لیست شامل یک عنصر واحد را بازیابی کرده‌ایم (هیچ دو دانش‌آموزی با شماره [id] یکسان وجود ندارند). بنابراین عنصر اول لیست را برمی‌گردانیم؛
  • خطوط ۲۱–۲۷: متد [get_notes_for_élève_by_id] باید نمرات دانش‌آموزی را که شناسه او [id] به آن ارسال شده، بازگرداند؛
  • خطوط ۲۲–۲۳: ما با جستجو برای دانش‌آموز با شناسه [élève_id] با استفاده از متد [get_élève_by_id] که همین‌الان آن را کامنت کرده‌ایم، شروع می‌کنیم. در صورتی که دانش‌آموز مورد جستجو وجود نداشته باشد، ممکن است یک استثنا پرتاب شود. از آنجا که هیچ بلوک try/catch در اطراف دستور در خط ۲۳ وجود ندارد، استثنا به کد فراخوانی منتقل خواهد شد. این رفتار مورد نظر است؛
  • خطوط ۲۴–۲۵: پس از بازیابی دانش‌آموز، تمام نمرات او را بازیابی می‌کنیم. این کار را دوباره با استفاده از یک فیلتر انجام می‌دهیم:
    • معیار فیلتر [filter(critère, self_getnotes()] است. بنابراین، فهرستی که باید فیلتر شود، فهرست تمام نمرات همه دانش‌آموزان مدرسه است؛
    • معیار فیلترسازی با استفاده از یک تابع [lambda] بیان می‌شود: lambda n: n.élève.id == élève_id. پارامتر n یک عنصر از لیست مورد نظر برای فیلتر شدن است و بنابراین یک نمره است. نوع [Note] دارای خاصیتی به نام [élève] است که شاگردی را که نمره به او تعلق دارد، نشان می‌دهد. بنابراین [n.élève.id که شماره آن شاگرد را نشان می‌دهد، باید با شماره شاگرد مورد جستجو برابر باشد؛

سپس اسکریپت [tests-dao.py] را اجرا می‌کنیم.


# راه‌اندازی برنامه
import config

config = config.configure()

#لایه [dao] را نمونه سازی می‌کند
from Dao import Dao

daoImpl = Dao()

# فهرست کلاس‌ها
for classe in daoImpl.get_classes():
    print(classe)

# فهرست موضوعات
for matière in daoImpl.get_matières():
    print(matière)

# فهرست کلاس‌ها
for élève in daoImpl.get_élèves():
    print(élève)

# فهرست نمرات
for note in daoImpl.get_notes():
    print(note)

# یک دانش‌آموز خاص
print(daoImpl.get_élève_by_id(11))

# فهرست نمرات آنها
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. لایه [métier]

  • [InterfaceMétier] رابط لایه [métier] است؛
  • [Métier] کلاس پیاده‌سازی برای لایه [métier] است؛
  • [Testmétier] یک کلاس تست برای کلاس [Métier] است؛

14.2.5.1. رابط [InterfaceMétier]

لایه [métier] رابط زیر [InterfaceMétier] (InterfaceMétier.py) را پیاده‌سازی خواهد کرد:


# واردات‌ها
from abc import ABC, abstractmethod

from StatsForElève import StatsForElève


#رابط تجاری
class InterfaceMétier(ABC):
    # محاسبه آمار برای یک شاگرد
    @abstractmethod
    def get_stats_for_élève(self, idElève: int) -> StatsForElève:
        pass
  • [get_stats_for_élève] نمرات دانشجوی شماره idElève را به همراه اطلاعاتی دربارهٔ آن‌ها: میانگین وزنی، کمترین نمره، بیشترین نمره بازمی‌گرداند. این اطلاعات در یک شیء از نوع [StatsForElève] جای داده شده است؛

14.2.5.2. این موجودیت [StatsForElève]

نوع [StatsForElève] (StatsForElève.py)، که آمار یک دانش‌آموز (نمرات، حداقل، حداکثر، میانگین وزنی) را در خود جای می‌دهد، به شرح زیر است:


# واردات‌ها
from BaseEntity import BaseEntity


#آمار برای یک دانش‌آموز خاص


class StatsForElève(BaseEntity):
    #ویژگی‌های حذف‌شده از گزارش کلاس
    excluded_keys = []

    # ویژگی‌های کلاس
    @staticmethod
    def get_allowed_keys() -> list:
        # id: شناسه نمره
        # دانش‌آموز: دانش‌آموز مورد نظر
        # نمرات: نمرات آنها
        # moyennePondérée: میانگین وزنی آن‌ها، بر اساس ضرایب موضوع
        # min: پایین‌ترین نمرهٔ آن‌ها
        # max: حداکثر نمره او

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

    # toString
    def __str__(self) -> str:
        # مورد شاگرد بدون نمره
        if len(self.notes) == 0:
            return f"Elève={self.élève}, notes=[]"
        # در مورد دانش‌آموزی با نمرات
        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}"

یادداشت‌ها:

  • خط ۸: کلاس [StatsForElève] از کلاس [BaseEntity] ارث می‌برد؛
  • خطوط ۱۳–۲۲: ویژگی‌های کلاس؛
    • یک شناسگر [id] مشتق شده از [BaseEntity]؛
    • دانشجو [élève که آمارهایش در خود نهفته است؛
    • نمرات آن‌ها [notes];
    • میانگین وزنی آن‌ها [moyenne_pondérée];
    • حداقل امتیاز آن [min];
    • حداکثر امتیاز آن [max];
  • هیچ گتر یا ستر برای این ویژگی‌ها تعریف نشده است. فرض بر این است که لایه [métier] اشیایی از این نوع ایجاد می‌کند و اشیای نامعتبر ایجاد نمی‌کند؛
  • خطوط ۲۳–۳۳: تابع [__str__] رشته‌ای را بازمی‌گرداند که حاوی ویژگی‌های شیء است؛

14.2.5.3. پیاده‌سازی [Métier]

پیاده‌سازی [Métier] (Metier.py) از رابط [InterfaceMétier] به شرح زیر خواهد بود:


# واردات
from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier
from StatsForElève import StatsForElève


class Métier(InterfaceMétier):

    # سازنده
    def __init__(self, dao: InterfaceDao):
        #پارامتر ذخیره می‌شود
        self.__dao = dao

    # -----------
    #رابط
    # -----------

    #شاخص‌های مربوط به نمرات یک دانش‌آموز خاص
    def get_stats_for_élève(self, id_élève: int) -> StatsForElève:
        #آمار برای دانش‌آموز شماره idEleve
        # id_élève: شماره شاگرد

        #نمرات آن‌ها با استفاده از لایه [dao] بازیابی می‌شوند
        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"]

        # اگر نمره‌ای وجود نداشته باشد، فرایند متوقف می‌شود
        if len(notes) == 0:
            # نتیجه را بازگردانید
            return StatsForElève().fromdict({"élève": élève, "notes": []})

        #پردازش نمرات دانشجو
        somme_pondérée = 0
        somme_coeff = 0
        max = -1
        min = 21
        for note in notes:
            # مقدار نمره
            valeur = note.valeur
            #وزن‌دهی به موضوعات
            coeff = note.matière.coefficient
            # جمع وزن‌ها
            somme_coeff += coeff
            # جمع وزنی
            somme_pondérée += valeur * coeff
            # یافتن حداقل
            if valeur < min:
                min = valeur
            # یافتن حداکثر
            if valeur > max:
                max = valeur
        #محاسبه شاخص‌های گمشده
        moyenne_pondérée = float(somme_pondérée) / somme_coeff

        # نتیجه به صورت نوع [StatsForElève] بازگردانده می‌شود
        return StatsForElève(). \
            fromdict({"élève": élève, "notes": notes,
                      "moyenne_pondérée": moyenne_pondérée,
                      "min": min, "max": max})

یادداشت‌ها

  • خط ۷: کلاس [Métier] از کلاس [InterfaceMétier] ارث می‌برد. معمولاً گفته می‌شود که این کلاس، رابط [InterfaceMétier] را پیاده‌سازی می‌کند؛
  • خطوط ۹–۱۲: سازنده یک پارامتر واحد می‌گیرد، یک مرجع به لایه [dao]. در خط ۱۰، توجه کنید که به پارامتر [dao] نوع [InterfaceDao] داده شده است. ما انتظار یک پیاده‌سازی خاص را نداریم، بلکه صرفاً پیاده‌سازی‌ای که به رابط [InterfaceDao] پایبند باشد. در این مورد، این موضوع اهمیتی ندارد، زیرا پایتون این نوع را نادیده می‌گیرد، اما کار با رابط‌ها به جای پیاده‌سازی‌های خاص، یک روش کار خوب است. در این صورت، تغییر کد آسان‌تر می‌شود؛
  • خطوط ۱۹–۶۰: پیاده‌سازی متد [get_stats_for_élève]؛
  • خط ۱۹: متد یک پارامتر واحد می‌گیرد، شناسه دانشجوی [idElève] که برای او می‌خواهیم آمار را مشاهده کنیم؛
  • خط ۲۴: از لایه [dao] برای دریافت نمرات دانشجو استعلام گرفته می‌شود. این استعلام در صورتی که دانشجو وجود نداشته باشد، منجر به یک استثنا می‌شود. این استثنا مدیریت نمی‌شود (بدون بلاک try/catch) و بنابراین به کد فراخوانی بازگردانده می‌شود؛
  • خط ۲۵: اگر هیچ استثنایی رخ نداده باشد، به این نقطه می‌رسیم. در این صورت [notes_élève] یک دیکشنری با دو کلید است، [élève, note]:
    • خط ۲۵: جزئیات شاگرد را بازیابی می‌کنیم (نام، کلاس و غیره)؛
    • خط ۲۶: نمرات او را بازیابی می‌کنیم؛
  • خطوط ۲۸–۳۱: بررسی می‌کنیم که آیا شاگرد نمره‌ای دارد یا خیر. اگر نداشته باشد، آماری برای محاسبه وجود ندارد؛
  • خط ۳۱: ما یک شیء [StatsForElève] را که با استفاده از متد [BaseEntity.fromdict] از یک دیکشنری ساخته شده است، بازمی‌گردانیم؛
  • خطوط ۳۳–۵۴: نمرات دانش‌آموز برای محاسبه آمارهای درخواستی استفاده می‌شوند. توضیحات کد باید برای درک این موضوع کافی باشد؛
  • خطوط ۵۶–۶۰: یک شیء [StatsForElève] بازگردانده می‌شود که از یک دیکشنری با استفاده از متد [BaseEntity.fromdict] ساخته شده است؛

14.2.5.4. آزمایش لایه [métier]

یک اسکریپت [UnitTest] از لایه [métier] می‌تواند به صورت زیر باشد (TestMétier.py):


# واردات‌ها
import unittest


class Testmétier(unittest.TestCase):
    def setUp(self):
        #پیکربندی برنامه
        import config
        config.configure()

    def test_statsForEleve11(self):
        # واردات
        from Dao import Dao
        from Métier import Métier
        # شاخص‌های شاگرد آزمایشی ۱۱
        dao = Dao()
        stats_for_élève = Métier(dao).get_stats_for_élève(11)
        # نمایش
        print(f"\nstats={stats_for_élève}")
        #بررسی‌ها
        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()

یادداشت‌ها

  • خطوط ۶–۹: تابع [setUp] در اینجا برای پیکربندی مسیر پایتون برای تست استفاده می‌شود؛
  • خط ۱۶: لایه [dao] نمونه برداری می‌شود؛
  • خط ۱۷: لایه [métier] نمونه برداری شده و از متد [get_stats_for_élève] آن برای محاسبه آمار برای دانش‌آموز شماره ۱۱ استفاده می‌شود؛
  • خط ۱۹: نتیجه [StatsForElève] نمایش داده می‌شود. از آنجایی که [StatsForElève] از [BaseEntity] مشتق شده است، در اینجا رشته jSON از [StatsForElève] نمایش داده می‌شود؛
  • خط ۲۱: حداقل نمره شاگرد بررسی می‌شود؛
  • خط ۲۲: حداکثر نمره دانش‌آموز بررسی می‌شود؛
  • خط ۲۳: بررسی می‌کنیم که میانگین وزنی برابر با ۷.۳۳۳ است، با دقت تا ۱۰⁻³. به طور کلی، مقایسه دقیق اعداد حقیقی امکان‌پذیر نیست، زیرا آن‌ها معمولاً به صورت تقریبی نمایش داده می‌شوند؛

نتایج آزمون به شرح زیر است:


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] به شرح زیر خواهد بود:


# واردات
from abc import ABC, abstractmethod


# رابط UI
class InterfaceUi(ABC):
    #اجرای لایه UI
    @abstractmethod
    def run(self: object):
        pass

یادداشت‌ها

  • خطوط ۹–۱۰: لایه [UI] تنها یک متد خواهد داشت، [run];

14.2.6.2. پیاده‌سازی [Console]

لایه [console] توسط اسکریپت زیر [Console.py] پیاده‌سازی شده است:


# واردات لایه

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

# وابستگی‌های دیگر
from MyException import MyException


class Console(InterfaceUi):
    # سازنده
    def __init__(self: object, métier: InterfaceMétier):
        #منطق کسب‌وکار: لایه [métier]

        #ویژگی‌ها ذخیره می‌شوند
        self.métier = métier


        # -----------
        #رابط
        # -----------

    def run(self):
        # گفتگوی کاربر
        fini = False
        while not fini:
            # سوال / پاسخ
            réponse = input("Numéro de l'élève (>=1 et * pour arrêter) : ").strip()
            # انجام شد؟
            if réponse == "*":
                break
            #آیا ورودی صحیح است؟
            ok = False
            try:
                id_élève = int(réponse, 10)
                ok = id_élève >= 1
            except ValueError as erreur:
                pass
            #آیا داده‌ها صحیح هستند؟
            if not ok:
                print("Saisie incorrecte. Recommencez...")
                continue
            # محاسبه آمار برای شاگرد انتخاب‌شده
            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}")
  • خطوط ۳–۵: وارد کردن تمام رابط‌ها؛
  • خط ۱۱: کلاس [Console] رابط [InterfaceUi] را پیاده‌سازی می‌کند؛
  • خطوط ۱۲–۱۷: سازنده کلاس [Console] یک مرجع به لایه [métier] را به‌عنوان پارامتر می‌پذیرد. توجه داشته باشید که این پارامتر از نوع [InterfaceMétier] در نظر گرفته شده است تا تأکید شود که ما با رابط‌ها کار می‌کنیم، نه با پیاده‌سازی‌های خاص؛
  • خط 24: پیاده‌سازی متد [run] اینفترفیس؛
  • خط ۲۷: یک حلقه که وقتی شرط خط ۳۱ برقرار شود، پایان می‌یابد؛
  • خط ۲۹: ورودی داده‌های تایپ‌شده با صفحه‌کلید. تابع [input] یک پارامتر اختیاری می‌گیرد: پیامی که باید برای درخواست ورودی روی صفحه نمایش داده شود. این ورودی همیشه به صورت یک رشته دریافت می‌شود. تابع [strip] هرگونه «فضاهای خالی» قبل یا بعد از ورودی را حذف می‌کند؛
  • خطوط 34–39: ما بررسی می‌کنیم که ورودی—شماره دانشجویی—معتبر است. این عدد باید یک عدد صحیح بزرگ‌تر یا مساوی با 1 باشد. به یاد داشته باشید که ورودی به صورت یک رشته وارد شده است؛
  • خط ۳۶: ما سعی می‌کنیم ورودی را به یک عدد صحیح مبنای ۱۰ تبدیل کنیم. تابع [int] اگر این کار امکان‌پذیر نباشد، یک استثنا (exception) ایجاد می‌کند؛
  • خط ۳۷: ما تنها در صورتی به این نقطه می‌رسیم که هیچ استثنایی پرتاب نشده باشد. ما بررسی می‌کنیم که عدد صحیح بازیابی‌شده واقعاً >=1 است؛
  • خطوط ۳۸–۳۹: استثنا مدیریت می‌شود. اگر استثنایی رخ داده باشد، متغیر [ok] از خط ۳۴ همچنان روی [False] تنظیم باقی می‌ماند؛
  • خطوط ۴۱–۴۳: اگر ورودی نادرست باشد، یک پیام خطا نمایش داده می‌شود و حلقه مجدداً اجرا می‌شود (خط ۴۳)؛
  • خطوط ۴۵–۴۸: آمار برای شاگردی که شماره‌اش وارد شده است، محاسبه می‌شود؛
  • خط ۴۶: از متد [get_stats_for_élève] در لایه [métier] استفاده می‌شود. این متد در صورتی که دانش‌آموز وجود نداشته باشد، یک استثنا ایجاد می‌کند. این مورد در خطوط ۴۷–۴۸ مدیریت می‌شود. ما می‌دانیم که لایه‌های [dao] و [métier] استثنای [MyException] را ایجاد می‌کنند؛

14.3. اسکریپت اصلی [main]

اسکریپت اصلی [main] به شرح زیر است (main.py):


# راه‌اندازی برنامه
import config

config = config.configure()

#مسیر سیستم پیکربندی شده است – اکنون می‌توانیم واردات را انجام دهیم
from Console import Console
from Dao import Dao
from Métier import Métier

# ----------- لایه [console]
try:
    # نمونه سازی لایه [dao]
    dao = Dao()
    #نمونه‌سازی لایه [métier]
    métier = Métier(dao)
    # نمونه سازی لایه [ui]
    console = Console(métier)
    #اجرای لایه [console]
    console.run()
except BaseException as ex:
    # خطا نمایش داده می‌شود
    print(f"L'erreur suivante s'est produite : {ex}")
finally:
    pass
  • خطوط ۱–۴: پیکربندی مسیر پایتون برنامه؛
  • خطوط ۶–۹: وارد کردن کلاس‌ها و رابط‌های مورد نیاز؛
  • خط ۱۴: نمونه‌سازی لایه [dao]؛
  • خط ۱۶: ایجاد لایه [métier
  • خط ۱۸: ایجاد لایه [ui]؛
  • خط ۲۰: آغاز گفتگو با کاربر؛
  • خطوط ۱۳–۲۰: معمولاً هیچ استثنایی توسط این خطوط پرتاب نمی‌شود. هر استثنایی که توسط لایه‌های [dao] و [métier] پرتاب شود، توسط لایه [Console] گرفته می‌شود. مدیریت استثناءها کار دشواری است وقتی درک کاملی از لایه‌هایی که استفاده می‌شوند وجود ندارد (که در اینجا همین‌طور است). در صورت تردید، می‌توان کدی اضافه کرد تا هر نوع استثناء‌ای را که ممکن است توسط کد در حال اجرا پرتاب شود، بگیرد. این کاری است که در اینجا انجام شده است، خطوط ۲۱–۲۳. ما هر استثنایی را که از [BaseException] مشتق شده باشد، یعنی تمام استثناها را، می‌گیریم؛
  • خطوط ۲۴–۲۵: عبارت [finally] در اینجا کاری انجام نمی‌دهد. این عبارت فقط برای این وجود دارد که خطوط ۲۱–۲۳ را بتوان به صورت تفسیرشده (commented out) درآورد. در واقع، در حالت اشکال‌زدایی (debug)، توصیه نمی‌شود که استثناها را گرفت. در این حالت، این تفسیرگر پایتون است که آن‌ها را می‌گیرد و سپس شماره خطی را که خطا در آن رخ داده است گزارش می‌کند. این اطلاعات ضروری است. هنگامی که خطوط ۲۱ تا ۲۳ کامنت می‌شوند، وجود خطوط ۲۴ تا ۲۵ تضمین می‌کند که بلوک try/catch از نظر نحوی صحیح است. بدون آن‌ها، پایتون یک خطا ایجاد می‌کند؛

در اینجا مثالی از اجرای برنامه آورده شده است:


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. مثال ۲

این مثال جدید از معماری‌های لایه‌ای با هدف نشان دادن مزایای برنامه‌نویسی مبتنی بر رابط است. این رویکرد نگهداری و تست برنامه‌ها را تسهیل می‌کند. ما بار دیگر از یک معماری سه‌لایه استفاده خواهیم کرد:

Image

هر لایه به دو روش متفاوت پیاده‌سازی خواهد شد. ما می‌خواهیم نشان دهیم که می‌توان پیاده‌سازی یک لایه را به راحتی و با کمترین تأثیر بر لایه‌های دیگر تغییر داد.

14.4.1. لایه [dao]

Image

رابط [InterfaceDao] به شرح زیر است:


# واردات
from abc import ABC, abstractmethod


#رابط DAO
class InterfaceDao(ABC):
    #یک متد واحد
    @abstractmethod
    def do_something_in_dao_layer(self, x: int, y: int) -> int:
        pass
  • خطوط ۸–۱۰: متد [do_something_in_dao_layer] تنها متد در این رابط است؛

کلاس [DaoImpl1] رابط [InterfaceDao] را به شرح زیر پیاده‌سازی می‌کند:


from InterfaceDao import InterfaceDao


class DaoImpl1(InterfaceDao):
    #پیاده‌سازی InterfaceDao
    def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
        return x + y

کلاس [DaoImpl2] رابط [InterfaceDao] را به شرح زیر پیاده‌سازی می‌کند:


from InterfaceDao import InterfaceDao


class DaoImpl2(InterfaceDao):
    #پیاده‌سازی InterfaceDao
    def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
        return x - y

14.4.2. لایه [métier]

Image

رابط [InterfaceMétier] به شرح زیر است:


# واردات
from abc import ABC, abstractmethod


#رابط کسب‌وکار
class InterfaceMétier(ABC):
    #یک متد واحد
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • خطوط ۸–۱۰: متد [do_something_in_métier_layer] تنها متد در این رابط است؛

کلاس [AbstractBaseMétier] رابط [InterfaceMétier] را به شرح زیر پیاده‌سازی می‌کند:


# واردات
from abc import ABC, abstractmethod

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


class AbstractBaseMétier(InterfaceMétier, ABC):
    # ویژگی‌ها
    # __dao یک مرجع در لایه [dao] است
    @property
    def dao(self) -> InterfaceDao:
        return self.__dao

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

    #پیاده‌سازی رابط [InterfaceMétier]
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • خط ۸: کلاس [AbstractBaseMétier] از دو کلاس ارث می‌برد:
    • [InterfaceMétier]: کلاس [AbstractBaseMétier] این رابط را در خطوط ۱۹–۲۲ پیاده‌سازی می‌کند. در واقع، می‌توانیم ببینیم که متد [do_something_in_métier_layer] را که به صورت انتزاعی (abstract) اعلام کرده است (خط ۲۰)، پیاده‌سازی نکرده است. پیاده‌سازی این متد بر عهده کلاس‌های مشتق خواهد بود؛
    • [ABC] به منظور دسترسی به حاشیه‌نویسی‌های [@abstractmethod]؛
    • ترتیب اهمیت دارد: اگر آن را در اینجا معکوس کنیم، پایتون یک خطای زمان اجرا ایجاد می‌کند؛

این اولین باری است که از ارث‌بری چندگانه (ارث‌بری از چندین کلاس) استفاده می‌کنیم. کلاس [AbstractBaseMétier] ویژگی‌ها را از هر دو کلاس [InterfaceMétier] و [ABC] به ارث می‌برد.

  • خطوط ۹–۱۷: ما ویژگی [dao] را تعریف می‌کنیم که مرجعی به لایه [dao] خواهد بود؛

یک رابط قرار است پیاده‌سازی شود. وقتی پیاده‌سازی‌های مختلف ویژگی‌ها را به اشتراک می‌گذارند، مفید است که این ویژگی‌ها را در یک کلاس والد قرار دهیم تا از تکرار جلوگیری شود. این مورد در اینجا برای ویژگی [dao] صدق می‌کند. کلاس والد معمولاً همیشه انتزاعی است زیرا همه متدهای رابط را پیاده‌سازی نمی‌کند.

کلاس [MétierImpl1] رابط [InterfaceMétier] را به شرح زیر پیاده‌سازی می‌کند:


from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl1(AbstractBaseMétier):
    #پیاده‌سازی رابط [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)
  • خط ۴: کلاس [MétierImpl1] از کلاس [AbstractbaseMétier] ارث می‌برد. بنابراین، این کلاس خاصیت [dao] را از آن کلاس به ارث می‌برد؛
  • خطوط ۶–۹: پیاده‌سازی رابط [InterfaceMétier] که کلاس والد [AbstractbaseMétier] آن را پیاده‌سازی نکرده است؛
  • خط ۹: لایه [dao] استفاده می‌شود؛

کلاس [MétierImpl2] رابط [InterfaceMétier] را به روشی مشابه پیاده‌سازی می‌کند:


from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl2(AbstractBaseMétier):
    #پیاده‌سازی رابط [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] به شرح زیر است:


#وارداتی
from abc import ABC, abstractmethod


#رابط کاربری
class InterfaceUi(ABC):
    #یک متد واحد
    @abstractmethod
    def do_something_in_ui_layer(self, x: int, y: int) -> int:
        pass
  • خطوط ۸–۱۰: تنها متد اینفترفیس؛

کلاس [AbstractBaseUi] رابط [InterfaceUi] را به شرح زیر پیاده‌سازی می‌کند:


# واردات‌ها
from abc import ABC, abstractmethod

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


class AbstractBaseUi(InterfaceUi, ABC):
    # ویژگی‌ها
    #منطق کسب‌وکار مرجعی بر روی لایه [métier] است
    @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

    #پیاده‌سازی رابط [InterfaceUI]
    @abstractmethod
    def do_something_in_ui_layer(self: InterfaceUi, x: int, y: int) -> int:
        pass
  • کلاس [AbstractBaseUi] یک کلاس انتزاعی است (خط ۲۰). برای پیاده‌سازی رابط [InterfaceUi] باید از آن ارث‌بری شود؛
  • خطوط ۹–۱۷: کلاس [AbstractBaseUi] دارای مرجعی به لایه [métier] است؛

کلاس پیاده‌سازی [UiImpl1] به شرح زیر است:


from AbstractBaseUi import AbstractBaseUi


class UiImpl1(AbstractBaseUi):
    #پیاده‌سازی رابط [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)
  • خط ۴: کلاس [UiImpl1] از کلاس [AbstractBaseUi] ارث می‌برد و بنابراین ویژگی [métier] آن را به ارث می‌برد. این مورد در خط ۹ استفاده شده است؛

کلاس پیاده‌سازی [UiImpl2] مشابه است:


from AbstractBaseUi import AbstractBaseUi


class UiImpl2(AbstractBaseUi):
    #پیاده‌سازی رابط [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)
  • خط ۴: کلاس [UiImpl2] از کلاس [AbstractBaseUi] مشتق شده و در نتیجه ویژگی [métier] را به ارث می‌برد. این ویژگی در خط ۹ استفاده شده است؛

14.4.4. فایل‌های پیکربندی

Image

  • فایل‌های [config1, config2] برنامه را به دو روش مختلف پیکربندی می‌کنند؛
  • فایل [main] اسکریپت اصلی برنامه است؛

فایل [config1] به شرح زیر است:


def configure():
    # مرحله ۱ ------
    # مسیر مطلق پوشه‌ای که این اسکریپت را در خود دارد
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # وابستگی‌ها
    absolute_dependencies = [
        # زیرپوشه‌های محلی در مسیر پایتون
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    #پیکربندی syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # مرحله ۲ ------
    #پیکربندی لایه‌های برنامه
    from DaoImpl1 import DaoImpl1
    from MétierImpl1 import MétierImpl1
    from UiImpl1 import UiImpl1
    # مثال‌زدن لایه‌ها
    #DAO
    dao = DaoImpl1()
    # منطق کسب‌وکار
    métier = MétierImpl1()
    métier.dao = dao
    # رابط کاربری
    ui = UiImpl1()
    ui.métier = métier

    # ما نمونه‌های لایه‌ها را در پیکربندی قرار می‌دهیم
    # در اینجا تنها لایهٔ رابط کاربری مورد نیاز است
    config = {"ui": ui}

    # ما پیکربندی را نهایی می‌کنیم
    return config
  • خطوط ۲–۱۶: پیکربندی مسیر پایتون برنامه؛
  • خطوط ۱۸–۳۱: نمونه‌سازی لایه‌های [dao, métier, ui]. برای پیاده‌سازی رابط‌های آن‌ها، اولین پیاده‌سازی ایجادشده هر بار انتخاب می‌شود؛
  • خطوط ۳۳–۳۵: ارجاعات لایه‌ها به پیکربندی اضافه می‌شوند. در اینجا، اسکریپت اصلی تنها به لایه [ui] نیاز دارد؛

فایل [config2] مشابه است و هر رابط را با استفاده از دومین پیاده‌سازی موجود پیاده‌سازی می‌کند:


def configure():
    # مرحلهٔ ۱ ---
    # مسیر مطلق پوشه‌ای که این اسکریپت را در خود دارد
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    #وابستگی‌ها
    absolute_dependencies = [
        # زیرپوشه‌های محلی در مسیر پایتون
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    # تنظیم syspath
    from myutils import set_syspath

    set_syspath(absolute_dependencies)

    # مرحله ۲ ------
    # پیکربندی لایه‌های برنامه
    from DaoImpl2 import DaoImpl2
    from MétierImpl2 import MétierImpl2
    from UiImpl2 import UiImpl2
    # مثال‌زدن لایه‌ها
    #DAO
    dao = DaoImpl2()
    # منطق کسب‌وکار
    métier = MétierImpl2()
    métier.dao = dao
    # رابط کاربری
    ui = UiImpl2()
    ui.métier = métier

    # ما نمونه‌های لایه‌ها را در پیکربندی قرار می‌دهیم
    # در اینجا تنها لایهٔ رابط کاربری مورد نیاز است
    config = {"ui": ui}

    # ما پیکربندی را رندر می‌کنیم
    return config

14.4.5. اسکریپت اصلی [main]

Image

اسکریپت اصلی به شرح زیر است:


# واردات
import importlib
import sys

# main ---------

# دو آرگومان لازم است
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()

#پیکربندی برنامه
module = importlib.import_module(sys.argv[1])
config = module.configure()

#اجرای لایه [ui]
print(config["ui"].do_something_in_ui_layer(10, 20))

این اسکریپت یک پارامتر دارد:

  • [config1] برای استفاده از پیکربندی شمارهٔ ۱؛
  • [config2] برای استفاده از پیکربندی شمارهٔ ۲؛

پایتون پارامترها را در یک لیست به نام [sys.argv] ذخیره می‌کند:

  • sys.argv[0] نام اسکریپت است، در این مورد [main]. این پارامتر همیشه موجود است؛
  • sys.argv[1] اولین پارامتر ارسال‌شده به اسکریپت است، sys.argv[2] دومین، …

  • خط ۸: تعداد پارامترها را بازیابی کنید؛

  • خطوط ۹–۱۱: بررسی می‌کنیم که واقعاً یک پارامتر وجود دارد و مقدار آن یا [config1] است یا [config2]. اگر اینطور نباشد، یک پیام خطا نمایش داده می‌شود (خط ۱۰) و برنامه خاتمه می‌یابد (خط ۱۱)؛

پس از مشخص شدن پیکربندی مورد نظر، باید آن پیکربندی را اجرا کنیم. برای مثال، اگر پیکربندی ۱ انتخاب شده باشد، باید کد زیر را اجرا کنیم:

import config1
config1.configure()

مشکل اینجا این است که پیکربندی مورد استفاده در یک متغیر به نام [len[liste1]] ذخیره شده است. برای وارد کردن ماژولی که نامش در یک متغیر ذخیره شده است، باید از بسته [importlib] (خط ۲) استفاده کنیم.

  • خط 14: ما ماژولی را وارد می‌کنیم که نامش در [sys.argv[1];
  • خط ۱۵: پس از انجام این کار، تابع [configure] را از این ماژول فراخوانی می‌کنیم. ما یک فرهنگ لغت [config] را بازیابی می‌کنیم که پیکربندی برنامه است؛
  • خط ۱۸: می‌دانیم که یک مرجع به لایه [ui] در config[‘ui’] وجود دارد. از این برای فراخوانی متد [do_something_in_ui_layer] استفاده می‌کنیم. می‌دانیم که این متد، متدی را در لایه [métier] فراخوانی خواهد کرد که به نوبه خود متدی را در لایه [dao] فراخوانی می‌کند؛

برای مثال، تابع [do_something_in_ui_layer] به شرح زیر است:


class UiImpl1(AbstractBaseUi):
    #پیاده‌سازی رابط [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)
  • خط ۶ بالا از خاصیت [métier] کلاس [UiImpl1]، خط ۱ استفاده می‌کند. با این حال، در پیکربندی [config1 موارد زیر نوشته شده است:

# منطق کسب‌وکار
    métier = MétierImpl1()
    métier.dao = dao
    #رابط کاربری
    ui = UiImpl1()
    ui.métier = métier
  • خط ۶: ویژگی [métier] از [UIImpl1] یک مرجع به کلاس [MétierImpl1] (خط ۲) است. بنابراین، متد [do_something_in_ui_layer] از کلاس [MétierImpl1] اجرا خواهد شد؛

در کلاس [MétierUiImpl1] آمده است:


class MétierImpl1(AbstractBaseMétier):
    #پیاده‌سازی رابط [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)
  • در خط ۶، متدی که توسط لایه [ui] فراخوانی می‌شود، به نوبه خود یک متد از خاصیت [dao] از کلاس [MétierImpl1] را فراخوانی خواهد کرد؛

با این حال، در پیکربندی [config1]، موارد زیر نوشته شده است:


# DAO
    dao = DaoImpl1()
    # کسب‌وکار
    métier = MétierImpl1()
    métier.dao = dao
  • خط ۵: ویژگی [MétierImpl1.dao] از نوع [DaoImpl1] (خط ۲) است؛

آنچه ما در اینجا سعی داریم نشان دهیم این است که اسکریپت [main] نیازی ندارد که خود را با لایه‌های [métier] و [dao] درگیر کند. این اسکریپت تنها باید به لایه [ui] توجه کند، زیرا ارتباطات بین این لایه و لایه‌های دیگر از طریق پیکربندی برقرار شده است.

Image

برای ارسال پارامتر [config1] یا [config2] به اسکریپت [main به شرح زیر عمل کنید:

Image

  • در [1-2]، آنچه به عنوان پیکربندی اجرا (run configuration) شناخته می‌شود را ایجاد کنید؛
  • در [3]، به این پیکربندی نامی بدهید تا بتوان آن را بازیابی کرد؛
  • در [4]، اسکریپت مورد نظر برای اجرا را انتخاب کنید. اگر مراحل را در [1-2] دنبال کرده باشید، اسکریپت صحیح از قبل انتخاب شده است؛
  • در [5]، اینجا جایی است که پارامترهایی را که باید به اسکریپت ارسال شوند، وارد می‌کنید. در اینجا، رشته [config1] را وارد می‌کنید تا به اسکریپت دستور دهید از پیکربندی شمارهٔ ۱ استفاده کند؛
  • در [6]، پیکربندی اجرای را تأیید می‌کنیم؛

Image

  • در [1-2]، درخواست مشاهدهٔ زمینه‌های اجرای موجود را داریم؛
  • در [3]، زمینه اجرای موجود را انتخاب کرده و آن را به عنوان [4] کپی کنید؛

Image

  • در [5]، نامی که به پیکربندی جدید داده شده است. این همان موردی است که اسکریپت [main] [6] را با ارسال پارامتر [config2] [7] اجرا می‌کند؛

پیکربندی‌های اجرا در گوشه بالا سمت راست پنجره PyCharm در دسترس هستند:

Image

به‌سادگی [2] یا [3] را انتخاب کنید، سپس برای اجرای اسکریپت [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

از خوانندگان دعوت می‌شود این نتایج را تأیید کنند.