设计原则——面向对象的 SOLID 原则(python) 在软件开发中,如何做到代码的可读,可复用,可扩展,稳定是需要一些编程原则和编程思维。故开始对设计原则进行理解和学习,并结合具体案例分析。本文采用 python3 进行代码编写。
职责单一原则- SRP
Single Responsibility Principle
A class or module should have a single responsibility
一个类或者模块只负责完成一个职责或者说功能
一个类只负责完成一个职责或者功能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。
在我的理解中,对于日常开发而言,对于一个产品可能有很多需求原型,有很多模块和零散的模块。对于每个模块的功能点,可以进行归纳总结,业务抽象,具体到设计类和方法。
那么问题来了?如何判断该是否需要用到职责单一的原则?
我这里举个例子:用户模块和订单模块就是两个独立的模块。这里只举例用户模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class UserMeta (object ): """ 用户信息 """ def __init__ (self, user_id: str , name: str , age: str , address: str , cellphone: str ): self.user_id = user_id self.name = name self.age = age self.address = address self.cellphone = cellphone def to_dict (self ): pass class UserCtrl (object ): """ 用户操作 """ def login (self ): pass def logout (self ): pass def register (self ): pass def get_user_info (self ): pass
在上述的代码中,我们创建了一个用户模块,其中包含 UserMeta
用户信息类和 UserCtrl
用户操作类。这种类设计的模式在 MVC 架构的设计中很常见。
当然,随着业务的变化和公司产品的升级,需要不断的扩展和设计类,那么就需要结合实际的情况来进行扩展了。
例如,当公司的社交产品比较多的时候,需要一个统一的用户登陆认证中心,那么就需要从顶层开始设计登陆模块,这个时候就不单单是代码的重构,可能也包含服务的重构,对于多来源,多用户,需要对认证相关的信息(例如邮件、手机号、用户名)拆分开来。
以下是我的理解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 from abc import ABCMeta, abstractmethodclass UserAuthMeta (object ): """ 用户认证信息 """ def __init__ (self, user_id, name, password, cellphone, email, third_id, login_type ): self.user_id = user_id self.name = name self.password = password self.cellphone = cellphone self.email = email self.third_id = third_id self.login_type = login_type class LoginInterface (metaclass=ABCMeta ): @abstractmethod def login (self, userinfo: dict ): pass class CellphoneLogin (LoginInterface ): def login (self, userinfo: dict ): pass class EmailLogin (LoginInterface ): def login (self, userinfo: dict ): pass class WechatLogin (LoginInterface ): def login (self, userinfo: dict ): pass
在上述代码中,我从顶层抽象了一个登陆功能的接口,然后支持不同方式的登陆,对于用户认证,每种方式需要有不同的处理逻辑,故封装了 login 方法来处理。
综上,在实际的代码开发中,一个业务总是从简单到复杂,那么对业务的发展也非常考验开发人员的重构能力,并没有一个非常明确的、可以量化的标准。我们也没有必要过度设计,需要结合场景具体分析。在设计初期,可以先写一个粗粒度的类,拆分成几个更细粒度的类,然后随着业务的设计迭代不断重构。
对扩展开放、修改关闭 - OCP
Open Closed Principle
software entities should be open for extension, but closed for modification.
简单表述一下就是,添加一个新的功能应该是,在已有代码的基础上扩展代码(新增模块、类、方法等),而非修改已有的代码(修改模块、类、方法)。
对于这条原则,是一个难以应用和最有用的一个原则,这是因为,扩展性是代码质量最重要的衡量标准之一,大部分设计模式都是为了解决代码的扩展性问题而存在的,主要遵循的设计原则就是开闭原则。
这里为了更好地理解这个原则,举一个 API 接口监控告警的代码:
1 2 3 4 5 6 7 8 9 10 11 12 class Alert (object ): def __init__ (self, alert_rule, notification ): self.alert_rule = alert_rule self.notification = notification def check (self, api, request_count, error_count, duration_of_seconds ): tps = request_count / duration_of_seconds if tps > self.alert_rule.get_matched_rule(api).get_max_tps: self.notification.notify(notificationEmergencyLevel.URgency, "..." ) if error_count > rule.get_matched_rule(api).get_max_errorcount(): notification.notify(notificationEmergencyLevel.URgency, "..." )
上面这段代码中,当接口的 TPS 超过某个预先设置的最大值时,以及当前接口请求出错数大于某个最大允许值时,就会触发告警,通知接口的相关负责人或者团队。
现在有一个新的需求是:当接口的超时时间超过我们设计的阈值时,也要触发告警,应该如何设计?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Alert (object ): def __init__ (self, alert_rule, notification ): self.alert_rule = alert_rule self.notification = notification def check (self, api, request_count, error_count, duration_of_seconds, timeout_count ): tps = request_count / duration_of_seconds if tps > self.alert_rule.get_matched_rule(api).get_max_tps: self.notification.notify(notificationEmergencyLevel.URgency, "..." ) if error_count > rule.get_matched_rule(api).get_max_errorcount(): notification.notify(notificationEmergencyLevel.URgency, "..." ) if (timeout_count / duration_of_seconds) > self.rule.get_matchedRule(api).get_max_tps(): notification.notify(notificationEmergencyLevel.URgency, "..." )
在上述的代码中,我们新增了 timeout_count
和 判断是否告警的逻辑,这样就违背了开闭原则
那么应该如何设计呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class APIStatInfo (object ): def __init__ (self, api: str , requestCount: int , errorCount: int , durationOfSeconds: int , timeout_count ): self.api = api self.requestCount = requestCount self.errorCount = errorCount self.timeoutCount = timeout_count self.durationOfSeconds = durationOfSeconds class Alert (object ): def __init__ (self ): self.handlers = [] def add_handler (self, handler ): pass def check (self, api_stat_info: APIStatInfo ): for handler in self.handlers: handler.check(api_stat_info) from abc import ABC, abstractmethodclass AlertHandler (ABC ): def __init__ (self, rule, notification ): self.rule = rule self.notification = notification @abstractmethod def check (self ): pass class TPSAlertHandler (AlertHandler ): def __init__ (self, rule, notification ): super ().__init__(rule, notification) def check (self ): if self.rule > "设定的阈值" : print ("监控告警逻辑" ) class TimeoutAlertHandler (AlertHandler ): def __init__ (self, rule, notification ): super ().__init__(rule, notification) def check (self ): if self.rule > "设定的阈值" : print ("监控告警逻辑" )
在上述代码中,我对原有的类进行了扩展,对于每个 API 的状态信息进行了封装:APIStatInfo
类,然后对于每种监控抽象出来了一个接口类用于扩展。
当新增一个 timeout 的需求时,我只需要在 APIStatInfo
类中添加 timeout_count
信息和扩展一个 TimeoutAlertHandler
类就可以做到满足开闭原则了。
综上所述:在上述例子中,演示了如何设计一个可扩展的类,用于后续的扩展。那么有人就要问了,不是对修改关闭吗,为什么 APIStatInfo
中的代码修改了,对于修改这个词,我们要正确的认识,在本人的开发经历中,对于新需求的添加,很少有做到完全对原有代码不修改的,我们所谓的对修改关闭,实则是要做到让修改更集中,更少,更上层,尽量让最核心、最复杂的部分逻辑满足开闭原则。
里氏替换——LSP
Liskov Substitution Principle, 缩写为 LSP。这个原则最早是在 1986 年由 Barbara Liskov 提出:
if S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。
在 1996 年,Robert Martin 在他的 SOLID 原则中,重新描述了这个原则:
Functions that use pointers of references to base classes must be able to use objects of derived classes without konwing it。
用中文结合描述就是:子类对象(object of subtype/derived class) 能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
具体定义可以分为:
子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
子类中可以增加自己特有的方法。
当子类覆盖或实现父类的方法时,方法的前置条件(方法的形参)要比父类的输入参数更宽松。
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
我们用一个鸟类的继承来说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from abc import ABC, abstractmethodclass Bird (ABC ): def __init__ (self, name ): self.name = name self.birds = [] @abstractmethod def fly (self ): pass def collect_bird (self, name ): self.birds.append(name) class Swallow (Bird ): def __init__ (self, name ): super ().__init__(name) def fly (self ): print ("swallow flying" ) def collect_bird (self, name ): print ("" ) def sing (self ): print ("swallow singing" )
对于为什么子类必须实现父类的抽象方法,个人的理解是这样的,在面向对象中的继承中,父类的抽象方法对于子类而言是一种协议,是必须实现的,是用来约束的。
而对于父类中非抽象的方法,多数是一些常用方法,对于子类或者父类都是通用的逻辑,如果改变,将导致一些问题。
接口隔离原则——ISP
Interface Segregation Principle, 缩写为 ISP. Robert Martin 在 SOLID 原则中是这样定义的: Client should not be forced to depend upon interfaces that they donot use.
即客户端不应该被强迫依赖它不需要的接口。
对于接口隔离而言,首先我们需要理解接口二字,有以下常规理解:
一组 API 接口集合
单个 API 接口或函数
OOP 中的接口概念
这里结合一个例子来理解:在一个微服务用户中心中,有一组跟用户相关的 API 给其他系统使用,比如注册、登陆、获取用户信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from abc import ABC, abstractmethodclass UserService (ABC ): @abstractmethod def register (self ): pass @abstractmethod def login (self ): pass @abstractmethod def get_user_info (self ): pass
现在对于后台系统而言,对于一些违规用户,我们是需要对其进行拉黑处理或者封禁处理的。那么应该如何操作,常规的思路就是在该接口下定义一个 unbidden()
方法。但是这样设计的话,会有一些操作隐患,因为这个接口只限于后台管理系统,对于前端系统而言是不可调用的,如果不加限制的被其他业务系统调用,就会导致一些安全问题。
一组 API 接口集合 最好的办法就是从架构设计的层面,通过接口鉴权的方式来限制接口的调用。再者可以参考接口隔离原则,调用者不应该强迫依赖它不需要的接口,将删除接口单独放置另外一个接口。
于是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from abc import ABC, abstractmethodclass UserService (ABC ): @abstractmethod def register (self ): pass @abstractmethod def login (self ): pass @abstractmethod def get_user_info (self ): pass class RestrictUserInterface (ABC ): @abstractmethod def unbidden_user (self ): pass class UnbiddenUser (RestrictUserInterface ): def unbidden_user (self ): print ("限制用户的一些操作" )
接口理解为单个 API 或者函数 在这部分我们可以理解为:函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现。
1 2 3 4 5 6 7 8 9 10 class Statistics (object ): def __init__ (self, max , min , average, sum ): self.max = max self.min = min self.average = average self.sum = sum def statistics (self ): print ("计算的具体逻辑" )
在上述的代码中,对于统计而言,有很多个统计项,按照接口隔离而言,应该将不同的统计拆分。
1 2 3 4 5 6 7 def max (): pass def min (): pass ...
当然,在这方面,接口隔离和职责单一而言比较类似,但是单一职责针对的是模块、类、接口的设计。接口隔离更多指的是每种接口应该实现其特有的方法。
将接口理解为 OOP 中的接口概念 这里是将接口理解为 interface
,例如 go/java 中的 interface
例如在我们的项目中,有三个外部系统:Redis
, mysql
, kafka
。每个系统都对应一系列的配置信息,比如地址、端口、访问超时时间等。为了在内存中存储这些配置信息,分别设计了三个 Config 类: RedisConfig, MysqlConfig, KafkaConfig.
like this:
1 2 3 4 5 6 7 8 9 10 class RedisConfig (object ): def __init__ (self, configSource, address, timeout, maxTotal, ): self.configSource = configSource self.address = address self.timeout = timeout self.maxTotal = maxTotal def update (self ): print ("更新的具体逻辑" )
但是现在有一个需求,就是以固定时间的方式更新配置,且在不重启服务的情况下,这就是热更新;
为了实现这个需求,我们需要设计一个调度类SchedulerUpdater
来支持热更新,
于是,为了使得接口隔离,抽象出 Updater
与 Viewer
但是来实现,前者更新,后者显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Updater (ABC ): @abstractmethod def update (self ): pass class Viewer (ABC ): @abstractmethod def view (self ): pass class RedisConfig (Updater ): def __init__ (self, configSource, address, timeout, maxTotal, ): self.configSource = configSource self.address = address self.timeout = timeout self.maxTotal = maxTotal def update (self ): print ("更新的具体逻辑" ) class SchedulerUpdater (object ): def __init__ (self, schedulerservice, per_seconds, updater ): self.scheulserservice = schedulerservice self.per_seconds = per_seconds self.updater = updater def run (self ): self.scheulserservice(self.per_seconds, self.per_seconds)
在这样的扩展下,就实现了接口隔离,每个接口对应不同的功能。
依赖反转原则——DIP
High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.
高层模块(high-level modules )不要依赖底层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象不要依赖具体的实现细节,具体的实现细节依赖抽象。
所谓高层模块和低层模块,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。
在 python 中,实现依赖倒置原则通常使用接口抽象和依赖注入技术。具体来说,我们可以将代码组织为两个层次,即高层代码和低层模块。高层模块负责处理业务逻辑,低层模块负责提供基础服务。二者之间通过抽象接口进行通信,从而实现了解耦合。
同样,我们通过一个例子来学习:
当我们需要一份数据时,这份数据可能有很多个来源,例如文件或者数据库,当对文件进行解析时,需要用到不同的解析器,我们可以抽象出一个 Reader
接口管理低层解析类,在 DataView
使用时,不用关心 reader
的具体实现,只需要知道调用其中的 read()
方法即可,这样就可以满足 DIP 原则了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 from abc import ABC, abstractmethodclass Reader (ABC ): @abstractmethod def read (self ): pass class FileReader (Reader ): def read (self ): print ("Reading from file" ) class DatabaseReader (Reader ): def read (self ): print ("reading from database" ) class DataView (object ): def __init__ (self, reader ): self.reader = reader def show_data (self ): self.reader.read() if __name__ == '__main__' : file_reader = FileReader() database_reader = DatabaseReader() processor1 = DataView(file_reader) processor2 = DataView(DatabaseReader) processor1.show_data() processor2.show_data()
总结 今天总结学习了面向对象的五大原则:
其中职责单一原则是为了对模块,类进行划分,每个类,每个模块都只做一件事。
开闭原则更多的是关注代码的可扩展性,在软件开发中,业务需求是不断迭代的,对于如何写出可扩展的代码,需要我们使用开闭原则进行规范,即对组合开放,对修改关闭。
接口隔离适用于比较复杂的业务场景,和职责单一原则息息相关,对于每种接口而言,是隔离的,这样在使用和扩展时,就会比较清晰。
里氏代换原则更多的考虑是代码的健壮性,即在继承和多态时,对于抽象方法和非抽象方法的约束,保证继承链条的健壮,保证调用时不会出错。
依赖反转描述的是高层和低层的关系,二者通过接口抽象协议进行通信,低层负责干活,高层只负责到指定的地方去拿数据,不需要考虑低层的实现方式,高层是不依赖于低层的具体实现的。
以上就是全部内容了,上述原则都是为了写出可读,可扩展,可复用,稳定的代码。代码设计讲究均衡和适合,在了解这些原则的时候也要清楚一点,就是不要强搬硬套,避免过度设计