最佳实践系列:Python中的SOLID原则
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)
单一职责原则
一个软件组件(通常是类)只能有一个职责,如果要修改这个类,那它的原因只能有一个。类越小越好
案例:
一个应用程序负责从数据源(可以是日志文件、数据库或众多其他的数据源)读取有关事件的信息,并根据事件确定要采取的措施
反例
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作静态检查,找出这种不兼容的签名
接口分离原则
拆分非常庞大臃肿的接口成为更小的和更具体的接口
反例
如果某个类不需要XML方法,它依然从接口获得了方法from_xml(),尽管不需要这个方法,却不得不保留它
正例
更佳的做法是,将这个接口分成两个,每个方法一个
依赖倒置原则
是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。
该原则规定:
高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。
假设设计中有两个需要协作的对象——A和B。A使用B的实例,但我们的模块不能直接控制B(它可能是一个外部库或者是由另一个团队维护的模块)。如果代码严重依赖于B,一旦B发生变化,代码就将崩溃。为避免这种情况发生,必须倒置依赖,让B适应A。
案例
反例:图1,高层对象A依赖于底层对象B的实现;
正例:图2,把高层对象A对底层对象的需求抽象为一个接口A,底层对象B实现了接口A,这就是依赖反转。