设计模式——软件设计的太极剑法

设计模式——软件设计的太极剑法

起势,左右野马分鬃,白鹤亮翅……随着一声声响亮的招式,无忌打出了一套漂亮的连击。等等,这里不是设计模式专场吗?是不是搞错了?
没错,设计模式其实就是软件设计世界里面的一套武功秘籍。是由历代“武学大师”呕心沥血总结出来,并在实战中大展拳脚。下面就让我们一起来看看,设计模式的庐山真面目吧。

  • 设计模式是什么?
  • 为什么要使用设计模式?设计模式有什么好处?能用来干嘛?
  • 怎么使用设计模式?
  • 设计模式有哪些?

一、设计模式概述

1.1 设计模式产生的背景

“设计模式”这个术语最初并不是出现在软件设计中,而是被用于建筑领域的设计中。

后来有四个牛人出版了《设计模式:可复用面向对象软件的基础》,他们分别是:艾瑞克·伽马(ErichGamma)、理査德·海尔姆(Richard Helm)、拉尔夫·约翰森(Ralph Johnson)、约翰·威利斯迪斯(John Vlissides)。也被称为“四人组(Gang of Four,GOF)”

在这本书中收录了 23 种设计模式,导致了软件设计模式的突破。

1.2 什么是设计模式

软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。

一般而言,一个模式有四个基本要素:

  1. 模式名称(pattern name): 一个助记名,它用一两个词来描述模式的问题、解决方案、功能和效果。模式名称有助于我们理解和记忆该模式,也方便我们来讨论自己的设计。
  2. 问题(problem) : 描述了应该在何时使用模式。它解释了设计问题和问题存在的前因后果,以及模式必须满足的一系列先决条件。
  3. 解决方案(solution) : 描述了设计的组成成分,它们之间的相互关系及各自的职责和协作方式。因为模式就像一个模板,可应用于多种不同场合,所以解决方案并不描述一个特定而具体的设计或实现,而是提供设计问题的抽象描述和怎样用一个具有一般意义的元素组合(类或对象组合)来解决这个问题。
  4. 效果(consequences) : 描述了模式应用的效果及使用模式应权衡的问题,即模式的优缺点。主要是对时间和空间的衡量,以及该模式对系统的灵活性、扩充性、可移植性的影响,也考虑其实现问题。显式地列出这些效果对理解和评价这些模式有很大的帮助。
1.3 为什么要学习设计模式

学习设计模式,你至少可以获得如下几个好处:

  1. 使设计的代码质量更高,设计模式代表了最佳的实践,是前辈们的代码设计经验的总结,具有一定的普遍性。
  2. 拥有了一套共享的词汇,在和其他开发人员交流时,一个模式名称就代表着一整套模式背后所象征的质量、特性、约束。别人很容易知道你对设计的想法。
  3. 可以帮助我们阅读源码,大多数规模较大的面向对象系统都使用了这些设计模式。如果我们不了解这些设计模式的话,在学习面向对象编程时,可能会比较困难,感到费解。
  4. 重构时的好工具:当你们的系统要重构时,不用说,设计模式绝对能派上用场了。设计模式能帮你重新组织一个设计,同时还能减少以后的重构工作。

二、设计模式的原则

在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性。我们要尽量遵循下面这些原则:

2.1 开闭原则

开闭原则(Open Closed Principle,OCP): 对扩展开放,对修改关闭(Software entities should be open for extension,but closed for modification)。

其含义是,在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。为了使程序的扩展性好,易于维护和升级。

可以通过 “抽象约束、封装变化” 来实现开闭原则,即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。

2.2 单一职责原则

单一职责原则(Single Responsibility Principle,SRP): 不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,如若不然,就应该把类拆分。There should never be more than one reason for a class to change.

我们知道要避免类内的改变,因为修改代码容易造成许多潜在的错误。如果一个类具有两个以上改变的原因,那么这会使得这个类将来的变化几率上升。而当它真的面临改变时,你的设计中同时有两个方面受到影响。

这个原则告诉我们,只将一个责任指派给一个类。这听起来很容易,但其实做起来并不简单。区分设计中的责任,是最困难的事情之一。我们的大脑很习惯看着一大群行为,然后将他们集中在一起,尽管他们可能属于两个或多个不同的责任。想要成功的唯一方法,就是努力不懈的检查你的设计,随着系统的成长,随时观察有没有迹象显示某个类改变的原因超出一个。

单一职责原则的优点: 降低类的复杂度,可维护性、可读性高,变更的风险降低。

2.3 里氏替换原则

里氏替换原则(Liskov Substitution Principle,LSP): 任何基类可以出现的地方,子类一定可以出现。

LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。

里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

滥用继承是很危险的,只有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的,否则,使用组合会更加灵活。

2.4 依赖倒置原则

依赖倒置原则(Dependence Inversion Principle,DIP): 高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。其核心思想是:要面向接口编程,不要面向实现编程。

依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,一下指导方针能帮助你避免违反依赖倒置:

  • 每个类尽量提供接口或抽象类,或者两者都具备。
  • 变量的声明类型尽量是接口或者是抽象类。
  • 任何类都不应该从具体类派生。
  • 使用继承时尽量遵循里氏替换原则。

尽量避免违反。并不要求随时都遵循这样的原则,比如一些已知不可变的类。

2.5 接口隔离原则

接口隔离原则(Interface Segregation Principle,ISP):每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。

要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。

2.6 迪米特法则

迪米特法则(Law of Demeter,LoD): 又叫作最少知识原则(Least Knowledge Principle,LKP):一个类对自己依赖的类知道的越少越好。也就是说无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。

最少知道原则的另一个表达方式是:只与直接的朋友通信。类之间只要有耦合关系,就叫朋友关系。

耦合分为依赖、关联、聚合、组合等。我们称出现为成员变量、方法参数、方法返回值中的类为直接朋友。局部变量、临时变量则不是直接的朋友。我们要求陌生的类不要作为局部变量出现在类中。

优点: 降低了类之间的耦合度,提高了模块间的相对独立性。由于耦合度低,因而提高了可复用率和扩展性。

2.7 合成复用原则

合成复用原则(Composite Reuse Principle,CRP):一句话,组合优先于继承。

如果要使用继承关系,则必须严格遵循里氏替换原则。

继承复用虽然有简单和易实现的优点,但它也存在以下缺点:

  • 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的。
  • 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  • 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:

  • 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的。
  • 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
  • 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
2.8 其他类似的 OO原则
  • 封装变化
  • 多用组合,少用继承
  • 针对接口编程,不针对实现编程
  • 为交互对象之间的松耦合设计而努力
  • 类应该对扩展开放,对修改关闭
  • 依赖抽象,不要依赖具体类
  • 只和朋友交谈
  • 别找我,我会找你
  • 类应该只有一个改变的理由

三、如何使用设计模式

3.1 何时使用

​ 书上说:保持简单。当你设计时,尽可能的用最简单的方式解决问题。如果你确定在你的设计种可以利用某个模式解决某个问题,那就使用这个模式。如果还有更简单的解决方案,那你在决定使用模式之前应该先考虑这个简单的方案。

​ 但是,对于一个刚学习设计模式的人来说,我的建议是,尽管去用吧。你只有多实际的使用了,才会对设计模式有更深的体会。你甚至可以给 HelloWorld 写一个设计模式。

​ 另一个使用设计模式的时机,就是重构的时候了。

3.2 怎么使用

1)、选择一个适合当前情境的设计模式。可以参照下面几种做法:

  • 考虑设计模式是怎么样解决设计问题的。
  • 浏览各个模式的意图,找到与你问题相关的一个或多个模式。
  • 研究模式怎么相互关联的。
  • 研究目的相似的模式。
  • 考虑你的设计中哪些是可变的

2)、大致浏览一遍模式,特别注意其适用性部分和效果部分,确定它适合你的问题。

3)、回头研究结构部分、参与者部分和协作部分, 确保你理解这个模式的类和对象以及它们是怎样关联的。

4)、找到这个模式的示例代码,看一下是怎么实现的。

5)、选择模式参与者的名字,使它们在应用上下文中有意义 。

四、GOF 的 23 种设计模式

设计模式在粒度和抽象层次上各不相同。由于存在众多的设计模式,我们希望用一种方式将他们组织起来,便于我们对各族相关的模式进行引用,也有助于更快的学习目录中的模式。

设计模式有两种分类方法,即根据模式的目的来分根据模式的作用范围来分

4.1 根据模式的目的来分

即根据模式是用来完成什么工作的来分,可以分为创建型、结构型和行为型三种:

  • 创建型: 描述“怎样创建对象”,主要特点是将对象的创建与使用分离。
  • 结构型: 描述将类或对象按某种布局组合到更大的结构中。
  • 行为型: 描述类或对象如何交互及分配职责。
4.2 根据作用范围来分

根据模式主要是用于类还是用于对象来分,可以分为类模式和对象模式两种。

  • 类模式: 描述类之间的关系如何通过继承定义。类模式的关系是在编译时建立的。
  • 对象模式: 描述对象之间的关系,而且主要是利用组合定义。对象模式的关系通常在运行时建立,而且更具动态,更有弹性。

设计模式分类

4.3 23 种设计模式
  1. 工厂方法(Factory Method): 定义一个用于创建对象的接口,让子类决定实例化哪一个类。 工厂方法使一个类的实例化延迟到其子类。
  2. 抽象工厂(AbstractFactory): 提供一个接口,用于创建一系列相关或相互依赖的对象,而无需指定它们具体的类。
  3. 建造者模式(Builder): 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
  4. 原型模式(Prototype): 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。当创建给定类的实例的过程很昂贵或很复杂时,就使用原型模式。
  5. 单例模式(Singleton): 确保一个类只有一个实例,并提供一个全局访问点。
  6. 适配器模式(Adapter): 将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
  7. 桥接模式(Bridge): 将抽象部分与它的实现部分分离,使它们都可以独立地变化。 它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
  8. 组合模式(Composite): 将对象组合成树形结构以表示“部分 -整体”的层次结构。组合模式能让客户以一致的方式处理个别对象以及对象组合。
  9. 装饰者模式(Decorator): 动态地给一个对象添加一些额外的职责。就增加功能来说, 装饰者模式相比生成子类更为灵活。
  10. 外观模式(Facade): 为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
  11. 享元模式(Flyweight): 运用共享技术有效地支持大量细粒度的对象。
  12. 代理模式(Proxy): 为其他对象提供一种代理以控制对这个对象的访问。
  13. **解释器模式(Interpreter):**给定一个语言, 定义它的文法的一种表示,并定义一个解释器 , 该解释 器使用该表示来解释语言中的句子。
  14. 模板方法模式(TemplateMethod): 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
  15. 责任链模式(Chain of Responsibility): 为解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。 把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
  16. 命令模式(Command): 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。 发出请求的责任和执行请求的责任分割开。
  17. 迭代器模式(Iterator): 提供一种方法顺序访问一个聚合对象中各个元素 , 而又不暴露该对象的内部表示。
  18. 中介者模式(Mediator): 用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式 地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
  19. 备忘录模式(Memento): 在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
  20. 观察者模式(Observer): 定义了对象之间的一对多的依赖关系,这样一来,当一个对象状态改变时,所有依赖于它的对象都会收到通知,并自动更新。
  21. 状态模式(State): 允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类。
  22. 策略模式(Strategy): 定义了算法族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化独立于使用算法的客户。
  23. 访问者模式(Visitor): 表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。象状态改变时,所有依赖于它的对象都会收到通知,并自动更新。
  24. 状态模式(State): 允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类。
  25. 策略模式(Strategy): 定义了算法族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化独立于使用算法的客户。
  26. 访问者模式(Visitor): 表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值