MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python面向对象设计原则

2022-11-105.8k 阅读

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))

在这段代码中,我们将获取用户数据、验证用户数据和格式化用户数据展示这三个职责分别分配到了UserDataFetcherUserValidatorUserFormatter三个不同的类中,遵循了单一职责原则。这样,当其中一个职责发生变化时,不会影响到其他类的功能。

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。具体的图形类如CircleRectangleTriangle继承自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_widthset_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_task1do_task2方法。对于Client1来说,do_task2do_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()

在这段代码中,我们将原来的大接口拆分成了三个小接口Task1InterfaceTask2InterfaceTask3InterfaceMyClass实现了这三个接口,而Client1Client2根据自己的需求依赖相应的接口,遵循了接口隔离原则。

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)

在这段代码中,我们定义了一个抽象基类PaymentAlipayPaymentWechatPayment类继承自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类的streetcity成员变量。这违反了迪米特法则,因为Company类对PersonAddress类的了解过多。

遵循迪米特法则的代码

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类对PersonAddress类的了解仅限于它们提供的接口,遵循了迪米特法则。