《大话设计模式》这本书买了很久,但是一直没有坚持看完。毕业之后,丢了很多书在学校,但是这本书没有舍得丢下。现在看来当初是明智的,再次阅读的时候,才发现这本书浅显易懂,有趣,通过一个个小故事,缓缓道来设计模式的妙用。在阅读的时候,有很多的收获,其实说的这些模式,虽然没有真正的去了解,但是发现在开发过程中,很多都是常用到的,没有总结出来而已。在阅读的过程中,很多时候有感,但是又觉得差一点就能突破。在这里做下阅读笔记,算是阅读的输出。
原书中的代码都是用C#是实现的,在阅读的过程中,我根据对Python的掌握进行了转换。 demo仓库地址
设计模式的基本原则
开放-封闭原则
对于拓展是开放的,对于更改是封闭的。
无论模块是多么的封闭,都会存在一些无法对之封闭的变化,既然不可能完全封闭,设计人员必须对于他设计的模块应该对哪种变化做出选择。他必须先猜测出最有可能发生的种类,然后构造抽象来隔离那些变化。
面对需求,对程序的改动是通过新代码进行的,而不是更改现有的代码。
拒绝不成熟的抽象和抽象本省一样重要。
单一职责原则
就一个类而言,应该仅有一个引起它变化的原因。
软件设计真正要做的许多内容,就是发现职责并把这些职责分离。其实要去判断是否应该分离出类来,也不难,那就是你能够想到对于一个动机去改变一个类,那么这个类就是具有多于一个的职责。
依赖倒转原则
A、高层模块不应该依赖低层模块,两个都应该依赖抽象
B、抽象不应该依赖细节。细节应该依赖抽象。
依赖倒转原则其实可以说是面向对象设计的标志,用哪种语言来写程序不重要,如果编写时,考虑都是如何针对抽象编程而不是针对细节编程,即程序中所有的依赖关系都是终止于抽象类,或者接口,那就是面向对象的设计,反之就是过程化的设计了。
里氏替换原则
子类必须能够替换掉它们的父类。换句话你说,任何基类可以出现的地方,子类一定可以出现。 里氏替换原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。
里氏代换原则是对“开-闭”原则的补充。实现“开闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。里氏替换原则中,子类对父类的方法尽量不要重写和重载。因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。
迪米特法则(最少知道原则)
一个类对自己依赖的类知道的越少越好。
无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过公用方法提供给外部。这样当被依赖的类变化时,才能最少的影响该类。
最少知道原则的另外一个表达方式是:只与直接的朋友通信,类之间只要有耦合关系,就叫朋友关系。耦合分为依赖、关联、聚合、组合等。我们称出现为成员变量、方法参数、方法返回值中的类为直接朋友。局部变量、临时变量则不是直接关系。我们要求陌生的类不要作为局部变量出现在类中。
迪米特法则是面向对象的根本思想,是强调了类的松耦合关系。类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对关系的类造成波及。
合成复用原则
尽量首先使用合成\聚合的方式,而不是使用继承。
设计模式分类:
- 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
- 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
- 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代器模式、职责链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
参考
- 23种设计模式汇总整理
设计模式详述
源码仓库https://github.com/suAdminWen/studyForPython/tree/master/design_patterns/dahua
简单工厂模式
>>> oper = OperationFactory.create_operate('+')
>>> oper.number1 = 12
>>> oper.number2 = 13
>>> oper.get_result()
25
>>> oper = OperationFactory.create_operate('/')
>>> oper.number1 = 12
>>> oper.number2 = 0
>>> oper.get_result()
Traceback (most recent call last):
...
ValueError: 除数不能为0
策略模式
strategy.py
策略模式:它定义了算法家族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化,不会影响到使用算法的客户。
>>> csuper = CashContext('正常收费')
>>> csuper.get_result(20)
20.0
>>> csuper = CashContext('满300返100')
>>> csuper.get_result(300)
200.0
>>> csuper.get_result(200)
200.0
>>> csuper = CashContext('打八折')
>>> csuper.get_result(300)
240.0
策略模式就是用来封装算法的,但是在实践中,我们可以用它来封装几乎任何类型的规则,只要在分析过程中根据需要在不同时间应用不同的业务规则,就可以考虑使用策略模式处理这种变化的可能性。
装饰模式
decorator.py
装饰模式:动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更灵活。
装饰模式的优点就是把类中的装饰功能从类中搬移去除,这样可以简化原有的类。有效地把类的核心职责和装饰功能区分开了,而且可以去除相关类中的重复的装饰逻辑。
代理模式
proxy.py
为其他对象提供一种代理以控制对这个对象的访问。
代码示例:A追求C,委托B送礼物给C。需求的重点是A和C不能直接接触::
>>> jiaojiao = SchoolGirl()
>>> jiaojiao.name = '李娇娇'
工厂方法模式
factory_method.py
工厂方法模式,定义一个用于创建对象的接口,让子类决定实例化哪个类。工厂方法使一个类的实例化延迟到其子类。
代码示例:学习雷锋好榜样,继承雷锋精神的大学生和社区的构建::
>>> factory = UndergraduateFactory()
>>> student = factory.create_leifeng()
>>> student.buyrice()
买米
>>> student.sweep()
扫地
>>> student.wash()
洗衣
工厂方法把简单工厂的内部逻辑判断移到了客户端代码来进行。
原型模式
prototype.py
原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。其实就是从一个对象再创建另外一个可定制的对象,并且不需要知道任何创建的细节::
>>> a = Resume('大鸟')
>>> a.set_personal_info('男', '29')
>>> a.set_work_experience('1998-2000', 'xx公司')
>>> b = a.clone()
>>> b.set_work_experience('2000-2006', 'xx企业')
>>> c = a.clone()
>>> c.set_personal_info('男', '24')
>>> a.display()
大鸟 男 29
工作经历: 1998-2000 xx公司
>>> b.display()
大鸟 男 29
工作经历: 2000-2006 xx企业
>>> c.display()
大鸟 男 24
工作经历: 1998-2000 xx公司
一般在初始化的信息不发生变化的情况下,克隆是最好的办法,它既隐藏了对象的创建细节,又对性能是大大的提升。
模板方法模式
template_method.py
模板方法模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,模板方法是的子类可以不改变一个算法的结构即可重新定义该算法的特定步骤::
>>> a = ConcreteClassA()
>>> a.template_method()
具体类A方法1实现
具体类A方法2实现
>>> b = ConcreteClassB()
>>> b.template_method()
具体类B方法1实现
具体类B方法2实现
外观模式
facade.py
外观模式:为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用::
>>> facade = Facade()
>>> facade.method_a()
--- 方法组A() ---
子系统方法一
子系统方法二
子系统方法四
>>> facade.method_b()
--- 方法组B() ---
子系统方法二
子系统方法三
外观模式完美的体现了依赖倒转原则和迪米特法则的思想,是常用的模式之一。经典的三层架构,就需要考虑在层与层之间建立外观Facade。
建造者模式
builder.py
建造者模式:将一个复杂对象的构建与它的表示分离,是的同样的构建过程,可以创建不同的表示::
>>> director = Director()
>>> b1 = ConcreateBuilder1()
>>> b2 = ConcreateBuilder2()
>>> director.construct(b1)
>>> p1 = b1.get_result()
>>> p1.show()
产品 创建 ----
部件A
部件B
>>> director.construct(b2)
>>> p2 = b2.get_result()
>>> p2.show()
产品 创建 ----
部件X
部件Y
建造者模式是当常见复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时使用的模式。
如果我们用了建造者模式,那么用户就只需指定需要建造的类型就可以得到他们,而具体建造的过程和细节就不需要知道了。
观察者模式
observer.py
观察者模式:定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主体对象。这个主题对象在状态发生变化时,会通知所有的观察者对象,让它们能够自动更新自己::
>>> s = ConcreteSubject()
>>> s.attach(ConcreteObserver(s, 'X'))
>>> s.attach(ConcreteObserver(s, 'Y'))
>>> s.attach(ConcreteObserver(s, 'Z'))
>>> s.subject_state = 'ABC'
>>> s.notify()
观察者X的新状态是ABC
观察者Y的新状态是ABC
观察者Z的新状态是ABC
当一个对象的改变需要同时改变其他对象的时候,且它不知道具体有多少对象有待改变时,应该考虑使用观察者模式。
观察者模式所做的工作其实就是解除耦合,让耦合的双方都依赖抽象,而不是依赖于具体。从而使得各自的变化都不会影响另一边的变化。是依赖倒转原则的最佳体现。
但是抽象通知者还是依赖抽象观察者。在代码中抽象观察者中有一个update()方法,具体观察者中都要实现该方法,但是在实际中,可能有一些不一样的操作,根本就不是同名的方法。这是不足的地方。
抽象工厂模式
abstract_factory.py
抽象工厂方法:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类::
>>> user = User()
>>> dept = Department()
>>> factory = SqlserverFactory()
>>> iu = factory.create_user()
>>> iu.insert(user)
在sql server 中给User表增加一条记录
>>> iu.get_user(1)
在sql server 中根据ID得到User表一条记录
>>> factory = AccessFactory()
>>> id = factory.create_department()
>>> id.insert(dept)
在Access中给Department表增加一条记录
>>> id.get_dept(1)
在Access中根据ID得到Department表一条记录
>>> iu = factory.create_user()
>>> iu.insert(user)
在Access中给User表增加一条记录
>>> iu.get_user(1)
在Access中根据Id得到User表一条记录
抽象工厂模式的优点:
- 1、易于交换产品系列,由于具体工厂类,在一个应用中只需要在初始化的时候出现一次,这就使得改变一个应用的具体工厂变得非常容易,它只需要改变具体工厂即可使用不 同
的产品配置。 - 2、让具体的创建实例过程和客户端分离,客户端是通过它们的抽象接口操作实例,产品的具体类名也被具体工厂的实现分离,不会出现在客户代码中。
缺点:
新增或修改时,可能需要大量的改动。
状态模式
state.py
状态模式:当一个对象的内在状态改变时,允许改变其行为,这个对象看起来像是一个改变了其类::
>>> c = Context(ConcreteStateA())
>>> c.request()
当前状态:stateB
>>> c.request()
当前状态:stateA
>>> c.request()
当前状态:stateB
>>> c.request()
当前状态:stateA
>>> c.request()
当前状态:stateB
状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况,把状态的判断转移到表示不同状态的一系列类中可以把复杂的判断逻辑简化。
状态模式的好处是将与特定状态相关的行为局部化,并且将不同状态的行为分割开来。
适配器模式
adapter.py
适配器模式:将一个类的接口转换成客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的那些类可以一起工作::
>>> target = Adapter()
>>> target.request()
特殊请求
备忘录模式
memento.py
备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样就可以将该对象恢复到原先保存的状态::
>>> o = Originator()
>>> o.state = 'ON' # 初始状态
>>> o.show()
state= ON
>>> caretaker = Caretaker()
>>> caretaker.memento = o.create_memento()
>>> o.state = 'OFF' # 改变状态
>>> o.show()
state= OFF
>>> o.set_memento(caretaker.memento) # 恢复
>>> o.show()
state= ON
缺点:当需要备忘的对象状态数据很大很多时,那么在资源消耗上,备忘录对象会非常消耗资源。
组合模式
composite.py
组合模式:将对象组合成树形结构以表示部分整体的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性::
>>> root = Composite('root')
>>> root.add(Leaf('Leaf A'))
>>> root.add(Leaf('Leaf B'))
>>> comp = Composite('Composite X')
>>> comp.add(Leaf('Leaf A'))
>>> comp.add(Leaf('Leaf B'))
>>> root.add(comp)
>>> comp2 = Composite('Composite XY')
>>> comp2.add(Leaf('Leaf A'))
>>> comp2.add(Leaf('Leaf B'))
>>> comp.add(comp2)
>>> root.display(1)
- root
-- Leaf A
-- Leaf B
-- Composite X
--- Leaf A
--- Leaf B
--- Composite XY
---- Leaf A
---- Leaf B
组合模式定义了包含基本对象和组合对象的类层次结构,基本对象可以被组合成更复杂的组合对象,而这个组合对象又可以被组合这样不断地递归下去。
迭代器模式
iterator.py
迭代器模式:提供一种方法顺序访问一个聚合对象中各个元素,而不是暴露该对象的内部表示。代码事例为乘务员迭代检票的模拟演示::
>>> a = ConcreteAggregate()
>>> a[0] = '大鸟'
>>> a[1] = '小菜'
>>> a[2] = '行李'
>>> a[3] = '老外'
>>> a[4] = '员工'
>>> a[5] = '小偷'
>>> i = ConcreteIterator(a)
>>> item = i.first()
>>> while not i.is_done():
... print('{} 请买票!'.format(i.current_item()))
... i.next()
大鸟 请买票!
小菜 请买票!
行李 请买票!
老外 请买票!
员工 请买票!
小偷 请买票!
为遍历不同的聚合结构提供如开始、下一个、是否结束、当前哪一项等统一的接口。现在高级编程语言如C#、JAVA、Python等本身已经把这个模式做在语言中。
单例模式
singleton.py
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点::
>>> singleton1 = Singleton('wen') # doctest: +ELLIPSIS
单例:...
>>> singleton2 = Singleton('wen1') # doctest: +ELLIPSIS
单例:...
>>> print(singleton1 is singleton2)
True
通常我们可以让一个全局变量使得一个对象被访问,但是它不能防止你实例化多个对象,一个最好的办法就是:让类自身负责它的唯一实例。这个类可以保证没有其他的实例可以被创建,并且它可以提供一个访问该实例的方法。
桥接模式
bridge.py
桥接模式:将抽象部分与它的实现部分分离,使他们都可以独立的变化::
>>> ab = RefinedAbstraction()
>>> ab.implementor = ConcreteImplementorA()
>>> ab.operation()
具体实现A的方法执行。
>>> ab.implementor = ConcreteImplementorB()
>>> ab.operation()
具体实现B的方法执行。
命令模式
command.py
命令模式:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
>>> r = Receiver()
>>> c = ConcreteCommand(r)
>>> i = Invoker()
>>> i.command = c
>>> i.execute_command()
执行请求!
命令模式的优点:
- 1 能较容易的设计一个命令队列
- 2 在需要的情况下,可以较容易的将命令记入日志
- 3 允许接收请求的一方决定是否要否决请求
- 4 可以容易的实现对请求的撤销和重做
- 5 由于加新的具体命令类不影响其他的类,因此增加新的具体命令类很容易
- 6 命令模式把请求一个操作的对象与知道怎么执行一个操作的对象分割开。
职责链模式
chain_of_responsibility.py
职责链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
>>> h1 = ConcreteHandler1()
>>> h2 = ConcreteHandler2()
>>> h3 = ConcreteHandler3()
>>> h1.successor = h2 # 设置职责链上家和下家
>>> h2.successor = h3
>>> requests = [2, 5, 14, 22, 18, 3, 27, 20]
>>> for request in requests:
... h1.handle_request(request)
ConcreteHandler1 处理请求 2
ConcreteHandler1 处理请求 5
ConcreteHandler2 处理请求 14
ConcreteHandler3 处理请求 22
ConcreteHandler2 处理请求 18
ConcreteHandler1 处理请求 3
ConcreteHandler3 处理请求 27
ConcreteHandler3 处理请求 20
需要注意的地方,一个请求极有可能到了链的末端都得不到处理,或者因为没有正确配置而得不到处理。
中介者模式
mediator.py
中介者模式:用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式的相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互::
>>> m = ConcreteMediator()
>>> c1 = ConcreteHandler1(m)
>>> c2 = ConcreteHandler2(m)
>>> m.colleague1 = c1
>>> m.colleague2 = c2
>>> c1.send('吃饭了吗?')
同事2得到消息: 吃饭了吗?
>>> c2.send('没有呢,你打算请客?')
同事1得到消息: 没有呢,你打算请客?
享元模式
flyweight.py
享元模式:运用共享技术有效地支持大量细粒度的对象::
>>> extrinsicstate = 22
>>> f = FlyweightFactory()
>>> fx = f.get_flyweight('X')
>>> fx.operation(extrinsicstate - 1)
具体Flyweight: 21
>>> fy = f.get_flyweight('Y')
>>> fy.operation(extrinsicstate - 2)
具体Flyweight: 20
>>> uf = UnsharedConcreteFlyweight()
>>> uf.operation(extrinsicstate - 3)
不共享的具体Flyweight: 19
享元模式可以避免大量非常相似类的开销。在程序设计中,有时需要生成大量的细粒度的类示例来表示数据,如果能够发现这些实例除了几个参数外基本上都相同的,有时就能够受大幅度的减少需要实例化的类数量。如果把这些参数移到类实例的外面,在方法调用的时候将它们传递进来,就可以通过共享大幅度的减少单个实例的数目。
应用场合:如果一个应用程序使用了大量的对象,而大量的这些对象造成了很大的存储开销时,应该考虑使用,还有就是对象的大多数状态可以外部状态,如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多的组对象,此时可以考虑使用享元模式。
解释器模式
interpreter.py
解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子::
>>> context = Context()
>>> ll = []
>>> ll.append(TerminalExpression())
>>> ll.append(TerminalExpression())
>>> ll.append(NonterminalExpression())
>>> ll.append(TerminalExpression())
>>> for exp in ll:
... exp.interpret(context)
终端解释器
终端解释器
非终端解释器
终端解释器
比如,在字符串搜索匹配的字符或判断一个字符串是否符合我们规定的格式,我们会用到的正则表达式技术,就是该模式的很好应用。
优点:容易的改变和扩展文法,因为该模式使用类来表示文法规则,你可使用继承来改变或扩展该方法。也比较容易实现文法,因为定义抽象语法树中各个节点的类的实现大体类似,这些类都易于直接编写。
缺点:该模式为文法中的每一条规则至少定义了一个类,因此包含许多规则的文法就很难管理和维护。建议当文法非常复杂时,使用其他的技术如语法分析程序或编译器生成器来处理。
访问者模式
visitor.py
访问者模式:表示一个作用于某对象结构中各元素的操作。它使你可以在不改变各元素的类的前提下定义于这些元素的新操作::
>>> o = ObjectStructure()
>>> o.attach(ConcreteElementA())
>>> o.attach(ConcreteElementB())
>>> v1 = ConcreteVisitor1()
>>> v2 = ConcreteVisitor2()
>>> o.accept(v1)
ConcreteElementA被ConcreteVisitor1访问
ConcreteElementB被ConcreteVisitor1访问
>>> o.accept(v2)
ConcreteElementA被ConcreteVisitor2访问
ConcreteElementB被ConcreteVisitor2访问
访问者模式适用于数据结构相对稳定的系统,它把数据结构作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由地演化。
如果系统有比较稳定的数据结构又有易于变化的算法的话,使用访问者模式是比较合适的因为该模式使得算法操作的增加变得容易。
缺点:就是使增加新的数据结构变得困难了。该模式较为复杂。当你真正需要它的时候,才考虑使用它。