7. Багатошарова архітектура та програмування за допомогою інтерфейсів
7.1. Introduction
Ми пропонуємо написати додаток, що дозволяє відображати оцінки учнів середньої школи. Цей додаток матиме багаторівневу архітектуру:
![]() |
- шар [présentation] — це шар, що взаємодіє з користувачем програми.
- Рівень [metier] реалізує правила управління додатком, такі як розрахунок заробітної плати або рахунку-фактури. Цей рівень використовує дані, отримані від користувача через рівень [présentation], а також дані з SGBD через рівень [dao].
- Рівень [dao] (Data Access Objects) керує доступом до даних з рівня 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: ok є істинним лише в тому випадку, якщо було виконано рядок 25;
- рядок 31: continue дозволяє повернутися до середини циклу;
- рядок 34: обчислення показників;
- рядок 35: перехоплюється виняток RuntimeError, який може виникнути на рівні [métier].
Ось приклад виконання:






