├── .pylintrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── before.py ├── with_composition.py └── with_inheritance.py /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | extension-pkg-whitelist=pydantic 3 | [FORMAT] 4 | good-names=i,j,x,y,id -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "[python]": { 4 | "editor.defaultFormatter": null 5 | }, 6 | "editor.formatOnSaveMode": "file", 7 | "editor.formatOnSave": true, 8 | "editor.codeActionsOnSave": { 9 | "source.organizeImports": true 10 | }, 11 | "python.linting.pylintEnabled": true, 12 | "python.linting.mypyEnabled": true, 13 | "python.linting.enabled": true, 14 | "python.formatting.provider": "black", 15 | "python.analysis.useImportHeuristic": true 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ArjanCodes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Why composition is better than inheritance 2 | 3 | This repository contains the code for the Composition vs Inheritance video on the ArjanCodes channel (watch the video [here](https://youtu.be/0mcP8ZpUR38)). 4 | 5 | The example is about different employee types and reward structures. In the first version (`before.py`), there is no inheritance, just three classes (= three employee types) with low cohesion (lots of responsibilities per class), and code duplication. Then there is a version that tries to solve those issues with inheritance (`with_inheritance.py`), and another version that uses composition (`with_composition.py`). 6 | -------------------------------------------------------------------------------- /before.py: -------------------------------------------------------------------------------- 1 | """ 2 | Very advanced Employee management system. 3 | """ 4 | 5 | from dataclasses import dataclass 6 | 7 | 8 | @dataclass 9 | class HourlyEmployee: 10 | """Employee that's paid based on number of worked hours.""" 11 | 12 | name: str 13 | id: int 14 | commission: float = 100 15 | contracts_landed: float = 0 16 | pay_rate: float = 0 17 | hours_worked: int = 0 18 | employer_cost: float = 1000 19 | 20 | def compute_pay(self) -> float: 21 | """Compute how much the employee should be paid.""" 22 | return ( 23 | self.pay_rate * self.hours_worked 24 | + self.employer_cost 25 | + self.commission * self.contracts_landed 26 | ) 27 | 28 | 29 | @dataclass 30 | class SalariedEmployee: 31 | """Employee that's paid based on a fixed monthly salary.""" 32 | 33 | name: str 34 | id: int 35 | commission: float = 100 36 | contracts_landed: float = 0 37 | monthly_salary: float = 0 38 | percentage: float = 1 39 | 40 | def compute_pay(self) -> float: 41 | """Compute how much the employee should be paid.""" 42 | return ( 43 | self.monthly_salary * self.percentage 44 | + self.commission * self.contracts_landed 45 | ) 46 | 47 | 48 | @dataclass 49 | class Freelancer: 50 | """Freelancer that's paid based on number of worked hours.""" 51 | 52 | name: str 53 | id: int 54 | commission: float = 100 55 | contracts_landed: float = 0 56 | pay_rate: float = 0 57 | hours_worked: int = 0 58 | vat_number: str = "" 59 | 60 | def compute_pay(self) -> float: 61 | """Compute how much the employee should be paid.""" 62 | return ( 63 | self.pay_rate * self.hours_worked + self.commission * self.contracts_landed 64 | ) 65 | 66 | 67 | def main() -> None: 68 | """Main function.""" 69 | 70 | henry = HourlyEmployee(name="Henry", id=12346, pay_rate=50, hours_worked=100) 71 | print( 72 | f"{henry.name} worked for {henry.hours_worked} hours and earned ${henry.compute_pay()}." 73 | ) 74 | 75 | sarah = SalariedEmployee( 76 | name="Sarah", id=47832, monthly_salary=5000, contracts_landed=10 77 | ) 78 | print( 79 | f"{sarah.name} landed {sarah.contracts_landed} contracts and earned ${sarah.compute_pay()}." 80 | ) 81 | 82 | 83 | if __name__ == "__main__": 84 | main() 85 | -------------------------------------------------------------------------------- /with_composition.py: -------------------------------------------------------------------------------- 1 | """ 2 | Very advanced Employee management system. 3 | """ 4 | 5 | from abc import ABC, abstractmethod 6 | from dataclasses import dataclass 7 | from typing import Optional 8 | 9 | 10 | class Contract(ABC): 11 | """Represents a contract and a payment process for a particular employeee.""" 12 | 13 | @abstractmethod 14 | def get_payment(self) -> float: 15 | """Compute how much to pay an employee under this contract.""" 16 | 17 | 18 | @dataclass 19 | class Commission(ABC): 20 | """Represents a commission payment process.""" 21 | 22 | @abstractmethod 23 | def get_payment(self) -> float: 24 | """Returns the commission to be paid out.""" 25 | 26 | 27 | @dataclass 28 | class ContractCommission(Commission): 29 | """Represents a commission payment process based on the number of contracts landed.""" 30 | 31 | commission: float = 100 32 | contracts_landed: int = 0 33 | 34 | def get_payment(self) -> float: 35 | """Returns the commission to be paid out.""" 36 | return self.commission * self.contracts_landed 37 | 38 | 39 | @dataclass 40 | class Employee: 41 | """Basic representation of an employee at the company.""" 42 | 43 | name: str 44 | id: int 45 | contract: Contract 46 | commission: Optional[Commission] = None 47 | 48 | def compute_pay(self) -> float: 49 | """Compute how much the employee should be paid.""" 50 | payout = self.contract.get_payment() 51 | if self.commission is not None: 52 | payout += self.commission.get_payment() 53 | return payout 54 | 55 | 56 | @dataclass 57 | class HourlyContract(Contract): 58 | """Contract type for an employee being paid on an hourly basis.""" 59 | 60 | pay_rate: float 61 | hours_worked: int = 0 62 | employer_cost: float = 1000 63 | 64 | def get_payment(self) -> float: 65 | return self.pay_rate * self.hours_worked + self.employer_cost 66 | 67 | 68 | @dataclass 69 | class SalariedContract(Contract): 70 | """Contract type for an employee being paid a monthly salary.""" 71 | 72 | monthly_salary: float 73 | percentage: float = 1 74 | 75 | def get_payment(self) -> float: 76 | return self.monthly_salary * self.percentage 77 | 78 | 79 | @dataclass 80 | class FreelancerContract(Contract): 81 | """Contract type for a freelancer (paid on an hourly basis).""" 82 | 83 | pay_rate: float 84 | hours_worked: int = 0 85 | vat_number: str = "" 86 | 87 | def get_payment(self) -> float: 88 | return self.pay_rate * self.hours_worked 89 | 90 | 91 | def main() -> None: 92 | """Main function.""" 93 | 94 | henry_contract = HourlyContract(pay_rate=50, hours_worked=100) 95 | henry = Employee(name="Henry", id=12346, contract=henry_contract) 96 | print( 97 | f"{henry.name} worked for {henry_contract.hours_worked} hours " 98 | f"and earned ${henry.compute_pay()}." 99 | ) 100 | 101 | sarah_contract = SalariedContract(monthly_salary=5000) 102 | sarah_commission = ContractCommission(contracts_landed=10) 103 | sarah = Employee( 104 | name="Sarah", id=47832, contract=sarah_contract, commission=sarah_commission 105 | ) 106 | print( 107 | f"{sarah.name} landed {sarah_commission.contracts_landed} contracts " 108 | f"and earned ${sarah.compute_pay()}." 109 | ) 110 | 111 | 112 | if __name__ == "__main__": 113 | main() 114 | -------------------------------------------------------------------------------- /with_inheritance.py: -------------------------------------------------------------------------------- 1 | """ 2 | Very advanced Employee management system. 3 | """ 4 | 5 | from abc import ABC, abstractmethod 6 | from dataclasses import dataclass 7 | 8 | 9 | @dataclass 10 | class Employee(ABC): 11 | """Basic representation of an employee at the company.""" 12 | 13 | name: str 14 | id: int 15 | 16 | @abstractmethod 17 | def compute_pay(self) -> float: 18 | """Compute how much the employee should be paid.""" 19 | 20 | 21 | @dataclass 22 | class HourlyEmployee(Employee): 23 | """Employee that's paid based on number of worked hours.""" 24 | 25 | pay_rate: float 26 | hours_worked: int = 0 27 | employer_cost: float = 1000 28 | 29 | def compute_pay(self) -> float: 30 | return self.pay_rate * self.hours_worked + self.employer_cost 31 | 32 | 33 | @dataclass 34 | class SalariedEmployee(Employee): 35 | """Employee that's paid based on a fixed monthly salary.""" 36 | 37 | monthly_salary: float 38 | percentage: float = 1 39 | 40 | def compute_pay(self) -> float: 41 | return self.monthly_salary * self.percentage 42 | 43 | 44 | @dataclass 45 | class Freelancer(Employee): 46 | """Freelancer that's paid based on number of worked hours.""" 47 | 48 | pay_rate: float 49 | hours_worked: int = 0 50 | vat_number: str = "" 51 | 52 | def compute_pay(self) -> float: 53 | return self.pay_rate * self.hours_worked 54 | 55 | 56 | @dataclass 57 | class SalariedEmployeeWithCommission(SalariedEmployee): 58 | """Employee that's paid based on a fixed monthly salary and that gets a commission.""" 59 | 60 | commission: float = 100 61 | contracts_landed: float = 0 62 | 63 | def compute_pay(self) -> float: 64 | return super().compute_pay() + self.commission * self.contracts_landed 65 | 66 | 67 | @dataclass 68 | class HourlyEmployeeWithCommission(HourlyEmployee): 69 | """Employee that's paid based on number of worked hours and that gets a commission.""" 70 | 71 | commission: float = 100 72 | contracts_landed: float = 0 73 | 74 | def compute_pay(self) -> float: 75 | return super().compute_pay() + self.commission * self.contracts_landed 76 | 77 | 78 | @dataclass 79 | class FreelancerWithCommission(Freelancer): 80 | """Freelancer that's paid based on number of worked hours and that gets a commission.""" 81 | 82 | commission: float = 100 83 | contracts_landed: float = 0 84 | 85 | def compute_pay(self) -> float: 86 | return super().compute_pay() + self.commission * self.contracts_landed 87 | 88 | 89 | def main() -> None: 90 | """Main function.""" 91 | 92 | henry = HourlyEmployee(name="Henry", id=12346, pay_rate=50, hours_worked=100) 93 | print( 94 | f"{henry.name} worked for {henry.hours_worked} hours and earned ${henry.compute_pay()}." 95 | ) 96 | 97 | sarah = SalariedEmployeeWithCommission( 98 | name="Sarah", id=47832, monthly_salary=5000, contracts_landed=10 99 | ) 100 | print( 101 | f"{sarah.name} landed {sarah.contracts_landed} contracts and earned ${sarah.compute_pay()}." 102 | ) 103 | 104 | 105 | if __name__ == "__main__": 106 | main() 107 | --------------------------------------------------------------------------------