---
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)