Further Improving Design With Composition
00:00 One problem you might run into when using composition is that your composite class might start getting crowded with many different components that all need to be initialized.
00:13 This can be cumbersome and might make the composite class hard to use. One way to remedy this is by using the factory method to construct your classes. With the factory method, a class contains a method for constructing objects with the correct parameters. To create the object you just call that method, which has everything it needs to create the object.
00:40
We’ve already done this. Inside the EmployeeDatabase
class, we have a method called ._create_employee()
, which is used to construct an Employee
with the correct parameters.
00:56
This works, but it would be a lot nicer if we could create an Employee
by just supplying its id
. After all, the employee database contains the ID of every employee in the company, so it should be a pretty simple lookup. In this video, we’re going to further leverage our understanding of composition to make this possible. First, I want to work on the ProductivitySystem
. In its current state, the ProductivitySystem
can be instantiated as many times as we want.
01:30
This doesn’t really make much sense, as this program will only ever have one ProductivitySystem
in charge of creating role objects and tracking employees. To fix this, I’m going to make this class a singleton.
01:47 A singleton is a class that is only instantiated once, and that same instance is used everywhere else in the program. We could force this by rewriting the class, but an easier way is just to follow a naming convention.
02:05
I’m going to insert an underscore (_
) at the beginning of this class name, which tells other developers not to instantiate the class manually.
02:14
We can say the class is internal to this module—it should only be instantiated inside of this module. I’m going to move to the bottom of this class and create a single instance of _ProductivitySystem
at the module level. After all, we can’t use the class without an instance of it. While we’re at it, let’s create two functions that can be accessed by any other module that imports this one.
02:45 These are functions because they are defined outside of a class, at the module level. If they were inside of a class, they would be called methods.
02:56
The first function is called get_role()
, which will take in a role_id
and return the correct role object by utilizing our _productivity_system
object.
03:11
I’ll do something similar with track()
. This function will take in a collection of employees and a number of hours, and then track them.
03:24
What we’ve done here is create a public-facing interface for this module. That’s the get_role()
and track()
functions. Modules that import productivity.py
should use these functions—and these functions only—to interact with the _ProductivitySystem
.
03:45
They will also have access to the _ProductivitySystem
object and class, but they are marked as private by appending an underscore (_
) to the beginning of their names, which tells other developers not to use them directly.
03:59
Instead, they should access them through the two functions, which will utilize the _productivity_system
object to return the requested data.
04:10
We can follow this same singleton pattern with our PayrollSystem
.
04:17 I’ll mark this class and then instantiate it at the module level.
04:23
Now, I’ll create the two functions that will act as a public interface to this module. The contacts
module will follow. I’m going to mark the AddressBook
, instantiate it, and create the function to use it.
04:44 What we’ve done here is a form of abstraction. We’ve abstracted away the complex inner workings of all these systems, including instantiating them, and instead, we’ve provided one or two public functions that other modules can use to interact with these systems. It’s like a car.
05:06 I don’t understand the first thing about combustion engines—or any other car components, for that matter. Luckily, that’s all abstracted away from me and, instead, I’m given a steering wheel and some pedals to use, which in turn operates all of the complex inner car parts.
05:25 These pedals are like the functions in our modules, and the engine is like the classes. Other modules use the class through the functions alone.
05:37
We’re going to do this to one more class, but this one is going to be a little more complex. The EmployeeDatabase
class is going to become a singleton too.
05:49
That lives in the employees
module, so I’ll move over there. Before I can start working on it, I need make sure our imports are all correct since we’ve changed some things. Instead of importing classes, those are now private, and so I’m just going to import the required functions from each of the three modules we worked on. As usual, I’ll mark this class so other developers know not to instantiate it directly. In my car analogy, that’s like the manufacturer putting a sign on the car that says, “Don’t try to ignite the engine manually.
06:29
Use your key.” Right now, the ._employees
instance attribute is a list of dictionaries, but I think there’s an even better way that we can use this for an ID lookup.
06:42
This _EmployeeDatabase
class will be given an employee ID, which will then match against an employee in this list. So to make that process easier, I’m going to turn this list of dictionaries into a dictionary of dictionaries.
07:03
The key of each entry in the outer dictionary will be the employee ID, and the value will be the inner dictionary, which describes the Employee
object to be created.
07:18
Since we have the ID in the outer dictionary, we can get rid of the 'id'
entries of the inner dictionaries.
07:31
That looks good, but because we’ve changed the format of the internal database, we have to modify these methods that utilize it. The first thing we have to change is .employees()
.
07:45
We need to modify it to return a list of Employee
objects created from the database above.
07:55
We no longer need a method for creating instances of Employee
, so I’ll replace this method with one called .get_employee_info()
, which will take in an ID and return information about the employee with that ID. info
will be the inner dictionary in the database above, which we can get with the dictionary’s .get()
method, passing in the employee_id
. Before we can return the dictionary we should make sure it exists, so I’ll do a quick None
check.
08:32
An exception will be raised if an invalid employee ID, like 6
or 7
, is passed into this method. Finally, we can return the dictionary. That takes care of the _EmployeeDatabase
class,
08:48
but if you look at the .employees()
method, we’re instantiating an Employee
with just its id
. In its current state, the Employee
class doesn’t allow that, so let’s make that modification next.
09:02
I’m first going to delete all of these parameters, as well as all of their associated instance attributes. Now, all we’re accepting is an ID. In order to finish constructing this Employee
object, we first need the dictionary of information associated with that employee’s ID.
09:27
We can obtain that with the .get_employee_info()
method we just wrote, which we have access to thanks to the employee_database
object at the bottom of this file.
09:40
The name, we can get by accessing the value associated with the 'name'
key of that info
dictionary. To get the address we can use the get_employee_address()
function we imported at the top of this module, which will utilize that single _AddressBook
instance that lives in that module.
10:04
The role is similar, although this time I will mark it as private so it doesn’t show up in a dictionary generated from an Employee
object. For this, we can use the get_role()
function we imported at the top, but this function takes a role name as a string—not some ID.
10:28
The role name of the employee can be obtained with info.get()
, passing in the string 'role'
.
10:37
Finally, we can use the get_policy()
class to get the payroll policy associated with this employee ID.
10:48
And it looks like everything else in this module can stay the same. It’s time to move over to program.py
and finish this off. We have to make some pretty big changes to this file, so I’m just going to clear it and start fresh.
11:06
Just like before, we’re going to utilize the json
module to represent an Employee
object in JSON format. Then, we need to import the necessary functions, objects, and classes we’ll use to write the main program instructions.
11:24
calculate_payroll
and track
are functions, employee_database
is the database object, and Employee
is the class which can create Employee
objects given just an employee ID. Just like before, I’m going to write the one-liner method that will take a dictionary and print it in JSON format.
11:49
Now, let’s grab the list of Employee
objects from the employee_database
.
11:56 We could have actually made this method into a property since it’s not accepting any arguments, but I’ll be like a math book, and I’ll say that that’s left as an exercise for the reader—or I suppose, the viewer.
12:11 We now have the ability to track the employees for 40 hours and calculate their payrolls using the appropriate functions.
12:22
The last thing I want to do is grab an employee from the database and print out information about them. I’m going to grab the TemporarySecretary
object, which is employee with ID 5
in the database.
12:40
Then, I’ll print out a heading for them and I’ll use the print_dict()
function to turn their dictionary into an easy-to-read JSON output.
12:56 Okay! And this is where I reveal that I made a pretty big mistake. You might have caught this as we’ve been going along, but if not, that’s totally fine. Just follow along with me and I’ll show you how to fix it.
13:11
The mistakes all live in the employees
module.
13:16
For one, we need to make sure that we have a valid _EmployeeDatabase
object in this module. The program
module relies on this to get a list of employees, but if we don’t have this object in the first place, we can’t get the list.
13:36
We also don’t need these system objects anymore since our _EmployeeDatabase
is no longer in charge of handling the things we obtained from them. In its current state, these would just give us exceptions because we don’t have imports for them anymore.
13:53
Finally, it looks like we added an underscore (_
) to some attribute names in Employee
, but we didn’t change their name in some of the methods.
14:04 That’s just a simple naming mistake, which happens all the time. You changed the name of one variable in one place, and you forget to change it somewhere else. There we go. That should work now. Let’s try this out.
14:21
Great! It looks like we’re getting all of the right output, and if I scroll down here, you’ll see that we’re even printing information about employee 5
in JSON format. Before I close off this lengthy video, I want to point out how composition is being used in this modified design.
14:44
If we inspect the Employee
class, we can observe composition being used in two different ways. Notice how the Address
class is providing extra data to the Employee
class,
14:59 whereas the role and payroll objects provide extra functionality—notably, tracking productivity and calculating payroll. And along the way, we turned those classes into singletons and created public-facing interfaces in the form of functions that can be used to access that functionality.
15:23
And yet the relationship between Employee
and the various systems is still loosely-coupled, which provides some interesting capabilities you’ll see in the next video.
Martin Breuss RP Team on July 20, 2022
@YZ glad you found the course helpful. If you’re curious to change the project to read data from a file, here are a couple of resources that might be helpful:
- 📝 Reading and Writing Files in Python
- 📺 Reading and Writing Files With Pandas
- 📝 Reading and Writing CSV Files in Python
- 📝 Working With JSON Data in Python
Hope that’s what you’re looking for!
YZ on Aug. 15, 2022
I think the answer to my question above is metaclass
, according to this answer to the StackOverflow question how to create singleton class with arguments in python.
Lucas Dondo on May 17, 2023
Hey, @YZ! This is my approach to what you asked.
Przemysław Stępniak on Jan. 9, 2024
I don’t understand how having employee_database.get_employee_info()
inside .__init__()
method of Employee
class does not cause tight-coupling. I come from .NET background and this clearly is a sign of a code smell there. Why should User
class know anything about a database singleton?
Become a Member to join the conversation.
YZ on July 18, 2022
First I have to appreciate how well this course was designed to allow us to see the evolution of the project.
One question if I may - how would you modify the codes such that the employee database is, say, read from an external .csv or .json file rather than stored internally inside the classes?
Thank you!