在面向对象编程中,如何通过很小的设计变化就可以应对项目的需求变化,这是开发者极为关注的问题。设计模式由此而生,在讲设计模式前先要理解六大设计原则。
六大原则分别是:单一职责原则、里氏替换原则、依赖倒置原则、接口隔离原则、迪米特原则、开放-封闭原则。
单一职责原则(SRP)
在《敏捷软件开发》中,把“职责”定义为“变化的原因”,也就是说,就一个类而言应当只有一个引起他变化的原因。例如菜刀可以用来切菜,也可以用来砍人,菜刀的两个功能就是引起这个类变化的两个原因,那么就应当写成两个类。这是一个简单的却最不容易实现的原则。
从上面的描述中应该能够看出,单一职责原则具备两个含义:一个是避免相同的职责分散到不同的类中,另一个是避免一个类承担太多的职责。
那为什么要遵守SRP呢?
- 可以减少类之间的耦合。
当需求变化时,只需要改变极少的几个类即可完成目标,而无需“伤筋动骨”这就是低耦合的好处。 - 提高类的复用性。
由于类的低耦合性使得类像车的零件部位一样可重复利用的可能性极高。
举个例子:把销售、程序员、经理等都隶属于一个职员类里,如果把他们的职能都放在一个类里则会非常的混乱,从类的结构上来说非常的臃肿。无论哪一个需求变化都会改变所有的职员,这是我们不想要看到的。
"""单一职责原则"""
class Staff(object):
"""职员"""
def __init__(self,name,position):
self.name =name
self.position =position
def work(self):
"""工作"""
return "Working..."
def journal(self):
"""写日志"""
return "Write a journal"
person1 =Staff("张三","程序员")
person2 =Staff("李四","经理")
此时如果细分责任到每一个人,岂不是一堆的判断,肯定不科学,所以尽量让一个类或者一个模块做一件事
"""单一职责原则"""
class Staff(object):
"""职员"""
def __init__(self,name,position):
self.name = name
self.position = position
def work(self):
"""工作"""
return "Working..."
class Coding(Staff):
def __init__(self,name,positing):
super(Coding,self).__init__(name,positing)
def work(self):
"""工作"""
return "Working..."
def journal(self):
"""写日志"""
return "Write a journal"
class Manager(Staff):
def __init__(self,name,positing):
super(Manager,self).__init__(name,positing)
def work(self):
"""工作"""
return "Working..."
person1 = Coding("张三","程序员")
person2 = Manager("李四","经理")
print(person1.work()) #Working...
print(person1.journal()) #Write a journal
接口隔离原则(ISP)
python中没有真正意义上的接口类,但是并不妨碍我们实现接口类。
设想项目由多个类来组成,我们可以把它系统抽象为一个接口,但是如果有新的需求只需要实现部分接口功能,那么这个接口会强制我们实现所有的方法,编写一些无用的方法。这样的接口被称为胖接口或被污染的接口,这样的接口会给系统引入一些不当的行为。
接口隔离原则表明客户端不应该被强迫实现一些他们不会使用的接口,应该把胖接口中的方法分组,然后用多个接口代替他,每一个接口服务与一个子模块(与单一职责原则有类似的思想)。
接口隔离原则主要观点
1)一个类对另一个类的依赖性应当是建立在最小的接口上的。ISP还可以降低各个客户端之间的依赖性(低耦合)。
2)客户端程序不应该依赖它不需要的接口方法,这就要求对接口进行细化,保证其纯洁性。
“接口隔离”其实就是定制化服务设计的原则,使用接口的多重继承实现对不同的接口的组合,从而对外提供组合功能----达到“按需提供服务”。
"""
接口隔离原则
"""
import abc
class Staff(metaclass=abc.ABCMeta):
"""职员"""
def __init__(self,name,position):
self.name = name
self.position = position
@abc.abstractmethod
def work(self):
"""工作"""
pass
class Log(metaclass=abc.ABCMeta):
@abc.abstractmethod
def journal(self):
"""写日志"""
pass
class Coding(Staff,Log):
def __init__(self,name,positing):
super(Coding,self).__init__(name,positing)
def work(self):
"""工作"""
return "Working..."
def journal(self):
"""写日志"""
return "Write a journal"
class Manager(Staff):
def __init__(self,name,positing):
super(Manager,self).__init__(name,positing)
def work(self):
"""工作"""
return "Working..."
person1 = Coding("张三","程序员")
person2 = Manager("李四","经理")
print(person1.work()) #Working...
print(person1.journal()) #Write a journal
开放-封闭原则(OCP)
随着项目规模的不断增大,系统的维护和修改的复杂性不断提高,“开放-封闭”原则在1998年被提出。这条原则的基本思想是:
- (open)模块的行为必须是开放的、支持拓展的。
- (closed)在对模块的功能进行拓展时,不应该影响或是大规模影响已有的程序模块。应该对扩展开放,对修改关闭。
开放-封闭原则能够提高系统的可拓展性和可维护性,但是这也是相对的,对于一台电脑而言不可能完全开放,有些设备和功能必须保持稳定才能够减少维护上的困难。电脑对接口是开放性的,利用电脑上的接口即可以轻松完成对电脑的拓展。
如何遵循“开放-封闭”原则
实现开放-封闭原则的核心思想是对抽象编程,而不对具体编程,因为抽象相对稳定。让类依赖于固定的抽象,这样的修改是封闭的;而通过面向对象的继承和多态机制,可以实现对抽象体的继承,通过覆写其方法来改变固有行为,实现新的拓展方法,所以对于拓展是开放的。
- 在设计方面充分利用“抽象”和“封装”思想。
一方面是要在软件系统中找出各种可能的“可变因素”并将之封装起来。 - 在系统功能编程实现方面应用面向接口编程。
当需求变化时,可以提供该接口新的实现类,以适应需求变化。
下面以一个播放器为例子:
"""
接口隔离原则
"""
import abc
"""定义一个抽象的接口(接口是开放的)"""
class Process(metaclass=abc.ABCMeta):
@abc.abstractmethod
def process(self):
"""必须实现一个功能"""
pass
"""实现对接口的拓展"""
"""解码功能"""
class Playerencode(Process):
def process(self):
return "encoding..."
"""输出流功能"""
class Playeroutput(Process):
def process(self):
return "ouputing..."
"""定义线程调度管理类"""
class PlayProcess(object):
def __init__(self):
self.message = None
"""判断某个对象或类是否继承于某个类"""
def ischildof(self,obj, cls):
try:
for i in obj.__bases__:
if i is cls or isinstance(i, cls):
return True
for i in obj.__bases__:
if self.ischildof(i, cls):
return True
except AttributeError: #当是对象时捕获的异常
return self.ischildof(obj.__class__, cls)
return False
def callback(self,event):
self.message = event.click()
if self.ischildof(self.message,Process):
return self.message.process()
"""实现一个MP4产品(产品功能是封闭的)"""
class Mp4(object):
def work(self):
playProcessOjb = PlayProcess()
print(playProcessOjb.callback(Event("encode")))
print(playProcessOjb.callback(Event("output")))
"""工厂类"""
class Event(object):
def __init__(self,msg):
self.__m = msg
def click(self):
if self.__m == "encode":
return Playerencode()
elif self.__m == "output":
return Playeroutput()
mp4 = Mp4()
mp4.work()
里氏替换原则(LSP)
由于面向对象编程技术中继承在具体编程中过于简单,在许多系统的设计和编程实现中,我们并没有认真、理性地思考应用系统中类之间的继承关系是否何合理,这给后来的维护带来不少的麻烦,这就需要我们有一个设计原则来遵循。他就是里氏替换原则。
任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。
那么如何遵守里氏替换原则
- 子类必须完全实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类可以实现自己特有的方法
- 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
- 子类的实例可以替代任何父类的实例,但反之不成立
- 在客户端程序中只应该使用父类对象而不应当直接使用子类对象,这样可以实现运行期绑定(动态多态)
import abc
import time
"""
里氏替换原则
"""
HOUR = 3600
class Person(object):
""" 人类 """
@abc.abstractmethod
def duty(self):
""" 职责 """
pass
class Coder(Person):
""" 程序员 """
def duty(self):
""" Coder 的职责是写代码 """
return "Coding"
def sleep(self):
""" 有时睡5小时 """
time.sleep(5*HOUR)
class Farmer(Person):
""" 农民"""
def duty(self):
""" 农民就负责斗地主 """
return "Chinese poker"
def sleep(self):
""" 农民可以睡八小时 """
time.sleep(8*HOUR)
依赖倒置原则(DIP)
这个原则的意思是:每个接口中不存在子类用不到却必须实现的方法,如果不然就要将接口拆分,使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个接口)。
概念如下:
- 上层模块不应该依赖于下层模块,它们共同依赖于一个抽象类(父类不能依赖子类,它们都要依赖抽象类)
- 抽象不能依赖具体,具体应该要依赖于抽象
- 子类应该依赖于“抽象”而不应该依赖于某个实体对象
依赖倒置原则的核心原则在于解耦,事实上许多设计模式已经隐含了依赖导致原则。
迪米特法则(LoD)
迪米特法则又叫最少知道原则,最早是在1987年由美国Northeastern University的Ian Holland提出。通俗的来讲,就是一个类对自己依赖的类知道的越少越好。
也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的公共方法,不对外泄漏任何信息。
迪米特法则还有一个更简单的定义:只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。
耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
总的原则:低耦合,高内聚。无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。低耦合的优点不言而喻,要做到低耦合,正是迪米特法则要去完成的。
网友评论