7. 分层架构与基于接口的编程
7.1. 简介
我们计划编写一个应用程序,用于显示初中生的成绩。该应用程序将采用多层架构:
![]() |
- [展示]层是与应用程序用户进行交互的层。
- [业务逻辑]层负责实现应用程序的业务规则,例如计算工资或生成发票。该层通过[展示]层获取用户数据,并通过[DAO]层从数据库管理系统(DBMS)获取数据。
- [DAO](数据访问对象)层负责管理对数据库管理系统中数据的访问。
我们将通过一个简单的控制台应用程序来说明这种架构:
- 该应用将不使用数据库,
- [DAO] 层将管理 Student、Class、Subject 和 Grade 实体以处理学生成绩,
- [业务逻辑]层将根据特定学生的成绩计算各项指标,
- [展示]层将是一个控制台应用程序,用于显示由[业务]层计算出的结果。
该应用程序的 Visual Studio 项目结构如下:
![]() |
7.2. 应用程序的实体
实体即对象。在此,我们将为每个实体分别创建四个类:Student、Class、Subject 和 Grade。
![]() |
[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] 类通过以下方式实现该接口:
- 第 4 行:我们导入包含 [DAO] 层所处理的实体的模块;
- 第 8 行:构造函数没有参数。它硬编码了四个列表:
- 第 10–12 行:类列表;
- 第 14–16 行:科目列表;
- 第 18–22 行:学生列表;
- 第24–32行:成绩列表。
- 第 39–52 行:[dao] 层接口的四个方法仅返回对构造函数构建的四个列表的引用。
一个测试程序可能如下所示:
注释已说明一切。第 11–24 行使用了 [dao] 层接口。这里并未对该层的实际实现做出任何假设。在第 8 行,我们实例化了 [dao] 层。在此处,我们做出了以下假设:类名和构造函数类型。存在一些解决方案可以让我们避免这种依赖。
运行此脚本的结果如下:
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 行指定的格式显示班级成绩的方法。
该类的测试脚本可以如下所示:
屏幕输出如下:
让我们回到我们的分层架构:
![]() |
[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 方法接收一个参数,即需要返回统计信息的学生的 ID。
- 第 17 行:将在所有学生的列表中搜索该学生;
- 第 18–20 行:搜索循环;
- 第 23 行:如果未找到该学生,则抛出异常;
- 否则,第 25 行:将找到的学生存储起来;
- 第 28–31 行:遍历所有初中年级,查找属于该存储学生所属的年级;
- 一旦找到,即可构建所需的 StatsForEleve 对象。
7.5. [控制台] 层
![]() |
[控制台]层由以下脚本实现:
- 第 10 行:同时实例化 [dao] 和 [business] 层。这是我们的代码对这些层实现的唯一依赖;
- 第 34 行:我们使用了 [business] 层的接口;
- 第 19 行:strip 方法用于去除字符串首尾的空格;
- 第 20 行:break 语句用于退出循环;
- 第 24 行:尝试将输入的字符串转换为十进制整数;
- 第 29 行:只有当第 25 行已执行时,
ok才为真; - 第 31 行:
continue允许循环从中途重新开始; - 第 34 行:计算指标;
- 第 35 行:捕获可能源自 [业务] 层的 RuntimeError 异常。
以下是一个执行示例:






