Skip to content

7. Arquitectura por capas y programación por interfaces

7.1. Introducción

Nos proponemos escribir una aplicación que permita visualizar las notas de los alumnos de un instituto. Esta aplicación tendrá una arquitectura multicapa:

  • la capa [présentation] es la capa en contacto con el usuario de la aplicación.
  • La capa [metier] implementa las reglas de gestión de la aplicación, como el cálculo de un salario o de una factura. Esta capa utiliza datos procedentes del usuario a través de la capa [présentation] y de SGBD a través de la capa [dao].
  • La capa [dao] (Data Access Objects) gestiona el acceso a los datos de SGBD.

Ilustraremos esta arquitectura con una sencilla aplicación de consola:

  • no habrá base de datos,
  • la capa [dao] gestionará las entidades Alumno, Clase, Asignatura y Nota, lo que permitirá gestionar las notas de los alumnos,
  • la capa [métier] permitirá calcular indicadores sobre las notas de un alumno concreto,
  • la capa [présentation] será una aplicación de consola que mostrará los resultados calculados por la capa [métier].

El proyecto de Visual Studio de la aplicación es el siguiente:

  

7.2. Las entidades de la aplicación

Las entidades son objetos. Aquí tendremos cuatro clases para cada una de las entidades: Alumno, Clase, Asignatura y Nota.

El archivo [entites.py] agrupa cuatro clases.

La clase [Classe] representa una clase de secundaria:


class Classe:
    # constructor
    def __init__(self,id,nom):
        # se guardan los parámetros
        self.id=id
        self.nom=nom

    # toString
    def __str__(self):
        return "Classe[{0},{1}]".format(self.id,self.nom)
  • líneas 3-6: una clase se define mediante un n.º id (línea 5) y un nombre (línea 6);
  • líneas 9-10: el método de visualización de la clase.

La clase [Matiere] es la siguiente:


class Matiere:
    # fabricante
    def __init__(self,id,nom,coefficient):
        # se guardan los 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)

  • líneas 3-7: una asignatura se define por su n.º (línea 5), su nombre (línea 6) y su coeficiente (línea 7);
  • líneas 10-11: el método de visualización de la asignatura.

La clase [Eleve] es la siguiente:


class Eleve:
    # fabricante
    def __init__(self,id,nom,prenom,classe):
        # se guardan los 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)

  • líneas 3-8: un alumno se caracteriza por su n.º (línea 5), su apellido (línea 6), su nombre (línea 7) y su clase (línea 8). Este último parámetro es una referencia a un objeto [Classe];
  • líneas 11-12: el método de visualización del alumno.

La clase [Note] es la siguiente:


class Note:
    # fabricante
    def __init__(self,id,valeur,eleve,matiere):
        # se guardan los 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)

  • líneas 3-8: un objeto [Note] se identifica por su número (línea 5), el valor de la nota (línea 6), una referencia al alumno que tiene esa nota (línea 7) y una referencia a la asignatura a la que corresponde la nota (línea 8);
  • líneas 11-12: el método de visualización del objeto [Note].

7.3. La capa [dao]

La capa [dao] ofrecerá la siguiente interfaz a la capa [métier]:

  • getClasses muestra la lista de clases del instituto;
  • getMatieres muestra la lista de asignaturas;
  • getEleves muestra la lista de alumnos;
  • getNotes muestra la lista de notas.

La capa [métier] solo utilizará estos métodos. No tiene por qué saber cómo se implementan. Estos datos pueden proceder de diferentes fuentes (almacenados en memoria, de una base de datos, de archivos de texto, etc.) sin que ello afecte a la capa [métier]. A esto se le llama programación por interfaces.

La clase [Dao] implementa esta interfaz de la siguiente manera:


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

# importación del módulo de entidades
from entites import *

class Dao:
    # constructor
    def __init__(self):
        # instanciamos las clases
        classe1=Classe(1,"classe1")
        classe2=Classe(2,"classe2")
        self.classes=[classe1,classe2]
        # las asignaturas
        matiere1=Matiere(1,"matiere1",1)
        matiere2=Matiere(2,"matiere2",2)
        self.matieres=[matiere1,matiere2]
        # los alumnos
        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]
        # las 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]

    #-----------
    # interfaz
    #-----------
    
    # lista de clases
    def getClasses(self):
        return self.classes

    # lista de asignaturas
    def getMatieres(self):
        return self.matieres
    
    # lista de alumnos
    def getEleves(self):
        return self.eleves

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

  • línea 4: se importa el módulo que contiene las entidades manipuladas por la capa [dao];
  • línea 8: el constructor no tiene parámetros. Crea de forma fija cuatro listas:
    • líneas 10-12: la lista de clases;
    • líneas 14-16: la lista de asignaturas;
    • líneas 18-22: la lista de alumnos;
    • líneas 24-32: la lista de notas.
  • líneas 39-52: los cuatro métodos de la interfaz de la capa [dao] se limitan a devolver una referencia a las cuatro listas creadas por el constructor.

Un programa de prueba podría ser el siguiente:


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

# importación del módulo de entidades y de la capa [dao]
from entites import *
from dao import *

# instanciación de capa [dao]
dao=Dao()

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

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

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

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

Los comentarios se explican por sí mismos. Las líneas 11-24 utilizan la interfaz de la capa [dao]. Aquí no hay suposiciones sobre la implementación real de la capa. En la línea 8, se instancia la capa [dao]. Aquí se hacen suposiciones: nombre de la clase y tipo de constructor. Existen soluciones que permiten evitar esta dependencia.

Los resultados de la ejecución de este script son los siguientes:

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. La capa [métier]

La capa [métier] implementa la siguiente interfaz:

  • getClasses muestra la lista de clases del instituto;
  • getMatieres muestra la lista de asignaturas;
  • getEleves muestra la lista de alumnos;
  • getNotes muestra la lista de notas;
  • getStatsForEleve muestra las notas del alumno n.º idEleve, así como información sobre ellas: nota media ponderada, nota más baja y nota más alta.

La capa [présentation] solo utilizará estos métodos. No necesita saber cómo se implementan.

El método getStatsForEleve devuelve un objeto de tipo [StatsForEleve] como sigue:


class StatsForEleve:
    # constructor
    def __init__(self, eleve, notes):
        # se guardan los parámetros
        self.eleve=eleve
        self.notes=notes
        # se detiene si no hay notas
        if len(notes)==0:
            return
        # análisis de las calificaciones
        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 de la nota media del alumno
        self.moyennePonderee=float(sommePonderee)/sommeCoeff
        
    # toString
    def __str__(self):
        # caso del alumno sin notas
        if len(self.notes)==0:
            return "Eleve={0}, notes=[]".format(self.eleve)
        # caso del alumno con 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)

  • línea 3: el constructor recibe dos parámetros:
    • una referencia al alumno de tipo [Eleve] para el que se calculan los indicadores;
    • una referencia a sus notas, una lista de objetos [Note].
  • líneas 8-9: si la lista de notas está vacía, no se sigue adelante.
  • De lo contrario, en las líneas 11-25, se calculan los siguientes indicadores:
    • self.moyennePonderee: la media del alumno ponderada por los coeficientes de las asignaturas;
    • self.min: la nota más baja del alumno;
    • self.max: su nota más alta.
  • línea 28: el método para mostrar la clase en el formato indicado en la línea 36.

Un script de prueba de esta clase podría ser el siguiente:


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

# importación de los módulos de las entidades, de la capa [dao] y de la capa [metier]
from entites import *
from dao import *
from metier import *

# una clase
classe1=Classe(1,"6e A")
# un alumno en esta clase
paul_durand=Eleve(1,"durand","paul",classe1)
# tres asignaturas
maths=Matiere(1,"maths",1)
francais=Matiere(2,"francais",2)
anglais=Matiere(3,"anglais",3)
# las notas en estas asignaturas del alumno
note_maths=Note(1,10,paul_durand,maths)
note_francais=Note(2,12,paul_durand,francais)
note_anglais=Note(3,14,paul_durand,anglais)
# se muestran los indicadores
print StatsForEleve(paul_durand,[note_maths, note_francais,note_anglais])

