最佳实践系列:Python中的SOLID原则

发布时间 2023-07-22 00:54:02作者: Jeff_blog

SOLID原则:

S:单一职责原则(Single Responsibility Principle, SRP)。
O:开/闭原则(Open/Closed Principle, OCP)。
L:里氏替换原则(Liskov's Substitution Principle, LSP)。
I:接口分离原则(Interface Segregation Principle, ISP)
D:依赖倒置(反转)原则(Dependency Inversion Principle, DIP)

单一职责原则

一个软件组件(通常是类)只能有一个职责,如果要修改这个类,那它的原因只能有一个。类越小越好

案例:
一个应用程序负责从数据源(可以是日志文件、数据库或众多其他的数据源)读取有关事件的信息,并根据事件确定要采取的措施

classDiagram class SystemMonitor{ +load_activaty() +identify_events() +stream_events() }

反例

class SystemMonitor:
    def load_activity(self):
        """Get the events from a source, to be processed."""

    def identify_events(self):
        """Parse the source raw data into events (domain objects)."""

    def stream_events(self):
        """Send the parsed events to an external agent."""

存在的问题:
这个类定义了一个包含一系列方法的接口,但这些方法对应的操作是相互正交的:
每个操作都可以独立于其他操作完成,每个方法都表示类的一个职责,而每个职责都是导致类可能需要修改的原因。

正例:
将每个方法都放在不同的类中
在类之间分配职责

可将单一职责作为一种思路,不需要在刚开始设计时就试图完全遵循它

开/闭原则

软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的

案例:
需要设计一个监控系统,它对于另一个系统中发生的不同事件。能够根据以前收集的数据确定事件的类型

反例

# openclosed_1.py
@dataclass
class Event:
    raw_data: dict

class UnknownEvent(Event):
    """A type of event that cannot be identified from its data."""

class LoginEvent(Event):
    """A event representing a user that has just entered the system."""

class LogoutEvent(Event):
    """An event representing a user that has just left the system."""

class SystemMonitor:
    """Identify events that occurred in the system."""

    def __init__(self, event_data):
        self.event_data = event_data

    def identify_event(self):
        if (
            self.event_data["before"]["session"] == 0
            and self.event_data["after"]["session"] == 1
        ):
            return LoginEvent(self.event_data)
        elif (
            self.event_data["before"]["session"] == 1
            and self.event_data["after"]["session"] == 0
        ):
            return LogoutEvent(self.event_data)

        return UnknownEvent(self.event_data)

预期行为

>>> l1 = SystemMonitor({"before": {"session": 0}, "after": {"session":
1}})
>>> l1.identify_event().__class__.__name__
'LoginEvent'

>>> l2 = SystemMonitor({"before": {"session": 1}, "after": {"session":
0}})
>>> l2.identify_event().__class__.__name__
'LogoutEvent'

>>> l3 = SystemMonitor({"before": {"session": 1}, "after": {"session":
1}})
>>> l3.identify_event().__class__.__name__
'UnknownEvent'

存在的问题
确定事件类型的逻辑集中放在一个大一统的方法中,每当需要在系统中新增事件类型时,都必须修改这个方法。这个方法并不是对修改关闭的。
我们希望能够在不修改这个方法(对修改关闭)的情况下添加新的事件类型,同时希望能够支持新的事件类型(对扩展开放),即添加新的事件时,只需添加代码,而无须修改既有的代码

正例:

# openclosed_2.py
class Event:
    def __init__(self, raw_data):
        self.raw_data = raw_data

    @staticmethod
    def meets_condition(event_data: dict) -> bool:
        return False

class UnknownEvent(Event):
    """A type of event that cannot be identified from its data"""

class LoginEvent(Event):
    @staticmethod
    def meets_condition(event_data: dict):
        return (
            event_data["before"]["session"] == 0
            and event_data["after"]["session"] == 1
        )

class LogoutEvent(Event):
    @staticmethod
    def meets_condition(event_data: dict):
        return (
            event_data["before"]["session"] == 1
            and event_data["after"]["session"] == 0
        )

class SystemMonitor:
    """Identify events that occurred in the system."""

    def __init__(self, event_data):
        self.event_data = event_data

    def identify_event(self):
        for event_cls in Event.__subclasses__():
            try:
                if event_cls.meets_condition(self.event_data):
                    return event_cls(self.event_data)
            except KeyError:
                continue
        return UnknownEvent(self.event_data)

在这个设计中,方法identify_event是关闭的:向域中添加新的事件类型时,无须修改它。相反,事件类层次结构对扩展是开放的:当新的事件类型出现在域中时,我们只需创建一个新类,并根据它实现的接口定义判断这种事件的标准

可扩展性
假设有新事件,来验证以上方式的可扩展性

class TransactionEvent(Event):
    """Represents a transaction that has just occurred on the
system."""
    @staticmethod
    def meets_condition(event_data: dict):
        return event_data["after"].get("transaction") is not None

可以看到只需要新增事件类

里氏替换原则

LSP的原始定义(LISKOV 01)如下:如果S是T的子类型,则可将类型为T的对象替换为类型为S的对象,而不会破坏程序

案例
假设有一个客户端类,需要(包含)另一种类型的对象。通常,我们希望这个客户端与这种类型的对象交互,即通过接口进行工作

反例

class Event:
    ...
    def meets_condition(self, event_data: dict) -> bool:
        return False

class LoginEvent(Event):
    def meets_condition(self, event_data: list) -> bool:
        return bool(event_data)

正例
可以通过工具Mypy和Pylint作静态检查,找出这种不兼容的签名

接口分离原则

拆分非常庞大臃肿的接口成为更小的和更具体的接口

反例

classDiagram class EventParse{ +from_xml() +from_json() }

如果某个类不需要XML方法,它依然从接口获得了方法from_xml(),尽管不需要这个方法,却不得不保留它

正例

更佳的做法是,将这个接口分成两个,每个方法一个

依赖倒置原则

是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。

该原则规定:

高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

假设设计中有两个需要协作的对象——A和B。A使用B的实例,但我们的模块不能直接控制B(它可能是一个外部库或者是由另一个团队维护的模块)。如果代码严重依赖于B,一旦B发生变化,代码就将崩溃。为避免这种情况发生,必须倒置依赖,让B适应A。

案例
反例:图1,高层对象A依赖于底层对象B的实现;
正例:图2,把高层对象A对底层对象的需求抽象为一个接口A,底层对象B实现了接口A,这就是依赖反转。

参考

编写整洁的Python代码