Flexible Designs With Composition
00:00 I think we can do more with this class. Let’s modify it so that it handles the instantiation of new roles. To do that,
00:10
I’m first going to add an .__init__()
method to the ProductivitySystem
.
00:17
Inside here, we’re going to create a new instance attribute that will map an ID to a role class. The ID 'manager'
will map to a ManagerRole
, the 'secretary'
will map to a SecretaryRole
, the 'sales'
will map to a SalesRole
, and finally, the 'factory'
will map to a FactoryRole
.
00:44
If you noticed, I’ve named this instance attribute starting with an underscore (_
). This tells other developers that they shouldn’t access the ._roles
attribute from outside of this class. Instead, we’re going to create a method inside of this class that can be used to obtain the desired role without having to have access to the dictionary above.
01:09 This gives us more control over how the data internal to our class is being accessed from other parts of our program, which will help to reduce the chance of a bug and prevent misuse.
01:24
This method will be called .get_role()
, and it will take in a role_id
and return an object representing that role. In order to get the class associated with the role_id
, we can call the dictionary’s .get()
method, passing in the role_id
provided.
01:44
Now, if role_id
is any of the four roles, the class associated with that role will be stored in this role_type
variable. The only problem is if something that is not a role is passed into this method, the .get()
method will return None
, which will lead to an exception when we try to instantiate it. To check for this I’ll type if not role_type:
then raise a ValueError
and we’ll make it say 'invalid role_id'
.
02:23
Finally, if role_type
is valid, we’re going to return a new instance of that class. Remember, role_type
is referencing one of our role classes right now, and not an object. In order to instantiate that class into an object, we add these parentheses after the class name.
02:47
One last change I’m going to make to this class is within the .track()
method. Instead of storing the result of working and printing it, I just want to call the .work()
method on the current employee
, passing in hours
.
03:05
Next, we are going to work on the PayrollSystem
. The PayrollSystem
will operate in a similar way. It will keep an internal database of payroll policies for each employee depending on their ID. In other words, this dictionary will map employee IDs to their payroll policy.
03:27
Notice here that because we are passing in the parameters each policy needs, our dictionary is actually storing PayrollPolicy
objects, and not just the PayrollPolicy
classes.
03:42 This means that when I return the value in the dictionary, I don’t have to instantiate it with the opening and closing parentheses. I’m going to write the method to return the object now.
03:56
This looks almost identical to the last method we wrote, except the return
line doesn’t include the parentheses. All that’s left to do in this module is create the payroll policies to be instantiated.
04:13
The first class is PayrollPolicy
, which will be the base class to every other policy. It will keep track of the number of hours worked and add the new hours as appropriate using a .track_work()
method.
04:29
This plus equals operator (+=
) is a quick way to add a value to a variable, rather than saying self.hours_worked = self.hours_worked + hours
.
04:44
We can now make the SalaryPolicy
and HourlyPolicy
classes derive from PayrollPolicy
.
04:54
The last payroll class is for the commission policy. A commission employee is one who receives a commission on top of their base salary. Because they earn a salary too, that class inherits from SalaryEmployee
so that we also have access to the salary attribute.
05:15
In the past, we made this class simply accept a commission and add it onto their salary. This time we’re going to accept a commission_per_sale
value, and then calculate their total commission based on this number and the number of sales. To start, I’m going to rename commission
to commission_per_sale
, and I’ll change the instance attribute too.
05:47
Let’s create a method for calculating the total commissions value. The number of sales they make is dependent on time. Of course, this isn’t exactly representative of the real world, but let’s just say they make a sale every 5
hours.
06:06
Then, we can return the number of sales they make times the commission_per_sale
, and that will give us the total commission amount. The last thing we have to do here is modify the .calculate_payroll()
method to obtain the commission value from the method we just wrote.
06:27
The next module we are going to attack is contact.py
. This module currently has an Address
class that represents an employee’s address.
06:40
Just like before, I want to add an internal employee address database to this module, along with a method for obtaining the address of some employee. I’ll do this by creating a new class called AddressBook
above the existing Address
class.
07:00
I created an instance attribute out of a dictionary that maps employee IDs to their addresses. Then, I create a new method called .get_employee_address()
, which returns the Address
object associated with a given employee_id
.
07:22
This is very similar to what we’ve already been doing. Define a dictionary, which acts like a database, then provide a method to access a specific entry in that dictionary given an employee_id
.
07:40
Now that we have independent mechanisms in place for creating the components of an employee, we’re going to move on to the employees
module and get rid of everything except the basic definition of an Employee
. At the top, I’ll make sure we have access to the ProductivitySystem
, PayrollSystem
, and AddressBook
.
08:05
We’re going to create a new class called EmployeeDatabase
, which will act as a database of employees all together. I will give this class an .__init__()
method, and inside here, I will create a new list of dictionaries where each dictionary represents an employee with their ID, name, and role.
08:31 Their payroll policy and address will be obtained from their ID.
08:37
This class will also need instances of the ProductivitySystem
, the PayrollSystem
, and the AddressBook
.
08:48
Next, I’m going to write a single-line method that will return a list containing Employee
objects constructed from the list of dictionaries above.
09:00
That looks like this. Basically, when this function is called, it’s going to return a list and each item in that list will be an Employee
object constructed from one of the dictionaries in the list above. This line of code is relying on some ._create_employee()
method, so let’s create that now. This method will be in charge of taking in an id
, a name
, and a role
and returning a fully-functional Employee
object.
09:35
I started this name with an underscore (_
) to indicate that this method should only be called from within this class, such as in the .employees()
method above. The address can be obtained with the id
provided, like this.
09:55
We are utilizing the AddressBook
instance stored within this class to call its .get_employee_address()
method, passing in the id
.
10:06 The employee role will follow in the exact same way.
10:12
This will convert the string representation of the role into an actual role object that the Employee
objects can use. Finally, we’ll get the payroll object too. Now, all that’s left to do here is return a new Employee
object constructed and initialized with all the data that we just gathered.
10:38
This EmployeeDatabase
is a prime example of composition. It tracks the name, the ID, and the role of each employee. It has an instance of the ProductivitySystem
, the PayrollSystem
, and the AddressBook
, which are all used to create Employee
objects.
11:00
All that’s left to do in this module is modify our Employee
class to store instance attributes for the address, role, and payroll policy.
11:16
We also need to define a .work()
method that will be used by the ProductivitySystem
and a .calculate_payroll()
method that will be used by the PayrollSystem
.
11:28
I’ll start with the .work()
method.
11:32
This method will accept the number of hours to make the employee work for. The string representing its duties will be stored in this variable called duties
, which we can obtain with self.role.work()
, passing in the number of hours to work for.
11:54
Then, we can print some information about the employee, along with a string containing information about the work they did. Lastly, I’ll call the .track_work()
method in the employee’s payroll policy, which will add this many hours to their total working hours.
12:16
The .calculate_payroll()
method will be pretty short. It will accept no arguments and return the result of calling the PayrollSystem
’s, .calculate_payroll()
method.
12:30
The last module to work on is the main one, program.py
. I’ll move in to there and I’m going to delete everything and start fresh. The first thing I need to do is make sure the PayrollSystem
, ProductivitySystem
, and EmployeeDatabase
classes are all imported so we can use them.
12:53
Then, I’m going to instantiate each of these systems. Rather than using them to obtain new components like the individual employee classes do, we’re just going to use them to call their .track()
or .calculate_payroll()
methods, passing in a list of employees.
13:14
This list of employees can be obtained like this: employees = employee_database.employees()
. Then, we can call the .track()
method on the productivity_system
, passing in a list of employees and the number of hours to work.
13:37
And finally, the .calculate_payroll()
method on the PayrollSystem
, passing in a list of employees. I will run this program, and it looks like we got an exception.
13:51
It’s saying our hourly policy is missing an hour_rate
argument. That’s on line 7 of hr.py
, so let me head over there and see what’s going on. All right.
14:05
There’s line 7. And if I scroll down, we can see the HourlyPolicy
class. Ah, there’s a problem. We’re using the PayrollPolicy
class to track the hours worked, so we can remove that from the HourlyPolicy
constructor. Instead, we need to call the parent class’s .__init__()
method, which will give us access to the .hours_worked
attribute.
14:32
We also need to do this same thing with the SalaryPolicy
. Simply inheriting from PayrollPolicy
wasn’t enough. This goes to show you what happens when you forget to initialize the parent class’s constructor, which has attributes that the child class needs.
14:54 All right. I think that should be good. I’m going to move back over to the main module and run it.
15:03
Great! You can see that every class is being tracked in the ProductivitySystem
and they’re all being paid to the correct street address. This design is what’s called a policy-based design, where modules are composed of different policies, which are in charge of doing the actual work.
15:25
Other classes like the PayrollSystem
take in some information and employ the correct policy. As you’ll see in the next video, this type of design gives you flexibility that you’ll need in case requirements change in the future.
Austin Cepalia RP Team on April 20, 2020
@chrisstinemetz Yes! I just uploaded to source code for this course so it should be online soon
rskoenig33 on April 24, 2020
I found this module to be dense and a little confusing on first pass. I subsequently ran through it a few times with code and now follow it well. I feel like it would have helped to refer to the UML diagram throughout to help me understand the flow and how each class/method fit into the design.
BUT, after I multiple passes, I now understand and found it helpful!
chrisstinemetz on April 25, 2020
@Austin, where may I find the code you posted?
Thanks
bnik on April 26, 2020
To me this was a brilliant and crystal example.
fieldsierra on May 22, 2020
Where can one find the code?
Dan Bader RP Team on May 22, 2020
Thanks, the code is available under Supporting Material below the video player. I’m also adding a note to this lesson.
dwalsh on May 25, 2020
I love all the references to Concord NH where I’m from and currenly do data analytics. Are you from Concord, Austin?
Austin Cepalia RP Team on May 26, 2020
@dwalsh I’m actually from Connecticut. Most of the places used in the course were taken directly from the article this course is based on.
Dan B on Nov. 16, 2020
I’m lost… I think I stopped following because I didn’t understand /why/ you were making these changes. What is the benefit of restructuring like this?
Also, sometimes you raise ValueError and sometimes you return ValueError… small inconsistencies like that without a clear rationale tend to confuse me.
e.g. why do some dicts return classes, others instances?
I’ve been enjoying the course up to now :-)
Thanks
petrovicdjordje750 on Jan. 10, 2021
To add a little twist to the code, for those who are interesting :)
If we try to use dictionary a little bit different, and leave the same error checking in ‘if’ statement, we will get a problem, this is something to consider when writing python code.
Example of code that looks good, but will throw an error:
# Simple dictionary that stores employee id, bease on role as a key.
employee_ids = {
'manager': 0,
'secretary': 1,
'sales': 2,
'factory': 3
}
# Method for getting data from dict.
def get_employeeid_by_role(employee_role):
role = employee_ids.get(employee_role)
# this is similar to writing 'if role == false' which is the same as writing 'if role == 0',
# that is why we will get error when passing 'manager' key
if not role:
raise ValueError(f'{employee_role} is Invalid')
return role
# Call methond and print outputs.
print(get_employeeid_by_role('secretary'))
print(get_employeeid_by_role('sales'))
print(get_employeeid_by_role('manager')) # fails with ValueError.
Checkout this stackoverflow question for more details.
charles7276 on May 7, 2021
At the 3:55 mark, why is the HourlyPolicy object only getting passed one parameter and not two? Doesn’t it need hours_worked and hour_rate?
charles7276 on May 7, 2021
Never mind. I see it was address subsequently. Apologies.
alazejha on May 30, 2021
On 4:30 min mark, you moved the calculate_payroll()
method to ane class PayrollSystem
. However, you left the method trace
with the class PayrollPolicy
. Why not move the method trace()
to PayrollSystem
? Why keep class PayrollPolicy
?
Many thanks, Alina
enveralagoz on Dec. 19, 2021
First of all, thanks for the course. I wonder if you could describe which software design pattern(s) you have covered in this course. It would be nice to know about the pattern as it will clarify for me in designing perspective.
I enjoyed and still enjoying/learning this course.
Enver
nitin24sandbox on Aug. 17, 2022
Why do some dicts return classes, others instances?
mp on April 26, 2023
For me this section was pretty hard to follow - midway through I’d lost track of why we were making any of the changes. Enjoying the rest of this course, but this piece maybe needs to be broken up a bit.
Lucas Dondo on May 11, 2023
Hi there! I’m currently, as many, having some issues in understanding this video… Yes. It’s dense. Yes. There’s no diagram available (as @rskoenig33 said).
However, I found one! 😁 Here it is!
The image can be found in this section of this course’s article. I recommend having a quick read to it and it’s conclusions so as to get this lesson’s big picture.
Lucas Dondo on May 12, 2023
Also, everyone asked: why are we doing this? And I understood that! (Well, I hope)
Let’s go some lessons back… At the end of the Avoiding the Diamond Problem lesson, the diagram looked like this. If you pay enough attention, in that diagram TemporarySecretary
is an HourlyPolicy
.
But that doesn’t make sense… In reality, a TemporarySecretary
has an HourlyPolicy
, but it isn’t one.
Colloquially speaking, in real life, a temporary secretary is a person and employee who has an hourly policy, but is not (that person in itself) an hourly policy.
Since the is a relation doesn’t apply here, but has a does, we must correct this in the diagram. How? Going from inheritance (is a) to composition (has a).
[My] conclusion: the goal of this present lesson is to redesign the previous diagram to correct the mentioned incoherence and go from an inheritance-based diagram to a composition-based one. Finally, the diagram would look like this one from this section of the article.
Become a Member to join the conversation.
chrisstinemetz on April 20, 2020
Is it possible to get the source code for Composition approach?