Skip to content

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

7.1. مقدمة

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

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

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

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

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

  

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

الكيانات هي كائنات. هنا، سيكون لدينا أربع فئات لكل كيان من الكيانات: الطالب، والصف، والمادة، والدرجة.

يحتوي ملف [entities.py] على أربع فئات.

تمثل فئة [Class] فصلاً دراسيًا في المرحلة الإعدادية:


class Classe:
    # constructeur
    def __init__(self,id,nom):
        # on mémorise les paramètres
        self.id=id
        self.nom=nom
 
    # toString
    def __str__(self):
        return "Classe[{0},{1}]".format(self.id,self.nom)
  • الأسطر 3–6: يتم تعريف الفئة بواسطة معرف (السطر 5) واسم (السطر 6)؛
  • السطور 9-10: طريقة عرض الفئة.

فئة [Subject] هي كما يلي:


class Matiere:
    # constructeur
    def __init__(self,id,nom,coefficient):
        # on mémorise les paramètres
        self.id=id
        self.nom=nom
        self.coefficient=coefficient
 
    # toString
    def __str__(self):
        return "Matiere[{0},{1},{2}]".format(self.id,self.nom,self.coefficient)
 
  • الأسطر 3-7: يتم تعريف الموضوع من خلال معرّفه (السطر 5)، واسمه (السطر 6)، ووزنه (السطر 7)؛
  • السطور 10-11: طريقة عرض الموضوع.

فئة [Student] هي كما يلي:


class Eleve:
    # constructeur
    def __init__(self,id,nom,prenom,classe):
        # on mémorise les paramètres
        self.id=id
        self.nom=nom
        self.prenom=prenom
        self.classe=classe
 
    # toString
    def __str__(self):
        return "Eleve[{0},{1},{2},{3}]".format(self.id,self.prenom,self.nom,self.classe)

  • الأسطر 3–8: يتم تمييز الطالب برقمه التعريفي (السطر 5)، واسم عائلته (السطر 6)، واسمه الأول (السطر 7)، وفصله (السطر 8). هذا المعامل الأخير هو مرجع إلى كائن [Class
  • السطور 11-12: الطريقة الخاصة بعرض الطالب.

فئة [Grade] هي كما يلي:


class Note:
    # constructeur
    def __init__(self,id,valeur,eleve,matiere):
        # on mémorise les paramètres
        self.id=id
        self.valeur=valeur
        self.eleve=eleve
        self.matiere=matiere
 
    # toString
    def __str__(self):
        return "Note[{0},{1},{2},{3}]".format(self.id,self.valeur,self.eleve,self.matiere)
 
  • الأسطر 3-8: يتميز كائن [Grade] برقمه التعريفي (السطر 5)، وقيمة الدرجة (السطر 6)، وإشارة إلى الطالب الذي حصل على هذه الدرجة (السطر 7)، وإشارة إلى المادة التي تم منح الدرجة فيها (السطر 8)؛
  • الأسطر 11-12: طريقة عرض كائن [Note].

7.3. طبقة [dao]

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

  • تُرجع getClasses قائمة بفصول المرحلة الإعدادية؛
  • تُرجع getMatieres قائمة المواد الدراسية؛
  • تُرجع getStudents قائمة الطلاب؛
  • تُرجع getGrades قائمة بالدرجات.

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

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

#    -*- coding=utf-8 -*-

#    import entity module
from entites import *

class Dao:
    #    manufacturer
    def __init__(self):
        #  classes are instantiated
        classe1=Classe(1,"classe1")
        classe2=Classe(2,"classe2")
        self.classes=[classe1,classe2]
        #    materials
        matiere1=Matiere(1,"matiere1",1)
        matiere2=Matiere(2,"matiere2",2)
        self.matieres=[matiere1,matiere2]
        #    students
        eleve11=Eleve(11,"nom1","prenom1",classe1)
        eleve21=Eleve(21,"nom2","prenom2",classe1)
        eleve32=Eleve(32,"nom3","prenom3",classe2)
        eleve42=Eleve(42,"nom4","prenom4",classe2)
        self.eleves=[eleve11,eleve21,eleve32,eleve42]
        #  the notes
        note1=Note(1,10,eleve11,matiere1)
        note2=Note(2,12,eleve21,matiere1)
        note3=Note(3,14,eleve32,matiere1)
        note4=Note(4,16,eleve42,matiere1)
        note5=Note(5,6,eleve11,matiere2)
        note6=Note(6,8,eleve21,matiere2)
        note7=Note(7,10,eleve32,matiere2)
        note8=Note(8,12,eleve42,matiere2)
        self.notes=[note1,note2,note3,note4,note5,note6,note7,note8]

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

    #    class list
    def getClasses(self):
        return self.classes

    #    list of materials
    def getMatieres(self):
        return self.matieres

    #    list of students
    def getEleves(self):
        return self.eleves

    #    lIST OF NOTES
    def getNotes(self):
        return self.notes
  • السطر 4: نستورد الوحدة النمطية التي تحتوي على الكيانات التي تتعامل معها طبقة [DAO
  • السطر 8: لا يحتوي المنشئ على معلمات. وهو يرمز أربع قوائم بشكل ثابت:
    • الأسطر 10–12: قائمة الفئات؛
    • الأسطر 14–16: قائمة المواد؛
    • الأسطر 18-22: قائمة الطلاب؛
    • الأسطر 24-32: قائمة الدرجات.
  • الأسطر 39–52: تعيد الطرق الأربع لواجهة طبقة [dao] ببساطة مرجعًا إلى القوائم الأربع التي أنشأها المنشئ.

قد يبدو برنامج الاختبار كما يلي:

#    -*- coding=utf-8 -*-

#  importing the [dao] entity and layer module
from entites import *
from dao import *

#    layer instantiation [dao]
dao=Dao()

#    class list
for classe in dao.getClasses():
    print classe

#    class list
for matiere in dao.getMatieres():
    print matiere

#    class list
for eleve in dao.getEleves():
    print eleve

#    list of classes
for note in dao.getNotes():
    print note

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

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

Classe[1,classe1]
Classe[2,classe2]
Matiere[1,matiere1,1]
Matiere[2,matiere2,2]
Eleve[11,prenom1,nom1,Classe[1,classe1]]
Eleve[21,prenom2,nom2,Classe[1,classe1]]
Eleve[32,prenom3,nom3,Classe[2,classe2]]
Eleve[42,prenom4,nom4,Classe[2,classe2]]
Note[1,10,Eleve[11,prenom1,nom1,Classe[1,classe1]],Matiere[1,matiere1,1]]
Note[2,12,Eleve[21,prenom2,nom2,Classe[1,classe1]],Matiere[1,matiere1,1]]
Note[3,14,Eleve[32,prenom3,nom3,Classe[2,classe2]],Matiere[1,matiere1,1]]
Note[4,16,Eleve[42,prenom4,nom4,Classe[2,classe2]],Matiere[1,matiere1,1]]
Note[5,6,Eleve[11,prenom1,nom1,Classe[1,classe1]],Matiere[2,matiere2,2]]
Note[6,8,Eleve[21,prenom2,nom2,Classe[1,classe1]],Matiere[2,matiere2,2]]
Note[7,10,Eleve[32,prenom3,nom3,Classe[2,classe2]],Matiere[2,matiere2,2]]
Note[8,12,Eleve[42,prenom4,nom4,Classe[2,classe2]],Matiere[2,matiere2,2]]

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

تنفذ طبقة [الأعمال] الواجهة التالية:

  • تُرجع getClasses قائمة بفصول المرحلة الإعدادية؛
  • تُرجع getMatieres قائمة المواد الدراسية؛
  • تُرجع getStudents قائمة الطلاب؛
  • تُرجع getGrades قائمة بالدرجات؛
  • تُرجع getStatsForStudent الدرجات الخاصة بالطالب idStudent مع معلومات عنها: المتوسط المرجح، وأدنى درجة، وأعلى درجة.

ستستخدم طبقة [العرض] هذه الطرق فقط. ولا تحتاج إلى معرفة كيفية تنفيذها.

تُرجع طريقة getStatsForStudent كائنًا من النوع [StatsForStudent] على النحو التالي:


class StatsForEleve:
    # constructeur
    def __init__(self, eleve, notes):
        # on mémorise les paramètres
        self.eleve=eleve
        self.notes=notes
        # on s'arrête s'il n'y a pas de notes
        if len(notes)==0:
            return
        # exploitation des notes
        sommePonderee=0
        sommeCoeff=0
        self.max=-1
        self.min=21
        for note in notes:
            valeur=note.valeur
            coeff=note.matiere.coefficient
            sommeCoeff+=coeff
            sommePonderee+=valeur*coeff
            if valeur<self.min:
                self.min=valeur
            if valeur>self.max:
                self.max=valeur
        # calcul de la moyenne de l'élève
        self.moyennePonderee=float(sommePonderee)/sommeCoeff
 
    # toString
    def __str__(self):
        # cas de l'élève sans notes
        if len(self.notes)==0:
            return "Eleve={0}, notes=[]".format(self.eleve)
        # cas de l'élève avec notes
        str=""
        for note in self.notes:
            str+="{0} ".format(note.valeur)
        return "Eleve={0}, notes=[{1}], max={2}, min={3}, moyenne={4}".format(self.eleve, str, self.max, self.min, self.moyennePonderee)

  • السطر 3: يأخذ المنشئ معلمتين:
    • مرجع إلى الطالب من النوع [Student] الذي يتم حساب المقاييس له؛
    • مرجع إلى درجاته، وهي قائمة من كائنات [Grade].
  • السطران 8-9: إذا كانت قائمة الدرجات فارغة، نتوقف هنا.
  • وإلا، الأسطر 11-25: يتم حساب المقاييس التالية:
    • self.weightedAverage: متوسط الطالب المرجح بمعاملات المواد؛
    • self.min: أدنى درجة للطالب؛
    • self.max: أعلى درجة حصل عليها الطالب.
  • السطر 28: طريقة عرض الفصل بالصيغة المحددة في السطر 36.

يمكن أن يكون نص الاختبار لهذه الفئة كما يلي:

#    -*- coding=utf-8 -*-

#  import of entity modules, [dao] layer and [metier] layer
from entites import *
from dao import *
from metier import *

#    a class
classe1=Classe(1,"6e A")
#  one student in this class
paul_durand=Eleve(1,"durand","paul",classe1)
#    three materials
maths=Matiere(1,"maths",1)
francais=Matiere(2,"francais",2)
anglais=Matiere(3,"anglais",3)
#  grades in these subjects for the student
note_maths=Note(1,10,paul_durand,maths)
note_francais=Note(2,12,paul_durand,francais)
note_anglais=Note(3,14,paul_durand,anglais)
#  indicators are displayed
print StatsForEleve(paul_durand,[note_maths, note_francais,note_anglais])

يكون إخراج الشاشة كما يلي:

Eleve=Eleve[1,paul,durand,Classe[1,6e A]], notes=[10 12 14 ], max=14, min=10, moyenne=12.6666666667

لنعد إلى بنية الطبقات لدينا:

تنفذ فئة [Business] الطبقة [business] على النحو التالي:


class Metier:
    # constructeur
    def __init__(self,dao):
        # on mémorise la référence sur la couche [dao]
        self.dao=dao
 
    #-----------
    # interface
    #-----------
 
    # les indicateurs sur les notes
    def getStatsForEleve(self,idEleve):
        # Stats pour l'élève de n° idEleve
        # recherche de l'élève
        trouve=False
        i=0
        eleves=self.getEleves()
        while not trouve and i<len(eleves):
            trouve=eleves[i].id==idEleve
            i+=1
        # a-t-on trouvé ?
        if not trouve:
            raise RuntimeError("L'eleve [{0}] n'existe pas".format(idEleve))
        else:
            eleve=eleves[i-1]
        # liste de toutes les notes
        notes=[]
        for note in self.getNotes():
            # on ajoute à notes, toutes les notes de l'élève
            if note.eleve.id==idEleve:
                notes.append(note)
        # on rend le résultat
        return StatsForEleve(eleve,notes)
 
    # la liste des classes
    def getClasses(self):
        return self.dao.getClasses()
 
    # la liste des matières
    def getMatieres(self):
        return self.dao.getMatieres()
 
    # la liste des élèves
    def getEleves(self):
        return self.dao.getEleves()
 
    # la liste des notes
    def getNotes(self):
        return self.dao.getNotes()
 
  • الأسطر 3–5: يتلقى المنشئ مرجعًا إلى طبقة [dao]. يجب أن تحتوي طبقة [business] على هذا المرجع. هنا، نوفره عبر منشئها. هناك حلول أخرى ممكنة. في بنية الطبقات الممثلة أفقيًا، يجب أن تحتوي كل طبقة على مرجع إلى الطبقة الموجودة على يمينها؛
  • الأسطر 36-49: تقوم الطرق getClasses و getSubjects و getStudents و getGrades ببساطة بتفويض الاستدعاء إلى الطرق التي تحمل الأسماء نفسها في طبقة [dao]؛
  • السطر 12: تتلقى الطريقة getStatsForStudent كمعلمة رقم هوية الطالب الذي سيتم إرجاع إحصائياته.
  • السطر 17: سيتم البحث عن الطالب في قائمة جميع الطلاب؛
  • الأسطر 18-20: حلقة البحث؛
  • السطر 23: إذا لم يتم العثور على الطالب، يتم إصدار استثناء؛
  • وإلا، السطر 25: يتم تخزين الطالب الذي تم العثور عليه؛
  • الأسطر 28-31: نبحث في جميع صفوف المرحلة الإعدادية عن تلك التي تنتمي إلى الطالب المخزن؛
  • بمجرد العثور عليها، يمكن إنشاء كائن StatsForEleve المطلوب.

7.5. طبقة [console]

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

#    -*- coding=utf-8 -*-

#  import from entity module, [dao] module, [metier] module
from entites import *
from dao import *
from metier import *

#    ----------- layer [console]
#    instantiation layer [metier]
metier=Metier(Dao())
#  request/response
fini=False
while not fini:
    #    question
    print "numero de l'eleve (>=1 et * pour arreter) : "
    #    answer
    reponse=raw_input()
    #    finished?
    if reponse.strip()=="*":
        break
    #  is the input correct?
    ok=False
    try:
        idEleve=int(reponse,10)
        ok=idEleve>=1
    except:
        pass
    #    correct data?
    if not ok:
        print "Saisie incorrecte. Recommencez..."
        continue
    #  calculation
    try:
        print metier.getStatsForEleve(idEleve)
    except RuntimeError,erreur:
        print "L'erreur suivante s'est produite : {0}".format(erreur)
  • السطر 10: إنشاء مثيل لكل من طبقتي [dao] و[business]. هذه هي التبعية الوحيدة التي يعتمد عليها كودنا في تنفيذ هاتين الطبقتين؛
  • السطر 34: نستخدم واجهة طبقة [business
  • السطر 19: تزيل طريقة strip المسافات البادئة والختامية من السلسلة؛
  • السطر 20: break يخرج من الحلقة؛
  • السطر 24: محاولة تحويل السلسلة المدخلة إلى عدد صحيح عشري؛
  • السطر 29: ok تكون صحيحة فقط إذا تم تنفيذ السطر 25؛
  • السطر 31: تسمح continue بإعادة تشغيل الحلقة من المنتصف؛
  • السطر 34: حساب المؤشرات؛
  • السطر 35: يلتقط استثناء RuntimeError الذي قد ينشأ من طبقة [الأعمال].

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

numero de l'eleve (>=1 et * pour arreter) :
xx
Saisie incorrecte. Recommencez...
numero de l'eleve (>=1 et * pour arreter) :
-4
Saisie incorrecte. Recommencez...
numero de l'eleve (>=1 et * pour arreter) :
11
Eleve=Eleve[11,prenom1,nom1,Classe[1,classe1]], notes=[10 6 ], max=10, min=6, mo
yenne=7.33333333333
numero de l'eleve (>=1 et * pour arreter) :
111
L'erreur suivante s'est produite : L'eleve [111] n'existe pas
numero de l'eleve (>=1 et * pour arreter) :
*