--- title: 软件设计模式系列(第一期) --- # 软件设计模式系列(第一期) > 作者:Tom哥 >
公众号:微观技术 >
博客:[https://offercome.cn](https://offercome.cn) >
人生理念:知道的越多,不知道的越多,努力去学 面对复杂的业务场景,千变万化的客户需求,如何以一变应万变,以最小的开发成本快速落地实现,同时保证系统有着较低的复杂度,能够保证系统后续de持续迭代能力,让系统拥有较高的可扩展性。 `这些是一个合格的架构师必须修炼的基础内功,但是如何修炼这门神功???`
不要着急,慢慢看下去。学了真本事,拿了阿里、头条的offer,女神还会远吗!❤️💖💘 接下来我们来系统性汇总下,软件架构设计需要知晓的设计模式,主要是提炼精髓、核心设计思路、代码示例、以及应用场景等。 > CRUD很多人都会,不懂设计模式也可以开发软件,但是当开发及维护大型软件系统过程中就痛苦不堪,懂了人自然听得懂我在说什么,不懂的人说了你也不会懂。 我将常用的软件设计模式,做了汇总,目录如下:
**考虑到内容篇幅较大,为了便于大家阅读,将软件设计模式系列(共23个)拆分成四篇文章,每篇文章讲解六个设计模式,采用不同的颜色区分,便于快速消化记忆** 本文是首篇,主要讲解`单例模式`、`建造者模式`、`抽象工厂`、`工厂方法`、`原型模式`、`适配器模式` ,共6个设计模式。 ## 1、单例模式 **定义:** > 单例模式(Singleton)允许存在一个和仅存在一个给定类的实例。它提供一种机制让任何实体都可以访问该实例。
**核心思路:** 1️⃣ 保证一个类只有一个实例。如果该对象已经被创建, 则返回已有的对象。为什么要这样设计呢?因为某些业务场景要控制共享资源 (例如数据库或文件) 的访问权限。 2️⃣ 为该实例提供一个全局访问入口, 提供一个`static`访问方法。 **代码示例:** ``` /** * @author 微信公众号:微观技术 */ public class Singleton { private static Singleton instance = new Singleton(); // 让构造函数为 private,这样该类就不会被实例化 private Singleton() {} // 获取唯一可用的对象 public static Singleton getInstance() { return instance; } } ``` 在类中添加一个私有静态成员变量用于保存单例实例,声明一个公有静态构建方法用于获取单例实例。 **注意事项:** 多个业务场景,多个线程访问同一个类实例的全局变量,频发的写操作,可能会引发线程安全问题。另外,为了防止其他对象使用单例类的 `new` 运算符,编码时需要将默认构造函数设为私有。 如果想要采用`延迟初始化对象`,多线程并发初始化时,可能会有并发安全问题。假如:线程A,线程B都阻塞在了获取锁的步骤上,其中线程A获得锁---实例化了对象----释放锁;之后线程B---获得锁---实例化对象,此时违反了我们单例模式的初衷。 **如何解决?** 采用`双重判空检查`。首先保证了安全,且在多线程情况下能保持高性能,第一个if判断避免了其他无用线程竞争锁造成性能浪费,第二个if判断能拦截除第一个获得对象锁线程以外的线程。 ``` /** * @author 微信公众号:微观技术 */ public class SingleonLock { private static SingleonLock doubleLock; private SingleonLock() {} // 双重校验锁 public static SingleonLock getInstance() { if (doubleLock == null) { synchronized (SingleonLock.class) { if (doubleLock == null) { doubleLock = new SingleonLock(); } } } return doubleLock; } } ``` ## 2、建造者模式 **定义:** 建造者模式,也称 `Builder` 模式。 > 将复杂对象的构造与其表示分离,以便同一构造过程可以创建不同的表示。 简单来说,建造者模式就是如何一步步构建一个包含多个组成部件的对象,相同的构建过程可以创建不同的产品 **核心思路:**
|角色|类别|说明 |---|---|--- |Builder|接口或抽象类|抽象的建造者,**不是必须的** |ConcreteBuilder|具体的建造者|可以有多个「因为每个建造风格可能不一样」,**必须要有** |Product|普通类|最终构建的对象,**必须要有** |Director|指挥者|统一指挥建造者去建造目标,**不是必须的** **代码示例:** ``` /** * @author 微信公众号:微观技术 */ public class Person { private String name; private int age; private String address; public static PersonBuilder builder() { return new PersonBuilder(); } private Person(PersonBuilder builder) { this.name = builder.name; this.age = builder.age; this.address = builder.address; } // 建造者 static class PersonBuilder { private String name; private int age; private String address; public PersonBuilder() { } public PersonBuilder name(String name) { this.name = name; return this; } public PersonBuilder age(int age) { this.age = age; return this; } public PersonBuilder address(String address) { this.address = address; return this; } public Person build() { return new Person(this); } } } ``` * `Person` 中创建一个静态内部类 `PersonBuilder`,然后将 `Person` 中的参数都复制到 `PersonBuilder`类中。 * `Person`中创建一个private的构造函数,入参为 `PersonBuilder`类型 * `PersonBuilder`中创建一个public的构造函数 * `PersonBuilder`中创建设置函数,对`Person` 中那些可选参数进行赋值,返回值为`PersonBuilder`类型的实例 * `PersonBuilder`中创建一个build()方法,在其中构建`Person` 的实例并返回 ``` /** * @author 微信公众号:微观技术 */ public class PersonBuilderTest { public static void main(String[] args) { Person person = Person.builder() .name("Tom哥") .age(18) .address("杭州") .build(); System.out.println(JSON.toJSONString(person)); } } ``` 客户端使用链式调用,一步一步的把对象构建出来。 **适用场景:** * 分阶段、分步骤的方法更适合多次运算结果类创建场景。比如创建一个类实例的参数并不会一次准备好,有些参数可能需要调用多个服务运算后才能拿得到,这时,我们可以根据已知参数,预先对类进行创建,待后续的参数准备好了后,再设置。 * 不关心特定类型的建造者的具体算法实现。比如,我们并不关心`StringBuilder`的具体代码实现,只关心它提供了字符串拼接功能。 使用建造者模式能更方便地帮助我们按需进行对象的实例化,避免写很多不同参数的构造函数,同时还能解决同一类型参数只能写一个构造函数的弊端。 最后,实际项目中,为了简化编码,通常可以直接使用`lombok`的 `@Builder` 注解实现`类自身的建造者模式`。 ## 3、抽象工厂模式 **定义:** > 抽象工厂模式围绕一个超级工厂创建其他工厂,又称为其他工厂的工厂。是一种创建型设计模式,它能创建一系列相关的对象,而无需指定其具体类。 **抽象工厂模式的关键点:如何找到正确的抽象。** 对于软件调用者来说,他们更关心软件提供了什么功能。至于内部如何实现的,他们并不关心。另外,考虑到安全问题,一般内部具体的实现细节通常会隐藏掉。 我们以电视、冰箱、洗衣机等家用电器生产为例,很多厂商像`Haier`、`Sony`、`小米`、`Hisense`等能生产上述电器,不过在外观、性能、功率、智能化、特色功能等方面会有差异。面对这样的需求,我们如何借助`抽象工厂模式`来实现编码。
抽象工厂模式体现为定义一个抽象工厂类,多个不同的具体工厂继承这个抽象工厂类后,再各自实现相同的抽象功能,从而实现代码上的`多态性`。 **代码示例:** ``` /** * @author 微信公众号:微观技术 */ public abstract class AbstractFactory { // 生产电视 abstract Object createTV(); // 生产洗衣机 abstract Object createWasher(); // 生产冰箱 abstract Object createRefrigerator(); } public class HaierFactory extends AbstractFactory { @Override Object createTV() { return null; } @Override Object createWasher() { return null; } @Override Object createRefrigerator() { return null; } } public class XiaomiFactory extends AbstractFactory { @Override Object createTV() { return null; } @Override Object createWasher() { return null; } @Override Object createRefrigerator() { return null; } } ``` `AbstractFactory `是抽象工厂类,能够创建电视、洗衣机、冰箱抽象产品;而`HaierFactory`和`XiaomiFactory` 是具体的工厂,负责生产具体的产品。当我们要生产具体的产品时,只需要告诉`AbstractFactory`即可。 **解决问题:** * 对于不同产品系列有比较多共性特征时,可以使用抽象工厂模式,有助于提升组件的复用性。 * 当需要提升代码的扩展性并降低维护成本时,把对象的创建和使用过程分开,能有效地将代码统一到一个级别上。 **适用场景:** * 解决跨平台兼容性的问题。当一个应用程序需要支持Windows、Mac、Linux等多套操作系统。 * 电商的商品、订单、物流系统,需要根据区域政策、用户的购买习惯,差异化处理 * 不同的数据库产品,JDBC 就是对于数据库增删改查建立的抽象工厂类,无论使用什么类型的数据库,只要具体的数据库组件能够支持 JDBC,就能对数据库进行读写操作。 ## 4、工厂方法模式 工厂方法模式与抽象工厂模式类似。工厂方法模式因为只围绕着一类接口来进行对象的创建与使用,使用场景更加单一,项目中更常见些。 **定义:** > 定义一个创建对象的接口,让其子类自己决定实例化哪一个类,工厂模式使其创建过程延迟到子类进行。 **核心点:封装对象创建的过程,提升创建对象方法的可复用性。**
工厂方法模式包含三个关键角色:抽象产品、具体产品、工厂类。 定义一个抽象产品接口`ITV`,`HaierTV`和`XiaomiTV`是具体产品类,`TVFactory`是工厂类,负责生产具体的对象实例。 **代码示例:** ``` /** * @author 微信公众号:微观技术 */ public interface ITV { // 描述 Object desc(); } public class HaierTV implements ITV { @Override public Object desc() { return "海尔电视"; } } public class XiaomiTV implements ITV { @Override public Object desc() { return "小米电视"; } } public class TVFactory { public static ITV getTV(String name) { switch (name) { case "haier": return new HaierTV(); case "xiaomi": return new XiaomiTV(); default: return null; } } } public class Client { public static void main(String[] args) { ITV tv = TVFactory.getTV("xiaomi"); Object result = tv.desc(); System.out.println(result); } } ``` 工厂方法模式是围绕着特定的抽象产品(接口)来封装对象的创建过程,`Client`只需要通过工厂类来创建具体对象实例,然后就可以使用其功能。 工厂方法模式将对象的创建和使用过程分开,降低代码耦合性。 ## 5、原型模式 原型模式是创建型模式的一种,其特点在于通过“复制”一个已经存在的实例来返回新的实例,而不是新建实例。被复制的实例就是我们所称的“原型”,这个原型是可定制的。 **定义:** > 使用原型实例指定创建对象的种类,然后通过拷贝这些原型来创建新的对象。
**代码示例:** ``` /** * @author 微信公众号:微观技术 */ public interface Prototype extends Cloneable { public Prototype clone() throws CloneNotSupportedException; } public class APrototype implements Prototype { @Override public Prototype clone() throws CloneNotSupportedException { System.out.println("开始克隆《微观技术》对象"); return (APrototype) super.clone(); } } public class Client { @SneakyThrows public static void main(String[] args) { Prototype a = new APrototype(); Prototype b = a.clone(); System.out.println("a的对象引用:" + a); System.out.println("b的对象引用:" + b); } } ``` 执行结果: ``` 开始克隆《微观技术》对象 a的对象引用:course.p14.p5.APrototype@7cc355be b的对象引用:course.p14.p5.APrototype@6e8cf4c6 ``` 打印出两个对象的地址,发现不相同,在内存中为两个对象。 > Cloneable 接口本身是空方法,调用的 clone() 方法其实是 Object.clone() 方法 **优点:** * 性能优良。不用重新初始化对象,而是动态地获取对象运行时的状态。 * 可以摆脱构造函数的约束。 **特别注意:** `clone()`是`浅复制`,也就是基本类型数据,会给你重新复制一份新的。但是引用类型(对象中包含对象),他就不会重新复制份新的。引用类型如:bean实例引用、集合等一些引用类型。 **如何解决?** 你需要在执行完`super.clone()` 获得浅复制对象后,再手动对其中的全局变量重新构造对象并赋值。当然,经过这个过程,得到的对象我们称之为`深复制`。 **适用场景:** * 反序列化,比如 fastjson的JSON.parseObject() ,将字符串转变为对象 * 每次创建新对象资源损耗较大 * 对象中的属性非常多,通过get和set方法创建对象,复制黏贴非常痛苦 **加餐:** Spring 框架中提供了一个工具类,`BeanUtils.copyProperties` 可以方便的完成对象属性的拷贝,其实也是`浅复制`,只能对`基本类型数据`、`对象引用`拷贝。使用时特别要注意,如果全局变量有对象类型,原型对象和克隆的对象会二次修改,要特殊处理,采用深复制,否则会引发安全问题。 ## 6、适配器模式 我们都知道美国的电压是110V,而中国是220V,如果你去要美国旅行时,一定要记得带电源适配器,将不同国家使用的电源电流标准转化为适合我们自己电器的标准,否则很容易烧坏电子设备。 **定义:** > 将类的接口转换为客户期望的另一个接口,适配器可以让不兼容的两个类一起协同工作。核心点在于转换! **核心思路:** 在原有的接口或类的外层封装一个新的适配器层,以实现扩展对象结构的效果,并且这种扩展可以无限扩展下去。
* Adaptee:源接口,需要适配的接口 * Target:目标接口,暴露出去的接口 * Adapter:适配器,将源接口适配成目标接口 **适用场景:** * 原有接口无法修改时,又必须快速兼容部分新功能 * 需要依赖外部系统时,一般会单独封装`防腐层`,降低外部系统的突发风险带来的影响 * 适配不同数据格式,不同接口协议转换 * 旧接口过渡升级 **案例:** 比如查物流信息,由于物流公司的系统都是各自独立,在编程语言和交互方式上有很大差异,需要针对不同的物流公司做单独适配,同时结合不同公司的系统性能,配置不同的响应超时时间
适配器模式号称为“最好用打补丁模式”,就是因为只要是一个接口,都可以用它来进行适配。 ## 写在最后 设计模式很多人都学习过,但项目实战时总是晕晕乎乎,原因在于没有了解其核心是什么,底层逻辑是什么,《设计模式:可复用面向对象的基础》有讲过, > 在设计中思考什么应该变化,并封装会发生变化的概念。 **软件架构的精髓:找到变化,封装变化。** 业务千变万化,没有固定的编码答案,千万不要硬套设计模式。无论选择哪一种设计模式,尽量要能满足`SOLID`原则,自我review是否满足业务的持续扩展性。有句话说的好,“不论白猫黑猫,能抓老鼠就是好猫。” # 参考资料 * [17 | 单例模式:如何有效进行程序初始化?](https://kaiwu.lagou.com/course/courseInfo.htm?courseId=710&sid=20-h5Url-0&buyFrom=2&pageId=1pz4#/detail/pc?id=6882) * [廖雪峰的官方网站](https://www.liaoxuefeng.com/wiki/1252599548343744/1281319134822433)