Skip to content

7. Arquitetura em camadas e programação por interfaces

7.1. Introduction

Propomos escrever uma aplicação que permita a visualização das notas dos alunos de uma escola secundária. Esta aplicação terá uma arquitetura multicamadas:

  • a camada [présentation] é a camada que interage com o utilizador da aplicação.
  • A camada [metier] implementa as regras de gestão da aplicação, tais como o cálculo de um salário ou de uma fatura. Esta camada utiliza dados provenientes do utilizador através da camada [présentation] e da camada SGBD através da camada [dao].
  • A camada [dao] (Data Access Objects) gere o acesso aos dados da camada SGBD.

Vamos ilustrar esta arquitetura com uma aplicação de consola simples:

  • não haverá base de dados,
  • a camada [dao] irá gerir as entidades Eleve, Classe, Matiere e Note, permitindo a gestão das notas dos alunos,
  • a camada [métier] permitirá calcular indicadores com base nas notas de um aluno específico,
  • a camada [présentation] será uma aplicação de consola que apresentará os resultados calculados pela camada [métier].

O projeto do Visual Studio da aplicação é o seguinte:

  

7.2. As entidades da aplicação

As entidades são objetos. Teremos aqui quatro classes para cada uma das entidades Eleve, Classe, Matiere e Note.

O ficheiro [entites.py] agrupa quatro turmas.

A turma [Classe] representa uma turma do ensino básico:


class Classe:
    # construtor
    def __init__(self,id,nom):
        # guardam-se os parâmetros
        self.id=id
        self.nom=nom

    # toString
    def __str__(self):
        return "Classe[{0},{1}]".format(self.id,self.nom)
  • linhas 3-6: uma turma é definida por um n.º id (linha 5) e um nom (linha 6);
  • linhas 9-10: o método de exibição da turma.

A classe [Matiere] é a seguinte:


class Matiere:
    # fabricante
    def __init__(self,id,nom,coefficient):
        # memorização dos parâmetros
        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)

  • linhas 3-7: uma disciplina é definida pelo seu n.º (linha 5), pelo seu nome (linha 6) e pelo seu coeficiente (linha 7);
  • linhas 10-11: o método de exibição da disciplina.

A classe [Eleve] é a seguinte:


class Eleve:
    # fabricante
    def __init__(self,id,nom,prenom,classe):
        # memorização dos parâmetros
        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)

  • linhas 3-8: um aluno é caracterizado pelo seu n.º (linha 5), o seu apelido (linha 6), o seu nome próprio (linha 7) e a sua turma (linha 8). Este último parâmetro é uma referência a um objeto [Classe];
  • linhas 11-12: o método de visualização do aluno.

A classe [Note] é a seguinte:


class Note:
    # fabricante
    def __init__(self,id,valeur,eleve,matiere):
        # memorização dos parâmetros
        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)

  • linhas 3-8: um objeto [Note] é caracterizado pelo seu n.º (linha 5), pelo valor da nota (linha 6), por uma referência ao aluno que obteve essa nota (linha 7) e por uma referência à disciplina a que a nota se refere (linha 8);
  • linhas 11-12: o método de exibição do objeto [Note].

7.3. A camada [dao]

A camada [dao] disponibilizará a seguinte interface à camada [métier]:

  • getClasses apresenta a lista das turmas do ensino básico;
  • getMatieres apresenta a lista das disciplinas;
  • getEleves apresenta a lista de alunos;
  • getNotes apresenta a lista de notas.

A camada [métier] utilizará apenas estes métodos. Não precisa de saber como são implementados. Estes dados podem, assim, provir de diferentes fontes (armazenados em memória, de uma base de dados, de ficheiros de texto, etc.) sem que isso tenha impacto na camada [métier]. A isto chama-se programação por interfaces.

A classe [Dao] implementa esta interface da seguinte forma:


# -*- coding=utf-8 -*-

# importação do módulo de entidades
from entites import *

class Dao:
    # construtor
    def __init__(self):
        # instanciamos as classes
        classe1=Classe(1,"classe1")
        classe2=Classe(2,"classe2")
        self.classes=[classe1,classe2]
        # as disciplinas
        matiere1=Matiere(1,"matiere1",1)
        matiere2=Matiere(2,"matiere2",2)
        self.matieres=[matiere1,matiere2]
        # os alunos
        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]
        # as notas
        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
    #-----------
    
    # lista de turmas
    def getClasses(self):
        return self.classes

    # lista de disciplinas
    def getMatieres(self):
        return self.matieres
    
    # lista de alunos
    def getEleves(self):
        return self.eleves

    # lista de notas
    def getNotes(self):
        return self.notes

  • linha 4: importa-se o módulo que contém as entidades manipuladas pela camada [dao];
  • linha 8: o construtor não tem parâmetros. Ele cria de forma estática quatro listas:
    • linhas 10-12: a lista de classes;
    • linhas 14-16: a lista das disciplinas;
    • linhas 18-22: a lista de alunos;
    • linhas 24-32: a lista de notas.
  • linhas 39-52: os quatro métodos da interface da camada [dao] limitam-se a devolver uma referência às quatro listas criadas pelo construtor.

Um programa de teste poderia ser o seguinte:


# -*- coding=utf-8 -*-

# importação do módulo de entidades e da camada [dao]
from entites import *
from dao import *

# instanciação da camada [dao]
dao=Dao()

# lista de classes
for classe in dao.getClasses():
    print classe

# lista de classes
for matiere in dao.getMatieres():
    print matiere

# lista de classes
for eleve in dao.getEleves():
    print eleve

# lista de classes
for note in dao.getNotes():
    print note

Os comentários são autoexplicativos. As linhas 11-24 utilizam a interface da camada [dao]. Não há aqui quaisquer pressupostos sobre a implementação real da camada. Na linha 8, instanciamos a camada [dao]. Aqui, fazem-se suposições: nome da classe e tipo de construtor. Existem soluções que permitem evitar esta dependência.

Os resultados da execução deste script são os seguintes:

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. A camada [métier]

A camada [métier] implementa a seguinte interface:

  • getClasses apresenta a lista das turmas do ensino básico;
  • getMatieres apresenta a lista das disciplinas;
  • getEleves apresenta a lista de alunos;
  • getNotes apresenta a lista de notas;
  • getStatsForEleve apresenta as notas do aluno n.º idEleve, bem como informações sobre as mesmas: média ponderada, nota mais baixa, nota mais alta.

A camada [présentation] utilizará apenas estes métodos. Não precisa de saber como são implementados.

O método getStatsForEleve devolve um objeto do tipo [StatsForEleve] com as seguintes características:


class StatsForEleve:
    # fabricante
    def __init__(self, eleve, notes):
        # os parâmetros são guardados
        self.eleve=eleve
        self.notes=notes
        # pára-se se não houver notas
        if len(notes)==0:
            return
        # análise das notas
        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
        # cálculo da média do aluno
        self.moyennePonderee=float(sommePonderee)/sommeCoeff
        
    # toString
    def __str__(self):
        # caso do aluno sem notas
        if len(self.notes)==0:
            return "Eleve={0}, notes=[]".format(self.eleve)
        # caso do aluno com notas
        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)

  • linha 3: o construtor recebe dois parâmetros:
    • uma referência ao aluno do tipo [Eleve] para o qual se calculam os indicadores;
    • uma referência às suas notas, uma lista de objetos [Note].
  • linhas 8-9: se a lista de notas estiver vazia, o processo não prossegue.
  • caso contrário, linhas 11-25, calculam-se os seguintes indicadores:
    • self.moyennePonderee: a média do aluno ponderada pelos coeficientes das disciplinas;
    • self.min: a nota mais baixa do aluno;
    • self.max: a sua nota mais alta.
  • linha 28: o método de apresentação da turma na forma indicada na linha 36.

Um script de teste desta classe poderia ser o seguinte:


# -*- coding=utf-8 -*-

# importação dos módulos das entidades, da camada [dao] e da camada [metier]
from entites import *
from dao import *
from metier import *

# uma turma
classe1=Classe(1,"6e A")
# um aluno nesta turma
paul_durand=Eleve(1,"durand","paul",classe1)
# três disciplinas
maths=Matiere(1,"maths",1)
francais=Matiere(2,"francais",2)
anglais=Matiere(3,"anglais",3)
# as notas nessas disciplinas para o aluno
note_maths=Note(1,10,paul_durand,maths)
note_francais=Note(2,12,paul_durand,francais)
note_anglais=Note(3,14,paul_durand,anglais)
# são apresentados os indicadores
print StatsForEleve(paul_durand,[note_maths, note_francais,note_anglais])

