JavaScript设计模式之简介及创建型模式

895 阅读10分钟

Github 源码地址

设计模式

简介

  • 设计模式(Design pattern),1994 年,由四人帮(Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides) 合著出版了一本名为 Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面向对象软件元素) 的书,首次提到了软件开发中设计模式的概念。
  • 是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。

目的

  • 重用代码;
  • 代码更容易被他人理解;
  • 保证代码可靠性。

分类

  • 23 种,分为三类:创建型模式(Creational Patterns)、结构型模式(Structural Patterns)、行为型模式(Behavioral Patterns)。
创建型模式
  • 工厂模式(Factory Pattern
  • 抽象工厂模式(Abstract Factory Pattern
  • 单例模式(Singleton Pattern
  • 建造者模式(Builder Pattern
  • 原型模式(Prototype Pattern
结构型模式
  • 适配器模式(Adapter Pattern
  • 桥接模式(Bridge Pattern
  • 组合模式(Composite Pattern
  • 装饰器模式(Decorator Pattern
  • 外观模式(Facade Pattern
  • 享元模式(Flyweight Pattern
  • 代理模式(Proxy Pattern
行为型模式
  • 责任链模式(Chain of Responsibility Pattern
  • 命令模式(Command Pattern
  • 解释器模式(Interpreter Pattern
  • 迭代器模式(Iterator Pattern
  • 中介者模式(Mediator Pattern
  • 备忘录模式(Memento Pattern
  • 观察者模式(Observer Pattern
  • 状态模式(State Pattern
  • 策略模式(Strategy Pattern
  • 模板模式(Template Pattern
  • 访问者模式(Visitor Pattern

六大原则

1、开闭原则(Open Close Principle
  • 开闭原则:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类。
2、里氏代换原则(Liskov Substitution Principle
  • 里氏代换原则是面向对象设计的基本原则之一。 任何基类可以出现的地方,子类一定可以出现。LSP(Liskov Substitution Principle) 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
3、依赖倒转原则(Dependence Inversion Principle
  • 针对接口编程,依赖于抽象而不依赖于具体。
4、接口隔离原则(Interface Segregation Principle
  • 使用多个隔离的接口,比使用单个接口要好。还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。
5、迪米特法则,又称最少知道原则(Demeter Principle
  • 一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
6、合成复用原则(Composite Reuse Principle
  • 尽量使用合成/聚合的方式,而不是使用继承。

工厂模式

简介

  • 解决:接口选择的问题(定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类)。
  • 使用:明确地计划不同条件下创建不同实例时。
  • 方式:让其子类实现工厂接口,返回的也是一个抽象的产品。
  • 场景:选择教师。

优缺点

  • 优点:
    1. 创建一个对象只需要知道它的名称即可;
    2. 扩展性高,新增一个产品只要扩展一个工厂类即可;
    3. 屏蔽内部具体实现,只关心产品对应接口。
  • 缺点:
    1. 每增加一个产品,都需要增加一个具体类和对象实现工厂,使系统中类成倍增加,一定程度上增加了系统的复杂度。

举例

  • 你要选取一个某个科目的老师,那你只需要传入你需要的科目即可。
// teacher类
class Teacher {
    teach() {
        return 'Teacher';
    }
}
// 数学老师
class MathTeacher extends Teacher {
    teach() {
        return 'Math';
    }
}
// 语文老师
class ChineseTeacher extends Teacher {
    teach() {
        return 'Chinese';
    }
}

// teacher 工厂
class TeacherFactory {
    getTeacher(teacher) {
        if (teacher === 'math') {
            return new MathTeacher();
        }
        else if (teacher === 'chinese') {
            return new ChineseTeacher();
        }
        return new Teacher();
    }
}

function factoryDemo() {
    const teacherFactory = new TeacherFactory();
    console.log('math', teacherFactory.getTeacher('math').teach());
    console.log('chinese', teacherFactory.getTeacher('chinese').teach());
    console.log('empty', teacherFactory.getTeacher('').teach());
}
factoryDemo();

抽象工厂模式(工厂的工厂)

简介

  • 与工厂模式类似,他也是解决接口选择问题(提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。)
  • 使用:系统的产品有多于一个的产品族,而系统只消费其中某一族的产品。
  • 方式:在一个产品族里面,定义多个产品。
  • 场景:选择教师;网页、App等换肤。

优缺点

  • 优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
  • 缺点:产品族扩展非常困难,要增加一个系列的某一产品,既要在抽象的 AbstractFactory 里加代码,又要在具体的里面加代码。

举例

  • 接上面的列子,你要选取一个某个科目的老师,或者你还可以指定男性或者女性。
// 在上述示例代码中添加下列代码,替换TeacherFactory及factoryDemo代码
// 性别
class Sex {
    sex() {
        return '';
    }
}

class ManSex extends Sex {
    sex() {
        return 'Man';
    }
}
class WomanSex extends Sex {
    sex() {
        return 'Woman';
    }
}

// 抽象工厂
class AbstractFactory {
    getTeacher(teacher) {
        return new Teacher();
    }
    getSex(sex) {
        return new Sex();
    }
}

// 教师工厂
class TeacherFactory extends AbstractFactory {
    getTeacher(teacher) {
        if (teacher === 'math') {
            return new MathTeacher();
        }
        else if (teacher === 'chinese') {
            return new ChineseTeacher();
        }
        return new Teacher();
    }
    getSex(sex) {
        return new Sex();

    }
}

// 性别工厂
class SexFactory extends AbstractFactory {
    getTeacher(teacher) {
        return new Teacher();
    }
    getSex(sex) {
        if (sex === 'woman') {
            return new WomanSex();
        }
        else if (sex === 'man') {
            return new ManSex();
        }
        return new Sex();

    }
}
// 工厂创造器/生成器类
class FactoryProvider {
    getFactory(type) {
        if (type === 'teacher') {
            return new TeacherFactory()
        } else if (type === 'sex') {
            return new SexFactory()
        }
    }
}

function factoryDemo() {
    const factoryProvider = new FactoryProvider();
    console.log('teacher: math ----', factoryProvider.getFactory('teacher').getTeacher('math').teach());
    console.log('sex: woman ----', factoryProvider.getFactory('sex').getSex('woman').sex());

}
factoryDemo();

单例模式

简介

  • 解决:一个全局使用的类频繁地创建与销毁。(保证一个类仅有一个实例,并提供一个访问它的全局访问点。)
  • 使用:想控制实例数目,节省系统资源的时候。
  • 方式:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
  • 场景:页面的错误弹窗始终保持只有一个。

优缺点

  • 优点:
    1. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
    2. 避免对资源的多重占用(比如写文件操作)。
  • 缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

实现

class Singleton {
    constructor(name) {
        this.name = name;
    }

    getName() {
        return this.name;
    }

    // 获取单例的接口
    static getInstance(name) {
        // 判断是否存在实例
        if (Singleton.instance === undefined) {
            Singleton.instance = new Singleton(name);
        }
        return Singleton.instance;
    }
}

// test
const ins1 = Singleton.getInstance('小小小十七');
const ins2 = Singleton.getInstance('17');
console.log('ins1 === ins2 ?', ins1 === ins2); // ins1 === ins2 ? true
console.log(ins1.name, ins2.getName()); // 小小小十七 小小小十七

建造者模式

简介

  • 解决:面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。(将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。)
  • 使用:一些基本部件不会变,而其组合经常变化的时候。
  • 方式:将变与不变分离开。
  • 场景:需要生成的对象具有复杂的内部结构。需要生成的对象内部属性本身相互依赖。(例如:点不同的套餐、修建房屋)

优缺点

  • 优点:
    1. 建造者独立,易扩展。
    2. 便于控制细节风险。
  • 缺点:
    1. 产品必须有共同点,范围有限制。
    2. 如内部变化复杂,会有很多的建造类。

举例

// 修建房屋
// 工人
class Worker {
    constructor(partName) {
        console.log('工人:我在建造', partName);
        this.partName = partName;
    }
}

// 抽象房屋结构
class Part {
    constructor() {
        console.log('创建项目');
    }
    build(partName) {
        console.log('开始建造', partName);
    }
}

class CreatePart extends Part {
    constructor(partName) {
        super();
        this.partName = partName;
    }
    build() {
        super.build(this.partName);
        this.worker = new Worker(this.partName);
    }

    getResult() {
        console.log(this.partName, '建造完成');
        return this.worker
    }
}

// 建筑商
class Developer {
    constructor(need) {
        this.need = need;
        console.log('我的建造需求', need);
    }
    start() {
        const WorkOver = this.need.map(element => {
            console.log(`查看'${element}'图纸`);
            const builder = new CreatePart(element);
            builder.build();
            return builder.getResult();
        });
        console.log('所有建造完成', WorkOver);
    }
}

const develop = new Developer(['客厅', '一室一卫'])
develop.start();

原型模式

简介

  • 解决在运行期建立和删除原型。(用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。)
  • 使用:
    1. 当一个系统应该独立于它的产品创建,构成和表示时。
    2. 当要实例化的类是在运行时刻指定时,例如,通过动态装载。
    3. 为了避免创建一个与产品类层次平行的工厂类层次时。
    4. 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
  • 方式:利用已有的一个原型对象,快速地生成和原型对象一样的实例。
  • 场景:资源优化、性能和安全要求、一个对象多个修改者等

优缺点

  • 优点:
    1. 性能提高。
    2. 逃避构造函数的约束。
  • 缺点:
    1. 配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。

实现

// 通过ECMAScript5中的Object.create方法

// 创建原型对象
const prototype = {
    name: '小小小十七',
    getName() {
        return this.name;
    }
}

// 根据原型对象生成新的实例
const xiao17 = Object.create(prototype, {
    name: {
        value: '小17'
    },
    parentName: {
        value() {
            return prototype.name
        }
    }
});
console.log(xiao17.getName());
console.log(xiao17.parentName());
console.log(xiao17.__proto__ === prototype);

总结

  • 创建型模式的特点是将对象的创建与使用分离。我们只需要关注使用,使用者不需要关注对象的创建细节,对象的创建由相关的工厂来完成。这样可以降低系统的耦合度。
  • 在日常开发中,用需求对比不同的设计模式有相似的,我们即可选择对应设计模式去处理对应的问题,降低代码耦合度、提升代码可读性。

更多优质文章

「点赞、收藏和评论」

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章,谢谢🙏大家。