14. Layered architecture and interface-based programming
14.1. Introduction
We propose to write an application that displays the grades of middle school students. This application can have a multi-layer architecture:

- the [ui] (User Interface) layer is the layer that interacts with the application’s user;
- the [business] 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 (Database Management System).
This is the architecture that was used in the |Python 2 course|. A variant can also be introduced:

The differences from the previous layered structure are as follows:
- a main script called [main] above organizes the instantiation of the layers;
- the [ui, business, dao] layers no longer necessarily communicate with each other. If they need to, the [main] script provides them with the references to the layers they require;
The code here is organized into functional areas with a central coordinator:
- the orchestrator is the main script [main];
- the [ui], [dao], and [business] layers are the centers of expertise;
We could call this structure an orchestral organization.
14.2. Example 1
We will illustrate the layered architecture using 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] layer will calculate metrics based on a specific student’s grades;
- the [ui] layer will be a console application that displays student results;
The PyCharm project for the application is as follows:
![]() |
Note: The folders in blue are part of the [Root Sources] of the PyCharm project.
14.2.1. The application's entities
We will refer to classes whose sole role is to encapsulate data as entities. Dictionaries could be used for this purpose. The advantage of a class is that it allows us to test the validity of the data stored in the object and to provide a method that returns the object’s identity as a string.
![]() |
14.2.1.1. The [Class] entity
The [Class] entity (Class.py) represents a middle school class:
Notes
- line 7: the [Class] entity derives from the [BaseEntity] entity discussed in the section |The BaseEntity class|;
- lines 11–16: a class is defined by an ID and a name (line 16). The [id] property is provided by the [BaseEntity] class and the name by the [Class] class;
- lines 18–30: getter/setter for the [name] attribute;
14.2.1.2. The [Subject] entity
The [Subject] class (subject.py) is as follows:
Notes
- line 7: the [Class] class derives from the [BaseEntity] class;
- lines 11–17: a subject is defined by its ID [id], its name [name], and its weight [coefficient];
- lines 19–50: getters/setters for the class attributes;
14.2.1.3. The [Student] entity
The [Student] class (student.py) is as follows:
Notes
- line 9: the [Student] class derives from the [BaseEntity] class;
- lines 13–20: A student is characterized by their ID [id], last name [lastName], first name [firstName], and class [class]. The latter parameter is a reference to a [Class] object;
- lines 22–65: getters/setters for the class attributes;
14.2.1.4. The [Note] entity
The [Note] class (note.py) is as follows:
Notes
- line 8: the [Note] class derives from the [BaseEntity] class;
- lines 12–20: A [Note] object is characterized by its ID [id], the grade value [value], a reference [student] to the student who received this grade, and a reference to the subject [subject] associated with the grade;
- lines 22–75: getters/setters for the class attributes;
14.2.2. Application Configuration
![]() |
The [config.py] file configures the environment for the main script [main] (1) as well as for the tests (2). All these scripts have an [import config] statement at the beginning of the code. Note that the directory containing the script targeted by the [python script] command is automatically part of the Python Path. Therefore, if [config] is in the same directory as the scripts containing the [import config] statement, it will be found. Files [1] and [2] are identical here. This may not always be the case.
The [config.sys] file is as follows:
- Lines 11–14: the directories that must be part of the Python Path (sys.path);
- The directory [f"{root_dir}/02/entities"] provides access to the classes [BaseEntity] and [MyException];
- the folder [f"{script_dir}/../entities"] provides access to the classes [Student], [Class], [Subject], [Grade];
- the folder [f"{script_dir}/../interfaces"] provides access to the application's interfaces;
- the folder [f"{script_dir}/../services"] provides access to the classes implementing the interfaces;
14.2.3. Entity Testing
![]() |
Here, we will write tests executed by a tool called [unittest]. PyCharm comes with several testing frameworks. You can choose one of them in the PyCharm configuration:

- in [4], several testing frameworks are available:

14.2.3.1. The [TestBaseEntity] test class
The [TestBaseEntity] test script will be as follows:
Notes
- line 1: we import the [unittest] module, which provides the various testing methods;
- lines 3–6: We configure the application so that the classes needed for testing can be found;
- line 9: a [unittest] test class must extend the [unittest.TestCase] class;
- lines 11, 27: test functions must have a name starting with [test], otherwise they will not be recognized;
- lines 13–16: we import the classes we need;
- In this test class, we want to verify the behavior of the methods [BaseEntity.fromdict] (line 34) and [BaseEntity.fromjson] (line 18). The [Note] class has properties that are references to other classes. We want to verify that the two preceding methods create valid [Note] objects;
- line 18: we create a [Note] object from a JSON object;
- Line 21: We verify that the created object is indeed of type [Note]. The [assertIsInstance] method is a method of the [unittest.TestCase] class, which is the parent class of the [TestBaseEntity] class;
- line 22: we verify that [note.student] is indeed of type [Student];
- line 23: we verify that [note.student.class] is indeed of type [Class];
- line 24: we verify that [note.subject] is indeed of type [Subject];
- lines 33–42: we do the same with the [BaseEntity.fromdict] method;
There are several ways to run the tests:
![]() |
- in [1-2], we run [TestBaseEntity] using the [UnitTest] framework;
- in [3-5], the tests fail. [UnitTests] indicates that it found no tests to run;
The tests fail due to the structure of the [TestBaseEntity] code:
What causes issues for the [UnitTest] framework is the presence of executable code (lines 3–6) before the definition of the test class (line 9).
We therefore reorganize the code as follows:
- Lines 6–10: We define a [setUp] function. This function has a specific role: it is executed before each test function (test_note1, test_note2);
Once this is done, executing the [TestBaseEntity] class yields the following results:
![]() |
This time, both test methods were executed and the tests passed.
Let's see what happens when a test fails. Let's modify the code in [test_note1] as follows:
- line 2: we check that 1==2;
The results of the execution are as follows:
![]() |
You can find out the cause of the error by clicking on the failed test [2]:
![]() |
- in [7-8], the cause of the error;
Another way to run a test class is to run it in a terminal:
![]() |
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests>python -m unittest TestBaseEntity.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.026s
OK
Line 6 indicates that both tests passed (we removed the 1==2 error);
Finally, a third way to run the [TestBaseEntity] test class, still in a terminal, is as follows. We end the test class with the following lines 6–7;
…
self.assertIsInstance(note.élève.classe, Classe)
self.assertIsInstance(note.matière, Matière)
if __name__ == '__main__':
unittest.main()
- line 6: the variable [__name__] is the name given to the script being executed. When the script is the one launched by the command [python script.py], the variable [__name__] is [__main__] (2 underscores before and after the identifier). Thus, line 7 is executed only when the [TestBaseEntity] script is launched by the command [python TestBaseEntity.py]. The statement [unittest.main()] launches the execution of the script via the [UnitTest] framework. Here is an example:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests>python TestBaseEntity.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.013s
OK
14.2.3.2. The [TestEntities] test class
The [TestEntities] test class is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | |
- The purpose of the test script is to test the class setters: to verify that incorrect values cannot be assigned to the attributes of the various entities;
- lines 11–24: we test that an invalid ID cannot be assigned to a student. Since we pass the value 'x' on line 16 as the student’s ID, we expect an exception to occur. We should therefore proceed to lines 20–22;
- line 21: display the error message;
- line 22: retrieve the error code (see section |The MyException Entity|);
- line 24: we verify (assert) that the error code is 1. Here, we verify two things:
- that an error actually occurred;
- that the error code is 1;
- this process is repeated with the functions in lines 24–213;
- lines 215–222: we test whether an action throws an exception of a certain type;
- line 220: we indicate that the test is successful if it throws an exception of type [MyException];
Results
We run the test script:
![]() |
The results obtained are as follows:
Testing started at 09:39 ...
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py" --path C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestEntités.py
Launching unittests with arguments python -m unittest C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestEntités.py in C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests
code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Elève.Elève'> doit être un entier >=0]
code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Classe.Classe'> doit être un entier >=0]
code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Matière.Matière'> doit être un entier >=0]
code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Note.Note'> doit être un entier >=0]
code erreur=21, message=MyException[21, Le nom de la matière 1 doit être une chaîne de caractères non vide]
code erreur=22, message=MyException[22, Le coefficient de la matière y doit être un réel >=0]
code erreur=31, message=MyException[31, L'attribut x de la note 1 doit être un nombre dans l'intervalle [0,20]]
code erreur=32, message=MyException[32, L'attribut [y] de la note 1 doit être de type Elève ou dict ou json. Erreur : Expecting value: line 1 column 1 (char 0)]
code erreur=33, message=MyException[33, L'attribut [z] de la note 1 doit être de type Matière ou dict ou json. Erreur : Expecting value: line 1 column 1 (char 0)]
code erreur=41, message=MyException[41, Le nom de l'élève 1 doit être une chaîne de caractères non vide]
code erreur=42, message=MyException[42, Le prénom de l'élève 1 doit être une chaîne de caractères non vide]
code erreur=43, message=MyException[43, L'attribut [t] de l'élève 1 doit être de type Classe ou dict ou json. Erreur : Expecting value: line 1 column 1 (char 0)]
Ran 14 tests in 0.040s
OK
Process finished with exit code 0
14.2.4. The [dao] layer

The [dao] layer implements the [InterfaceDao] interface [1]. This is implemented by the [Dao] class (2). The [tests_dao] script (3) tests the methods of the [dao] layer.
14.2.4.1. Interface [InterfaceDao]
An interface is a contract between calling code and called code. It is the called code that provides the interface:
![]() |
- the calling code [1] does not know the implementation of the called code [3]. It only knows how to call it. The interface [2] tells it how. This interface defines a set of methods/functions to be used to interact with the called code. This interface is also known as an API (Application Programming Interface);
The [dao] layer will provide the following interface:
- [get_classes] returns the list of middle school classes;
- [get_subjects] returns the list of subjects taught at the middle school;
- [get_students] returns the list of students at the middle school;
- [get_grades] returns a list of all students’ grades;
- [get_grades_for_student_by_id] returns the grades for a specific student;
- [get_student_by_id] returns a student identified by their ID;
The calling code will only use these methods. It does not need to know how they are implemented. The data can then come from different sources (hard-coded, from a database, from text files, etc.) without affecting the calling code. This is called interface-based programming.
Python 3 has a concept similar to that of an interface: the abstract class. We will use it. We will group the interfaces for this example in the [interfaces] folder.
We define an abstract class [InterfaceDao] (InterfaceDao.py) for the [dao] layer:
Notes:
- line 2: ABC = Abstract Base Class. We import the ABC class from the [abc] module, as well as the [abstractmethod] decorator used on lines 10, 15, 20, 25, 30, and 35;
- line 8: the abstract class is called [InterfaceDao] and derives from the [ABC] class;
- the methods of the abstract class are decorated with the [@abstractmethod] decorator, which makes the decorated method an abstract method: its code is not defined. However, we include code there: the [pass] statement, which does nothing;
- the abstract class [InterfaceDao] cannot be instantiated. Only classes derived from [InterfaceDao] that have implemented all the methods of [InterfaceDao] can be instantiated. Therefore, if we create two classes [Dao1] and [Dao2] derived from the class [InterfaceDao], they will both implement the abstract methods of [InterfaceDao]. We could thus say that they implement the interface [InterfaceDao];
- languages that support both interfaces and abstract classes assign a different role to the interface than to the abstract class. An interface has no attributes and cannot be instantiated. A class can implement an interface by defining all of its methods;
14.2.4.2. [Dao] Implementation
The [Dao] class (dao.py) implements the [InterfaceDao] interface as follows:
Notes:
- lines 1-7: we import the entities and the [InterfaceDao] interface;
- line 11: the [Dao] class derives from the abstract class [InterfaceDao]. We say that it implements the [InterfaceDao] interface;
- line 14: the constructor has no parameters. It hard-codes four lists:
- lines 15–18: the list of classes;
- lines 19–22: the list of subjects;
- lines 23–28: the list of students;
- lines 29–38: the list of grades;
- lines 40–44: implementation of the methods of the [Dao Interface]. Here, we do not define them to see the error message issued by Python;
A test program could look like this [tests-dao.py]:
Note: The [tests-dao.py] script is not a [unittest] because it does not contain any methods whose names begin with [test_].
The comments are self-explanatory. Lines 11–25 use the [dao] layer interface. There are no assumptions here about the actual implementation of the layer. On line 9, we instantiate the [dao] layer.
The results of running this script are as follows:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/tests_dao.py
Traceback (most recent call last):
File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/tests_dao.py", line 9, in <module>
daoImpl = Dao()
TypeError: Can't instantiate abstract class Dao with abstract methods get_classes, get_matières, get_notes, get_notes_for_élève_by_id, get_élève_by_id, get_élèves
Process finished with exit code 1
We see that an error occurs as soon as the [Dao] class is instantiated (line 3 above). The Python 3 interpreter tells us that it cannot instantiate the class because we have not defined the abstract methods [get_classes, get_subjects, get_grades, get_grades_for_student_by_id, get_student_by_id, get_students].
PyCharm also supports abstract classes and offers to define their methods:
![]() |
- in [1], right-click on the code;
- in [2-3], select [Generate / Implement Methods] to implement the missing methods of the [Dao] class;
- in [4], select the methods to implement—in this case, all of them;
Once this is done, PyCharm completes the [Dao] class as follows:
We complete the [Dao] class as follows:
- Lines 5–19 are straightforward;
- lines 29–36: the method that returns the student whose ID is passed. If the student does not exist, an exception is raised;
- line 31: the [filter] function allows you to filter a list:
- the first parameter is the filtering criterion;
- the second parameter is the list to be filtered, in this case the list of students;
- line 31: the filtering criterion for the list is implemented using a function [f(e:Student) -> bool]. This is applied to each element of the list to be filtered. If the element satisfies the filtering criterion, it is retained in the filtered list; otherwise, it is excluded. Here, we can either:
- specify the name of the function f and implement it elsewhere. The call to the [filter] function then becomes [filter(f, self.get_students)];
- provide the definition of the function f. The call to the [filter] function then becomes [filter(f(e :Student){…}, self.get_students())], where [e] represents an element of the filtered list, i.e., a student. This is what has been done here. The definition of the function f here would be [f(e :Student){return e.id == student_id)]: a student is selected only if their ID number [id] matches the one being searched for. Such a function can be replaced by a so-called lambda function: [lambda e: e.id == student_id]:
- e: represents the parameter of the function f, here a student. You can use any name you like;
- e.id==student_id is the filtering criterion: a student [e] is selected only if their ID [id] matches the one being searched for;
- line 31: the [filter] function returns the filtered list as a type that is not of type [list] but can be converted to type [list]. This is what we do here with the expression [list(filtered_list)];
- lines 33–34: if the filtered list is empty, it means the student being searched for does not exist. An exception is then raised;
- line 36: if we reach this point, it means no exception was thrown. We then know that we have retrieved a list with 1 element (there are no two students with the same [id] number). We therefore return the first element of the list;
- lines 21–27: the [get_notes_for_élève_by_id] method must return the grades for the student whose [id] is passed to it;
- Lines 22–23: We start by looking up the student with ID [student_id] using the [get_student_by_id] method, which we just commented out. An exception may occur if the student we’re looking for does not exist. Since there is no try/catch block around the statement on line 23, the exception will be propagated to the calling code. This is the desired behavior;
- Lines 24–25: Once the student is retrieved, we retrieve all their grades. We do this again using a filter:
- the filter is [filter(criterion, self_getnotes()]. The list to be filtered is therefore the list of all grades for all students in the school;
- the filtering criterion is expressed using a [lambda] function: lambda n: n.student.id == student_id. The parameter n is an element of the list to be filtered, i.e., a grade. The [Note] type has a [student] property that represents the student who owns the grade. Therefore, [n.student.id], which represents that student’s ID, must be equal to the ID of the student we’re looking for;
Then we run the [tests-dao.py] script.
We then get the following results:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/tests_dao.py
{"id": 1, "nom": "classe1"}
{"id": 2, "nom": "classe2"}
{"id": 1, "nom": "matière1", "coefficient": 1}
{"id": 2, "nom": "matière2", "coefficient": 2}
{"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
{"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom": "classe1"}}
{"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom": "classe2"}}
{"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom": "classe2"}}
{"id": 1, "valeur": 10, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 2, "valeur": 12, "élève": {"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 3, "valeur": 14, "élève": {"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 4, "valeur": 16, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 5, "valeur": 6, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 6, "valeur": 8, "élève": {"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 7, "valeur": 10, "élève": {"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 8, "valeur": 12, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
élève n° 11 = {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
note de l'élève n° 11 = {"id": 1, "valeur": 10, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
note de l'élève n° 11 = {"id": 5, "valeur": 6, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
Process finished with exit code 0
Note that when displaying a grade (the process is similar for other objects), we also have:
- the student associated with the grade;
- the subject referenced by the grade;
This result is produced by the [BaseEntity.asdict] function (see the "link" section).
14.2.5. The [business] layer
![]() | ![]() |
- [InterfaceMétier] is the interface of the [business] layer;
- [Business] is the implementation class of the [business] layer;
- [TestBusiness] is a [UnitTest] class for testing the [Business] class;
14.2.5.1. Interface [BusinessInterface]
The [business] layer will implement the following [BusinessInterface] interface (BusinessInterface.py):
- [get_stats_for_student] returns the grades for student idStudent along with information about them: weighted average, lowest grade, highest grade. This information is encapsulated in an object of type [StudentStats];
14.2.5.2. The [StatsForStudent] entity
The [StatsForStudent] type (StatsForStudent.py), which encapsulates a student’s statistics (grades, min, max, weighted average), is as follows:
Notes:
- line 8: the [StatsForStudent] class derives from the [BaseEntity] class;
- lines 13–22: the class properties;
- an identifier [id] from [BaseEntity];
- the student [student] whose statistics are encapsulated;
- their grades [grades];
- their weighted average [weighted_average];
- their lowest grade [min];
- their maximum grade [max];
- We do not define getters/setters for these attributes. We assume that the [business] layer creates objects of this type and that it does not create invalid objects;
- lines 23–33: the [__str__] function returns a string containing the object’s properties;
14.2.5.3. The [Business] implementation
The [Business] implementation (Metier.py) of the [BusinessInterface] interface will be as follows:
Notes
- line 7: the [Métier] class derives from the [InterfaceMétier] class. It is customary to say that it implements the [InterfaceMétier] interface;
- lines 9–12: the constructor takes a single parameter, a reference to the [dao] layer. On line 10, note that we have assigned the type [InterfaceDao] to the [dao] parameter. We do not expect a specific implementation, but simply an implementation that respects the [DaoInterface] interface. Here, it does not matter since Python will not take this type into account, but it is good practice to work with interfaces rather than specific implementations. The code is then easier to modify;
- lines 19–60: implementation of the [get_stats_for_élève] method;
- line 19: the method receives a single parameter, the [idElève] of the student for whom we want the statistics;
- line 24: we request the student’s grades from the [dao] layer. This request results in an exception if the student does not exist. This exception is not handled (no try/catch) and is therefore propagated back to the calling code;
- line 25: we reach this point if no exception occurred. [student_grades] is then a dictionary with two keys [student, grade]:
- line 25: We retrieve information about the student (their name, class, etc.);
- line 26: we retrieve their grades;
- lines 28–31: we check if the student has any grades. If they don’t, there are no statistics to calculate;
- line 31: we return an object [StatsForStudent] constructed from a dictionary using the method [BaseEntity.fromdict];
- lines 33–54: use the student’s grades to calculate the requested statistics. The code comments should be sufficient for understanding;
- lines 56–60: we return a [StatsForStudent] object constructed from a dictionary using the [BaseEntity.fromdict] method;
14.2.5.4. Testing the [business] layer
A [UnitTest] script for the [business] layer could look like this (TestMétier.py):
Notes
- lines 6–9: the [setUp] function is used here to configure the test’s Python path;
- line 16: we instantiate the [dao] layer;
- line 17: we instantiate the [business] layer and use its [get_stats_for_student] method to calculate the statistics for student #11;
- line 19: the resulting [StatsForStudent] is displayed. Since [StatsForStudent] derives from [BaseEntity], the JSON string of [StatsForStudent] is displayed here;
- line 21: we check the student’s minimum grade;
- line 22: we check their maximum grade;
- line 23: we test that the weighted average is 7.333, accurate to 10⁻³. In general, it is not possible to compare real numbers exactly because internally, they are usually only represented as approximations;
The test results are as follows:
Testing started at 18:17 ...
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py" --path C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestMétier.py
Launching unittests with arguments python -m unittest C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestMétier.py in C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests
Ran 1 test in 0.015s
OK
stats=Elève={"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, notes=[10 6], max=10, min=6, moyenne pondérée=7.33
Process finished with exit code 0
14.2.6. The [ui] layer

- in [1], the interface of the [ui] layer;
- in [2], the implementation of this interface;
- in [3], the application's main script;
14.2.6.1. Interface [InterfaceUi]
The interface of the [UI] layer will be as follows:
Notes
- lines 9-10: the [UI] layer will have only one method, [run];
14.2.6.2. The [Console] implementation
The [console] layer is implemented by the following [Console.py] script:
- lines 3-5: import all interfaces;
- line 11: the [Console] class implements the [InterfaceUi] interface;
- lines 12-17: the constructor of the [Console] class receives a reference to the [business] layer as a parameter. Note that we have given this parameter the type [BusinessInterface] to emphasize that we are working with interfaces rather than specific implementations;
- line 24: implementation of the [run] method of the interface;
- line 27: a loop that stops when the condition on line 31 is met;
- line 29: input of data typed on the keyboard. The [input] function receives an optional parameter: the message to display on the screen requesting the input. This input is always retrieved as a string. The [strip] function removes any leading or trailing whitespace from the string;
- lines 34–39: we verify that the input, a student ID, is valid. It must be an integer >= 1. Recall that the input was entered as a string;
- line 36: we attempt to convert the input to a base-10 integer. The [int] function raises an exception if this is not possible;
- line 37: we reach this point only if no exception occurred. We verify that the retrieved integer is indeed >=1;
- lines 38–39: we handle the exception. If an exception occurred, the [ok] variable from line 34 remains set to [False];
- lines 41–43: if the input was incorrect, an error message is displayed and the loop is restarted (line 43);
- lines 45–48: we calculate the statistics for the student whose ID was entered;
- line 46: the [get_stats_for_student] method from the [business] layer is used. This method throws an exception if the student does not exist. This exception is handled on lines 47–48. We know that the [DAO] and [business] layers throw the [MyException] exception;
14.3. The main script [main]
The main script [main] is as follows (main.py):
- lines 1–4: configure the application’s Python Path;
- lines 6-9: import the classes and interfaces needed;
- line 14: instantiate the [DAO] layer;
- line 16: instantiate the [business] layer;
- line 18: instantiate the [ui] layer;
- line 20: initiate the user interface;
- lines 13–20: normally, no exceptions are thrown from these lines. Any exceptions propagating from the [DAO] and [business] layers are caught by the [Console] layer. Exception handling is a difficult art when you don’t fully understand the layers being used (which is not the case here). In case of doubt, code can be added to catch any type of exception that might be thrown by the executing code. This is what is done here, lines 21–23. We catch any exception deriving from [BaseException], i.e., all exceptions;
- lines 24–25: the [finally] clause does nothing here. It is only there to allow lines 21–23 to be commented out. Indeed, in debug mode, it is not advisable to catch exceptions. In this case, the Python interpreter catches them and then reports the line number where the exception occurred. This is essential information. When lines 21–23 are commented out, the presence of lines 24–25 ensures a syntactically correct try/catch block. Without them, Python raises an error;
Here is an example of execution:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/main/main.py
Numéro de l'élève (>=1 et * pour arrêter) : 11
Elève={"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, notes=[10 6], max=10, min=6, moyenne pondérée=7.33
Numéro de l'élève (>=1 et * pour arrêter) : 1
L'erreur suivante s'est produite : MyException[10, L'élève d'identifiant 1 n'existe pas]
Numéro de l'élève (>=1 et * pour arrêter) : *
Process finished with exit code 0
14.4. Example 2
This new example of layered architectures aims to demonstrate the benefits of interface-based programming. This approach facilitates application maintenance and testing. We will again use a three-layer architecture:

Each layer will be implemented in two different ways. We want to show that the implementation of a layer can be easily changed with minimal impact on the others.
14.4.1. The [dao] layer

The [InterfaceDao] interface is as follows:
- lines 8–10: the method [do_something_in_dao_layer] is the only method of the interface;
The [DaoImpl1] class implements the [InterfaceDao] interface as follows:
The [DaoImpl2] class implements the [InterfaceDao] interface as follows:
14.4.2. The [business] layer

The [BusinessInterface] interface is as follows:
- lines 8–10: the method [do_something_in_business_layer] is the only method in the interface;
The [AbstractBaseMétier] class implements the [InterfaceMétier] interface as follows:
- line 8: the [AbstractBaseMétier] class derives two classes:
- [BusinessInterface]: the class [AbstractBusinessBase] implements this interface on lines 19–22. In fact, we see that it has not implemented the method [do_something_in_business_layer], which it has declared as abstract (line 20). It will be up to the derived classes to implement the method;
- [ABC] to access the [@abstractmethod] annotations;
- the order matters: if we reverse it here, Python raises a runtime error;
This is the first time we’ve used multiple inheritance (inheriting from multiple classes). The [AbstractBaseMétier] class inherits properties from both the [InterfaceMétier] and [ABC] classes.
- Lines 9–17: We define the [dao] property, which will be a reference to the [dao] layer;
An interface is intended to be implemented. When different implementations share properties, it is useful to place these in a parent class to avoid duplication. This is the case here with the [dao] property. The parent class is generally always abstract because it does not implement all the methods of the interface.
The [BusinessImpl1] class implements the [BusinessInterface] interface as follows:
- line 4: the class [BusinessImpl1] derives from the class [AbstractBusinessBase]. It therefore inherits the [dao] property from this class;
- lines 6–9: implementation of the [BusinessInterface] interface that the parent class [AbstractBusinessBase] did not implement;
- line 9: the [dao] layer is used;
The class [BusinessImpl2] implements the interface [BusinessInterface] in a similar way:
14.4.3. The [ui] layer

The [InterfaceUi] interface is as follows:
- lines 8–10: the interface’s single method;
The [AbstractBaseUi] class implements the [InterfaceUi] interface as follows:
- The [AbstractBaseUi] class is an abstract class (line 20). It must be derived from to implement the [InterfaceUi] interface;
- lines 9–17: the [AbstractBaseUi] class has a reference to the [business] layer;
The implementation class [UiImpl1] is as follows:
- line 4: the class [UiImpl1] derives from the class [AbstractBaseUi] and therefore inherits its [business] property. This is used on line 9;
The implementation class [UiImpl2] is similar:
- Line 4: The class [UiImpl2] derives from the class [AbstractBaseUi] and therefore inherits its [business] property. This is used on line 9;
14.4.4. The configuration files

- The [config1, config2] files configure the application in two different ways;
- The [main] file is the application’s main script;
The [config1] file is as follows:
- lines 2–16: configuration of the application’s Python Path;
- lines 18–31: instantiation of the [DAO, business, UI] layers. To implement their interfaces, we choose the first built implementation each time;
- lines 33–35: we add the layer references to the configuration. Here, the main script only needs the [ui] layer;
The [config2] file is similar and implements each interface with the second available implementation:
14.4.5. The main script [main]

The main script is as follows:
This script takes one parameter:
- [config1] to use configuration #1;
- [config2] to use configuration #2;
Python stores the parameters in a list [sys.argv]:
- sys.argv[0] is the name of the script, here [main]. This parameter is always present;
- sys.argv[1] is the first parameter passed to the script, sys.argv[2] is the second, …
- line 8: we retrieve the number of parameters;
- lines 9–11: we check that there is indeed an argument and that its value is either [config1] or [config2]. If this is not the case, an error message is displayed (line 10) and we exit the program (line 11);
Once the desired configuration is known, we need to execute that configuration. For example, if configuration 1 was chosen, we need to execute the code:
The problem here is that the configuration to be used is stored in a variable, namely [sys.argv[1]. To import a module whose name is stored in a variable, we need to use the [importlib] package (line 2).
- Line 14: We import the module whose name is in [sys.argv[1]
- line 15: once this is done, we execute the [configure] function of this module. We retrieve a dictionary [config] which is the application’s configuration;
- line 18: we know that a reference to the [ui] layer is in config['ui']. We use it to call the [do_something_in_ui_layer] method. We know that this method will call a method in the [business] layer, which in turn will call a method in the [dao] layer;
For example, the [do_something_in_ui_layer] function is as follows:
- Line 6 above uses the [business] property of the [UiImpl1] class, line 1. However, in the [config1] configuration, the following was written:
# métier
métier = MétierImpl1()
métier.dao = dao
# ui
ui = UiImpl1()
ui.métier = métier
- Line 6: The [business] property of [UIImpl1] is a reference to the [BusinessImpl1] class (line 2). Therefore, the [do_something_in_ui_layer] method of the [BusinessImpl1] class will be executed;
In the [MétierUiImpl1] class, it is written:
- Line 6: the method called by the [ui] layer in turn calls a method of the [dao] property of the [BusinessImpl1] class;
However, in the [config1] configuration, the following was written:
# dao
dao = DaoImpl1()
# métier
métier = MétierImpl1()
métier.dao = dao
- line 5: the property [BusinessImpl1.dao] is of type [DaoImpl1] (line 2);
What we want to show here is that the [main] script does not need to concern itself with the [business] and [DAO] layers. It only needs to concern itself with the [UI] layer, as the connections between this layer and the others have been established through configuration.

To pass the [config1] or [config2] parameter to the [main] script, proceed as follows:

- in [1-2], create what is called a runtime configuration;
- in [3], give this configuration a name so you can find it later;
- in [4], select the script to run. If you followed the procedure in [1-2], the correct script has already been selected;
- in [5], enter the parameters to be passed to the script here. Here, we pass the string [config1] to instruct the script to use configuration #1;
- In [6], you confirm the execution configuration;

- In [1-2], view the existing execution contexts;
- in [3], select the existing execution context and duplicate it [4];

- in [5], the name given to the new configuration. This is the configuration that runs the [main] script [6] by passing it the [config2] parameter [7];
The execution configurations are available in the top-right corner of the PyCharm window:

Simply select [2] or [3] and then click [4] to run the [main] script with either the [config1] or [config2] parameter.
With [config1], running [main] yields the following results:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v02/main/main.py config1
34
Process finished with exit code 0
With [config2], running [main] yields the following results:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v02/main/main.py config2
-10
Process finished with exit code 0
The reader is invited to verify these results.