Los resultados en pantalla son los siguientes:

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

Volvamos a nuestra arquitectura por capas:

La clase [Metier] implementa la capa [métier] de la siguiente manera:


class Metier:
    # constructor
    def __init__(self,dao):
        # se almacena la referencia en la capa [dao]
        self.dao=dao

    #-----------
    # interfaz
    #-----------

    # los indicadores en las notas
    def getStatsForEleve(self,idEleve):
        # Estadísticas del alumno n.º idEleve
        # búsqueda del alumno
        trouve=False
        i=0
        eleves=self.getEleves()
        while not trouve and i<len(eleves):
            trouve=eleves[i].id==idEleve
            i+=1
        # ¿Se ha encontrado?
        if not trouve:
            raise RuntimeError("L'eleve [{0}] n'existe pas".format(idEleve))
        else:
            eleve=eleves[i-1]
        # Lista de todas las notas
        notes=[]
        for note in self.getNotes():
            # se añaden a «notas» todas las notas del alumno
            if note.eleve.id==idEleve:
                notes.append(note)
        # se muestra el resultado
        return StatsForEleve(eleve,notes)

    # la lista de clases
    def getClasses(self):
        return self.dao.getClasses()
    
    # la lista de asignaturas
    def getMatieres(self):
        return self.dao.getMatieres()
    
    # la lista de alumnos
    def getEleves(self):
        return self.dao.getEleves()
    
    # la lista de notas
    def getNotes(self):
        return self.dao.getNotes()

  • líneas 3-5: el constructor recibe una referencia a la capa [dao]. La capa [métier] debe tener esta referencia. Aquí se le proporciona a través de su constructor. Se podrían imaginar otras soluciones. En una arquitectura por capas representada horizontalmente, cada capa debe tener una referencia a la capa que se encuentra a su derecha;
  • líneas 36-49: los métodos getClasses, getMatieres, getEleves y getNotes se limitan a delegar la llamada a los métodos del mismo nombre de la capa [dao];
  • línea 12: el método getStatsForEleve recibe como parámetro el número de matrícula del alumno para el que se deben generar los indicadores.
  • línea 17: se buscará al alumno en la lista de todos los alumnos;
  • líneas 18-20: el bucle de búsqueda;
  • línea 23: si no se ha encontrado al alumno, se lanza una excepción;
  • si no, línea 25, se almacena el alumno encontrado;
  • líneas 28-31: se buscan, entre todas las notas del colegio, aquellas que pertenecen al alumno memorizado;
  • una vez encontradas, se puede construir el objeto StatsForEleve solicitado.

7.5. La capa [console]

La capa [console] se implementa mediante el siguiente script:


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

# importación del módulo de entidades, del módulo [dao], del módulo [metier]
from entites import *
from dao import *
from metier import *

# ----------- capa [console]
# instanciación de la capa [metier]
metier=Metier(Dao())
# solicitud/respuesta
fini=False
while not fini:
    # pregunta
    print "numero de l'eleve (>=1 et * pour arreter) : "
    # respuesta
    reponse=raw_input()
    # ¿Terminado?
    if reponse.strip()=="*":
        break
    # ¿Es correcta la entrada?
    ok=False
    try:
        idEleve=int(reponse,10)
        ok=idEleve>=1
    except:
        pass
    # ¿Datos correctos?
    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)

  • línea 10: instanciación simultánea de las capas [dao] y [métier]. Esta es la única dependencia de nuestro código con respecto a la implementación de estas capas;
  • línea 34: se utiliza la interfaz de la capa [métier];
  • línea 19: el método strip elimina los espacios al principio y al final de la cadena;
  • línea 20: break permite salir de un bucle;
  • línea 24: se intenta convertir la cadena introducida en un entero decimal;
  • línea 29: ok es verdadero solo si se ha pasado por la línea 25;
  • línea 31: continue permite volver a entrar en el bucle en medio del mismo;
  • línea 34: cálculo de los indicadores;
  • línea 35: se intercepta la excepción RuntimeError que puede salir de la capa [métier].

He aquí un ejemplo de ejecución:

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