设计模式笔记

Posted by Chen on October 3, 2019

一个好的开源游戏框架除了能给我们开发者带来开发效率的提升以外,还应该被我们吸收其设计思想,这样它的价值才能完整。

设计模式是一套被反复使用的、多数人知晓、经过分类编目的优秀代码设计经验的总结。
一句话就是: 在一定环境下,用固定套路解决问题。
1. 对问题行抽象、归纳、解耦合。
2. 可重用代码、让代码更容易被他人理解、保证代码可靠性。
3. 使代码编制真正工程化
4. 提高开发效率 正确使用设计模式,可以节省大量的时间

设计模式基本原则

最终目的:高内聚,低耦合

1. 开放封闭原则  (OCP,Open For Extension, Closed For Modification Principle)
类的改动是通过增加代码进行的,而不是修改源代码。
2. 单一职责原则  (SRP,Single Responsibility Principle)
类的职责要单一,对外只提供一种功能,而引起类变化的原因都应该只有一个。
3. 依赖倒置原则 (DIP,Dependence Inversion Principle)
依赖于抽象(接口),不要依赖具体的实现(类),也就是针对接口编程。
4. 接口隔离原则 (ISP,Interface Segegation Principle)
不应该强迫客户的程序依赖他们不需要的接口方法。一个接口应该只提供一种对外功能,不应该把所有操作都封装到一个接口中去。
5. 里氏替换原则 (LSP, Liskov Substitution Principle)
 	任何抽象类出现的地方都可以用他的实现类进行替换。实际就是虚拟机制,语言级别实现面向对象功能。
6. 优先使用组合而不是继承原则(CARP,Composite/Aggregate Reuse Principle)
如果使用继承,会导致父类的任何变换都可能影响到子类的行为。
如果使用对象组合,就降低了这种依赖关系。
7. 迪米特法则(LOD,Law of Demeter)
一个对象应当对其他对象尽可能少的了解,从而降低各个对象之间的耦合,提高系统的可维护性。例如在一个程序中,各个模块之间相互调用时,通常会提供一个统一的接口来实现。这样其他模块不需要了解另外一个模块的内部实现细节,这样当一个模块内部的实现发生改变时,不会影响其他模块的使用。(黑盒原理)

分类:

创建型模式: 通常和对象的创建有关,涉及到对象实例化的方式。(共5种模式)

创建型模式用来处理对象的创建过程,主要包含以下5种设计模式: 
1. 工厂方法模式(Factory Method Pattern)的用意是定义一个创建产品对象的工厂接口,将实际创建工作推迟到子类中。
2. 抽象工厂模式(Abstract Factory Pattern)的意图是提供一个创建一系列相关或者相互依赖的接口,而无需指定它们具体的类。
3. 建造者模式(Builder Pattern)的意图是将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
4. 原型模式(Prototype Pattern)是用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
5. 单例模式(Singleton Pattern)是保证一个类仅有一个实例,并提供一个访问它的全局访问点。 **结构型模式**: 描述的是如何组合类和对象以获得更大的结构。(共7种模式)

结构型模式用来处理类或者对象的组合,主要包含以下7种设计模式:
6. 代理模式(Proxy Pattern)就是为其他对象提供一种代理以控制对这个对象的访问。
7. 装饰者模式(Decorator Pattern)动态的给一个对象添加一些额外的职责。就增加功能来说,此模式比生成子类更为灵活。 
8. 适配器模式(Adapter Pattern)是将一个类的接口转换成客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 
9. 桥接模式(Bridge Pattern)是将抽象部分与实际部分分离,使它们都可以独立的变化。

10. 组合模式(Composite Pattern)是将对象组合成树形结构以表示“部分--整体”的层次结构。使得用户对单个对象和组合对象的使用具有一致性。
11. 外观模式(Facade Pattern)是为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
12. 享元模式(Flyweight Pattern)是以共享的方式高效的支持大量的细粒度的对象。 **行为型模式**: 用来对类或对象怎样交互和怎样分配职责进行描述。(共11种模式)

行为型模式用来对类或对象怎样交互和怎样分配职责进行描述,主要包含以下11种设计模式:
13. 模板方法模式(Template Method Pattern)使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 
14. 命令模式(Command Pattern)是将一个请求封装为一个对象,从而使你可用不同的请求对客户端进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
15. 责任链模式(Chain of Responsibility Pattern),在该模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求,这使得系统可以在不影响客户端的情况下动态地重新组织链和分配责任。 
16. 策略模式(Strategy Pattern)就是准备一组算法,并将每一个算法封装起来,使得它们可以互换。
17,中介者模式(Mediator Pattern)就是定义一个中介对象来封装系列对象之间的交互。终结者使各个对象不需要显示的相互调用 ,从而使其耦合性松散,而且可以独立的改变他们之间的交互。
18. 观察者模式(Observer Pattern)定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
19. 备忘录模式(Memento Pattern)是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
20. 访问者模式(Visitor Pattern)就是表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
21. 状态模式(State Pattern)就是对象的行为,依赖于它所处的状态。
22. 解释器模式(Interpreter Pattern)就是描述了如何为简单的语言定义一个语法,如何在该语言中表示一个句子,以及如何解释这些句子。 
23. 迭代器模式(Iterator Pattern)是提供了一种方法顺序来访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。

