如果您的设计依赖于继承,则需要找到一种方法来更改对象的类型以更改其行为。对于组合,您只需要更改对象使用的策略
想象一下,我们的经理突然变成了按小时计酬的临时雇员。您可以通过以下方式在程序执行期间修改对象
# In program.py
from hr import PayrollSystem, HourlyPolicy
from productivity import ProductivitySystem
from employees import EmployeeDatabase
productivity_system = ProductivitySystem()
payroll_system = PayrollSystem()
employee_database = EmployeeDatabase()
employees = employee_database.employees
manager = employees[0]
manager.payroll = HourlyPolicy(55)
productivity_system.track(employees, 40)
payroll_system.calculate_payroll(employees)
该程序从EmployeeDatabase
获取员工列表,并检索第一个员工,即我们想要的经理。然后,它会创建一个新的HourlyPolicy
,初始化为每小时$55
,并将其分配给manager
对象
$ python program.py
Tracking Employee Productivity
==============================
Employee 1 - Mary Poppins:
- screams and yells for 40 hours.
Employee 2 - John Smith:
- does paperwork for 40 hours.
Employee 3 - Kevin Bacon:
- expends 40 hours on the phone.
Employee 4 - Jane Doe:
- manufactures gadgets for 40 hours.
Employee 5 - Robin Williams:
- does paperwork for 40 hours.
Calculating Payroll
===================
Payroll for: 1 - Mary Poppins
- Check amount: 2200
- Sent to:
121 Admin Rd.
Concord, NH 03301
Payroll for: 2 - John Smith
- Check amount: 1500
- Sent to:
67 Paperwork Ave
Manchester, NH 03101
Payroll for: 3 - Kevin Bacon
- Check amount: 1800.0
- Sent to:
15 Rose St
Apt. B-1
Concord, NH 03301
Payroll for: 4 - Jane Doe
- Check amount: 600
- Sent to:
39 Sole St.
Concord, NH 03301
Payroll for: 5 - Robin Williams
- Check amount: 360
- Sent to:
99 Mountain Rd.
Concord, NH 03301
到目前为止,您已经了解了在Python中继承和组合是如何工作的。您已经看到派生类继承了它们的基类的接口和实现。您还看到了组合允许您重用另一个类的实现
对于同一个问题,您已经实现了两个解决方案。第一个解决方案使用多重继承,第二个使用复合
您还看到Python的duck类型化允许您通过实现所需的接口来重用具有程序现有部分的对象。在Python中,没有必要从基类派生出要重用的类
此时,您可能会问什么时候在Python中使用继承与组合。它们都支持代码重用。继承和组合可以解决Python程序中的类似问题
一般的建议是使用在两个类之间创建较少依赖关系的关系。这种关系就是组成。不过,有时继承会更有意义。
以下部分提供了一些指导原则,帮助您在Python中的继承和组合之间做出正确的选择
继承仅应用于为一个关系建模。Liskov的替换原理说,继承自Base的Derived类型的对象可以替换Base类型的对象,而无需更改程序的所需属性
Liskov的替代原则是决定继承是否是合适的设计解决方案的最重要的指导原则。不过,答案可能并非在所有情况下都是直截了当的。幸运的是,您可以使用一个简单的测试来确定您的设计是否遵循Liskov的替换原则
假设您有一个类a,它提供了一个您希望在另一个类B中重用的实现和接口。您最初的想法是可以从a派生出B,并继承接口和实现。为了确保这是正确的设计,您需要遵循以下步骤:
您有一个类矩形,它公开一个.area属性。您需要一个类Square,它也有一个.area。似乎正方形是一种特殊类型的矩形,所以您可以从它派生并利用接口和实现。
正方形是长方形,因为它的面积是由它的高乘以它的长计算出来的。约束条件是这个平方。高度和广场。长度必须相等。
它是有意义的。你可以证明这种关系,并解释为什么正方形是长方形。让我们来颠倒一下这种关系,看看它是否有意义
长方形是正方形,因为它的面积是由它的高乘以它的长计算出来的。差值就是这个矩形。高度和矩形。宽度可以独立变化
# In rectangle_square_demo.py
class Rectangle:
def __init__(self, length, height):
self._length = length
self._height = height
@property
def area(self):
return self._length * self._height
使用长度和高度初始化Rectangle
类,它提供一个.area
属性来返回该区域。长度和高度被封装,以避免直接改变它们。
# In rectangle_square_demo.py
class Square(Rectangle):
def __init__(self, side_size):
super().__init__(side_size, side_size)
Square
类使用side_size
初始化,该side_size
用于初始化基类的两个组件。现在,您编写一个小程序来测试行为
# In rectangle_square_demo.py
rectangle = Rectangle(2, 4)
assert rectangle.area == 8
square = Square(2)
assert square.area == 4
print('OK!')
运行程序
$ python rectangle_square_demo.py
OK!
# In rectangle_square_demo.py
class Rectangle:
def __init__(self, length, height):
self._length = length
self._height = height
@property
def area(self):
return self._length * self._height
def resize(self, new_length, new_height):
self._length = new_length
self._height = new_height
.resize()
采用对象的new_length
和new_width
。您可以将以下代码添加到程序中,以验证其是否正常运行
# In rectangle_square_demo.py
rectangle.resize(3, 5)
assert rectangle.area == 15
print('OK!')
您调整矩形对象的大小,并断言新区域正确。您可以运行该程序以验证行为
$ python rectangle_square_demo.py
OK!
那么,如果调整正方形大小会怎样?修改程序,然后尝试修改正方形对象
# In rectangle_square_demo.py
square.resize(3, 5)
print(f'Square area: {square.area}')
将与矩形相同的参数传递给square.resize(),然后打印该区域。当你运行程序时,你会看到
$ python rectangle_square_demo.py
Square area: 15
OK!
程序显示,新的区域是15像矩形对象。现在的问题是,square对象不再满足其长度和高度必须相等的square类约束
你怎么解决这个问题?你可以尝试几种方法,但所有的方法都会很尴尬。您可以在square中覆盖.resize()并忽略height参数,但是这对于查看程序的其他部分的人来说会很混乱,因为这些部分的矩形正在被调整大小,而其中一些矩形并没有得到预期的区域,因为它们实际上是正方形。
在一个像这样的小程序中,可能很容易发现奇怪行为的原因,但是在一个更复杂的程序中,问题就更难找到了
事实是,如果能够以两种方式证明两个类之间的继承关系,就不应该从另一个类派生出另一个类
在本例中,Square
从Rectangle
继承.resize()
的接口和实现是没有意义的。这并不意味着方形对象不能调整大小。这意味着接口是不同的,因为它只需要一个side_size参数