7. Layered architecture and interface-based programming
7.1. Introduction
We propose to write an application that displays the grades of middle school students. This application will have a multi-layer architecture:
![]() |
- The [presentation] layer is the layer that interacts with the application user.
- The [business logic] layer implements the application's business rules, such as calculating a salary or an invoice. This layer uses data from the user via the [presentation] layer and from the DBMS via the [DAO] layer.
- The [DAO] (Data Access Objects) layer manages access to data in the DBMS.
We will illustrate this architecture with a simple console application:
- there will be no database,
- the [DAO] layer will manage the Student, Class, Subject, and Grade entities to handle student grades,
- the [business logic] layer will calculate metrics based on a specific student’s grades,
- the [presentation] layer will be a console application that displays the results calculated by the [business] layer.
The Visual Studio project for the application is as follows:
![]() |
7.2. The application's entities
Entities are objects. Here, we will have four classes for each of the entities: Student, Class, Subject, and Grade.
![]() |
The [entities.py] file contains four classes.
The [Class] class represents a middle school class:
class Class:
# constructor
def __init__(self, id, name):
# store the parameters
self.id = id
self.name = name
# toString
def __str__(self):
return "Class[{0},{1}]".format(self.id, self.name)
- lines 3–6: a class is defined by an ID (line 5) and a name (line 6);
- lines 9-10: the method for displaying the class.
The [Subject] class is as follows:
class Subject:
# constructor
def __init__(self, id, name, coefficient):
# store the parameters
self.id = id
self.name = name
self.coefficient = coefficient
# toString
def __str__(self):
return "Subject[{0},{1},{2}]".format(self.id, self.name, self.coefficient)
- lines 3-7: a subject is defined by its ID (line 5), its name (line 6), and its weight (line 7);
- lines 10-11: the method for displaying the subject.
The [Student] class is as follows:
class Student:
# constructor
def __init__(self, id, last_name, first_name, class):
# store the parameters
self.id = id
self.last_name = last_name
self.first_name = first_name
self.class = class
# toString
def __str__(self):
return "Student[{0},{1},{2},{3}]".format(self.id, self.first_name, self.last_name, self.class)
- lines 3–8: a student is characterized by their ID (line 5), last name (line 6), first name (line 7), and class (line 8). This last parameter is a reference to a [Class] object;
- lines 11-12: the method for displaying the student.
The [Grade] class is as follows:
class Note:
# constructor
def __init__(self, id, value, student, subject):
# store the parameters
self.id = id
self.value = value
self.student = student
self.subject = subject
# toString
def __str__(self):
return "Grade[{0},{1},{2},{3}]".format(self.id, self.value, self.student, self.subject)
- lines 3-8: a [Grade] object is characterized by its ID (line 5), the grade value (line 6), a reference to the student who received this grade (line 7), and a reference to the subject for which the grade was given (line 8);
- lines 11-12: the method for displaying the [Note] object.
7.3. The [dao] layer
![]() |
The [dao] layer will provide the following interface to the [business] layer:
- getClasses returns the list of middle school classes;
- getMatieres returns the list of subjects;
- getStudents returns the list of students;
- getGrades returns a list of grades.
The [business] layer will use only these methods. It does not need to know how they are implemented. This data can therefore come from various sources (hard-coded values, a database, text files, etc.) without affecting the [business] layer. This is known as interface-based programming.
The [Dao] class implements this interface as follows:
# -*- coding=utf-8 -*-
# import the entities module
from entities import *
class Dao:
# constructor
def __init__(self):
# instantiate the classes
class1 = Class(1, "class1")
class2 = Class(2, "class2")
self.classes = [class1, class2]
# subjects
subject1 = Subject(1, "subject1", 1)
subject2 = Subject(2, "subject2", 2)
self.subjects = [subject1, subject2]
# students
student11 = Student(11, "last_name1", "first_name1", class1)
student21 = Student(21, "last_name2", "first_name2", class1)
student32 = Student(32, "last_name3", "first_name3", class2)
student42 = Student(42, "last_name4", "first_name4", class2)
self.students = [student11, student21, student32, student42]
# grades
grade1 = Grade(1, 10, student11, subject1)
grade2 = Grade(2, 12, student21, subject1)
grade3 = Grade(3, 14, student32, subject1)
grade4 = Grade(4, 16, student42, subject1)
grade5=Grade(5,6,student11,subject2)
grade6=Grade(6,8,student21,subject2)
grade7=Grade(7,10,student32,subject2)
grade8 = Grade(8, 12, student42, subject2)
self.grades = [grade1, grade2, grade3, grade4, grade5, grade6, grade7, grade8]
#-----------
# interface
#-----------
# list of classes
def getClasses(self):
return self.classes
# list of subjects
def getSubjects(self):
return self.subjects
# list of students
def getStudents(self):
return self.students
# list of grades
def getGrades(self):
return self.grades
- line 4: we import the module containing the entities handled by the [DAO] layer;
- line 8: the constructor has no parameters. It hard-codes four lists:
- lines 10–12: the list of classes;
- lines 14–16: the list of subjects;
- lines 18–22: the list of students;
- lines 24–32: the list of grades.
- lines 39–52: the four methods of the [dao] layer interface simply return a reference to the four lists constructed by the constructor.
A test program might look like this:
# -*- coding=utf-8 -*-
# import the entities module and the [dao] layer
from entities import *
from dao import *
# instantiate the [dao] layer
dao = Dao()
# list of classes
for class in dao.getClasses():
print(class)
# list of subjects
for subject in dao.getSubjects():
print subject
# list of classes
for student in dao.getStudents():
print student
# list of classes
for grade in dao.getGrades():
print grade
The comments speak for themselves. Lines 11–24 use the [dao] layer interface. There are no assumptions here about the actual implementation of the layer. On line 8, we instantiate the [dao] layer. Here we make assumptions: class name and constructor type. There are solutions that allow us to avoid this dependency.
The results of running this script are as follows:
7.4. The [business] layer
![]() |
The [business] layer implements the following interface:
- getClasses returns the list of middle school classes;
- getMatieres returns the list of subjects;
- getStudents returns the list of students;
- getGrades returns a list of grades;
- getStatsForStudent returns the grades for student idStudent along with information about them: weighted average, lowest grade, highest grade.
The [presentation] layer will use only these methods. It does not need to know how they are implemented.
The getStatsForStudent method returns an object of type [StatsForStudent] as follows:
class StatsForStudent:
# constructor
def __init__(self, student, grades):
# store the parameters
self.student = student
self.grades = grades
# stop if there are no grades
if len(grades) == 0:
return
# calculate the weighted sum
weightedSum = 0
weightedSum = 0
self.max=-1
self.min=21
for note in notes:
value = note.value
coeff = grade.subject.coefficient
coeffSum += coeff
weightedSum += value * coeff
if value < self.min:
self.min = value
if value > self.max:
self.max = value
# Calculate the student's average
self.weightedAverage = float(weightedSum) / coefficientSum
# toString
def __str__(self):
# case of a student with no grades
if len(self.grades) == 0:
return "Student={0}, grades=[]".format(self.student)
# case of a student with grades
str=""
for grade in self.grades:
str+="{0} ".format(note.value)
return "Student={0}, grades=[{1}], max={2}, min={3}, average={4}".format(self.student, str, self.max, self.min, self.weightedAverage)
- Line 3: The constructor takes two parameters:
- a reference to the student of type [Student] for whom metrics are calculated;
- a reference to their grades, a list of [Grade] objects.
- lines 8-9: if the list of grades is empty, we stop here.
- Otherwise, lines 11–25: the following metrics are calculated:
- self.weightedAverage: the student's average weighted by the subject coefficients;
- self.min: the student's lowest grade;
- self.max: the student's highest grade.
- line 28: the method for displaying the class in the format specified in line 36.
A test script for this class could be as follows:
# -*- coding=utf-8 -*-
# import modules from entities, the [dao] layer, and the [business] layer
from entities import *
from dao import *
from business_logic import *
# a class
class1 = Class(1, "6th Grade A")
# a student in this class
paul_durand = Student(1, "durand", "paul", class1)
# three subjects
maths = Subject(1, "maths", 1)
francais = Subject(2, "French", 2)
english = Subject(3, "english", 3)
# grades in these subjects for the student
math_grade=Grade(1,10,paul_durand,maths)
french_grade=Grade(2,12,paul_durand,french)
english_grade=Grade(3,14,paul_durand,english)
# display the indicators
print StatsForStudent(paul_durand,[math_score, french_score, english_score])
The screen output is as follows:
Student = Student[1, paul, durand, Class[1, 6th Grade A]], grades = [10 12 14], max = 14, min = 10, average = 12.6666666667
Let’s return to our layered architecture:
![]() |
The [Business] class implements the [business] layer as follows:
class Business:
# constructor
def __init__(self, dao):
# store the reference to the [dao] layer
self.dao = dao
#-----------
# interface
#-----------
# indicators for grades
def getStatsForStudent(self, studentId):
# Stats for the student with ID studentID
# search for student
found=False
i = 0
students = self.getStudents()
while not found and i < len(students):
found = students[i].id == studentId
i+=1
# Did we find it?
if not found:
raise RuntimeError("Student [{0}] does not exist".format(studentId))
else:
student = students[i-1]
# list of all grades
grades = []
for note in self.getNotes():
# Add all of the student's notes to notes
if note.student.id == studentId:
notes.append(note)
# return the result
return StatsForStudent(student, notes)
# The list of classes
def getClasses(self):
return self.dao.getClasses()
# the list of subjects
def getSubjects(self):
return self.dao.getSubjects()
# the list of students
def getStudents(self):
return self.dao.getStudents()
# the list of grades
def getGrades(self):
return self.dao.getGrades()
- lines 3–5: the constructor receives a reference to the [dao] layer. The [business] layer must have this reference. Here, we provide it via its constructor. Other solutions are possible. In a horizontally represented layered architecture, each layer must have a reference to the layer to its right;
- lines 36-49: the methods getClasses, getSubjects, getStudents, and getGrades simply delegate the call to the methods of the same names in the [dao] layer;
- line 12: the getStatsForStudent method receives as a parameter the student ID for whom statistics are to be returned.
- line 17: the student will be searched for in the list of all students;
- lines 18–20: the search loop;
- line 23: if the student is not found, an exception is thrown;
- otherwise, line 25: the found student is stored;
- lines 28–31: we search through all the middle school grades for those belonging to the stored student;
- once they are found, the requested StatsForEleve object can be constructed.
7.5. The [console] layer
![]() |
The [console] layer is implemented by the following script:
# -*- encoding=utf-8 -*-
# Import the entities module, the [dao] module, and the [business] module
from entities import *
from dao import *
from business_logic import *
# ----------- [console] layer
# instantiate the [business] layer
business = Business(Dao())
# request/response
finished=False
while not finished:
# question
print "Student number (>=1 and * to stop): "
# answer
answer = raw_input()
# done?
if response.strip() == "*":
break
# Is the input valid?
ok = False
try:
studentId = int(answer, 10)
ok = studentId >= 1
except:
pass
# Is the answer correct?
if not ok:
print "Incorrect input. Please try again..."
continue
# calculation
try:
print job.getStatsForStudent(studentId)
except RuntimeError, error:
print "The following error occurred: {0}".format(error)
- line 10: instantiation of both the [dao] and [business] layers. This is the only dependency our code has on the implementation of these layers;
- line 34: we use the interface of the [business] layer;
- Line 19: The strip method removes leading and trailing spaces from the string;
- line 20: break exits a loop;
- line 24: attempts to convert the entered string to a decimal integer;
- line 29:
okis true only if line 25 has been executed; - line 31: continue allows the loop to restart from the middle;
- line 34: calculation of indicators;
- line 35: catches the RuntimeError exception that may originate from the [business] layer.
Here is an example of execution:






