内容来自 《JavaScript设计模式》张容铭 著 (2015年)、《大话设计模式》程杰 著、“Java设计模式” C语言中文网
文章首发于 掘金
作者:MiyueFE
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
序:
设计模式(Design Patterns),指软件/程序开发过程中被经常使用的一种代码逻辑设计经验的集合,目的是为了提高代码的安全性、可靠性、可读性、可维护性、可拓展性。
使用设计模式来进行开发,就是为了降低代码的耦合度,增加代码复用的可能性。
在学习设计模式之前,我们先要了解一下内容:
设计模式遵循的七大原则:
设计模式目前常用的(或者说见得最多的)有23个,每个设计模式都有不同的用途。按照它们的用途大致分类,可以分为3种:
创建型 Creational Pattern
:与对象创建相关
Simple Factory
(非23个标准模式)Factory Method
Abstract Factory
Builder
Prototype
Singleton
结构型 Structural Pattern
:处理类或者对象的"组合"
Adapter
Bridge
Composite
Decorator
Facade
Flyweight
Proxy
行为型 Behavioral Pattern
:描述类或者对象的交互方式和职责分配
Chain of Responsibility
Command
Interpreter
Iterator
Mediator
Memento
Observer
State
Strategy
Template Method
Visitor
设计模式按照作用范围可以为分为两种,作用于类的类模式、作用于对象(实例)的对象模式
Factory Method
Adapter
Interpreter
Template Method
“类模式” 主要集中处理类与子类之间的关系,这些关系通过继承来建立,再确定后便不再更改,是静态的。
“对象模式” 主要处理各对象之间的关系,可以变化,更具有动态性
定义:一个类或者模块应该有且只有一个改变的原因。
目的是为极端的降低代码的耦合性和复杂性。
遵守单一职责原则,可以是代码逻辑和功能变得更加明确,并且更加易于扩展与维护,遇到错误在排查的时候也会更加方便;但是这样会严重增加代码量。
example:
class ShoppingCart {
constructor() {
this.goods = []
}
addGood(good) {
this.goods.push(good)
}
deleteGood(goodIndex) {
if(goodIndex < 0) {
throw new Error("The serial number of this product cannot be less than 0")
}
if(goodIndex > this.goods.length - 1) {
throw new Error("The serial number of this product cannot be greater than the total number of all products")
}
this.goods.splice(goodIndex, 1);
}
getGoods() {
return this.goods;
}
}
比如这个购物车 ShoppingCart
类,只针对本身做增删查的功能,而不会影响到其他类。
定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。模块应尽量在不修改“原代码”的情况下进行扩展。
开闭原则的目的是为了不改变程序原有设计和代码,在原来的基础上对其进行扩展,并保证原有代码的稳定性和正确性。
要实现开闭原则,需要在程序设计开始时就对程序进行抽象化设计。在抽象化模块设计完成之后,不允许修改接口或者抽象类的属性、方法;方法的参数类型、引用对象也必须是接口或者抽象类,尽量保证抽象层的稳定性;在进行扩展时必须定义具体实现的方法。
example:
class DrawChart {
constructor() {
}
draw() {
}
}
class DrawBar extends DrawChart {
draw() {
// 重写基础类的 draw 方法
// ...
}
}
定义:所有引用基类的地方必须能透明地使用其子类的对象,也可以简单理解为任何基类可以出现的地方,子类一定可以出现。
超类(基础类)拥有的属性和方法在派生类的实例中也能找到,在可以使用超类(基础类)的地方也一定可以使用派生类,反过来则不成立。只有当派生类可以替换掉超类且程序功能不受影响时,超类才能真正的被复用。
里氏代换原则是对"开闭原则"的补充,也是继承的基础。
该原则中,超类(基础类)SuperClass
派生出一个子类 SubClass
,如果一个方法可以接收一个 SuperClass
类型的实例对象 superObject
的话,那么这个方法一定也可以接收一个 SubClasss
类型的实例对象 subObject
,但是反过来则不能成立。
实现里氏代换原则:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类中可以增加自己特有的方法
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
- 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相等
– 摘自 C语言中文网-设计模式
example:
// 抽象类,通信设备
class CommunicationEquipment {
call() {
throw "Abstract methods cannot be called"
}
}
// 子类,手机
class Phone extends CommunicationEquipment {
// 实现父类的抽象方法
// number
call(number) {
console.log(number, "拨号中...")
}
// 提供一个非抽象方法
isMobile() {
console.log("是移动设备...")
}
}
// 具体类,苹果手机
class IPhone extends Phone {
// 可以重写父类方法,但是参数要更严格或者相等
call(number) {
console.log(number, "iphone拨号中...")
}
// 可以增加子类自身的方法
noCharger() {
console.log("没有充电器...")
}
}
定义:指一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象(来自维基百科)。
简单来说,不管是高层模块还是底层模块,都不应该依赖具体的实现方法,高层模块也不应该依赖于底层模块,而应该依赖于接口或者抽象类。
example:
// 抽象,车
class Vehicle {
shoot(){
throw "Abstract methods cannot be called";
}
}
// 具体类,SUV
class SUV extends Vehicle {
motion(){
console.log("SUV in motion...");
}
}
// 具体类,Sedan
class Sedan extends Vehicle {
motion(){
console.log("Sedan in motion...");
}
}
定义:最少知道原则(LKP
),即迪米特法则(Law of Demeter, LOD
),只与直接相关的实体之间发生联系,如果两个实体(实例对象)之前不需要互相通信,那么这两个对象就不应该有任何直接联系或者相互作用。
最少知道原则的作用就在于降低类与类之间的耦合性。
example:
class Star {
constructor(name) {
this.name = name
}
}
class Fans {
// ...
}
class Company {
// ...
}
class Agent {
setStar(star) {
this.star = star
}
setFans(fans) {
this.fans = fans
}
setCompany(company) {
this.company = company
}
meeting() {
console.log(`${
this.star}与${
this.fans}见面了`)
}
business() {
console.log(`${
this.star}与${
this.company}洽淡业务`)
}
}
Star
类不与 Fans
、Company
发生直接联系,而是由 Agent
在中间进行连接。
定义:客户(client)不应被迫使用对其而言无用的方法或功能。
即将提供复杂功能的单一接口,变为实现单一功能的多个接口。
这样可以降低接口的复杂程度与耦合性,使接口变得更灵活。
接口隔离原则的优点:
- 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
- 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
- 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
- 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
- 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
– 摘自 C语言中文网-设计模式
example:
// 没有分离的时候
interface ClickListener {
click(): void;
dblClick(): void;
rightClick(): void;
longClick(): void;
}
// 分离之后
interface ClickListener {
listener(): void;
}
interface DblClickListener {
listener(): void;
}
interface RightClickListener {
listener(): void;
}
interface LongClickListener {
listener(): void;
}
定义:又名"组合/聚合复用原则(Composition/Aggregate Reuse Principle)",尽量使用合成/聚合,而不是通过继承达到复用的目的。
合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
合成复用是通过将已知对象A纳入新对象B作为成员对象,B对象可以调用A对象的方法,从而实现复用。
组合:一种"强"拥有关系,拥有共同的生命周期。比如一个A对象包含B对象,那么A对象被销毁的时候B对象也一样被销毁。
聚合:一种"弱"拥有关系,但是彼此都可以单独存在。
example:
class Car {
constructor(color) {
this.color = new Color(color)
}
move() {
throw "Abstract methods cannot be called"
}
}
class Color {
constructor(color) {
this.color = color
}
getColor() {
return this.color
}
}