7. 分层架构与接口编程
7.1. Introduction
我们计划编写一个应用程序,用于显示某所中学学生的成绩。该应用程序将采用多层架构:
![]() |
- [présentation]层是与应用程序用户交互的层。
- [metier]层负责实现应用程序的管理规则,例如工资或账单的计算。 该层通过 [présentation] 层使用来自用户的数据,并通过 [dao] 层使用来自 SGBD 的数据。
- [dao] 层(数据访问对象)管理对 SGBD 层数据的访问。
我们将通过一个简单的控制台应用程序来说明这种架构:
- 该应用不涉及数据库,
- [dao]层将管理实体Eleve、Classe、Matiere、Note,用于管理学生的成绩,
- [métier] 层将用于计算特定学生的成绩指标,
- [présentation] 层将是一个控制台应用程序,用于显示由 [métier] 层计算出的结果。
该应用程序的 Visual Studio 项目如下:
![]() |
7.2. 应用程序的实体
实体即对象。此处针对每个实体(Eleve、Classe、Matiere、Note)将分别定义四个类。
![]() |
文件 [entites.py] 包含四个班级。
类 [Classe] 代表一个中学班级:
class Classe:
# 构造函数
def __init__(self,id,nom):
# 保存参数
self.id=id
self.nom=nom
# toString
def __str__(self):
return "Classe[{0},{1}]".format(self.id,self.nom)
- 第 3-6 行:一个班级由编号 id(第 5 行)和 nom(第 6 行)定义;
- 第9-10行:该班级的显示方法。
类 [Matiere] 如下所示:
class Matiere:
# 制造商
def __init__(self,id,nom,coefficient):
# 保存参数
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行:科目的显示方法。
类 [Eleve] 如下:
class Eleve:
# 制造商
def __init__(self,id,nom,prenom,classe):
# 保存参数
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行)定义。最后一个参数是对象[Classe]的引用;
- 第11-12行:学生的显示方式。
类 [Note] 如下:
class Note:
# 制造商
def __init__(self,id,valeur,eleve,matiere):
# 保存参数
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行:[Note]对象由其编号(第5行)、成绩值(第6行)、拥有该成绩的学生引用(第7行)以及成绩所属学科的引用(第8行)来描述;
- 第11-12行:对象[Note]的显示方法。
7.3. [dao]层
![]() |
[dao] 层将向 [métier] 层提供以下接口:
- getClasses 返回中学班级列表;
- getMatieres 返回科目列表;
- getEleves 返回学生列表;
- getNotes 返回成绩列表。
[métier] 层仅使用这些方法。它无需了解这些方法的具体实现方式。因此,这些数据可以来自不同的来源(硬编码、数据库、文本文件等),而不会影响 [métier] 层。 这被称为接口编程。
类 [Dao] 通过以下方式实现该接口:
# -*- coding=utf-8 -*-
# 导入实体模块
from entites import *
class Dao:
# 构造函数
def __init__(self):
# 实例化类
classe1=Classe(1,"classe1")
classe2=Classe(2,"classe2")
self.classes=[classe1,classe2]
# 学科
matiere1=Matiere(1,"matiere1",1)
matiere2=Matiere(2,"matiere2",2)
self.matieres=[matiere1,matiere2]
# 学生
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]
# 成绩
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]
#-----------
# 界面
#-----------
# 班级列表
def getClasses(self):
return self.classes
# 科目列表
def getMatieres(self):
return self.matieres
# 学生列表
def getEleves(self):
return self.eleves
# 成绩列表
def getNotes(self):
return self.notes
- 第 4 行:导入包含 [dao] 层所操作实体的模块;
- 第 8 行:构造函数无参数。它硬编码了四个列表:
- 第 10-12 行:类列表;
- 第14-16行:科目列表;
- 第18-22行:学生列表;
- 第 24-32 行:成绩列表。
- 第39-52行:[dao]层接口的四个方法仅返回构造函数生成的四个列表的引用。
一个测试程序可以如下所示:
# -*- coding=utf-8 -*-
# 导入实体模块和图层 [dao]
from entites import *
from dao import *
# 图层实例化 [dao]
dao=Dao()
# 类列表
for classe in dao.getClasses():
print classe
# 类列表
for matiere in dao.getMatieres():
print matiere
# 类列表
for eleve in dao.getEleves():
print eleve
# 类列表
for note in dao.getNotes():
print note
注释已足够说明。第11-24行使用[dao]层的接口。这里对该层的实际实现不作任何假设。第8行,实例化[dao]层。 此处存在一些假设:类名和构造函数类型。已有解决方案可避免这种依赖。
执行此脚本的结果如下:
7.4. [métier] 层
![]() |
[métier] 层实现了以下接口:
- getClasses 返回中学的班级列表;
- getMatieres 返回科目列表;
- getEleves 返回学生列表;
- getNotes 返回成绩列表;
- getStatsForEleve 返回编号为 idEleve 的学生的成绩,以及相关信息:加权平均分、最低分、最高分。
[présentation] 层仅使用这些方法。它无需了解这些方法的具体实现方式。
方法 getStatsForEleve 返回如下 [StatsForEleve] 类型的对象:
class StatsForEleve:
# 构造函数
def __init__(self, eleve, notes):
# 保存参数
self.eleve=eleve
self.notes=notes
# 若无评分则停止
if len(notes)==0:
return
# 成绩处理
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
# 计算学生平均分
self.moyennePonderee=float(sommePonderee)/sommeCoeff
# toString
def __str__(self):
# 无成绩的学生情况
if len(self.notes)==0:
return "Eleve={0}, notes=[]".format(self.eleve)
# 有成绩的学生情况
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 行:构造函数接收两个参数:
- 一个指向 [Eleve] 类型学生的引用,该学生的指标将被计算;
- 一个指向其成绩的引用,即一个 [Note] 对象列表。
- 第8-9行:如果成绩列表为空,则不再继续执行。
- 否则执行第11-25行,计算以下指标:
- self.moyennePonderee:按科目权重加权的学生平均分;
- self.min:该学生的最低成绩;
- self.max:该学生的最高成绩。
- 第28行:按第36行所示格式显示班级的方法。
该类的测试脚本可能如下所示:
# -*- coding=utf-8 -*-
# 导入实体模块、[dao]图层和[metier]图层
from entites import *
from dao import *
from metier import *
# 一个班级
classe1=Classe(1,"6e A")
# 该班级的一名学生
paul_durand=Eleve(1,"durand","paul",classe1)
# 三门课程
maths=Matiere(1,"maths",1)
francais=Matiere(2,"francais",2)
anglais=Matiere(3,"anglais",3)
# 该学生在这三门课程中的成绩
note_maths=Note(1,10,paul_durand,maths)
note_francais=Note(2,12,paul_durand,francais)
note_anglais=Note(3,14,paul_durand,anglais)
# 显示指标
print StatsForEleve(paul_durand,[note_maths, note_francais,note_anglais])
屏幕显示结果如下:
让我们回到分层架构:
![]() |
类 [Metier] 以如下方式实现了 [métier] 层:
class Metier:
# 生成器
def __init__(self,dao):
# 将引用存储在图层 [dao] 上
self.dao=dao
#-----------
# 界面
#-----------
# 成绩单上的指标
def getStatsForEleve(self,idEleve):
# 编号为 idEleve 的学生统计数据
# 学生搜索
trouve=False
i=0
eleves=self.getEleves()
while not trouve and i<len(eleves):
trouve=eleves[i].id==idEleve
i+=1
# 是否找到?
if not trouve:
raise RuntimeError("L'eleve [{0}] n'existe pas".format(idEleve))
else:
eleve=eleves[i-1]
# 所有成绩列表
notes=[]
for note in self.getNotes():
# 将该学生的所有成绩添加到成绩中
if note.eleve.id==idEleve:
notes.append(note)
# 返回结果
return StatsForEleve(eleve,notes)
# 班级列表
def getClasses(self):
return self.dao.getClasses()
# 科目列表
def getMatieres(self):
return self.dao.getMatieres()
# 学生列表
def getEleves(self):
return self.dao.getEleves()
# 成绩列表
def getNotes(self):
return self.dao.getNotes()
- 第 3-5 行:构造函数接收一个指向 [dao] 层的引用。 [métier] 层必须拥有该引用。在此,我们通过其构造函数将其传递给该层。当然也可以设想其他解决方案。在水平表示的分层架构中,每一层都必须拥有对其右侧层的引用;
- 第36-49行:方法getClasses、 getMatieres、getEleves、getNotes 仅将调用委托给 [dao] 层中同名的方法;
- 第 12 行:方法 getStatsForEleve 接收学生编号作为参数,用于获取该学生的相关指标。
- 第17行:将在所有学生的列表中搜索该学生;
- 第18-20行:搜索循环;
- 第23行:如果未找到该学生,则抛出异常;
- 否则执行第25行,将找到的学生记录下来;
- 第28-31行:在该中学的所有成绩中,查找属于已保存学生的成绩;
- 找到后,即可构建所需的 StatsForEleve 对象。
7.5. [console] 层
![]() |
[console]层由以下脚本实现:
# -*- coding=utf-8 -*-
# 导入实体模块、[dao]模块、[metier]模块
from entites import *
from dao import *
from metier import *
# ----------- 层 [console]
# 实例化层 [metier]
metier=Metier(Dao())
# 请求/响应
fini=False
while not fini:
# 问题
print "numero de l'eleve (>=1 et * pour arreter) : "
# 响应
reponse=raw_input()
# 结束了吗?
if reponse.strip()=="*":
break
# 输入正确吗?
ok=False
try:
idEleve=int(reponse,10)
ok=idEleve>=1
except:
pass
# 数据正确吗?
if not ok:
print "Saisie incorrecte. Recommencez..."
continue
# 计算
try:
print metier.getStatsForEleve(idEleve)
except RuntimeError,erreur:
print "L'erreur suivante s'est produite : {0}".format(erreur)
- 第 10 行:同时实例化 [dao] 和 [métier] 层。这是我们的代码对这些层实现的唯一依赖;
- 第 34 行:使用 [métier] 层的接口;
- 第 19 行:方法 strip 移除了字符串首尾的空格;
- 第 20 行:break 用于退出循环;
- 第24行:尝试将输入的字符串转换为十进制整数;
- 第29行:只有经过第25行时,ok才为真;
- 第31行:continue 允许在循环中途重新进入循环;
- 第 34 行:计算指标;
- 第 35 行:捕获可能从 [métier] 层抛出的 RuntimeError 异常。
以下是一个执行示例:






