Skip to content

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:

Class[1,class1]
Class[2,class2]
Subject[1,subject1,1]
Subject[2,subject2,2]
Student[11,first_name1,last_name1,Class[1,class1]]
Student[21,firstName2,lastName2,Class[1,class1]]
Student[32,firstName3,lastName3,Class[2,class2]]
Student[42,firstName4,lastName4,Class[2,class2]]
Grade[1,10,Student[11,firstName1,lastName1,Class[1,class1]],Subject[1,subject1,1]]
Grade[2,12,Student[21,firstName2,lastName2,Class[1,class1]],Subject[1,subject1,1]]
Grade[3,14,Student[32,first_name3,last_name3,Class[2,class2]],Subject[1,subject1,1]]
Grade[4,16,Student[42,firstName4,lastName4,Class[2,class2]],Subject[1,subject1,1]]
Grade[5,6,Student[11,firstName1,lastName1,Class[1,class1]],Subject[2,subject2,2]]
Grade[6,8,Student[21,firstName2,lastName2,Class[1,class1]],Subject[2,subject2,2]]
Grade[7,10,Student[32,firstName3,lastName3,Class[2,class2]],Subject[2,subject2,2]]
Grade[8,12,Student[42,firstName4,lastName4,Class[2,class2]],Subject[2,subject2,2]]

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

student number (>=1 and * to stop):
xx
Incorrect input. Please try again...
Student number (>=1 and * to stop):
-4
Incorrect entry. Please try again...
Student number (>=1 and * to stop):
11
Student = Student[11, first_name1, last_name1, Class[1, class1]], grades = [10 6 ], max = 10, min = 6, avg
average=7.33333333333
Student ID (>=1 and * to stop):
111
The following error occurred: Student [111] does not exist
student number (>=1 and * to stop):
*