设计模式之美

1,168 阅读10分钟

前言

写出能用的代码的人比比皆是,但是写出高质量代码的人却是凤毛麟角,若深熟谙知设计模式,那高质代码则顺水推舟。当然小编也是普通的一只前端菜鸟,本篇文章将通过对设计模式之美的学习,对设计模式做一个系统的总结。

正文

为什么要学习设计模式?

  • 我们在看一些优秀的开源项目、框架、中间件时,发现代码量和类的个数有很多,而且类之间的关系极其复杂,会有很多交叉调用,为了保证代码的扩展性、灵活性、可维护性等,其中会用很多的设计模式、设计原则和思想。因此掌握设计模式有助于我们参阅源码并很快将脑容量释放,相反如果不懂则要花费更多的时间来参悟作者设计思路。进一步说我们参悟真正又能get到多少点呢?优秀的开源项目、框架、中间件,可以类比成航空母舰,后者体现一个国家科技力量的顶尖及系统程度,前者亦是如此。

  • 普通的、低级别的开发工程师,只需要把框架、开发工具、编程语言用熟练,再做几个项目练练手,基本上就能应付平时的开发工作了。但是如果想成长为技术专家、大牛、技术 leader,就需要注意代码的优美度和质量。如果我们只是框架用得很溜,架构聊得头头是道,但写出来的代码很烂,让人一眼就能看出很多不合理的、可以改进的地方,那永远都成不了别人心目中的“技术大牛”。

  • 再有,如果你是一个技术 leader,负责一个项目整体的开发工作,你就需要为开发进度、开发效率和项目质量负责。我们都不希望团队堆砌垃圾代码,让整个项目无法维护,添加、修改一个功能都要费老大劲,最终拉低整个团队的开发效率。

  • 我也了解,很多面试官实际上对设计模式也并不是很了解,只能拿一些简单的单例模式、工厂模式来考察候选人,而且所出的题目往往都脱离实践,比如,如何设计一个餐厅系统、停车场系统、售票系统等。这些题目都是网上万年不变的老题目,几乎考察不出候选人的能力。

总结: 告别写被人吐槽的烂代码;提高复杂代码的设计和开发能力;让读源码、学框架事半功倍;为你的职场发展做铺垫。

什么是好的代码?

易扩展?易读?简单?易维护? 最常用到几个评判代码质量的标准是:可维护性、可读性、可扩展性、灵活性、简洁性、可复用性、可测 试性。其中,可维护性、可读性、可扩展性又是提到最多的、最重要的三个评价标准。

比如,面向对象中的继承、多态能让我们写出可复用的代码;编码规范能让我们写出可读性好的代码;设计原则中的单一职责、DRY、基于接口 而非实现、里式替换原则等,可以让我们写出可复用、灵活、可读性好、易扩展、易维护的代码;设计模式可以让我们写出易扩展的代码;持续 重构可以时刻保持代码的可维护性等等。

面向对象

历史

面向对象编程中有两个很基础的概念就是类(class)和对象(object),这两个概念出自1960年,在Simula语言中第一次被使用,面向对象编
程概念在Smalltalk编程语言中真正被使用。

1980 年左右,C++ 的出现,带动了面向对象编程的流行,也使得面向对象编程被越来越多的人认可。直到今天,如果不按照严格的定义来说,
大部分编程语言都是面向对象编程语言,比如 Java、C++、Go、Python、C#、Ruby、JavaScript、Objective-C、Scala、PHP、Perl 
等等。除此之外,大部分程序员在开发项目的时候,都是基于面向对象编程语言进行的面向对象编程。

什么语言才是真正的面向对象编程语言呢?

  • 面向对象编程是一种编码范式和编码风格。它以类和对象作为组织代码的基本单元,并将封装、抽象、继承、多态4个特性作为代码设计和实现的基石。

  • 面向对象编程语言支持类或对象的语法机制,也就是它有现成的语法机制方便实现面向对象4大特性:封装、抽象、继承和多态。

  • 注意: 面向对象编程语言写的代码不一定是面向对象编程规范,非面向对象编程语言也可以实现面向对象编程。

这个时候有的同学就会有疑问了,面向对象编程的特性不是有3个吗?确实有的版本是:封装、继承、多态。将抽象排除在外,为什么会有这种
分歧呢?这里大家保留疑问,作为思考。再有我们暂时没有必要纠结到底是几个特性,我们只需要理解每种特性的含义、存在的意义以及解决的
什么问题就好了。

如何判定某种编程语言是面向对象编程语言呢?

