Skip to content

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]. Тут робляться припущення: назва класу та тип конструктора. Існують рішення, які дозволяють уникнути цієї залежності.

Результати виконання цього скрипта такі:

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. Шар [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])

Результати на екрані такі:

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

Повернемося до нашої багатошарової архітектури:

Клас [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].

Ось приклад виконання:

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) :
*