Skip to content

7. 分层架构与基于接口的编程

7.1. 简介

我们计划编写一个应用程序,用于显示初中生的成绩。该应用程序将采用多层架构:

  • [展示]层是与应用程序用户进行交互的层。
  • [业务逻辑]层负责实现应用程序的业务规则,例如计算工资或生成发票。该层通过[展示]层获取用户数据,并通过[DAO]层从数据库管理系统(DBMS)获取数据。
  • [DAO](数据访问对象)层负责管理对数据库管理系统中数据的访问。

我们将通过一个简单的控制台应用程序来说明这种架构:

  • 该应用将不使用数据库,
  • [DAO] 层将管理 StudentClassSubjectGrade 实体以处理学生成绩,
  • [业务逻辑]层将根据特定学生的成绩计算各项指标,
  • [展示]层将是一个控制台应用程序,用于显示由[业务]层计算出的结果。

该应用程序的 Visual Studio 项目结构如下:

  

7.2. 应用程序的实体

实体即对象。在此,我们将为每个实体分别创建四个类:StudentClassSubjectGrade

[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 行:类由 ID(第 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行:一个科目由其ID(第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 行:学生由其 ID(第 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] 对象由其 ID(第 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 行:getClassesgetSubjectsgetStudentsgetGrades 方法仅将调用委托给 [dao] 层中同名的方法;
  • 第 12 行:getStatsForStudent 方法接收一个参数,即需要返回统计信息的学生的 ID。
  • 第 17 行:将在所有学生的列表中搜索该学生;
  • 第 18–20 行:搜索循环;
  • 第 23 行:如果未找到该学生,则抛出异常;
  • 否则,第 25 行:将找到的学生存储起来;
  • 第 28–31 行:遍历所有初中年级,查找属于该存储学生所属的年级;
  • 一旦找到,即可构建所需的 StatsForEleve 对象。

7.5. [控制台] 层

[控制台]层由以下脚本实现:

#    -*- 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 行:只有当第 25 行已执行时,ok 才为真;
  • 第 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) :
*