Python面向对象设计原则
1. 单一职责原则(Single Responsibility Principle,SRP)
1.1 原则定义
单一职责原则强调一个类应该只有一个引起它变化的原因。换而言之,一个类应该只负责一项职责,当这个职责发生变化时,不会影响到类的其他功能。
在Python中,这意味着每个类应该专注于一个特定的任务或功能。例如,我们创建一个处理用户数据的程序。如果我们有一个User
类,它既负责从数据库读取用户信息,又负责对用户信息进行验证,还负责格式化用户信息展示,那么这个类就违反了单一职责原则。因为读取数据、验证数据和格式化数据展示是三个不同的职责,任何一个职责的变化(比如数据库结构改变、验证规则改变或者展示格式改变)都可能影响到整个User
类。
1.2 代码示例
违反单一职责原则的代码
class User:
def __init__(self, user_id):
self.user_id = user_id
def get_user_from_db(self):
# 模拟从数据库获取用户数据
return {'user_id': self.user_id, 'name': 'John Doe', 'email': 'johndoe@example.com'}
def validate_user(self, user_data):
if 'email' not in user_data or '@' not in user_data['email']:
raise ValueError('Invalid email')
return True
def format_user_display(self, user_data):
return f"Name: {user_data['name']}, Email: {user_data['email']}"
user = User(1)
user_data = user.get_user_from_db()
if user.validate_user(user_data):
print(user.format_user_display(user_data))
在上述代码中,User
类承担了获取用户数据、验证用户数据和格式化用户数据展示这三个职责。
遵循单一职责原则的代码
class UserDataFetcher:
def __init__(self, user_id):
self.user_id = user_id
def get_user_from_db(self):
# 模拟从数据库获取用户数据
return {'user_id': self.user_id, 'name': 'John Doe', 'email': 'johndoe@example.com'}
class UserValidator:
def validate_user(self, user_data):
if 'email' not in user_data or '@' not in user_data['email']:
raise ValueError('Invalid email')
return True
class UserFormatter:
def format_user_display(self, user_data):
return f"Name: {user_data['name']}, Email: {user_data['email']}"
user_id = 1
fetcher = UserDataFetcher(user_id)
user_data = fetcher.get_user_from_db()
validator = UserValidator()
if validator.validate_user(user_data):
formatter = UserFormatter()
print(formatter.format_user_display(user_data))
在这段代码中,我们将获取用户数据、验证用户数据和格式化用户数据展示这三个职责分别分配到了UserDataFetcher
、UserValidator
和UserFormatter
三个不同的类中,遵循了单一职责原则。这样,当其中一个职责发生变化时,不会影响到其他类的功能。
2. 开闭原则(Open - Closed Principle,OCP)
2.1 原则定义
开闭原则指出软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着当我们需要增加新功能时,应该通过扩展现有代码来实现,而不是直接修改已有的代码。
在Python面向对象编程中,这一原则通过抽象和多态来实现。例如,我们有一个图形绘制程序,当前支持绘制圆形和矩形。如果后续需要支持绘制三角形,根据开闭原则,我们不应该修改已有的绘制圆形和矩形的代码,而是通过扩展的方式来添加绘制三角形的功能。
2.2 代码示例
违反开闭原则的代码
class Shape:
def __init__(self, shape_type):
self.shape_type = shape_type
def draw(self):
if self.shape_type == 'circle':
print('Drawing a circle')
elif self.shape_type =='rectangle':
print('Drawing a rectangle')
shapes = [Shape('circle'), Shape('rectangle')]
for shape in shapes:
shape.draw()
如果我们要添加绘制三角形的功能,就需要修改draw
方法,这违反了开闭原则。
class Shape:
def __init__(self, shape_type):
self.shape_type = shape_type
def draw(self):
if self.shape_type == 'circle':
print('Drawing a circle')
elif self.shape_type =='rectangle':
print('Drawing a rectangle')
elif self.shape_type == 'triangle':
print('Drawing a triangle')
shapes = [Shape('circle'), Shape('rectangle'), Shape('triangle')]
for shape in shapes:
shape.draw()
遵循开闭原则的代码
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def draw(self):
pass
class Circle(Shape):
def draw(self):
print('Drawing a circle')
class Rectangle(Shape):
def draw(self):
print('Drawing a rectangle')
class Triangle(Shape):
def draw(self):
print('Drawing a triangle')
shapes = [Circle(), Rectangle(), Triangle()]
for shape in shapes:
shape.draw()
在这个示例中,我们定义了一个抽象基类Shape
,并定义了抽象方法draw
。具体的图形类如Circle
、Rectangle
和Triangle
继承自Shape
类并实现draw
方法。当需要添加新的图形时,我们只需要创建一个新的类继承自Shape
类并实现draw
方法,而不需要修改已有的类,遵循了开闭原则。
3. 里氏替换原则(Liskov Substitution Principle,LSP)
3.1 原则定义
里氏替换原则指出,所有引用基类(父类)的地方必须能透明地使用其子类的对象。这意味着子类对象必须能够替换掉它们的父类对象,而程序的行为不会发生改变。
在Python中,这要求子类必须遵循父类定义的接口(方法签名等),并且子类对象的行为应该与父类对象的预期行为一致。例如,如果父类有一个方法用于计算某个值,子类重写这个方法后,其返回值的类型和意义应该与父类方法保持一致,不能出现子类方法返回值类型与父类不同或者语义完全改变的情况。
3.2 代码示例
违反里氏替换原则的代码
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def set_width(self, width):
self.width = width
def set_height(self, height):
self.height = height
def get_area(self):
return self.width * self.height
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
def set_width(self, width):
self.width = width
self.height = width
def set_height(self, height):
self.width = height
self.height = height
def calculate_area(rectangle):
rectangle.set_width(5)
rectangle.set_height(4)
return rectangle.get_area()
rectangle = Rectangle(3, 4)
print(calculate_area(rectangle))
square = Square(5)
print(calculate_area(square))
在上述代码中,Square
类继承自Rectangle
类。但是Square
类重写了set_width
和set_height
方法,导致其行为与Rectangle
类不一致。在calculate_area
函数中,原本期望传入的是Rectangle
对象,按照Rectangle
的行为来计算面积。但当传入Square
对象时,由于Square
的特殊行为(改变宽时高也跟着改变),导致计算结果与预期不符,违反了里氏替换原则。
遵循里氏替换原则的代码
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def set_width(self, width):
self.width = width
def set_height(self, height):
self.height = height
def get_area(self):
return self.width * self.height
class Square:
def __init__(self, side):
self.side = side
def set_side(self, side):
self.side = side
def get_area(self):
return self.side * self.side
def calculate_area(shape):
if isinstance(shape, Rectangle):
shape.set_width(5)
shape.set_height(4)
elif isinstance(shape, Square):
shape.set_side(5)
return shape.get_area()
rectangle = Rectangle(3, 4)
print(calculate_area(rectangle))
square = Square(5)
print(calculate_area(square))
在这段代码中,我们没有让Square
类继承自Rectangle
类,而是让它们保持相对独立。calculate_area
函数根据对象的类型进行不同的操作,这样就避免了因继承导致的行为不一致问题,遵循了里氏替换原则。
4. 接口隔离原则(Interface Segregation Principle,ISP)
4.1 原则定义
接口隔离原则提倡客户端不应该依赖它不需要的接口。也就是说,一个类对另一个类的依赖应该建立在最小的接口上。
在Python中,虽然没有像Java那样严格的接口概念,但我们可以通过抽象基类和抽象方法来模拟接口。例如,如果一个类需要实现多个功能,但某些客户端只需要其中部分功能,按照接口隔离原则,我们应该将这些功能拆分到不同的“接口”(抽象基类)中,让类有选择地实现这些“接口”,而不是实现一个大而全的接口。
4.2 代码示例
违反接口隔离原则的代码
from abc import ABC, abstractmethod
class AllInOneInterface(ABC):
@abstractmethod
def do_task1(self):
pass
@abstractmethod
def do_task2(self):
pass
@abstractmethod
def do_task3(self):
pass
class MyClass(AllInOneInterface):
def do_task1(self):
print('Doing task 1')
def do_task2(self):
print('Doing task 2')
def do_task3(self):
print('Doing task 3')
class Client1:
def __init__(self, obj):
self.obj = obj
def use_interface(self):
self.obj.do_task1()
class Client2:
def __init__(self, obj):
self.obj = obj
def use_interface(self):
self.obj.do_task1()
self.obj.do_task2()
obj = MyClass()
client1 = Client1(obj)
client1.use_interface()
client2 = Client2(obj)
client2.use_interface()
在上述代码中,MyClass
实现了一个包含三个方法的AllInOneInterface
接口。但Client1
只需要使用do_task1
方法,Client2
需要使用do_task1
和do_task2
方法。对于Client1
来说,do_task2
和do_task3
方法就是它不需要的接口,这违反了接口隔离原则。
遵循接口隔离原则的代码
from abc import ABC, abstractmethod
class Task1Interface(ABC):
@abstractmethod
def do_task1(self):
pass
class Task2Interface(ABC):
@abstractmethod
def do_task2(self):
pass
class Task3Interface(ABC):
@abstractmethod
def do_task3(self):
pass
class MyClass(Task1Interface, Task2Interface, Task3Interface):
def do_task1(self):
print('Doing task 1')
def do_task2(self):
print('Doing task 2')
def do_task3(self):
print('Doing task 3')
class Client1:
def __init__(self, obj):
self.obj = obj
def use_interface(self):
self.obj.do_task1()
class Client2:
def __init__(self, obj):
self.obj = obj
def use_interface(self):
self.obj.do_task1()
self.obj.do_task2()
obj = MyClass()
client1 = Client1(obj)
client1.use_interface()
client2 = Client2(obj)
client2.use_interface()
在这段代码中,我们将原来的大接口拆分成了三个小接口Task1Interface
、Task2Interface
和Task3Interface
。MyClass
实现了这三个接口,而Client1
和Client2
根据自己的需求依赖相应的接口,遵循了接口隔离原则。
5. 依赖倒置原则(Dependency Inversion Principle,DIP)
5.1 原则定义
依赖倒置原则强调高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
在Python中,这意味着我们应该尽量使用抽象类或接口来定义对象之间的依赖关系,而不是直接依赖具体的实现类。这样可以提高代码的可维护性和可扩展性。例如,在一个电子商务系统中,高层模块(如订单处理模块)不应该直接依赖低层模块(如具体的支付方式实现类),而是依赖支付方式的抽象接口,具体的支付方式实现类(如支付宝支付、微信支付)依赖这个抽象接口。
5.2 代码示例
违反依赖倒置原则的代码
class AlipayPayment:
def pay(self, amount):
print(f'Alipay pays {amount}')
class Order:
def __init__(self):
self.payment = AlipayPayment()
def process_order(self, amount):
self.payment.pay(amount)
order = Order()
order.process_order(100)
在上述代码中,Order
类直接依赖AlipayPayment
类,这是一个具体的实现类。如果后续需要添加微信支付,就需要修改Order
类的代码,违反了依赖倒置原则。
遵循依赖倒置原则的代码
from abc import ABC, abstractmethod
class Payment(ABC):
@abstractmethod
def pay(self, amount):
pass
class AlipayPayment(Payment):
def pay(self, amount):
print(f'Alipay pays {amount}')
class WechatPayment(Payment):
def pay(self, amount):
print(f'Wechat pays {amount}')
class Order:
def __init__(self, payment):
self.payment = payment
def process_order(self, amount):
self.payment.pay(amount)
alipay = AlipayPayment()
order1 = Order(alipay)
order1.process_order(100)
wechat = WechatPayment()
order2 = Order(wechat)
order2.process_order(200)
在这段代码中,我们定义了一个抽象基类Payment
,AlipayPayment
和WechatPayment
类继承自Payment
类并实现pay
方法。Order
类依赖Payment
抽象类,而不是具体的支付实现类。这样,当需要添加新的支付方式时,只需要创建一个新的类继承自Payment
类并实现pay
方法,不需要修改Order
类的代码,遵循了依赖倒置原则。
6. 组合复用原则(Composite Reuse Principle,CRP)
6.1 原则定义
组合复用原则建议优先使用对象组合,而不是类继承来实现代码复用。组合是指在一个类中使用另一个类的对象作为成员变量,这样可以将不同类的功能组合在一起。
与继承相比,组合更加灵活。继承是一种强耦合关系,子类依赖于父类的实现细节。而组合则是一种松耦合关系,一个类可以根据需要组合不同的对象来实现功能,并且可以在运行时动态改变组合关系。
6.2 代码示例
使用继承实现复用的代码
class Animal:
def move(self):
print('Animal moves')
class Dog(Animal):
def bark(self):
print('Dog barks')
dog = Dog()
dog.move()
dog.bark()
在这个示例中,Dog
类继承自Animal
类,复用了Animal
类的move
方法。但是如果Animal
类的move
方法实现发生改变,可能会影响到Dog
类。而且Dog
类与Animal
类之间是强耦合关系。
使用组合实现复用的代码
class Movement:
def move(self):
print('Moves')
class Dog:
def __init__(self):
self.movement = Movement()
def bark(self):
print('Dog barks')
def make_move(self):
self.movement.move()
dog = Dog()
dog.make_move()
dog.bark()
在这段代码中,Dog
类通过组合Movement
类的对象来实现移动功能。Dog
类与Movement
类之间是松耦合关系。如果Movement
类的实现发生改变,只要其接口(move
方法)不变,就不会影响到Dog
类。同时,Dog
类可以在运行时动态改变movement
对象,例如替换成其他实现了move
方法的类的对象。
7. 迪米特法则(Law of Demeter,LoD)
7.1 原则定义
迪米特法则也称为最少知识原则,它规定一个对象应该对其他对象有尽可能少的了解。也就是说,一个类应该尽量减少与其他类的交互,只与直接的朋友交互。
在Python中,这意味着一个类不应该直接访问另一个类的内部细节,而应该通过有限的接口进行交互。例如,如果一个类需要获取另一个类的某些数据,不应该直接访问其成员变量,而是应该通过该类提供的公开方法来获取。
7.2 代码示例
违反迪米特法则的代码
class Address:
def __init__(self, street, city):
self.street = street
self.city = city
class Person:
def __init__(self, name, address):
self.name = name
self.address = address
class Company:
def print_employee_address(self, person):
print(f"Employee {person.name} lives on {person.address.street}, {person.address.city}")
address = Address('123 Main St', 'Anytown')
person = Person('John', address)
company = Company()
company.print_employee_address(person)
在上述代码中,Company
类直接访问了Person
类的address
成员变量,又进一步访问了Address
类的street
和city
成员变量。这违反了迪米特法则,因为Company
类对Person
和Address
类的了解过多。
遵循迪米特法则的代码
class Address:
def __init__(self, street, city):
self.street = street
self.city = city
def get_address_info(self):
return f"{self.street}, {self.city}"
class Person:
def __init__(self, name, address):
self.name = name
self.address = address
def get_address(self):
return self.address.get_address_info()
class Company:
def print_employee_address(self, person):
print(f"Employee {person.name} lives at {person.get_address()}")
address = Address('123 Main St', 'Anytown')
person = Person('John', address)
company = Company()
company.print_employee_address(person)
在这段代码中,Company
类通过Person
类提供的get_address
方法来获取地址信息,Person
类又通过Address
类提供的get_address_info
方法来获取具体的地址信息。这样,Company
类对Person
和Address
类的了解仅限于它们提供的接口,遵循了迪米特法则。