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