Os resultados apresentados no ecrã são os seguintes:

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

Voltemos à nossa arquitetura em camadas:

A classe [Metier] implementa a camada [métier] da seguinte forma:


class Metier:
    # construtor
    def __init__(self,dao):
        # a referência é guardada na camada [dao]
        self.dao=dao

    #-----------
    # interface
    #-----------

    # os indicadores nas notas
    def getStatsForEleve(self,idEleve):
        # Estatísticas para o aluno com o n.º idEleve
        # pesquisa do aluno
        trouve=False
        i=0
        eleves=self.getEleves()
        while not trouve and i<len(eleves):
            trouve=eleves[i].id==idEleve
            i+=1
        # foi encontrado?
        if not trouve:
            raise RuntimeError("L'eleve [{0}] n'existe pas".format(idEleve))
        else:
            eleve=eleves[i-1]
        # Lista de todas as notas
        notes=[]
        for note in self.getNotes():
            # adiciona-se às notas todas as notas do aluno
            if note.eleve.id==idEleve:
                notes.append(note)
        # apresenta o resultado
        return StatsForEleve(eleve,notes)

    # a lista das turmas
    def getClasses(self):
        return self.dao.getClasses()
    
    # a lista de disciplinas
    def getMatieres(self):
        return self.dao.getMatieres()
    
    # a lista de alunos
    def getEleves(self):
        return self.dao.getEleves()
    
    # a lista de notas
    def getNotes(self):
        return self.dao.getNotes()

  • linhas 3-5: o construtor recebe uma referência à camada [dao]. A camada [métier] deve possuir essa referência. Neste caso, esta é-lhe fornecida através do seu construtor. Seria possível imaginar outras soluções. Numa arquitetura em camadas representada horizontalmente, cada camada deve possuir uma referência à camada que se encontra à sua direita;
  • linhas 36-49: os métodos getClasses, getMatieres, getEleves, getNotes limitam-se a delegar a chamada aos métodos com os mesmos nomes da camada [dao];
  • linha 12: o método getStatsForEleve recebe como parâmetro o número do aluno para o qual se devem apresentar os indicadores.
  • linha 17: o aluno será procurado na lista de todos os alunos;
  • linhas 18-20: o ciclo de pesquisa;
  • linha 23: se o aluno não tiver sido encontrado, é lançada uma exceção;
  • caso contrário, linha 25: o aluno encontrado é guardado;
  • linhas 28-31: procura-se, entre todas as notas do colégio, aquelas que pertencem ao aluno memorizado;
  • quando as encontrarmos, podemos construir o objeto StatsForEleve solicitado.

7.5. A camada [console]

A camada [console] é implementada pelo seguinte script:


# -*- coding=utf-8 -*-

# importação do módulo de entidades, do módulo [dao] e do módulo [metier]
from entites import *
from dao import *
from metier import *

# ----------- camada [console]
# instanciação da camada [metier]
metier=Metier(Dao())
# pedido/resposta
fini=False
while not fini:
    # pergunta
    print "numero de l'eleve (>=1 et * pour arreter) : "
    # resposta
    reponse=raw_input()
    # concluído?
    if reponse.strip()=="*":
        break
    # a entrada está correta?
    ok=False
    try:
        idEleve=int(reponse,10)
        ok=idEleve>=1
    except:
        pass
    # dados corretos?
    if not ok:
        print "Saisie incorrecte. Recommencez..."
        continue
    # cálculo
    try:
        print metier.getStatsForEleve(idEleve)
    except RuntimeError,erreur:
        print "L'erreur suivante s'est produite : {0}".format(erreur)

  • linha 10: instanciação simultânea das camadas [dao] e [métier]. Esta é a única dependência do nosso código em relação à implementação destas camadas;
  • linha 34: utiliza-se a interface da camada [métier];
  • linha 19: o método strip remove os espaços no início e no fim da cadeia;
  • linha 20: break permite sair de um ciclo;
  • linha 24: tenta-se converter a cadeia introduzida num número inteiro decimal;
  • linha 29: ok só é verdadeiro se tiver-se passado pela linha 25;
  • linha 31: continue permite voltar a entrar no meio do ciclo;
  • linha 34: cálculo dos indicadores;
  • linha 35: intercepta-se a exceção RuntimeError, que pode ser gerada pela camada [métier].

Eis um exemplo de execução:

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