这是我参与更文挑战的第13天,活动详情查看:更文挑战
我们在开发应用的时候会遇到很多问题,有的可以自己解决,有的会问别人。随着应用更复杂,处理问题也会更困难,对于一些普遍的问题,更好的方式是找到通用的解决方法。
设计模式提供的就是一种可以通用的代码设计经验,方便我们解决更多通用问题。这一概念早就存在,但直到四人组在上世纪九十年代正式提出,才成为一种广为人知的规范。他们还写了一本书来详细介绍:《设计模式》
四人组是指Erich Gamma, Richard Helm, Ralph Johnson & John Vlissides,被称为
Gang of Four,简称GoF。
书里的26种设计模式可以被归为三类:
- 创建型模式 —— 主要是关注对象的创建,将创建对象的过程进行了抽象和封装,使用者只需要去使用对象,无需关心创建过程的逻辑。
- 结构型模式 —— 主要涉及的是对象的组合,在结构上调整和组装它们,以便对象之间的关系更为简单。
- 行为型模式 —— 关注的是不同的对象之间如何去交互和通信。
下面会对每一类型举例讲解。
创建型
单例模式
单例模式的特点是限制类只存在一个实例对象。实例不存在的时候,就创建一个实例对象,如果已存在,就返回之前实例化的对象。
当我们需要通过给函数提供单点交互,来确保对象在应用内共享时,单例模式非常有用。
单例模式的应用包括日志记录、缓存、数据库连接等等。
我们看下面这个关于缓存的简单例子:
class MiniCache {
constructor() {
if (MiniCache.instance == null) {
this.cache = {};
MiniCache.instance = this;
}
return MiniCache.instance;
}
add(key, value) {
this.cache[key] = value;
}
remove(key) {
delete this.cache[key];
}
get(key) {
return this.cache[key];
}
}
const cache = new MiniCache();
Object.freeze(cache);
export { cache };
import { cache } from "./utils/cache";
export class First {
constructor() {
cache.add("book", "Harry Potter");
}
}
import { cache } from "./utils/cache";
export class Second {
constructor() {
console.log(cache.get("book"));
}
}
可以看到保存在First中的book可以在Second中取出,这是因为我们只允许在这两个类之间维持一个共享的cache对象。
尽管单例模式很有用,但如果你发现在应用中需要重复使用它的时候,就说明可能需要重新评估设计模式了。此外,单利模式由于耦合过紧也会给测试带来问题。
工厂模式
工厂模式也属于创造型设计模式的一种,它的核心也是创建对象。但是它不是用构造函数来创建对象,而是采用泛型接口来创建,这样我们可以指定对象的类型。
在这种更简单的模式下,我们只需要输入我们想要的对象类型和属性,工厂方法就会根据属性创建并返回一个合适的对象。这在对象创建过程复杂并且需要解耦的时候尤其有用。
工厂模式的一个典型例子是,根据输入的参数创建不同的UI组件。
结合下面的例子来理解。
class Entree {
constructor(name) {
this.name = name;
this.type = "Entree";
}
}
class MainCourse {
constructor(name) {
this.name = name;
this.type = "Main Course";
}
}
class Dessert {
constructor(name) {
this.name = name;
this.type = "Dessert";
}
}
export class DishFactory {
createDish(name, type) {
switch (type.toLowerCase()) {
case "entree":
return new Entree(name);
break;
case "main course":
return new MainCourse(name);
break;
case "dessert":
return new Dessert(name);
break;
}
}
}
import { DishFactory } from "./dishFactory.js";
let dishFactory = new DishFactory();
let shrimpEntree = dishFactory.createDish("Shrimp Cutlets", "entree");
let lambMain = dishFactory.createDish("Lamb Chops", "main course");
let baklavaDessert = dishFactory.createDish("Baklava", "dessert");
console.log(shrimpEntree, lambMain, baklavaDessert);
这个例子里,dish factory可以根据不同的参数创建对应的 dish。
通过将此设计模式应用于错误类型的问题,您可以轻松地将不必要的复杂性引入到您的应用程序中。您必须确保您的设计目标将通过正在实施的模式实现。
把这种模式应用于处理错误类型的问题,我们就可以把不必要的复杂代码通过factory引入到应用中。需要确认的是,采用这种模式能够达到我们的设计目的。
结构型
外观模式
外观模式的核心是给复杂的代码体提供一个高级接口,可以看作代码体的API,把复杂的地方封装起来,呈现一个简化的界面。因此,外观模式属于结构型模式。
这种模式可以和其他模式结合,例如,外观对象常常是一个单例对象,因为只需要一个外观对象。
使用外观模式的时候,需要注意性能开销,以及是否适合抽象出层级。
我们看下面这个涉及外观模式的例子:
// 采用普通模式
import axios from "axios";
export class PostService {
getPosts() {
return axios
.get("https://jsonplaceholder.typicode.com/posts")
.then(res => res.data);
}
getPostsById(id) {
return axios
.get(`https://jsonplaceholder.typicode.com/posts/${id}`)
.then(res => res.data);
}
getIndividualPostComments(id) {
return axios
.get(`https://jsonplaceholder.typicode.com/posts/${id}/comments`)
.then(res => res.data);
}
}
// 采用外观模式
import axios from "axios";
export class PostServiceFacade {
constructor() {
this.postsFacade = new PostServiceBaseFacade();
}
getPosts() {
return this.postsFacade.getAxiosPosts();
}
getPostsById(id) {
return this.postsFacade.getAxiosPosts(id);
}
getIndividualPostComments(id) {
return this.postsFacade.getAxiosPosts(id, true);
}
}
class PostServiceBaseFacade {
getAxiosPosts(id, comments) {
let baseUrl = "https://jsonplaceholder.typicode.com/posts";
if (id) baseUrl += `/${id}`;
if (comments) baseUrl += "/comments";
return axios.get(baseUrl).then(res => res.data);
}
}
上面的例子里,外观模式把每个 HTTP 请求重复的代码整合到一起,抽象出了一个更高级的接口。
装饰模式
装饰模式被归类为结构型设计模式的原因是,它主要针对的是代码的复用性。装饰模式是用一个函数包装另一个函数来扩展现有能力,我们可以通过另一段代码来包装现有代码的方式「装饰」现有代码。对于熟悉复合函数和高阶函数的人来说,这个概念并不陌生。
装饰模式允许我们写更简洁的代码然后组合,也可以帮我们把相同的功能扩展到多个函数或类中,从而写出更容易调试和维护的代码。
装饰模式让我们的代码更专注,因为它可以把功能优化的代码从核心代码中分开,也便于增加新功能。
//decorator function
const allArgsValid = function(fn) {
return function(...args) {
if (args.length != fn.length) {
throw new Error('Only submit required number of params');
}
const validArgs = args.filter(arg => Number.isInteger(arg));
if (validArgs.length < fn.length) {
throw new TypeError('Argument cannot be a non-integer');
}
return fn(...args);
}
}
//ordinary multiply function
let multiply = function(a,b){
return a*b;
}
//decorated multiply function that only accepts the required number of params and only integers
multiply = allArgsValid(multiply);
multiply(6, 8);
//48
multiply(6, 8, 7);
//Error: Only submit required number of params
multiply(3, null);
//TypeError: Argument cannot be a non-integer
multiply('',4);
//TypeError: Argument cannot be a non-integer
上述代码通过装饰模式把普通函数 multiply的功能进行了扩展。装饰器函数 allArgsValid接收一个函数当参数,然后返回一个新函数,这个新函数里包含了传入的函数和它的参数。allArgsValid的作用是校验传入multiply函数的参数是否符合要求(参数个数正确并且是整数),否则就直接报错不会调用multiply函数。
装饰模式让我们可以通过包裹一层函数来增加新的功能,而不需要担心改动原函数。这也可以让我们停止依赖很多子类去获取相同的功能。
另一方面,过度使用装饰模式会造成我们的命名空间里出现很多相似的小对象,对不熟悉的开发者来说,一开始也难以处理。
行为型
观察者模式
观察者模式就是一个对象(称为主体)维护着一个对这主体感兴趣的对象(称为观察者)列表。当主体状态变化的时候,就会通知给观察者们,并且包含一些相关数据。如果需要,观察者们也可以主动停止被通知。
Angular 开发者对这个模式一定不会陌生,流行的 RXJS 库也大量使用了这种模式。
通过下面的例子来看一下:
// 定义主题
export class Subject {
constructor() {
this.observables = [];
}
subscribe(fn) {
this.observables.push(fn);
}
unsubscribe(fn) {
this.observables = this.observables.filter(fns => {
if (fns != fn) return fn;
});
}
notifyAll() {
this.observables.forEach(fn => {
fn.call();
});
}
}
// 观察者订阅主题
import { Subject } from "./subject.js";
let subject = new Subject();
function Observer1() {
console.log("Hi from Observable 1");
}
function Observer2() {
console.log("Hi from Observable 2");
}
subject.subscribe(Observer1);
subject.subscribe(Observer2);
subject.notifyAll();
subject.unsubscribe(Observer2);
subject.notifyAll();
除了以上5种,你可能还了解其他设计模式。设计模式教我们的是解决一类问题的方法,而不是可以复制粘贴的方案。 如何利用设计模式去解决手头的问题,取决于开发者们自己的想法和能力。
还需要注意的是,用错误的设计模式解决错误的问题,可能会导致更差的效果。