Skip to content

7. Arquitectura en capas y programación por interfaces

7.1. Introduction

Nos proponemos escribir una aplicación que permita mostrar las calificaciones de los alumnos de una escuela secundaria. Esta aplicación tendrá una arquitectura de múltiples capas:

  • la capa [présentation] es la que interactúa 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 provenientes del usuario a través de la capa [présentation] y de la capa SGBD a través de la capa [dao].
  • La capa [dao] (Objetos de acceso a datos) gestiona el acceso a los datos de la capa SGBD.

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

  • no habrá base de datos,
  • la capa [dao] administrará las entidades Eleve, Classe, Matiere y Note, lo que permitirá gestionar las calificaciones de los alumnos,
  • la capa [métier] permitirá calcular indicadores sobre las calificaciones de un alumno específico,
  • 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: Eleve, Classe, Matiere y Note.

El archivo [entites.py] agrupa cuatro clases.

La clase [Classe] representa una clase de la escuela 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 nom (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 materia 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 identifica por su número (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 caracteriza por su número (línea 5), el valor de la calificación (línea 6), una referencia al estudiante que tiene esa calificación (línea 7) y una referencia a la materia a la que corresponde la calificación (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 de la escuela secundaria;
  • getMatieres muestra la lista de materias;
  • getEleves muestra la lista de alumnos;
  • getNotes devuelve la lista de calificaciones.

La capa [métier] solo utilizará estos métodos. No necesita saber cómo están implementados. Estos datos pueden provenir 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):
        # se instancian las clases
        classe1=Classe(1,"classe1")
        classe2=Classe(2,"classe2")
        self.classes=[classe1,classe2]
        # las materias
        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 calificaciones
        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 materias
    def getMatieres(self):
        return self.matieres
    
    # lista de alumnos
    def getEleves(self):
        return self.eleves

    # lista de calificaciones
    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 manera fija cuatro listas:
    • líneas 10-12: la lista de clases;
    • líneas 14-16: la lista de materias;
    • líneas 18-22: la lista de alumnos;
    • líneas 24-32: la lista de calificaciones.
  • líneas 39-52: los cuatro métodos de la interfaz de la capa [dao] simplemente devuelven 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 la 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: el nombre de la clase y el 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 de la escuela secundaria;
  • getMatieres muestra la lista de materias;
  • getEleves muestra la lista de alumnos;
  • getNotes muestra la lista de calificaciones;
  • getStatsForEleve muestra las calificaciones del alumno n.º idEleve, así como información sobre ellas: promedio ponderado, calificación más baja y calificación más alta.

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

El método getStatsForEleve devuelve un objeto de tipo [StatsForEleve] como se indica a continuación:


class StatsForEleve:
    # fabricante
    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 del promedio del alumno
        self.moyennePonderee=float(sommePonderee)/sommeCoeff
        
    # toString
    def __str__(self):
        # caso del alumno sin calificaciones
        if len(self.notes)==0:
            return "Eleve={0}, notes=[]".format(self.eleve)
        # caso del alumno con calificaciones
        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 cual se calculan los indicadores;
    • una referencia a sus calificaciones, una lista de objetos [Note].
  • líneas 8-9: si la lista de calificaciones está vacía, no se continúa.
  • De lo contrario, en las líneas 11-25, se calculan los siguientes indicadores:
    • self.moyennePonderee: el promedio del alumno ponderado por los coeficientes de las materias;
    • 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 materias
maths=Matiere(1,"maths",1)
francais=Matiere(2,"francais",2)
anglais=Matiere(3,"anglais",3)
# las calificaciones del alumno en esas materias
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 en capas:

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


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

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

    # los indicadores en las calificaciones
    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 calificaciones
        notes=[]
        for note in self.getNotes():
            # se agregan a «notas» todas las calificaciones 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 materias
    def getMatieres(self):
        return self.dao.getMatieres()
    
    # la lista de alumnos
    def getEleves(self):
        return self.dao.getEleves()
    
    # la lista de calificaciones
    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 en 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 del alumno para el que se deben generar los indicadores.
  • línea 17: se buscará al estudiante en la lista de todos los estudiantes;
  • líneas 18-20: el bucle de búsqueda;
  • línea 23: si no se ha encontrado al estudiante, se genera una excepción;
  • de lo contrario, en la línea 25, se almacena el alumno encontrado;
  • líneas 28-31: se buscan, entre todas las calificaciones de la escuela secundaria, aquellas que pertenecen al alumno almacenado;
  • 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] y 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
    # ¿La entrada es correcta?
    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 tanto de la capa [dao] como de la [métier]. Esta es la única dependencia de nuestro código 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 inicio y al final de la cadena;
  • línea 20: break permite salir de un bucle;
  • línea 24: se intenta convertir la cadena ingresada 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 surgir de la capa [métier].

A continuación se muestra 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) :
*