Python设计模式 - 基础 - 七大基本原则
提倡使用设计模式,主要出发点就是实现代码复用,增加代码的扩展性和可维护性。如何设计出简洁、易懂、灵活、优美的代码结构的确是一门学问,透彻理解并践行如下七大原则通常都能取得基本满意的结果:
- 单一职责原则(Single Responsibility Principle):一个类负责一项职责,单纯的快乐
- 开放关闭原则(Open-Closed Principle):对扩展开放,对修改关闭
- 里氏替换原则(Liskov Substitution Principle):继承与派生的规则,子类可替换父类
- 依赖倒转原则(Dependence Inversion Principle):高层模块不应该依赖于底层模块,二者都应该依赖于抽象
- 接口隔离原则(Interface Segregation Principle):建立单一接口,尽量细化接口,接口中的方法越少越好
- 迪米特法则(Law Of Demeter):高内聚,低耦合
- 组合/聚合复用原则 (Composition/Aggregation Reuse Principle):尽量使用组合/聚合,少使用类继承,尤其是多重继承
单一职责原则(Single Responsibility Principle)
概要说明
简单地说,就是一个类负责一项职责。
实质上就是希望一个类只有一个引起它变化的因子,这里说的变化因子就是类具备的“职责”,如果一个类有多个引起它变化的因子,也就意味着这个类有多个职责,就相当于把多个职责耦合在一起了。而我们进行框架设计的其中一个原则就是希望低耦合,所以任何高耦合的设计都是应该尽力避免的,这样当修改一个功能时,可以显著降低对其其他功能的影响。
适用场景
一个类负责多项职责的场景
同一个类C负责两个或多个不同的职责:职责func1...职责funcN。当类C中的某个职责发生改变需要修改类C时,有可能导致该类C原本运行正常的其他职责发生功能故障。
所以我们需要考虑将类C拆分成两个或多个类,将类C中经常发生变化的单个职责封装成一个类。这样新封装类中的职责发生变化时不会导致类C拆分后生成的其他类发生故障。
一个类负责一项职责时“职责扩散”场景
在实际项目中经常会出现”职责扩散“的现象,比如类P原本只有单个职责func1,但由于需求变更或其它原因,职责1被细分为职责func11和职责func12,导致类P无法正常工作。
Python实现
现有一Log类原本只有单个职责“离线收集日志”,现在新增需求希望给日志做实时大数据分析
# 现有实现 class Log(object):
def __init__(self):
pass def collect(self, log_dir):
print ("Collecting the log files under {} offline.".format(log_dir)) class Client(object):
def __init__(self):
pass def act(self):
log = Log()
log.collect("C:\\logs\\")
log.collect("/opt/logs") if __name__ == '__main__':
client = Client()
client.act() # 执行结果
Collecting the log files under C:\logs\ offline.
Collecting the log files under /opt/logs offline.
若新增需求“实时大数据分析”后,根据单一职责原则,需要对日志类进行职责拆分。实际项目中收集日志及实时分析日志的功能会复杂的多,本例中的实现出于简洁易懂的考虑代码非常简单。
# 职责拆分后的实现 class LogOfflineCollection(object):
def __init__(self):
pass def collect(self, log_dir):
print ("Collecting the log files under {}. offline.".format(log_dir)) class LogOnlineAnalysis(object):
def __init__(self):
pass def analyze(self, log_dir):
print ("Analyzing the log files under {}. online.".format(log_dir)) class Client(object):
def __init__(self):
pass def act(self):
collect = LogOfflineCollection()
collect.collect("C:\\logs\\")
collect.collect("/opt/logs")
print ("=================================================")
analysis = LogOnlineAnalysis()
analysis.analyze("C:\\logs\\")
analysis.analyze("/opt/logs") if __name__ == '__main__':
client = Client()
client.act() # 执行结果
Collecting the log files under C:\logs\. offline.
Collecting the log files under /opt/logs. offline.
=================================================
Analyzing the log files under C:\logs\. online.
Analyzing the log files under /opt/logs. online.
注意事项
- 尽量保证定义的类或模块都是单一职责的,尽可能编写短小的类
- 单个类的职责越少,则类与类之间的耦合度就越弱,受其他类的约束和牵制就越少,可扩展性就越强
- 职责拆分时尽量把变化频繁的部分单独拆分、封装
- 单一职责的要点在于职责的粒度,但是具体需要把职责细化到哪一步,取决于项目需求和业务的复杂度
- 单一职责不是刻板的教条,程序设计的主旨还是高内聚、低耦合
- 极端场景下,一味要求所有类都是100%的单一职责可能会带来类数量的爆炸、开发效率及程序性能的降低,需要均衡考虑、灵活运用
典型应用
- Android中的MVC—>MVP—>MVVM:类职能逐渐解耦的过程
- Java中的JPA EntityManager: 只负责管理与当前持久化上下文相关的实体
- Python中的Decorator:可以在运行时动态扩展多个功能,避免了一个类承担多个职责带来的衍生问题
开放关闭原则(Open-Closed Principle)
概要说明
简单地说,对扩展开放,对修改关闭。
我们在做项目的时候,不要指望需求一开始就完全确定,这是不切实际的想法,既然需求一定是会变化的,那么我们就应该把需求变化作为一种常态来处理,将系统设计成相对易扩展、易维护的软件。
开闭原则的关键在于抽象化,将经常发生变化的部分封装成接口或抽象类,这样有新的需求或变化时,可以对现有代码进行扩展,以适应新情况,不至于整个推倒重做,只要从项目一开始就遵循开放封闭原则易扩展和易维护都不难做到。所以对于项目开发来说,软件框架本身需要具备灵活的可扩展性,尽量通过扩展软件实体的行为、而不是通过修改已有的代码来实现变化。
适用场景
- 因为需求变化、软件升级或维护时时不去修改原有的代码,通过扩展实现热插拔的效果
- 将经常发生变化的部分封装为抽象,是实现开闭原则的重要手段。拒绝滥用抽象,只将经常变化的部分进行抽象
- 通过模板方法或策略模式进行重构,实现对修改关闭,对扩展开放的设计思路
Python实现
以书店销售书籍为例。原来书店只按照原价销售小说,现在由于经济下滑,书店为了生存开始打折销售:所有50元以上的书8折销售,其他书籍9折销售。
原实现代码
class Book(object):
def __init__(self, name, price, author):
self.name = name
self.price = price
self.author = author def get_name(self):
pass def get_price(self):
pass def get_author(self):
pass def get_book_info(self):
pass class NovelBook(Book):
def __init__(self, name, price, author):
super(NovelBook, self).__init__(name, price, author) def get_name(self):
return self.name def get_price(self):
return self.price def get_author(self):
return self.author def get_book_info(self):
return "Book name: " + self.get_name() + " Book author: " + self.get_author() + " Book price: " + str(self.get_price()/100.0) + "元" class BookStore(object):
def __init__(self):
self.book_list = []
self.book_list.append(NovelBook("西游记", 3000, "吴承恩"))
self.book_list.append(NovelBook("三国演义", 6000, "罗贯中"))
self.book_list.append(NovelBook("红楼梦", 8000, "曹雪芹")) def sell(self, book):
print ("Sell Info: {}".format(book.get_book_info())) >>> if __name__ == '__main__':
book_store = BookStore()
for book in book_store.book_list:
book_store.sell(book)
# 执行结果
Sell Info: Book name: 西游记 Book author: 吴承恩 Book price: 30.0元
Sell Info: Book name: 三国演义 Book author: 罗贯中 Book price: 60.0元
Sell Info: Book name: 红楼梦 Book author: 曹雪芹 Book price: 80.0元
需求变化后需要提供打折处理,根据开闭原则,增加一个子类OffNovelBook类覆写get_price(),而不是直接在原NovelBook类上做修改。
打折销售后的实现
class Book(object):
def __init__(self, name, price, author):
self.name = name
self.price = price
self.author = author def get_name(self):
pass def get_price(self):
pass def get_author(self):
pass def get_book_info(self):
pass class NovelBook(Book):
def __init__(self, name, price, author):
super(NovelBook, self).__init__(name, price, author) def get_name(self):
return self.name def get_price(self):
return self.price def get_author(self):
return self.author def get_book_info(self):
return "Book name: " + self.get_name() + " Book author: " + self.get_author() + " Book price: " + str(self.get_price()/100.0) + "元" class OffNovelBook(NovelBook): # 新增子类用于扩展
def __init__(self, name, price, author):
super(OffNovelBook, self).__init__(name, price, author) def get_name(self):
return self.name def get_price(self):
origin_price = super(OffNovelBook, self).get_price()
off_price = 0
if origin_price >= 5000:
off_price = origin_price * 0.8
else:
off_price = origin_price * 0.9 return off_price def get_author(self):
return self.author def get_book_info(self):
off_book_info = super(OffNovelBook, self).get_book_info()
return off_book_info
# 此处初始化区属于高层次模块因为业务需求变更也需要相应修改,但修改越少越好
class BookStore(object):
def __init__(self):
self.book_list = []
self.book_list.append(OffNovelBook("西游记", 3000, "吴承恩"))
self.book_list.append(OffNovelBook("三国演义", 6000, "罗贯中"))
self.book_list.append(OffNovelBook("红楼梦", 8000, "曹雪芹")) def sell(self, book):
print ("Sell Info: {}".format(book.get_book_info())) if __name__ == '__main__':
book_store = BookStore()
for book in book_store.book_list:
book_store.sell(book)
# 执行结果
Sell Info: Book name: 西游记 Book author: 吴承恩 Book price: 27.0元
Sell Info: Book name: 三国演义 Book author: 罗贯中 Book price: 48.0元
Sell Info: Book name: 红楼梦 Book author: 曹雪芹 Book price: 64.0元
对扩展开放、对修改关闭,并不意味着不作任何修改,需求的变化往往是底层模块的变更倒逼高层模块的逐步解耦,已达到灵活性、可扩展性、可维护性强的目标。
注意事项
- 新需求的实现是通过新增加代码来完成的,不是通过修改现有代码完成的
- 只对应用程序中频繁变化的部分进行抽象
- 将软件实现中易变动的细节抽象成接口或抽象类,当需求变化时可以根据实际要求重新派生一个实现类进行扩展
典型应用
- 模板方法模式
- 策略模式
- 责任链模式
里氏替换原则(Liskov Substitution Principle)
概要说明
简单地说,基类存在的地方,子类是可以替换的。
软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立。所以在程序中尽量使用基类类型定义对象,在运行时再确定其子类类型,用子类对象来替换父类对象。
里氏替换是继承复用的基础,只有当派生类可以替换掉基类,基类才能真正被复用,而派生类才能够在基类基础上增加新的功能。LSP是对开闭原则(关键是抽象化)的补充。
里氏替换的重点在不影响父类中的原功能。父类中除了未实现的抽象方法外,就是已经实现好的方法,这些已实现好的方法实际上是一系列设定好的规范和约定,如果子类对这些非抽象已实现好的方法任意修改,就会对整个继承复用造成破坏。
适用场景
根据基类扩展子类功能
类BaseClass提供一个功能existed_func, 现在需要将功能existed_func进行扩展,扩展后的功能为extended_func,这里的extended_func有原有功能existed_func与新功能new_func组成。那么我们就可以根据里氏替换原则通过新增方法给子类添加新功能new_func,而不是直接修改基类BaseClass中的existed_func来添加新功能。
里氏替换原则告诉我们,当使用继承复用的过程中,子类SubClass可以通过新增方法扩展新功能,但不能改变父类中非抽象方法实现的行为或功能。
Python实现
以枪战片中警察抓坏蛋为例,已实现Gun抽象类及HandGun和MachineGun两个子类,现新需求增加一个玩具手枪,但是玩具手枪又不能用来射击杀人,所以不能直接扩展为Gun抽象类的子类,选择的解决方案是另外创建一个抽象类Toy通过委托模式跟Gun抽象类建立代理关系,Toy抽象类的子类ToyGun可以自由扩展自己的行为。
原需求代码实现
class Gun(object):
def __init__(self, name="枪", shape="Gun Shape", sound="Ping Ping"):
self.name = name
self.shape = shape
self.sound = sound def shoot(self):
pass class Handgun(Gun):
def __init__(self, name="手枪"):
super(Handgun, self).__init__(name) def shoot(self):
print ("掏出{},开始射击...".format(self.name)) class MachineGun(Gun):
def __init__(self, name="机枪"):
super(MachineGun, self).__init__(name) def shoot(self):
print ("架好{},开始射击...".format(self.name)) class Police(object):
def __init__(self, gun):
self.__gun = gun def kill_badman(self):
print ("警察开始杀坏人...")
self.__gun.shoot() if __name__ == '__main__':
police = Police(Handgun())
police.kill_badman() # 执行结果
警察开始杀坏人...
掏出手枪,开始射击...
需求变更后的代码实现
class Gun(object):
def __init__(self, name="枪", shape="Gun Shape", sound="Ping Ping"):
self.name = name
self.shape = shape
self.sound = sound def shoot(self):
pass class Handgun(Gun):
def __init__(self, name="手枪"):
super(Handgun, self).__init__(name) def shoot(self):
print ("掏出{},开始射击...".format(self.name)) class MachineGun(Gun):
def __init__(self, name="机枪"):
super(MachineGun, self).__init__(name) def shoot(self):
print ("架好{},开始射击...".format(self.name)) class Police(object):
def __init__(self, gun):
self.__gun = gun def kill_badman(self):
print ("警察开始杀坏人...")
self.__gun.shoot() class Toy(object):
def __init__(self, delegate, name="玩具"):
self.wrapper = delegate
self.name = name def __getattr__(self, item):
print ("Trace: ", item)
return getattr(self.wrapper, item) def play(self):
pass class ToyGun(Toy):
def __init__(self, gun, name="玩具枪"):
super(ToyGun, self).__init__(gun, name) def play(self):
print ("Playing {}...".format(self.name)) if __name__ == '__main__':
toy_gun = ToyGun(Gun())
print ("ToyGun Info: name: {}, shape: {}, sound: {}".format(toy_gun.name, toy_gun.shape, toy_gun.sound))
toy_gun.play() # 执行结果
Trace: shape
Trace: sound
ToyGun Info: name: 玩具枪, shape: Gun Shape, sound: Ping Ping
Playing 玩具枪...
注意事项
- 子类必须实现父类中声明的所有抽象方法,但不能影响父类的非抽象方法,可以增加自己特有的方法
- 子类方法覆盖或实现父类方法时,其前置条件(即方法输入形参)必须与父类前置条件相同或更宽松(适用于静态语言)
- 子类方法覆盖或实现父类方法时,后置条件(即方法返回值)必须与父类方法返回值相同或者更缩小(适用于静态语言)
- 尽量把父类设计为接口或抽象类,让子类实现接口或继承父类,并实现父类中声明的未实现方法
- 新增的功能可以通过增加一个新子类来实现
- 子类派生的重点在于不影响父类原功能,而不是不覆盖原方法
典型应用
- Java中的多态
依赖倒转原则(Dependence Inversion Principle)
概要说明
简单地说,面向接口编程,依赖于抽象而不依赖于具体实现。
具体依赖抽象,上层依赖下层。层次化封装和调用时,尽量在高层次引入抽象层,也就是说使用接口或抽象类进行变量类型声明、参数类型声明、方法返回类型声明,及数据类型的转换,使高层不依赖于具体实现类来做这些。
依赖倒置的中心思想是面向接口编程,这里的接口指的是接口功能,在Java中指的是原生的interface和abstract,在Python中指的是接口类和抽象类。面向接口编程的目的是制定好规范和契约,而不用负责任何具体的操作,把实现具体操作的任务交给具体实现类即可。为确保依赖倒转原则的应用,下层具体类应该只实现上层接口或抽象类中声明过的方法,而不需要增加多余的方法,否则上层接口无法调用到在具体实现类中新增的方法。
适用场景
需求变更时,上层接口希望替换下层具体实现类
上层类C直接依赖于下层具体实现类B,现在新需求要将下层依赖类改为具体实现类A。如果此时上层类C非接口或抽象类的话,我们需要重构代码,将上层类C修改为依赖接口I,具体实现类B和类A各自实现依赖接口I,上层类通过接口I间接调用具体实现类B和A,通过这种面向接口编程的方式大大降低了后续上层类的修改几率和维护成本。
Python实现
以专车司机为例说明,原来公司车辆少的时候,单个专车司机只开一种车型,后来公司的车辆品牌越来越多,专车司机也越来越多,不同的专车司机除了原来的宝马、奔驰外,还要开路虎、丰田等。针对汽车和司机随时被轮换的新情况,更好的设计当然是抽象出driver和car接口。
原来单个专车司机只开奔驰的例子
class Driver(object):
def __init__(self, name):
self.name = name
# 司机的主要职责就是驾驶汽车
def drive(self, benz):
print ("{} is driving {}.".format(self.name, benz.name))
benz.run() class Benz(object):
def __init__(self, name="Benz"):
self.name = name def run(self):
print ("Benz is running.") if __name__ == '__main__':
tom = Driver("Tom")
benz = Benz()
tom.drive(benz) # 执行结果
Tom is driving Benz.
Benz is running.
后来不同专车司机搭配不同车型的例子
class Driver(object):
def __init__(self, name):
self.name = name
def drive(self):
pass class CarDriver(Driver):
def __init__(self, name):
super(CarDriver, self).__init__(name) def drive(self, car):
print ("{} is driving {}.".format(self.name, car.name))
car.run() class Car(object):
def __init__(self, name):
self.name = name def run(self):
pass class Benz(Car):
def __init__(self, name="Benz"):
super(Benz, self).__init__(name) def run(self):
print ("Benz is running.") class BMW(Car):
def __init__(self, name="BMW"):
super(BMW, self).__init__(name) def run(self):
print ("BMW is running.") if __name__ == '__main__':
tom = CarDriver("Tom")
joe = CarDriver("Joe")
benz = Benz()
bmw = BMW()
tom.drive(benz)
tom.drive(bmw)
joe.drive(benz)
joe.drive(bmw) # 执行结果
Tom is driving Benz.
Benz is running.
Tom is driving BMW.
BMW is running.
Joe is driving Benz.
Benz is running.
Joe is driving BMW.
BMW is running.
注意事项
- 抽象不应该依赖于具体实现,具体实现应该依赖于抽象
- 底层模块尽量都要有抽象类或接口,或者两者都有
- 变量的声明类型尽量是抽象类或接口
- 高层模块不应该依赖于底层模块,二者都应该依赖于抽象
- 使用继承时遵循里氏替换原则
典型应用
- Spring中的依赖注入
接口隔离原则(Interface Segregation Principle)
概要说明
简单地说,使用多个隔离的接口,而不是用单个的总接口
尽量细化接口,接口中的方法尽量少。依赖几个隔离的接口要比依赖于庞大的综合接口要灵活得多。接口本身就是软件设计时对外部提供的规范和契约,通过多个接口分散定义多个单一的接口,可以降低耦合、预防需求变更带来的代码修改和故障几率,提高系统的灵活性、可扩展性和维护成本。
Java的接口及Python的多重继承都是支撑接口隔离原则的基础特性。
适用场景
臃肿的综合接口拆分
原系统定义了一个客户管理的接口,该接口中有多个方法,有供公司内部调用的,有供第三方调用的,还有供Mobile调用,从单一职责的角度来看好像也是合理的,因为它只提供客户管理的接口功能,但从接口隔离原则来看,接口中的模块数量及功能都过多,耦合度太高,需要细化将总接口拆分成多个专用的单一接口。
Python实现
以学校的十佳评选为例,要求入选的同学必须“德智体美劳”全面领先,这个标准非常之高(这里强调的是标准,至于获选的是否名副其实就是另一回事了)。现在新的需求来了,要求选拔一批优秀学生参加奥数比赛,另一批参加体育比赛。已有的选拔标准太全面,显然不符合新需求,所以我们需要拆分原有的优秀学生评选接口。根据新需求可以先把“智商高”和“体育棒”拆分出来单独各自组成新的评选接口。
原有十佳评选接口
class BestStudent(object):
def __init__(self, name):
self.name = name # 品德好
def good_virtue(self):
pass # 智商高
def good_wisdom(self):
pass # 体育棒
def good_sports(self):
pass # 审美佳
def good_aesthetic(self):
pass # 爱劳动
def good_labor(self):
pass class TopTenBestStudent(BestStudent):
def __init__(self, name):
super(TopTenBestStudent, self).__init__(name) def good_virtue(self):
print ("{}同学--品德好".format(self.name)) def good_wisdom(self):
print ("{}同学--智商高".format(self.name)) def good_sports(self):
print ("{}同学--体育棒".format(self.name)) def good_aesthetic(self):
print ("{}同学--审美佳".format(self.name)) def good_labor(self):
print ("{}同学--爱劳动".format(self.name)) class Judge(object):
def __init__(self, best_student):
self.best_student = best_student def show(self):
pass class SchoolJudge(Judge):
def __init__(self, best_student):
super(SchoolJudge, self).__init__(best_student) def show(self):
print ("=================十佳学生的信息如下================")
self.best_student.good_virtue()
self.best_student.good_wisdom()
self.best_student.good_sports()
self.best_student.good_aesthetic()
self.best_student.good_labor() if __name__ == '__main__':
top_ten = TopTenBestStudent("天赐")
school_judge = SchoolJudge(top_ten)
school_judge.show()
# 执行结果
=================十佳学生的信息如下================
天赐同学--品德好
天赐同学--智商高
天赐同学--体育棒
天赐同学--审美佳
天赐同学--爱劳动
拆分后的优秀学生评选接口
class GoodWisdom(object):
# 智商高
def good_wisdom(self):
pass class GoodSports(object):
# 体育棒
def good_sports(self):
pass class GoodMerits(object):
# 品德好
def good_virtue(self):
pass # 审美佳
def good_aesthetic(self):
pass # 爱劳动
def good_labor(self):
pass class GoodWisdomStudent(GoodWisdom):
def __init__(self, name):
self.name = name # 智商高
def good_wisdom(self):
print ("{}同学--智商高".format(self.name)) class GoodSportsStudent(GoodSports):
def __init__(self, name):
self.name = name
# 体育棒
def good_sports(self):
print ("{}同学--体育棒".format(self.name)) class TopTenBestStudent(GoodWisdom, GoodSports, GoodMerits):
def __init__(self, name):
self.name = name def good_virtue(self):
print ("{}同学--品德好".format(self.name)) def good_wisdom(self):
print ("{}同学--智商高".format(self.name)) def good_sports(self):
print ("{}同学--体育棒".format(self.name)) def good_aesthetic(self):
print ("{}同学--审美佳".format(self.name)) def good_labor(self):
print ("{}同学--爱劳动".format(self.name)) class Judge(object):
def __init__(self, best_student):
self.best_student = best_student def show(self):
pass class GoodWisdomJudge(Judge):
def __init__(self, best_student):
super(GoodWisdomJudge, self).__init__(best_student) def show(self):
print ("=================参加奥数学生的信息如下================")
self.best_student.good_wisdom() class GoodSportsJudge(Judge):
def __init__(self, best_student):
super(GoodSportsJudge, self).__init__(best_student) def show(self):
print ("=================参加体育比赛学生的信息如下================")
self.best_student.good_sports() class TopTenJudge(Judge):
def __init__(self, best_student):
super(TopTenJudge, self).__init__(best_student) def show(self):
print ("=================十佳学生的信息如下================")
self.best_student.good_virtue()
self.best_student.good_wisdom()
self.best_student.good_sports()
self.best_student.good_aesthetic()
self.best_student.good_labor() if __name__ == '__main__':
good_wisdom = GoodWisdomStudent("高智")
good_wisdom_judge = GoodWisdomJudge(good_wisdom)
good_wisdom_judge.show()
good_sports = GoodSportsStudent("姚明")
good_sports_judge = GoodSportsJudge(good_sports)
good_sports_judge.show()
top_ten = TopTenBestStudent("全才")
top_ten_judge = TopTenJudge(top_ten)
top_ten_judge.show() # 执行结果
=================参加奥数学生的信息如下================
高智同学--智商高
=================参加体育比赛学生的信息如下================
姚明同学--体育棒
=================十佳学生的信息如下================
全才同学--品德好
全才同学--智商高
全才同学--体育棒
全才同学--审美佳
全才同学--爱劳动
注意事项
- 为客户端提供尽可能小的单独的接口,而不是提供大的总接口
- 注意控制接口的粒度,接口太小会导致系统中接口数量的爆炸,不利于维护;接口太大将违背接口隔离原则,灵活性较差
- 客户端不应该依赖于自身不需要的接口,一个类对另一个类的依赖应该建立在最小接口上
典型应用
- Java的Interface
- Python的多重继承
迪米特法则(Law of Demeter)
概要说明
简单地说,最少知道原则,即一个软件实体尽可能少地与其他实体发生相互作用。
类与类之间的关系越密切,耦合度就越大,扩展和复用就越难。如果两个类之间没必要直接通信,那么这两个类就不应该发生直接交互,可以通过合理引入第三方解耦这两个类之间的相互关系。
一个类对另一个类知道的越少越好,尽量降低类中成员变量及成员函数的访问权限就相当于不把相应的变量和方法暴露给其他类,这样可以尽量少地影响其他类或模块,扩展会相对容易。所以迪米特法则有一个更为直白的定义:不要和陌生人说话,只与直接的朋友通信。哪些类、对象、变量可以成为直接的朋友呢?只要两个对象之间有耦合关系就可以说这两个对象是朋友关系,耦合关系包括:继承、实现、依赖、关联、聚合、组合等。所以当前对象的直接朋友包括:
- 当前对象本身
- 当前对象创建的对象
- 当前对象的成员对象
- 当前对象的实力变量所引用的对象
- 以参数形式传入到当前对象方法中的对象
满足上面任一条件的对象都是当前对象的“直接朋友”,否则就是陌生人。
适用场景
使用中介类或代理类降低两个对象之间的耦合度
迪米特法则的应用非常广,比如个人向代理商而非航空公司购买飞机票,老师让班长点名等等
Python实现
老师让班长点名
不遵守迪米特法则的例子:老师只有一个方法,先定义出所有学生,然后发布命令给班长去点名
class Teacher(object):
def command(self, group_leader):
self.student_list = []
# 初始化学生
for _ in range(30):
self.student_list.append(Student())
group_leader.count_students(self.student_list) class Student(object):
pass class GroupLeader(object):
def count_students(self, student_list):
print ("学生的数量是: {}".format(len(student_list))) if __name__ == '__main__':
teacher = Teacher()
# 老师发布命令
teacher.command(GroupLeader()) # 执行结果
学生的数量是: 30
修改有遵循迪米特法则的例子:去掉老师类对学生类的依赖关系
class Teacher(object):
def command(self, group_leader):
# 告诉班长点名
group_leader.count_students() class Student(object):
pass class GroupLeader(object):
def __init__(self, student_list):
self.student_list = student_list def count_students(self):
print ("学生的数量是: {}".format(len(self.student_list))) if __name__ == '__main__':
# 初始化学生
student_list = []
for _ in range(30):
student_list.append(Student()) teacher = Teacher()
# 老师发布命令
teacher.command(GroupLeader(student_list)) # 执行结果
学生的数量是: 30
注意事项
- 在不违反需求的条件下尽量降低每个类中成员变量和成员函数的访问权限
- 合理引入第三方降低对象之间直接交互的耦合度
- 尽量使用不可变类
- 过度应用迪米特法则会产生大量的代理类或中介类,导致系统过于复杂。所以需要权衡使用,既满足高内聚低耦合,又能够做到结构清晰
典型应用
- Android中Fragment与Activity之间的交互
组合/聚合复用原则(Composition/Aggregation Reuse Principle)
概要说明
简单地说,尽量使用合成/聚合的方式复用,尽量少用继承
组合/聚合复用就是在一个新对象里使用一些已有的对象,使之成为新对象的一部分,新对象通过对这些已有对象的引用达到复用已有功能的目的。
我们知道通过集合也可以达到复用的目的,但是通过继承进行复用的主要问题在于耦合度较高,且会破坏系统的封装性,因为继承会将基类的内部实现细节暴露给子类,这本身就违反迪米特法则;如果基类发生改变,那么子类也就会随之变化,这种情况也违反开闭原则。基类和子类的继承关系本身就是静态的,缺乏足够的灵活性
通过聚合或组合将已有的对象纳入到新对象中,成为新对象的一部分,这样做可以使得已有对象的内部实现细节对于新对象不可见,且已有对象成员发生改变时对新对象并没有连锁式影响。通过聚合或组合实现的复用可以让新对象动态引用已有对象,增强了灵活性,降低了类与类之间的耦合度。
适用场景
优先使用组合/聚合而非继承
以常见画图的场景为例,有一只画笔,可以圆形、正方形、长方形,着色框有三种颜色:蓝色、白色、黑色,那么我们就可以画出 3 × 3 = 9种图形,现在有两种实现方案:
- 方案一:通过继承的方式给每种形状都提供各种颜色的图形
- 方案二:通过桥接模式对每种形状和颜色进行组合
采用方案一当然也是可以的,如果后续形状中增加了心形、菱形、星形等,颜色从3种增加到7种,这种工作量及类数目会很庞大。
采用方案二可以减少类个数,也有利于系统扩展。
Python实现
以老师、学生及课程为例,老师和学生都属于“人”的范畴,老师和学生都和课程发生关系:老师讲课、学生听课,所以我们做以下设计:
- 继承:老师、学生继承自"人"
- 组合:老师、学生中组合"课程"
class People(object):
def __init__(self, name, age, sex):
self.name = name
self.age = age
self.sex = sex def get_info(self):
pass class Course(object):
def __init__(self, name, period, price):
self.name = name
self.period = period
self.price = price def get_course_info(self):
return "course name: " + self.name + " course period: " + self.period + " course price: " + str(self.price) class Teacher(People):
def __init__(self, name, age, sex, job_title):
super(Teacher, self).__init__(name, age, sex)
self.job_title = job_title
self.courses = []
self.students = [] def add_course(self, course):
self.courses.append(course) def add_student(self, student):
self.students.append(student) def get_info(self):
return self.name + " is teaching " + ','.join([item.name for item in self.courses]) class Student(People):
def __init__(self, name, age, sex):
super(Student, self).__init__(name, age, sex)
self.courses = [] def add_course(self, course):
self.courses.append(course) def get_info(self):
return self.name + " is learning " + ','.join([item.name for item in self.courses]) if __name__ == '__main__':
# 初始化课程
scala = Course("scala", "2 month", 2000)
hadoop = Course("hadoop", "1 month", 3000)
spark = Course("spark", "1 month", 3000) # 初始化老师和学生
john = Teacher("John", 30, "male", "卞和一级讲师") kaka = Student("Kaka", 18, "male")
joe = Student("Joe", 19, "male")
riqo = Student("Riqo", 17, "female") # 为老师、学生添加课程
john.add_course(scala)
john.add_course(spark)
john.add_student(kaka)
john.add_student(joe)
kaka.add_course(hadoop) print (john.get_info())
print (kaka.get_info())
for course in john.courses:
print (course.get_course_info()) # 执行结果
John is teaching scala,spark
Kaka is learning hadoop
course name: scala course period: 2 month course price: 2000
course name: spark course period: 1 month course price: 3000
注意事项
- 优先使用对象组合/聚合,而非继承
- 新对象专注实现单一任务,通过组合/聚合引用已有对象复用已实现功能
- 组合/聚合复用多个对象时需要仔细定义相应的接口
- 过度应用组合/聚合复用会导致系统庞杂,可维护性相对较差
典型应用
- 桥接模式
Python设计模式 - 基础 - 七大基本原则的更多相关文章
- Python设计模式 - 基础 - 封装 & 继承 & 多态
面向对象的核心是对象,世间万物都可以看作对象,任何一个对象都可以通过一系列属性和行为来描述,可以包含任意数量和类型的数据或操作.类是用来描述具有相同属性和方法的所有对象的集合.类通常是抽象化的概念,而 ...
- Python设计模式 - 基础 - 类/接口之间的六种关系
在程序中需要把世间万物抽象成相应的类,现实世界中物与物之间的关系和程序中类与类之间的关系相对应,因为世间万物是普遍联系的,所以程序中类与类之间也不是孤立的.在系统分析和框架设计中,根据面向对象机制的三 ...
- Python设计模式 - 总览(更新中...)
最近打算重构部分python项目,有道是"工欲善其事,必先利其器",所以有必要梳理一下相关设计模式.每次回顾基本概念或底层实现时都会有一些新的收获,希望这次也不例外. 本系列打算先 ...
- 最全36种python设计模式
设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用.设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案.这些解决方案是众多软件开发人员经过 ...
- 浅谈Python设计模式 - 原型模式
声明,本系列文章主要参考<精通Python设计模式>一书,并且参考一些资料,结合自己的一些看法来总结而来. 在<精通Python设计模式>中把设计模式分为三种类型: 创建型模式 ...
- 简介Python设计模式中的代理模式与模板方法模式编程
简介Python设计模式中的代理模式与模板方法模式编程 这篇文章主要介绍了Python设计模式中的代理模式与模板方法模式编程,文中举了两个简单的代码片段来说明,需要的朋友可以参考下 代理模式 Prox ...
- python设计模式之解释器模式
python设计模式之解释器模式 对每个应用来说,至少有以下两种不同的用户分类. [ ] 基本用户:这类用户只希望能够凭直觉使用应用.他们不喜欢花太多时间配置或学习应用的内部.对他们来说,基本的用法就 ...
- python设计模式之适配器模式
python设计模式之适配器模式 结构型设计模式一个系统中不同实体(比如,类和对象)之间的关系,关注的是提供一种简单的对象组合方式来创造功能. 适配器模式( Adapter pattern)是一种结构 ...
- Python文件基础
===========Python文件基础========= 写,先写在了IO buffer了,所以要及时保存 关闭.关闭会自动保存. file.close() 读取全部文件内容用read,读取一行用 ...
随机推荐
- hadoop 单机模式 伪分布式 完全分布式区别
1.单机(非分布式)模式 这种模式在一台单机上运行,没有分布式文件系统,而是直接读写本地操作系统的文件系统,一般仅用于本地MR程序的调试 2.伪分布式运行模式 这种模式也是在一台单机上运行,但用不同的 ...
- 机顶盒安装apk系列
1.湖南移动九州PTV-8508机顶盒安装第三方apk包 1.先把安装包放入U盘根目录下,插入机顶盒usb口 2.查看8508机顶盒IP地址 3.使用adb工具连接机顶盒,这款盒子的adb默认端口是8 ...
- Java面向对象 第5节 抽象类和接口
一.抽象类和抽象方法 区分抽象方法和普通方法1)当一个方法被abstract修饰时,该方法成为抽象方法2)抽象类所在的类必须定义为抽象类3)抽象方法不会有具体的实现,而是在抽象类的子类中通过方法重写进 ...
- 1、Sql-oracle-日期问题
1.月份差 --MONTHS_BETWEEN(date2,date1) select months_between('19-12月-1999','19-3月-1999') from dual; sel ...
- Scrapy实战篇(一)之爬取链家网成交房源数据(上)
今天,我们就以链家网南京地区为例,来学习爬取链家网的成交房源数据. 这里推荐使用火狐浏览器,并且安装firebug和firepath两款插件,你会发现,这两款插件会给我们后续的数据提取带来很大的方便. ...
- Failed to resolve: common Open File 导入项目问题
Failed to resolve: common Open File Warning:Configuration 'compile' is obsolete and has been replac ...
- python:函数初始
一.函数 1.函数初始:函数就是封装一个功能 2.函数名,函数体,关键字,函数的返回值 def 关键字,定义一个函数 my_len 函数名书写规则和变量一样 def 与函数名中间一个空格 函数名(): ...
- PP.io的三个阶段,“强中心”——“弱中心”——“去中心”
什么是PP.io? PP.io是我和Bill发起的存储项目,目的在于为开发者提供一个去中心化的存储和分发平台,能做到更便宜,更高速,更隐私. 当然做去中心化存储的项目也有好几个,FileCoin,Si ...
- 使用pm2来保证Spring Boot应用稳定运行
Spring Boot开发web应用就像开发普通的java程序一般简洁,因为其内嵌了web容易,启动的时候只需要一条命令java -jar server.jar即可,非常方便.但是由此而来的问题是万一 ...
- windows本地eclispe运行linux上hadoop的maperduce程序
继续上一篇博文:hadoop集群的搭建 1.将linux节点上的hadoop安装包从linux上下载下来(你也可以从网上直接下载压缩包,解压后放到自己电脑上) 我的地址是: 2.配置环境变量: HAD ...