其实从严格意义上来将javascript并不是面向对象编程语言,因为它不支持封装和继承特性。

插曲:继承特性解决了代码复用的问题,但是也带来了层次不清、代码混乱确定,因此Go语言中没有继承的特性。但是我们直接否定Go不是面向对象编程语言了。

所以对于这个问题,我们不要学院派,非要给出明确的定义

什么是面向对象分析和设计?

  • 把客观对象抽象成属性对形式的数据的相关操作,把内部的细节隐藏起来。
  • 同一类型的客观对象属性数据和操作绑定在一起,封装成类。
  • 允许分成不同层次进行抽象,通过继承实现属性 简言之:围绕对象或类来做需求分析和设计,产物是类的设计(程序被拆解成哪些类,每个类有哪些属性和方法,类与类如何交互)。

继承

super调用父类的构造器函数
class Animal {
    constructor(name) {
        this.name = name;
    }
    eat() {
        console.log('animal' + this.name + 'eat');
    }
}

class Dog extends Animal{
    constructor(name) {
        super(name);
        this.name = name;
    }
    housekeeping() {
        console.log(this.name + '汪');
    }
}
let dog = new Dog('小狗');
dog.eat() //animal小狗eat

封装

把数据封装起来减少耦合;非外部访问的不让外部访问;利于数据的接口的权限管理;

修饰符

  • private 私有属性,只有自己类可以使用 ;constructor被标识,不可new,不可继承
  • protect 受保护的属性,自己类和子类可以使用 ;constructor被标识,不可new,可继承
  • public 所有类都可以使用。
  • readonly(ts)

问题: ES6目前不支持私有属性private

解决:

//1.typescript支持,ts为最优方案

//2.使用symbol变量配合ES6 class中的get方法

const _idCard = Symbol('idCard');

class Animal {
    constructor(name,idCard) {
        this.name = name;
        this[_idCard] = idCard;
        this.idCard = idCard;
    }
}
class People extends Animal {
    constructor(name, idCard) {
        super(name, idCard);
    }
}
let people = new People('人类', 10001);
console.log('people.name', people.name)
console.log('people.idCard', people.idCard);
console.log(people._idCard);    //这个_idCard不是上面定义的Symbol("_idCard") 
/*people.name 人类
this.idCard 10001
undefined*/
"但是也是有破绽的:我们可以通过Object.getOwnPropertySymbols()来获取_idCard属性的值。"

console.log(Object.getOwnPropertySymbols(people)[0])   // Symbol(idCard)
people[Object.getOwnPropertySymbols(people)[0]] // 10001

//3.使用weakMap + ES6的get方法      这样和Symbol的原理如出一辙了。


const _idCard = new WeakMap();
class Animal {
    constructor(name, idCard) {
        this.name = name;
        _idCard.set(this, idCard);
    }
    getIdCard() {
        console.log('this', this)
        return _idCard.get(this)
    }
}
class People extends Animal {
    constructor(name, idCard) {
        super(name, idCard);
    }
}
const people = new People('人类', 10001);
console.log('people.name', people.name);
console.log('people.getIdCard', people.getIdCard())
"同样也有破绽,就是通过WeakMap的实例的get方法获取idCard"
// console.log('people._idCard', _idCard.get(people))

多态

同一接口有不同实现;保持子类开放性和灵活性;面向接口编程;



class Animal {
  public name: string;
  protected age: number;
  private weight: string;
  constructor(name, age, weight) {
    this.name = name;
    this.age = age;
    this.weight = weight;
  }
}
class Person extends Animal {
  private money;
  constructor(name, age, weight, money) {
    super(name, age, weight);
    this.money = money;
  }
  speak() {
    console.log('nihao');
  }
}

class Dog extends Animal  {
  private money;
  constructor(name, age, weight) {
    super(name, age, weight);
  }
  speak() {
    console.log('汪汪');
  }
}

let person = new Person('tom', 19, "39kg", 33);
person.speak();

export { }

设计模式

solid:也就是按照哪一种思路或者标准来实现功能;功能相同也可以有不同的设计方式;当需求不断变化的时候,设计模式的作用就会体现出来。

首字母设计原则特点知识点
S单一职责原则对象应该仅具有一种单一功能一个程序(函数)只做一件事,功能复杂要进行拆分
O开放封闭原则软件体应该是对于扩展开放的对扩展开放,对修改关闭,有新需求时扩展性强,而非修改已有代码
L里式替换原则程序中的对象应该是可以在不改变程序正确性的前提下被他的子类所替换子类可以覆盖父类,父类能出现的地方子类就能出现;js中使用较少
I接口隔离原则多个特定客户端接口要好于一个宽泛用途的接口保持接口的单一独立,避免出现胖接口;js中使用较少,ts可以使用;类似于单一执行原则但是更关注接口
D依赖反转原则一个方法应该遵从“依赖于抽象而不是实例”或者叫依赖倒置原则,面向接口编程,依赖抽象而非依赖实现;ts可实现
  • 开放封闭原则

