laitimes

Another object-oriented alternative to Python: Composition

author:Hong is thinner and doesn't want to retire IT

Composition translates as composition, but in fact it is a composition

Combinations in Python

Composition is an object-oriented design concept that has a relationship with the model. In a composition, a class called a composite contains objects from another class called a component. In other words, a composite class has components from another class.

  • The essence of inheritance is the relationship between parents
  • The role of the combination is to take advantage of friends

Composition allows a composite class to reuse the implementation of the components it contains. A composite class does not inherit from a component class interface, but it can take advantage of its implementation.

The combinatorial relationship between the two classes is considered to be loosely coupled. This means that changes to component classes rarely affect composite classes, while changes to composite classes never affect component classes.

How to implement the composition of objects

In such a scenario, each employee has an address, so there is no inheritance relationship between the address and the employee. But the employee has address information, how can this relationship be represented?

Start by defining an Address class

class Address:
    def __init__(self, street, city, state, zipcode, street2=''):
        self.street = street
        self.street2 = street2
        self.city = city
        self.state = state
        self.zipcode = zipcode

    def __str__(self):
        lines = [self.street]
        if self.street2:
            lines.append(self.street2)
        lines.append(f'{self.city}, {self.state} {self.zipcode}')
        return '\n'.join(lines)           

Define an Employee class that has an address property

class Employee:
    def __init__(self, id, name):
        self.id = id
        self.name = name
        self.address = None           

In this way, the address and the employee are loosely related.

Another object-oriented alternative to Python: Composition

Then we output information about the employee

class PayrollSystem:
    def calculate_payroll(self, employees):
        print('Calculating Payroll')
        print('===================')
        for employee in employees:
            print(f'Payroll for: {employee.id} - {employee.name}')
            print(f'- Check amount: {employee.calculate_payroll()}')
            if employee.address:
                print('- Sent to:')
                print(employee.address)
            print('')           

If the employee has an address, the employee's address information is output

if employee.address:
       print('- Sent to:')
      print(employee.address)           

Flexible design of combinations

Composition is more flexible than inheritance because it simulates loosely coupled relationships. Changes to component classes have little or no impact on composite classes.

Rather than adding new classes to the hierarchy, you can change the behavior by providing new components that implement these behaviors.

Take a look at the multiple inheritance example above. Imagine how the new salary policy will affect the design. Try to imagine what the class hierarchy would look like if new roles were needed. As you've seen before, relying too much on inheritance can lead to a class explosion.

The biggest problem is not the number of classes in the design, but the degree of coupling between those classes. When changes are introduced, tightly coupled classes affect each other.

class ProductivitySystem:
    def __init__(self):
        self._roles = {
            'manager': ManagerRole,
            'secretary': SecretaryRole,
            'sales': SalesRole,
            'factory': FactoryRole,
        }

    def get_role(self, role_id):
        role_type = self._roles.get(role_id)
        if not role_type:
            raise ValueError('role_id')
        return role_type()

    def track(self, employees, hours):
        print('Tracking Employee Productivity')
        print('==============================')
        for employee in employees:
            employee.work(hours)
        print('')           
class ManagerRole:
    def perform_duties(self, hours):
        return f'screams and yells for {hours} hours.'

class SecretaryRole:
    def perform_duties(self, hours):
        return f'does paperwork for {hours} hours.'

class SalesRole:
    def perform_duties(self, hours):
        return f'expends {hours} hours on the phone.'

class FactoryRole:
    def perform_duties(self, hours):
        return f'manufactures gadgets for {hours} hours.'           

We can define a set of roles to implement an employee payroll strategy

class PayrollSystem:
    def __init__(self):
        self._employee_policies = {
            1: SalaryPolicy(3000),
            2: SalaryPolicy(1500),
            3: CommissionPolicy(1000, 100),
            4: HourlyPolicy(15),
            5: HourlyPolicy(9)
        }

    def get_policy(self, employee_id):
        policy = self._employee_policies.get(employee_id)
        if not policy:
            return ValueError(employee_id)
        return policy

    def calculate_payroll(self, employees):
        print('Calculating Payroll')
        print('===================')
        for employee in employees:
            print(f'Payroll for: {employee.id} - {employee.name}')
            print(f'- Check amount: {employee.calculate_payroll()}')
            if employee.address:
                print('- Sent to:')
                print(employee.address)
            print('')           

Keep an internal payroll policy database for each employee. It exposes one, given an employee, to return its payroll policy

class PayrollPolicy:
    def __init__(self):
        self.hours_worked = 0

    def track_work(self, hours):
        self.hours_worked += hours

class SalaryPolicy(PayrollPolicy):
    def __init__(self, weekly_salary):
        super().__init__()
        self.weekly_salary = weekly_salary

    def calculate_payroll(self):
        return self.weekly_salary

class HourlyPolicy(PayrollPolicy):
    def __init__(self, hour_rate):
        super().__init__()
        self.hour_rate = hour_rate

    def calculate_payroll(self):
        return self.hours_worked * self.hour_rate

class CommissionPolicy(SalaryPolicy):
    def __init__(self, weekly_salary, commission_per_sale):
        super().__init__(weekly_salary)
        self.commission_per_sale = commission_per_sale

    @property
    def commission(self):
        sales = self.hours_worked / 5
        return sales * self.commission_per_sale

    def calculate_payroll(self):
        fixed = super().calculate_payroll()
        return fixed + self.commission           

Address book

class AddressBook:
    def __init__(self):
        self._employee_addresses = {
            1: Address('121 Admin Rd.', 'Concord', 'NH', '03301'),
            2: Address('67 Paperwork Ave', 'Manchester', 'NH', '03101'),
            3: Address('15 Rose St', 'Concord', 'NH', '03301', 'Apt. B-1'),
            4: Address('39 Sole St.', 'Concord', 'NH', '03301'),
            5: Address('99 Mountain Rd.', 'Concord', 'NH', '03301'),
        }

    def get_employee_address(self, employee_id):
        address = self._employee_addresses.get(employee_id)
        if not address:
            raise ValueError(employee_id)
        return address           

AddressBook has a bunch of attributes_employee_addresses which maintain the mapping of employee ids to address objects

get_employee_address can obtain the address of the employee based on his or her ID, so that the employee does not actually have a direct relationship with the address, maintaining a loose relationship.

Continue to optimize the Address class

__str__ method is implemented on its own, when we print the address object, it will be output in a better form

class Address:
    def __init__(self, street, city, state, zipcode, street2=''):
        self.street = street
        self.street2 = street2
        self.city = city
        self.state = state
        self.zipcode = zipcode

    def __str__(self):
        lines = [self.street]
        if self.street2:
            lines.append(self.street2)
        lines.append(f'{self.city}, {self.state} {self.zipcode}')
        return '\n'.join(lines)           

To be continued

Another object-oriented alternative to Python: Composition