前端框架中的设计模式
在软件开发中,如果要保证编写代码的可读性和维护性,掌握设计模式是非常重要的。另外,如果我们不能对设计模式足够熟悉的话,当别人的代码用了大量设计模式的时候,也很难去读别人的框架或者源码。设计模式就是对软件开发过程中反复出现的某类问题的解决方案。众所周知,前端设计模式包括五大基本原则(SOLID)和 23 种设计模式,本文将介绍七种最常见的设计模式。
设计原则与思想
- SOLID 原则:
- 单一职责原则:一个类或者模块只负责完成一个职责(或者功能)。
- 开闭原则:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。
- 里式替换原则:子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。
- 接口隔离原则:客户端不应该被强迫依赖它不需要的接口。
- 依赖反转原则:高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象来互相依赖。
- KISS原则:尽量保持简单。
- YAGNI原则:不要做过度设计。
- DRY原则:不要写重复的代码。
- 迪米特法则:不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)。
单例模式
一个类只允许创建一个对象(或者实例),并供全局访问,这种设计模式就叫作单例设计模式,简称单例模式。有两种实现方法,首先都需要定义私有的静态属性,来保存对象实例,然后提供一个静态的方法来获取对象实例,代码如下:
// 方法一
class Singleton {
private static singleton: Singleton;
private constructor() {}
public static getInstance(): Singleton {
if (!Singleton.singleton) {
Singleton.singleton = new Singleton();
}
return Singleton.singleton;
}
}
// 方法二
class Singleton {
private static singleton: Singleton=new Singleton();
private constructor() {}
public static getInstance(): Singleton {
return Singleton.singleton;
}
}
// 使用
var instance1 = Singleton.getInstance();
var instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true
优点:
- 单例模式能保证全局的唯一性,可以减少命名变量。
- 单例模式在一定情况下可以节约内存,减少过多的类生成需要的内存和运行时间。
- 把代码都放在一个类里面维护,实现了高内聚。
缺点:
- 对 OOP 特性的支持不友好。
- 会隐藏类之间的依赖关系。
- 对代码的可测试性不友好。
- 不支持有参数的构造函数。
工厂模式
工厂模式是一种通过工厂方法创建对象的设计模式。工厂模式可以分为:简单工厂模式、工厂方法模式和抽象工厂模式。下面介绍一下简单工厂模式:
function Phone(name) {
this.name = name;
}
function createPhone(name) {
if (name === "Android") {
return new Phone("Android");
} else if (name === "iPhone") {
return new Phone("iPhone");
}
}
// 使用
var tele1 = createPhone("Android");
var tele2 = createPhone("iPhone");
console.log(tele1.name); // "Android"
console.log(tele2.name); // "iPhone"
优点:只需要一个传入的参数,就可以获取到所需要的对象,而无需知道其创建的具体细节。
缺点:当内部逻辑比较复杂时,这个函数将会变得很庞大并且难以维护。
观察者模式
在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知,并自动更新,包括观察者和被观察者。
//观察者
interface Observer {
notify: Function;
}
class ConcreteObserver implements Observer{
constructor(private name: string) {}
notify() {
console.log(`${this.name} has been notified.`);
}
}
//被观察者
class Subject {
private observers: Observer[] = [];
public addObserver(observer: Observer): void {
console.log(observer, "is pushed!");
this.observers.push(observer);
}
public deleteObserver(observer: Observer): void {
console.log("remove", observer);
const n: number = this.observers.indexOf(observer);
n != -1 && this.observers.splice(n, 1);
}
public notifyObservers(): void {
console.log("notify all the observers", this.observers);
this.observers.forEach(observer => observer.notify());
}
}
//使用
const subject: Subject = new Subject();
const zhangsan = new ConcreteObserver("张三");
const lisi = new ConcreteObserver("李四");
subject.addObserver(zhangsan);
subject.addObserver(lisi);
subject.notifyObservers();
subject.deleteObserver(zhangsan);
subject.notifyObservers();
优点:
- 建立抽象耦合,被观察者和观察者之间可以相互通信。
- 支持广播通讯,被观察者可以向所有登记过的观察者发出通知。
缺点:
- 观察者之间细节依赖过多,会增加时间消耗和程序的复杂程度。
- 观察者与被观察者之间如果循环依赖,会触发二者之间的循环调用,导致系统崩溃。
策略模式
定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换。
var strategies = {
"A": function () {
console.log("Strategy A");
},
"B": function () {
console.log("Strategy B");
},
};
function Context(strategy) {
this.strategy = strategy;
}
Context.prototype = {
execute: function () {
this.strategy();
},
};
// 使用
var contextA = new Context(strategies["A"]);
var contextB = new Context(strategies["B"]);
contextA.execute(); // 输出 "Strategy A"
contextB.execute(); // 输出 "Strategy B"
优点:
- 提供管理算法族的办法。
- 可以替换继承关系。
- 避免使用多重条件转移语句,如冗长的 if-else 或 switch 分支判断。
缺点:
- 如果备选的策略很多的话,那么对象的数目就会很多。
- 客户端必须知道所有的策略类,理解区别才能自行选择。
适配器模式
可以将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作,而不用更改这些类。
class GooleMap {
show() {
console.log('渲染地图')
}
}
class BaiduMap {
display() {
console.log('渲染地图')
}
}
class AdapterMap {
show() {
return new BaiduMap().display()
}
}
优点:
- 让两个没有任何关系的类在一起运行。
- 提高了复用度。
- 灵活,可以适配不同格式的数据。
缺点:
- 使用过多适配器会导致系统杂乱。
- 适配器模式需要增加一个额外的适配器类,增加了代码的量。
装饰者模式
装饰者模式可以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。经常用到 Function.pototype.after 和 Function.pototype.before 两个函数进行装饰。
// 原始对象
function Component() {}
Component.prototype = {
operation: function () {
console.log("Component operation");
},
};
// 装饰者对象
function Decorator(component) {
this.component = component;
}
Decorator.prototype = {
operation: function () {
this.component.operation();
console.log("Decorator operation");
},
};
// 使用
var component = new Component();
component.operation(); // 输出 "Component operation"
var decorator = new Decorator(component);
decorator.operation(); // 输出 "Component operation" 和 "Decorator operation"
优点:
- 采用装饰模式扩展对象的功能比采用继承方式更加灵活。
- 可以设计出多个不同的具体装饰类,创造出多个不同行为的组合。
缺点:装饰模式增加了许多子类,如果过度使用会使程序变得很复杂。
代理模式
前端设计模式中的代理模式是一种结构型模式,它允许在不改变原始对象的情况下,通过引入一个代理对象来控制对原始对象的访问。代理模式在前端中比较常用的虚拟代理和缓存代理。在前端开发中经常被用来处理一些复杂或者耗时的操作,例如图片的懒加载、缓存等。
const target = {
method() {
console.log("Target");
}
};
const proxy = new Proxy(target, {
get(target, prop) {
console.log(`Called ${prop} method.`);
return target[prop];
}
});
// 使用示例
proxy.method(); // "Called method. Target"
优点:
- 分离目标对象: 代理模式能将代理对象与真实被调用的目标对象分离。
- 降低耦合: 在一定程度上, 降低了系统耦合性, 扩展性好。
- 增强目标对象: 代理类可以在目标对象基础上, 添加新的功能。
缺点:
- 代理模式会造成系统中类的个数增加, 系统的复杂度增加。
- 在客户端和目标对象之间增加了一个代理对象, 会使请求处理速度变慢。
总结
学习设计模式更多的是理解各种模式的内在思想和解决的问题,最终的目的还是写出高质量的代码,保证代码的可维护性、可读性、可拓展性、可复用性。在不同的应用中,灵活处理,选用合适的设计模式,并且不滥用。