function parseJSON(response) {
  return response.json();
}

function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  }

  const error = new Error(response.statusText);
  error.response = response;
  throw error;
}


export default function request(url, options) {
  return fetch(url, options)
    .then(checkStatus)
    .then(parseJSON)
    .then(data => {data})
    .catch(err => ({ err }));
}

简单介绍设计模式

设计模式思维图

工厂模式

简单工厂模式

  • 定义: 由一个工厂对象决定创建出哪一种产品类的实例
  • 代码:
class Plant{
    constructor(name) {
        this.name = name;
    }
    grow() {
        console.log('growing~~~~~~');    
    }
}
class Apple extends Plant{
    constructor(name) {
        super(name);
        this.taste = '甜';
    }
}
class Orange extends Plant{
    constructor(name) {
        super(name);
        this.taste = '酸';
    }
}
class Factory{
    static create(name) {
        switch (name) {
            case 'apple':
                return new Apple('苹果');
            case 'orange':
                return new Orange('橘子');
        }
    }
}
const apple = Factory.create('apple');
console.log(apple);
const orange = Factory.create('orange');
console.log(orange);


  • 经典场景
//jquery场景
class jQuery{
    constructor(selector){
        let elements = Array.from(document.querySelectorAll(selector));
        let length = elements?elements.length:0;
        for(let i = 0;i < length; i++){
            this[i] = elements[i];
        }
        this.length = length;
    }
    html(){

    }
}
window.$ = function(selector){
   return new jQuery(selector);
}
//react场景
class Vnode{
    constructor(tag,attrs,children){
        this.tag = tag;
        this.attrs = attrs;
        this.children = children;
    }
}
React.createElement = function(tag, attrs, children){
  return new Vnode(tag, attr, children);
}

工厂方法模式

  • 在工厂方法模式中,核心的工厂类不再负责所有的产品的创建,而是将具体创建的工作交给子类去做。

class Plant{
    constructor(name) {
        this.name=name;
    }
    grow() {
        console.log('growing~~~~~~');    
    }
}
class Apple extends Plant{
    constructor(name) {
        super(name);
        this.taste = '甜';
    }
}
class Orange extends Plant{
    constructor(name) {
        super(name);
        this.taste = '酸';
    }
}
class AppleFactory{
    create() {
        return new Apple();
    }
}
class OrangeFactory{
    create() {
        return new Orange();
    }
}
const settings={
    'apple': AppleFactory,
    'orange': OrangeFactory
}
const apple = new settings['apple']().create();
console.log(apple);
const orange = new settings['orange']().create();
console.log(orange);

抽象工厂模式

  • 指当有多个抽象角色的时候,使用的一种工厂模式,抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体情况下,创建多个产品族中的产品对象。 我的另一篇:juejin.cn/post/694872…

单例模式

类图:

单例模式是一种常见的软件设计模式,核心结构:只包含一个被称为单例的特殊类,通过单例模式可以保证在系统中,应用该模式的类一个类只 有一个实例。也即是一个类只有一个对象实例。 当然单例模式有很多种实现方式。

实现:

  • 保证构造函数不能被多次实例化,也就是不能让构造函数多次被new出来,我们就需要将构造方法私有化。
  • 以静态方法返回实例对象,也就是在该类内部产生一个唯一的实例化对象,并且将其封装为private static类型,这样我们就实现了仅在内部实例化,不能在外部重复实例
  • 定义一个方法返回这个唯一对象,这样也就是第2点,在内部只对类进行了一次实例化,以后都会直接获取同一个实例化对象。

或者

  • 总而言之,不管是否在内部(将构造方法私有化)还是在外部实例化, 保证只能产生同一个实例化对象

代码

ts实现单例

//ts可以实现单例模式,js不能实现真正的单例模式,因为他没有修饰符
class Window {
    name: string;
    private static instance: Window;
    protected constructor(name) {
        this.name = name;
    }
    static getInstance(name) {
        if (!this.instance) {
            this.instance = new Window(name);
        }
        return this.instance;
    }
}
// const window = new Window("susan");
const w1 = Window.getInstance("tom");
const w2 = Window.getInstance('jarry');
console.log(w1 === w2);
export {}