1. 单例模式(懒汉式,饿汉式)

参考

  • 说明: 保证一个类仅有一个实例,并提供一个访问它的全局访问点. 线程安全问题: double-checked locking

  • 应用场景:

      需要一个对象的时候
      - 在多个线程之间,比如初始化一次socket资源;比如servlet环境,共享同一个资源或者操作同一个对象
      - 在整个程序空间使用全局变量,共享资源
      - 大规模系统中,为了性能的考虑,需要节省对象的创建时间等等。
      因为Singleton模式可以保证为一个类只生成唯一的实例对象,所以这些情况,Singleton模式就派上用场了。 cocos中:
    
      Director,TextureCache,SpriteFrameCache,AnimationCache,UserDefault,GLProgramStateCache,ScriptEngineManager,PoolManager,FileUtils,Profiler,SimleAudioEngie,Configuration,Application,GLProgramCache ...
    
  • 优劣:

      - 优点:<br>
      1. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存]。<br>
      2. 避免对资源的多重占用(比如写文件操作。<br>
      - 缺点:<br>
      1. 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。<br>
    
  • 注意事项: getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。

2. 工厂模式(简单工厂, 抽象工厂)

  • 说明: 定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。

    (工厂当系统扩展需要添加新的产品对象时,仅仅需要添加一个具体对象以及一个具体工厂对象,原有工厂对象不需要进行任何修改,也不需要修改客户端,很好的符合了“开放-封闭”原则。而简单工厂模式在添加新产品对象后不得不修改工厂方法,扩展性不好。工厂方法模式退化后可以演变成简单工厂模式。 )

    工厂模式只能生产一个产品。(要么香蕉、要么苹果) 抽象工厂可以一下生产一个产品族(里面有很多产品组成)

  • 应用场景:

      主要解决接口选择的问题
    
  • 优劣:

      -优点:
      1. 一个调用者想创建一个对象,只要知道其名称就可以了。 
      2. 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 
      3. 屏蔽产品的具体实现,调用者只关心产品的接口。
      -缺点:
      每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
    

3. 二段构建模式

参考

由于此模式在GoF的设计模式中并未出现,所以暂时不讨论与其它模式的关系。

最后看看cocos2d-x创始人王哲对于为什么要设计成二段构建的看法:

其实我们设计二段构造时首先考虑其优势而非兼容cocos2d-iphone. 初始化时会遇到图片资源不存在等异常,而C++构造函数无返回值,只能用try-catch来处理异常,启用try-catch会使编译后二进制文件大不少,故需要init返回bool值。Symbian, Bada SDK,objc的alloc + init也都是二阶段构造”

4. 管理者模式

参考

管理者(Manager)就是专门负责管理其它类的实例的类,比如Cocoa里面的NSFontManager、NSInputManager、NSFileManager和NSLayoutManager类。此模式和“二段构建模式”一样,也没有出现在GoF的23个设计模式中,但是《Cocoa设计模式》一书中有提及,感兴趣的读者可以去查阅一下。

  • 优劣:

      - 优点: 
      为一组相关的对象提供一个统一的全局访问点,同时可以提供一些简洁的接口来获取和操作这些对象。同时,使用此模式来缓存游戏中的常用资源,可以提高游戏运行时性能。从这个角度来讲,管理者模式有点像工厂模式,专门用来生产对象的。
    
      -缺点: 
      由于管理者大多采用单例模式,所以,它继承了单例模式所有的缺点,这里就不再赘述了。
    

5. 外观模式(facade Pattern)

参考

  • 说明: 它的本质是:封装交互、简化调用。

      隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性。(为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。)
    
  • 应用场景:

      为子系统中统一一套接口,让子系统更加容易使用。 
      去医院看病,可能要去挂号、门诊、划价、取药,让患者或患者家属觉得很复杂,如果有提供接待人员,只让接待人员来处理,就很方便。
      1.为复杂的模块或子系统提供外界访问的模块。 2.子系统相对独立。 
      3.预防低水平人员带来的风险。
    
  • 优劣:

      - 优点:
      1. 减少系统相互依赖。 
      2. 提高灵活性。 
      3. 提高了安全性。
      - 缺点:
      不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
    

6. 防御式编程模式

参考

声明:防御式编程是提高程序代码质量的一种手段,它不能算是真正意义上的模式。但是,这里,我还是要给它冠之以“模式”二字。

原因有三:

1. cocos2d-x的框架源代码大量采用了防御式编程技术,用来确保框架的代码质量和稳定性。

2. 标题党,引起大家对于防御式编程的重视。特别是当大家给cocos2d-x贡献源代码的时候,更应该要注意保证代码质量。

3. 防御式编程非常重要,运用得当可以减少被bug折腾的时间。
  • 应用场景:

    首先,第一个大量使用的是CCLayer的init函数:

          bool CCLayer::init()
          {
              bool bRet = false;
              do
              {
                  CCDirector * pDirector;
                  CC_BREAK_IF(!(pDirector = CCDirector::sharedDirector()));
                  this->setContentSize(pDirector->getWinSize());
                  m_bIsTouchEnabled = false;
                  m_bIsAccelerometerEnabled = false;
                  // success
                  bRet = true;
              } while(0);
              return bRet;
          }
    

    这里使用了do…while(0);惯用法,同时配合CC_BREAK_IF宏来做错误处理。关于为什么要使用do…while(0)惯用法,可以参考这篇文章:

  • 优劣:

      - 优点:
      提高代码质量,使得代码的健壮性和稳定性都有保障。可以防止子程序由于接收到了非法数据而遭受破坏。
    
      - 缺点:
      过度的防御式编程也会引来问题,如果你在每一个能想到的地方用每一种能想到的方法检查从参数传入的数据,那么你的程序将会变得臃肿而缓慢。更糟糕的是,防御式编程引入的额外代码增加了软件的复杂度。所以运用是需谨慎。
    
  • 实际开发中如何采用此模式

      在实际开发中,我个人觉得必要的防御式编程的态度和做法还是要有的。特别是函数的输入输出,因为函数的逻辑部分是我们关注地最多的,虽然它是最复杂的,但是,往往这部分出错的概率不高。
    
      相反,是函数的一些边界条件和异常情况导致程序bug的滋生。有些时候除了验证函数参数的数据值范围有效性以外,更加要注意的是验证数据的业务条件是否满足。这一点恰恰最容易被忽视。
    
      在做内存管理的时候,尽可能地使用cocos2d-x里面定义的一些宏来清理资源,比如CC_SAFE_DELETE等。当实现自定义的CCLayer对象的时候,也尽可能地采用do…while(0)的写法,如果里面出现问题,可以用CC_BREAK_IF来判断并退出。
    

7. 组合模式

参考

单个对象和组合对象的使用具有一致性。将对象组合成树形结构以表示“部分–整体”

  • 应用场景:

    Cocoa编程框架APPKit和UIKit都应用了组合模式,各种各样的View及其派生类组成了一棵树状结构的层级视图,而这里面就应用了组合模式。当然,Cocos2D-x里面的Node组织方式也采用的是这个方法,最终游戏界面中的Nodes也会形成一棵树。

  • 优劣:

      - 优点:
      1. 优化处理递归或分级数据结构。
      2. 一致地对待组合对象与单个对象,可以简化客户端调用
      - 缺点:
      1. 如果是透明实现的组合模式,单个对象也会包含组合对象的方法,这样会给客户端调用造成麻烦,因为单个对象实际上是不会实现这些组合对象的方法的。
    

8. 委托模式

参考

都是把业务的需要实现的逻辑交给一个目标实现类来完成。

  • 优劣:

      - 优点:
      1. 解耦,将应用相关的内容与框架完全分享出来,在设计可重用的组件的时候特别有用。
      2. 可扩展性和可配置性高,而且可以在运行时候切换委托对象,具有很强的灵活性。
    
      - 缺点:
      1. 采用接口的实现,由于使用了虚函数,所以性能上会有一点损失。虽然采用指向成员函数的指针的方式来实现效率非常高,但是,语法很诡异,使用起来其实还是不太爽的。尽管cocos2d-x已经用宏定义让使用方便了一些。
      2. 如果过度使用,容易导致职责分散,导致维护麻烦。
    

9. 观察者模式

参考

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

  • 优劣:

      - 优点:
      1. 实现了目标对象和观察者之间的抽象耦合,在本例中,则是实现了消息与观察者的抽象耦合。可以定义一种消息与消息处理对象的一对多的关系,而不用担心彼此的实现细节。
      2. 观察者模式可以定义某种意义上的广播通信机制。
      3. 实现订阅者与发布者的松散耦合,同时保障了良好的扩展性。 \
      - 缺点:
      1. 注册成为NotificationCenter的观察者后,如果忘记调用removeObserver,则会引起内存泄漏。因为addObserver会把观察者的引用计算加1.