再谈23种设计模式(1):开篇总结与创建者模型系列
Author:[email protected] Date:
这篇文章是对设计模式的再谈系列总结性笔记,详细的,推荐阅读:
设计模式 Java版本 https://www.bookstack.cn/read/design-pattern-java/README.md
https://fhfirehuo.github.io/Attacking-Java-Rookie/Chapter04/designPatterns.html
什么是设计模式
软件设计模式(Software Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。
它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。其目的是为了提高代码的可重用性、代码的可读性和代码的可靠性。
最早将模式的思想引入软件工程方法学的是1991-1992年以“四人组(Gang of Four,简称GoF,分别是Erich Gamma, Richard Helm, Ralph Johnson和John Vlissides)”自称的四位著名软件工程学者,他们在1994年归纳发表了23种在软件开发中使用频率较高的设计模式,旨在用模式来统一沟通面向对象方法在分析、设计和实现间的鸿沟。
GoF将模式的概念引入软件工程领域,这标志着软件模式的诞生。软件模式(Software Patterns)是将模式的一般概念应用于软件开发领域,即软件开发的总体指导思路或参照样板。软件模式并非仅限于设计模式,还包括架构模式、分析模式和过程模式等,实际上,在软件开发生命周期的每一个阶段都存在着一些被认同的模式。
1995年, GoF将收集和整理好的23种设计模式汇编成Design Patterns: Elements of Reusable Object-Oriented Software【《设计模式:可复用面向对象软件的基础》】一书,该书的出版也标志着设计模式正式成为面向对象(Object Oriented)软件工程的一个重要研究分支。
设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。正确使用设计模式具有以下优点:
可以提高程序员的思维能力、编程能力和设计能力。
使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。
设计模式有两种分类方法,即根据模式的目的来分和根据模式的作用的范围来分。
设计模式的分类
根据目的来分:即根据模式是用来完成什么工作来划分,这种方式可分为创建型模式、结构型模式和行为型模式 3 种。
创建型模式:对象实例化的模式,创建型模式用于解耦对象的实例化过程。描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”
共五种:单例模式、原型模式、工厂方法模式、抽象工厂模式、建造者模式。唯有工厂方法模式属于类创建型模式
结构型模式:把类或对象结合在一起形成一个更大的结构。用于描述如何将类或对象按某种布局组成更大的结构
共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式:类和对象如何交互,及划分责任和算法。描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。
共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
根据作用范围来分:即根据模式是主要用于类上还是主要用于对象上来分,这种方式可分为类模式和对象模式两种。
类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。
共四种:工厂方法、(类)适配器、模板方法、解释器。
对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。
GoF 中除了以上 4 种,其他的都是对象模式。
23 种设计模式的分类表
范围\目的 | 创建型模式 | 结构型模式 | 行为型模式 |
---|---|---|---|
类模式 | 工厂方法 | (类)适配器 | 模板方法、解释器 |
对象模式 | 单例 原型 抽象工厂 建造者 | 代理 (对象)适配器 桥接 装饰 外观 享元 组合 | 策略 命令 职责链 状态 观察者 中介者 迭代器 访问者 备忘录 |
GoF的23种软件设计模式列表
单例模式(Singleton pattern):某个类只能有一个实例,提供一个全局的访问点。
工厂方法(factory method pattern):定义一个创建对象的接口,让子类决定实例化那个类。
抽象工厂(Abstract factory pattern):创建相关或依赖对象的家族,而无需明确指定具体类。
建造者模式(Builder pattern):封装一个复杂对象的构建过程,并可以按步骤构造。
原型模式(prototype pattern):通过复制现有的实例来创建新的实例。
适配器模式(Adapter pattern):将一个类的方法接口转换成客户希望的另外一个接口。
组合模式(composite pattern):将对象组合成树形结构以表示“”部分-整体“”的层次结构。
装饰模式(decorator pattern):动态的给对象添加新的功能。
代理模式(Proxy pattern):为其他对象提供一个代理以便控制这个对象的访问。
亨元(蝇量)模式(Flyweight Pattern):通过共享技术来有效的支持大量细粒度的对象。
外观模式(facade pattern):对外提供一个统一的方法,来访问子系统中的一群接口。
桥接模式(Bridge pattern):将抽象部分和它的实现部分分离,使它们都可以独立的变化。
模板模式(Template pattern):定义一个算法结构,而将一些步骤延迟到子类实现。
解释器模式(Interpreter pattern):给定一个语言,定义它的文法的一种表示,并定义一个解释器。
策略模式(strategy pattern):定义一系列算法,把他们封装起来,并且使它们可以相互替换。
状态模式(State pattern):允许一个对象在其对象内部状态改变时改变它的行为。
观察者模式(observer pattern):对象间的一对多的依赖关系。
备忘录模式(Memento pattern):在不破坏封装的前提下,保持对象的内部状态。
中介者模式(Mediator pattern):用一个中介对象来封装一系列的对象交互。
命令模式(Command pattern):将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。
访问者模式(visitor pattern):在不改变数据结构的前提下,增加作用于一组对象元素的新功能。
责任链模式(Chain of responsibility pattern):将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。
迭代器模式(iterator pattern):一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。
其实还有两类:并发型模式和线程池模式。
用一个图片来整体描述一下:
概说24种设计模式
在程序开发中,有些对象我们只需要一个,比如说:线程池、对话框、缓存、唯一的序列号生成器、有状态的工具类对象等。如果我们实例化多个对象就可能会出错。那么怎样才能保证一个类只有一个实例化对象呢?
这里我们想到了全局变量,全局变量确实是可以。保证该类可以随时访问,但是它很难解决只有一个实例问题。
最好的办法就是让该自身来负责保存它的唯一实例。这个类必须要保证没有其他类来创建它。这里我们可以将其构造方法私有化。
单例模式(Singleton pattern)
确保某一个类只有一个实例,并且提供一个全局访问点。就是保证一个类仅有一个实例即可「new 一次」
单例模式具备典型的3个特点:1、只有一个实例。 2、自我实例化。 3、提供全局访问点。
因此当系统中只需要一个实例对象或者系统中只允许一个公共访问点,除了这个公共访问点外,不能通过其他访问点访问该实例时,可以使用单例模式。
单例模式的优缺点:
单例模式的主要优点就是节约系统资源、提高了系统效率,同时也能够严格控制客户对它的访问。也许就是因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,同时也没有抽象类,所以扩展起来有一定的困难。
Gson、Retrofit对象,ServiceManager等,Android系统提供的各种XXXManager(NotificationManager)等等,这些通过单例的方式去管理它,能够让你业务设计的更加严谨
单例模式的优点
1.在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
2.单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
3.提供了对唯一实例的受控访问。
4.由于在系统内存中只存在一个对象,因此可以 节约系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
5.允许可变数目的实例。
6.避免对共享资源的多重占用。
单例模式的缺点:
1.不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
2.由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
3.单例类的职责过重,在一定程度上违背了“单一职责原则”。
4.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
使用注意事项:
1.使用时不能用反射模式创建单例,否则会实例化一个新的对象
2.使用懒单例模式时注意线程安全问题
3.单例模式和懒单例模式构造方法都是私有的,因而是不能被继承的,有些单例模式可以被继承(如登记式模式)
单例模式的生命周期如下
创建阶段:当应用程序启动时,单例类的构造函数被调用一次,创建一个实例。这个实例是私有的,不能通过常规方式直接创建。
初始化阶段:在创建实例后,可以对实例进行初始化,设置其属性和状态。这通常在构造函数或专门的初始化方法中完成。
使用阶段:在整个应用程序的生命周期中,其他类可以通过单例类提供的静态方法或属性访问这个唯一实例。单例模式确保了在任何时候都只有一个实例存在,避免了重复创建和资源浪费。
销毁阶段:当应用程序关闭或不再需要单例实例时,应该销毁这个实例以释放资源。销毁可以通过显式调用销毁方法或在程序结束时自动完成。需要注意的是,单例实例的销毁时机可能因编程语言和运行环境的不同而有所差异。
单例模式的模式实现
由类是否在被加载时就将自己实例化,可以将单例模式分为两种:
饿汉式(Eager Initialization)
在被引用的时候就被实例化。类一旦加载就创建一个单例,保证在调用 getInstance 方法之前单例已经存在了,「把这一过程当作一个汉堡,也就是说必须要把汉堡提前准备好,饿货就知道吃」
饿汉式特点:是线程安全的,类不是延时加载「直接是类加载的时候就初始化」
饿汉式优点:没有加锁,执行效率非常高「其实是以空间来换时间」
饿汉式缺点:在类加载的时候就会初始化,浪费内存「你知道我要不要使用这个实例吗,你就给我初始化,太任性了」
懒汉式(Lazy Initialization)
在需要的时候才被实例化。类加载时没有生成单例,只有当第一次调用 getlnstance 方法时才去创建这个单例
懒汉模式特点:线程不安全,延时初始化类,在我需要的时候「也就调用 getInstance」的时候才去初始化
懒汉式优点:延时初始化类,省资源,不想用的时候就不会浪费内存
懒汉式缺点:线程不安全,多线程操作就会有问题;加锁虽然线程安全,但对性能影响非常大「相当于排队获取资源,没有拿到锁子就干等」,双重加锁(双重检查锁:double-checked locking)多线程仿问性能达到提升,但是双检锁会遇到指令重排的问题,导致多线程下失效
推进阅读《人人都会设计模式--单例模式--Singleton》
js代码实现:
class Single { getSingle(fn) { var result; //由于闭包,所以当single.getSingle函数执行完之后,内存中并不会销毁result。 return function() { //单例模式的主要思想就是,实例如果已经创建,则直接返回 return result || (result = fn.apply(this, arguments)) } } }
java代码实现
public class SingletonTest { private static SingletonTest instance = null; private Vector properties = null; public Vector getProperties() { return properties; } private SingletonTest() { } private static synchronized void syncInit() { if (instance == null) { instance = new SingletonTest(); } } public static SingletonTest getInstance() { if (instance == null) { syncInit(); } return instance; } public void updateProperties() { SingletonTest shadow = new SingletonTest(); properties = shadow.getProperties(); } }
懒汉与恶汉模式
在懒汉模式中,单例对象在第一次使用时才被创建,可以节省资源。
class LazySingleton { private static instance: LazySingleton; // 私有构造函数,防止外部直接new private constructor() {} // 全局访问点 public static getInstance(): LazySingleton { // 第一次访问时,创建实例 if (!LazySingleton.instance) { LazySingleton.instance = new LazySingleton(); } return LazySingleton.instance; } // 示例方法 public someMethod(): void { console.log('This is some method of the lazy singleton.'); } } // 使用 const lazyInstance = LazySingleton.getInstance(); lazyInstance.someMethod();
在饿汉模式中,单例对象在类加载时就被创建,不管是否需要使用这个对象。
class EagerSingleton { // 在类加载时就立即初始化,并创建单例对象 private static instance: EagerSingleton = new EagerSingleton(); // 私有构造函数,防止外部直接new private constructor() {} // 全局访问点 public static getInstance(): EagerSingleton { return EagerSingleton.instance; } // 示例方法 public someMethod(): void { console.log('This is some method of the eager singleton.'); } } // 使用 const eagerInstance = EagerSingleton.getInstance(); eagerInstance.someMethod();
在实际应用中,懒汉模式更加常见,因为它允许更好的资源管理和性能优化。然而,如果你确定一个对象总是需要被使用,或者对象的创建和初始化对启动性能影响不大,那么饿汉模式也是一个不错的选择。
单例模式的应用场景
在应用场景中,某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。
当某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
需要注意的是,单例模式可能导致代码之间的耦合度增加,降低系统的可测试性。
原型模式
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。在这里,原型实例指定了要创建的对象的种类。用这种方式创建对象非常高效,根本无须知道对象创建的细节。例如,JavaScript就是基于原型模式,由于 Java 提供了对象的 clone() 方法,所以用 Java 实现原型模式很简单。
原型模式的结构
原型模式包含以下主要角色。
抽象原型类:规定了具体原型对象必须实现的接口。
具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
访问类:使用具体原型类中的 clone() 方法来复制新的对象。
模式的实现
原型模式的克隆分为浅克隆和深克隆,Java 中的 Object 类提供了浅克隆的 clone() 方法,具体原型类只要实现 Cloneable 接口就可实现对象的浅克隆
//具体原型类 class Realizetype implements Cloneable { Realizetype() { System.out.println("具体原型创建成功!"); } public Object clone() throws CloneNotSupportedException { System.out.println("具体原型复制成功!"); return (Realizetype)super.clone(); } } //原型模式的测试类 public class PrototypeTest { public static void main(String[] args)throws CloneNotSupportedException { Realizetype obj1=new Realizetype(); Realizetype obj2=(Realizetype)obj1.clone(); System.out.println("obj1==obj2?"+(obj1==obj2)); } }
对象深、浅复制的概念:
浅复制:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。
深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。简单来说,就是深复制进行了完全彻底的复制,而浅复制不彻底。
public class Prototype implements Cloneable, Serializable { private static final long serialVersionUID = 1L; private String string; private SerializableObject obj; /* 浅复制 */ public Object clone() throws CloneNotSupportedException { Prototype proto = (Prototype) super.clone(); return proto; } /* 深复制 */ public Object deepClone() throws IOException, ClassNotFoundException { /* 写入当前对象的二进制流 */ ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); /* 读出二进制流产生的新对象 */ ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); } public String getString() { return string; } public void setString(String string) { this.string = string; } public SerializableObject getObj() { return obj; } public void setObj(SerializableObject obj) { this.obj = obj; } } class SerializableObject implements Serializable { private static final long serialVersionUID = 1L; }
要实现深复制,需要采用流的形式读入当前对象的二进制输入,再写出二进制数据对应的对象。
而JavaScript 使用原型模式就更加简单
// 原型对象 const carPrototype = { start() { console.log(`The ${this.make} ${this.model} car has started.`); }, stop() { console.log(`The ${this.make} ${this.model} car has stopped.`); } }; // 创建新对象的函数 function createCar(make, model) { let car = Object.create(carPrototype); car.make = make; car.model = model; return car; } // 使用原型模式创建新对象 const car1 = createCar('Toyota', 'Corolla'); const car2 = createCar('Honda', 'Civic'); car1.start(); // 输出: The Toyota Corolla car has started. car2.stop(); // 输出: The Honda Civic car has stopped. // 简单的代码 function Person(name){ this.name = name; }; Person.prototype.sayHello = function(){ console.log("Hello, my name is " + this.name); }; var john = new Person("John"); john.sayHello(); // Hello, my name is John
所有的jQuery对象都共享相同的方法,如.css(), .animate(), 等等。这是原型模式的一个典型例子。Prototype.js、D3.js何尝不是?
工厂设计模式
我们把被创建的对象称为“产品”,把创建产品的对象称为“工厂”。如果要创建的产品不多,只要一个工厂类就可以完成,这种模式叫“简单工厂模式”,它不属于 GoF 的 23 种经典设计模式,它的缺点是增加新产品时会违背“开闭原则”。
麦当劳和肯德基就是生产鸡翅的Factory 工厂模式:客户类和工厂类分开。消费者任何时候需要某种产品,只需向工厂请求即可。消费者无须修改就可以接纳新产品。缺点是当产品修改时,工厂类也要做相应的修改。如:如何创建及如何向客户端提供。
简单工厂一般分为:
普通简单工厂:就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建
多方法简单工厂:提供多个工厂方法,分别创建对象
静态方法简单工厂:将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可
工厂模式适合:凡是出现了大量的产品需要创建,并且具有共同的接口时,可以通过工厂方法模式进行创建。我们会选用静态工厂方法模式——不需要实例化工厂类。
工厂模式简而言之,就是要替代掉“new操作符”!
为什么需要替代new操作符?
因为有时候创建实例时需要大量的准备工作,而将这些准备工作全部放在构造函数中是非常危险的行为,有必要将创建实例的逻辑和使用实例的逻辑分开,方便以后扩展。
主要用来将实例化对象提取出来,放到一个类中统一维护和管理,从而达到解耦、提高项目的扩展性和维护性。
工厂模式js实现:
class Creator { create(name) { return new Animal(name) } } class Animal { constructor(name) { this.name = name } } var creator = new Creator() var duck = creator.create('Duck');console.log(duck.name) // Duck var chicken = creator.create('Chicken');console.log(chicken.name) // Chicken
一个工程模式优化例子
class People { constructor(des) { // 出现异步不能使用async await // 函数调用时可能还未完成初始化 get('someUrl').then(data => { this.name = data.name get('someUrl?name=' + this.name).then(data => { this.age = data.age }) }) // 非成员函数耦合性变大 this.des = handleDes(des) }}
工产模式
// 还真别说,形式上好看的代码,质量一般都比较高 class People { name: string = ''; age: number = 0; des: string = ''; constructor(name: string, age: number, des: string) { this.name = name; this.age = age; this.des = des; } } async function peopleFactory(description: any) { const name = await get('someUrl'); const age = await get('someUrl?name=' + name); const des = handle(description); return new People(name, age, des); }
工厂方法模式(Factory Method)
简单工厂模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了开闭原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到工厂方法模式:
创建一个工厂接口和创建多个工厂实现类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。
工厂方法模式:核心工厂类不再负责所有产品的创建,而是将具体创建的工作交给子类去做,成为一个抽象工厂角色,仅负责给出具体工厂类必须实现的接口,而不接触哪一个产品类应当被实例化这种细节。
请MM去麦当劳吃汉堡,不同的MM有不同的口味,要每个都记住是一件烦人的事情,我一般采用Factory Method模式,带着MM到服务员那儿,说“要一个汉堡”,具体要什么样的汉堡呢,让MM直接跟服务员说就行了。
实现方式:
抽象产品类(也可以是接口)
多个具体的产品类
工厂类(包括创建a的实例的方法)
下面是工厂方法的代码演示
// 抽象产品类 interface Button { paint(): void; } // 多个具体的产品类 class WindowsButton implements Button { paint(): void { console.log("Painting a Windows button."); } } class MacOSButton implements Button { paint(): void { console.log("Painting a macOS button."); } } // 工厂类 class ButtonFactory { static createButton(os: string): Button { switch (os) { case "Windows": return new WindowsButton(); case "macOS": return new MacOSButton(); default: throw new Error("Unsupported operating system."); } } } // 使用工厂方法创建按钮 const button = ButtonFactory.createButton("Windows"); button.paint(); // 输出:Painting a Windows button.
工厂方法模式的主要优点有:
用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;
在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则;、
工厂方法模式缺点是:
每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。
抽象工厂(Abstract factory pattern)
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。
系统中有多个产品族,每个具体工厂创建同一族但属于不同等级结构的产品。
系统一次只可能消费其中某一族产品,即同族的产品一起使用。
抽象工厂模式的结构
抽象工厂模式同工厂方法模式一样,也是由抽象工厂、具体工厂、抽象产品和具体产品等 4 个要素构成,但抽象工厂中方法个数不同,抽象产品的个数也不同。
抽象工厂模式的主要角色如下。
抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法 newProduct(),可以创建多个不同等级的产品。
具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它 同具体工厂之间是多对一的关系。
抽象工厂的代码演示
抽象工厂模式(Abstract Factory Pattern)提供了一个接口,用于创建一系列相关或依赖的对象,而不需要指定它们具体的类。以下是使用TypeScript实现的抽象工厂模式的示例:
假设我们有两种类型的UI组件:按钮和复选框。我们希望为不同的操作系统(如Windows和MacOS)创建不同风格的UI组件。
首先,我们定义抽象产品和抽象工厂的接口:
// 抽象产品:按钮 interface Button { paint(): void; } // 抽象产品:复选框 interface Checkbox { paint(): void; } // 抽象工厂 interface GUIFactory { createButton(): Button; createCheckbox(): Checkbox; }
然后,我们实现具体的产品类:
// Windows风格的按钮 class WindowsButton implements Button { public paint(): void { console.log('You have created WindowsButton.'); } } // MacOS风格的按钮 class MacOSButton implements Button { public paint(): void { console.log('You have created MacOSButton.'); } } // Windows风格的复选框 class WindowsCheckbox implements Checkbox { public paint(): void { console.log('You have created WindowsCheckbox.'); } } // MacOS风格的复选框 class MacOSCheckbox implements Checkbox { public paint(): void { console.log('You have created MacOSCheckbox.'); } }
接下来,我们实现具体的工厂类:
// Windows风格的UI工厂 class WindowsFactory implements GUIFactory { public createButton(): Button { return new WindowsButton(); } public createCheckbox(): Checkbox { return new WindowsCheckbox(); } } // MacOS风格的UI工厂 class MacOSFactory implements GUIFactory { public createButton(): Button { return new MacOSButton(); } public createCheckbox(): Checkbox { return new MacOSCheckbox(); } }
最后,我们使用抽象工厂来创建不同风格的UI组件:
// 客户端代码 function clientCode(factory: GUIFactory) { const button: Button = factory.createButton(); const checkbox: Checkbox = factory.createCheckbox(); button.paint(); checkbox.paint(); } // 应用程序可以根据配置或环境选择工厂 console.log('Client: Testing client code with the first factory type...'); clientCode(new WindowsFactory()); console.log('Client: Testing the same client code with the second factory type...'); clientCode(new MacOSFactory());
在这个示例中,GUIFactory 接口定义了创建按钮和复选框的方法。WindowsFactory 和 MacOSFactory 是实现了该接口的具体工厂,它们分别创建Windows风格和MacOS风格的UI组件。客户端代码通过抽象工厂接口与具体工厂交互,从而创建不同风格的UI组件。
这个例子展示了如何使用抽象工厂模式来创建一系列相关的对象,同时保持客户端代码与具体类的解耦。通过使用不同的工厂,我们可以轻松地切换产品族而不影响客户端代码。
工厂方法模式vs抽象工厂模
工厂方法模式和抽象工厂模式不好分清楚,他们的区别如下:
工厂方法模式:
一个抽象产品类,可以派生出多个具体产品类。
一个抽象工厂类,可以派生出多个具体工厂类。
每个具体工厂类只能创建一个具体产品类的实例。
抽象工厂模式:
多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。
一个抽象工厂类,可以派生出多个具体工厂类。
每个具体工厂类可以创建多个具体产品类的实例,也就是创建的是一个产品线下的多个产品。
工厂方法模式和抽象工厂模的区别:
工厂方法模式只有一个抽象产品类,而抽象工厂模式有多个。
工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个。
工厂方法创建 "一种" 产品,他的着重点在于"怎么创建",也就是说如果你开发,你的大量代码很可能围绕着这种产品的构造,初始化这些细节上面。也因为如此,类似的产品之间有很多可以复用的特征,所以会和模版方法相随。
抽象工厂需要创建一些列产品,着重点在于"创建哪些"产品上,也就是说,如果你开发,你的主要任务是划分不同差异的产品线,并且尽量保持每条产品线接口一致,从而可以从同一个抽象工厂继承。
建造者模式
建造者模式将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。
如果我们用了建造者模式,那么用户就只需要指定需要建造的类型就可以得到他们,而具体的过程以及细节就不需要知道。
用大白话解释就是:将一个复杂的对象分解为多个简单的对象,然后一步一步构建形成产品。它主要是将变化部分与不变部分相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。
例如,计算机是由 OPU、主板、内存、硬盘、显卡、机箱、显示器、键盘、鼠标等部件组装而成的,采购员不可能自己去组装计算机,而是将计算机的配置要求告诉计算机销售公司,计算机销售公司安排技术人员去组装计算机(根据经济情况和使用的场景不同,可选择每部分的不同配置,不然开发机、美术机、运营/秘书用的标配机),然后再交给要买计算机的采购员。
比如图表平台,像图表类,有柱线图系列,里面包含 普通折线图、双Y图、堆叠图、面积图等,可以通过抽象工厂来实现
比如画布容器,图表容器组件,具体渲染什么图表,可以采用建造者模式
建造者返回给客户一个完整的的产品对象,而客户端无须关心该对象所包含的额属性和组建方式,这就是建造者模式的设计动机。
建造者模式(Builder),将一个复杂对象的构建与它的表示分离,使得同样的构建过程就可以创建不同的表示。
建造者模式又称为“生成器模式”,建造者模式可以将一个产品的内部表象与产品的生产过程分割开来,从而可以使一个建造过程生成具有不同的内部产品对象。当使用建造者模式,那么用户就只需要指定需要建造的类型就可以得到它们,而不就不需要知道具体建造的过程和细节。
建造者模式的结构
建造者(Builder)模式的主要角色如下。
产品角色(Product):它是包含多个组成部件的复杂对象,由具体建造者来创建其各个滅部件。
抽象建造者(Builder):它是一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法 getResult()。
具体建造者(Concrete Builder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。
指挥者(Director):它调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。
作为开发人员,我们使用笔记本时,往往还会配置外置键盘、鼠标和外置显示器。此时,一整套电脑装备就包含了:笔记本电脑、鼠标、键盘、显示器。当然,根据个人的需求不同,有些人不需要鼠标,有些人不需要外置键盘,有些人不需要外置显示器,这就又构成了不同的产品。
先来定义产品类Computer,也就是开发电脑的整体配置:
public class Computer { /** * 笔记本电脑 */ private Laptop laptop; /** * 鼠标 */ private Mouse mouse; /** * 显示器 */ private Screen screen; /** * 键盘 */ private Keyboard keyboard; public void show() { System.out.println("笔记本配置:" + laptop.getName()); System.out.println("鼠标配置:" + mouse.getName()); System.out.println("显示器配置:" + screen.getName()); System.out.println("键盘配置:" + keyboard.getName()); } // 省略getter/setter方法 }
产品由四个组件构成:笔记本电脑、鼠标、显示器、键盘。
抽象建造者:包含创建产品各个子部件的抽象方法。通过接口声明在所有类型生成器中通用的产品构造步骤。
public interface ComputerBuilder { /** * 构建笔记本电脑 */ void constructLaptop(); /** * 构建鼠标 */ void constructMouse(); /** * 构建屏幕 */ void constructScreen(); /** * 构建键盘 */ void constructKeyboard(); /** * 返回最终产品对象 */ Computer getResult(); }
抽象建造者提供了每个组件构建的统一方法,至于具体每个组件内部的配置可能会有各种各样的变化,这就有具体的实现者来实现。
具体建造者:实现了抽象建造者接口。在当前业务场景中,我们假设公司给的电脑配置有两类,一类是普通的配置,一类是高端配置。这样就有两个具体的建造者了,用来提供构造过程的不同实现。
普通配置实现:
public class CommonComputerBuilder implements ComputerBuilder { private Computer computer = new Computer(); @Override public void constructLaptop() { Laptop laptop = new Laptop("A","华为笔记本"); computer.setLaptop(laptop); } @Override public void constructMouse() { Mouse mouse = new Mouse("A","无线鼠标"); computer.setMouse(mouse); } @Override public void constructScreen() { Screen screen = new Screen("A","液晶显示器"); computer.setScreen(screen); } @Override public void constructKeyboard() { Keyboard keyboard = new Keyboard("A","普通键盘"); computer.setKeyboard(keyboard); } @Override public Computer getResult() { return computer; } }
高级配置实现:
public class SupperComputerBuilder implements ComputerBuilder { private Computer computer = new Computer(); @Override public void constructLaptop() { Laptop laptop = new Laptop("S", "Mac Boor Pro"); computer.setLaptop(laptop); } @Override public void constructMouse() { Mouse mouse = new Mouse("A", "无线鼠标"); computer.setMouse(mouse); } @Override public void constructScreen() { Screen screen = new Screen("S", "液晶曲面屏"); computer.setScreen(screen); } @Override public void constructKeyboard() { Keyboard keyboard = new Keyboard("S", "机械键盘"); computer.setKeyboard(keyboard); } @Override public Computer getResult() { return computer; } }
在此具体建造者中,设置了每个组件的配置信息,这里做了简化,每个组件都只有类型和名称。而实际使用中,可能每个组件对象会包含不同的数据项。
指挥者:调用建造者中的方法完成复杂对象的创建。
public class Director { private ComputerBuilder builder; public Director(ComputerBuilder builder) { this.builder = builder; } //产品构建与组装方法 public Computer construct() { builder.constructLaptop(); builder.constructMouse(); builder.constructScreen(); builder.constructKeyboard(); return builder.getResult(); } }
指挥者对整个生产过程进行了封装,对客户端进行了隔离,客户端只用获得最终的产品即可,不用关心组装的过程。
客户端调用示例:
public class Client { public static void main(String[] args) { // 普通配置电脑的组装 ComputerBuilder builder = new CommonComputerBuilder(); Director director = new Director(builder); Computer product = director.construct(); product.show(); System.out.println("------------------"); // 高级配置电脑的组装 builder = new SupperComputerBuilder(); director = new Director(builder); product = director.construct(); product.show(); // 可拓展其他配置的构建中实现 } }
更多参看:什么是建造者模式? - 哪吒编程的回答 - 知乎 https://www.zhihu.com/question/21857130/answer/2660815049
下面用typescript来演示
// 定义一个接口,用于指定产品的组成部分 interface IProduct { partA: string; partB: string; partC: string; } // 定义一个抽象的建造者类,用于定义创建产品的各个组成部分的方法 abstract class Builder { abstract buildPartA(): void; abstract buildPartB(): void; abstract buildPartC(): void; abstract getProduct(): IProduct; } // 定义一个具体的建造者类,用于实现创建产品的各个组成部分的方法 class ConcreteBuilder extends Builder { private product: IProduct; constructor() { super(); this.product = { partA: '', partB: '', partC: '' }; } buildPartA() { this.product.partA = 'Part A'; } buildPartB() { this.product.partB = 'Part B'; } buildPartC() { this.product.partC = 'Part C'; } getProduct() { return this.product; } } // 定义一个指挥者类,用于控制建造过程 class Director { private builder: Builder; constructor(builder: Builder) { this.builder = builder; } construct() { this.builder.buildPartA(); this.builder.buildPartB(); this.builder.buildPartC(); } } // 使用建造者模式创建产品 const builder = new ConcreteBuilder(); const director = new Director(builder); director.construct(); const product = builder.getProduct(); console.log(product);
根据上述实例可以看出,建造者(Builder)模式创建的是复杂对象,适用于面向产品的各个具体部分经常出现变化,但将它们组合在一起的算法却相对稳定的场景。
建造者模式应用场景
创建的对象较复杂,由多个部件构成,各部件面临着复杂的变化,但构件间的建造顺序是稳定的,产生的结果又不相同。
产品类非常复杂,或者产品类中不同的调用顺序产生不同的作用;
需要创建的各种形式的产品, 它们的制造过程相似且仅有细节上的差异;
使用生成器构造组合树或其他复杂对象,建造者模式可以分步、延时构造产品;
构造函数中有N个可选参数,那new各种实例比较麻烦,需要重载构造函数多次,而且很多参数都具有默认值。
前端开发中,建造者模式可能不像在后端或系统设计中那么常见,但仍然有一些场景和开源项目中可以找到它的应用。例如:
表单构建器: 一些前端库允许你通过一个流畅的API来构建复杂的表单,这可以看作是建造者模式的一种应用。
可视化查询构建器: 例如,用于构建数据库查询的前端工具,允许用户通过图形界面选择不同的参数和选项来构建查询。
WebPack配置构建器: WebPack是一个强大的模块打包工具,它的配置可以非常复杂。有些工具提供了一个API来帮助你构建这个配置,而不是直接编辑JSON文件。
组件库: 一些组件库提供了一种方法,允许开发者通过链式调用来构建和配置复杂的组件,这也是建造者模式的一种体现。
测试数据生成器: 在前端测试中,你可能需要生成复杂的测试数据。使用建造者模式可以帮助你创建具有特定属性和结构的数据对象。
建造者模式的优点与缺点
通过上面的介绍,大概就可以了解到建造者模式的优缺点了。
建造者模式的优点:
各个具体的建造者相互独立,可以很方便地替换具体建造者或增加新的具体建造者,有利于系统的扩展。
封装性好,构建和表示分离,客户端不必知道产品内部组成的细节,便于控制细节风险。
可以更加精细地控制产品的创建过程,建造者可以对创建过程逐步细化,而不对其他模块产生任何影响。
建造者模式的缺点:
需要多创建额外的Builder接口和实现类;
如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大;
如果产品内部发生变化,建造者也要同步修改,后期维护成本较大。
建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制;
建造者(Builder)模式在应用过程中可以根据需要改变,如果创建的产品种类只有一种,只需要一个具体建造者,这时可以省略掉抽象建造者,甚至可以省略掉指挥者角色。
抽象工厂模式VS建造者模式的区别
我们可以将固定的逻辑操作抽象出来(Builder),然后在不同形状的人偶实现不同的具体类(ConcreteBuilder),并为了保证依赖倒置原则,再加一个指挥生产的类,是建造过程对客户端封闭。可能这个时候就有人会问,那这不就像是工厂模式么,抽象不同的的操作,使用工厂类生产。但是,我们要注意到:
工厂模式其实就是封装了new的“调用动作(new XXX)”,使客户端不用按服务端的规定显式的指定new什么,而是通过更高抽象的create函数并导入客户端的规定来new出服务端的东西。
建造者模式则是封装了new的“实现定义(以组合的角度来确定其结构)”,使客户端不用去接触new里复杂的组合步骤。
抽象工厂模式和建造者模式都是创建型设计模式,主要关注对象的创建过程。
抽象工厂模式的主要目标是提供一个接口,用于创建相关或依赖对象族,而不指定具体的类。换句话说,抽象工厂模式生产的是完整的产品家族,而不是单独的产品。
假设你正在开发一个电子商务网站,你想要支持多种支付方式,如信用卡、PayPal和比特币。每种支付方式都有自己独特的流程和验证规则。这时就可以考虑使用抽象工厂模式。
建造者模式的主要目的是分离产品的组装过程和表示。也就是说,建造者模式更注重一步步地构建复杂对象的过程,而不是一次性完成整个对象的创建。
假设你正在开发一个网页编辑器,用户可以拖拽各种组件来自定义他们的网页布局。为了简化这一过程,你可以使用建造者模式。
参考文章:
设计模式 https://blog.csdn.net/m0_37691414/column/info/21939
JavaScript中常用的设计模式 https://segmentfault.com/a/1190000017787537?utm_source=tag-newest
深入理解JavaScript系列/设计模式--汤姆大叔的博客
设计模式--菜鸟教程
JavaScript 中常见设计模式整理
设计模式分类 https://www.cnblogs.com/tonglingliangyong/p/3738761.html
23种设计模式的趣味理解 https://www.cnblogs.com/hegx/p/6094604.html
对象心思过于多变,工厂模式满足不了?用建造者模式啊! https://cloud.tencent.com/developer/article/1913561
转载本站文章《再谈23种设计模式(1):开篇总结与创建者模型系列》,
请注明出处:https://www.zhoulujun.cn/html/theory/engineering/model/6191.html