es5的单例模式(缺陷版)

//es5 
function Window(name) {
	this.name = name;
}
Window.prototype.getName = function() {
	console.log(this.name);
}
Window.getInstance = (function(){
	let window = null;
    return function(name) {
    	if(!window) {
        	window = new Window(name);
            return window;
        }
    }
})()
const window = Window.getInstance("tom")

缺陷: 我们发现,es5模式没有在真正意义上实现单例模式,因为在外部还是可以实例化为不同对象。那么接下来就是解决方案---透明单例模式

透明单例模式(es5单例模式完美版)

看着和普通的方法一样,但其实是单例模式。也就是说透明单例模式可以在函数外new很多次,但是返回的始终是同一个实例

let T = (function() {
    let window;
    const Test = function(name) {
    	if(window) {
        	return window; // 此处的this !=== window,所以不能return this;
        }else {
            this.name = name;
            return window = this;
        }
    }
    return Test;
})()

let a = new T("tom");
let b = new T('jarry');

单例与构建分离

和透明单例如初一折,唯一不同的就是把将要实例化的构造函数,提取出来了。

function Window(name) {
    this.name = name;
}
Window.prototype.getName = function () {
    console.log(this.name);
}
//明显这里就分离了
let createSingle = (function () {
    let instance;
    return function (name) {
        if (!instance) {
            instance = new Window();
        }
        return instance;
    }
})();

const window1 = new createSingle('tom');
const window2 = new createSingle('jarry');
window1.getName();
console.log(window1 === window2)

封装变化

function Window(name) {
    this.name = name;
}
Window.prototype.getName=function () {
    console.log(this.name);
}

const createSingle = function (Constructor) {
    let instance;
    return function () {
        if (!instance) {
            console.log('this1', this);  // 实例对象
            console.dir(Constructor)   // Window 构造函数
            console.log('Constructor.prototype', Constructor.prototype);
                                            // getName constructor 》》 this.__proto__ === Constructor.prototype
            Constructor.apply(this, arguments); // 改变this指向

            Object.setPrototypeOf(this, Constructor.prototype) // 原型继承

            console.log('this2', this)
            instance = this;
        }
        return instance;
    }
};
const CreateWindow = createSingle(Window);
const window1 = new CreateWindow('tom');
const window2 = new CreateWindow('tom');

window1.getName();
console.log(window1 === window2)

命名空间

场景

适配器模式

发布订阅模式(如果你想看懂vue源码的话需要读懂发布订阅模式和观察者模式)

发布订阅模式: 
比喻:你也可以说订阅发布其实理解起来很简单。打个比方,当我们在网购时我们首先把需要买的若干个物品装入购物车,这个过程相当
于订阅,当采购完毕我们进行付款,付完款的一刻就相当于发布(商品开始按流程向你走来)。
实现:把订阅的事件存储在数组中,当发布的时候触发则全部依次执行

简言之就是先将订阅的事件存储在数组中,当发布(emit)时依次执行数组中绑定的事件

示例


const fs = require('fs');

interface IEvents {
    arr: Array<Function>,
    on(fn: Function): void,
    emit(): void,
}
interface IPerson {
    name: string,
    age: number,
}

let events: IEvents = {
    arr: [],
    on(fn) {
        this.arr.push(fn)
    },
    emit() {
        this.arr.forEach(fn => fn())
    }
};
let person = {} as IPerson;
events.on(() => {
    console.log('绑定第一个事件');
})
events.on(() => {
    console.log('都有的')
    if(Object.keys(person).length === 2) {
        console.log('绑定的第二个事件')
    }
})
fs.readFile('./name.txt','utf-8',(err, data) => {
    person.name = data;
    events.emit();
})
fs.readFile('./age.txt','utf-8',(err, data) => {
    person.age = data;
    events.emit();
})
/*绑定第一个事件
都有的
绑定第一个事件
都有的
绑定的第二个事件*/

观察者模式

class Baby {
    name: string;
    state: string;
    observer: Observer[];
    constructor(name: string) {
        this.name = name;
        this.observer = [];
        this.state = '正常'
    }
    attach(instanceObject) {
        this.observer.push(instanceObject);
    }
    setState(state: string) {
        this.state = state;
        this.observer.forEach(o => o.update(this))
    }
}

class Observer {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    update(baby) {
        console.log(baby.name + baby.state + 'from' +  this.name)
    }
}

let baby = new Baby('小明')
let father = new Observer('小明爸爸');
let mother = new Observer('小明妈妈');
baby.attach(father);
baby.attach(mother);
baby.setState('异常')