# 软件设计模式系列(第一期)

作者:Tom哥
公众号:微观技术
博客:https://offercome.cn (opens new window)
人生理念:知道的越多,不知道的越多,努力去学

面对复杂的业务场景,千变万化的客户需求,如何以一变应万变,以最小的开发成本快速落地实现,同时保证系统有着较低的复杂度,能够保证系统后续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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在类中添加一个私有静态成员变量用于保存单例实例,声明一个公有静态构建方法用于获取单例实例。

注意事项:

多个业务场景,多个线程访问同一个类实例的全局变量,频发的写操作,可能会引发线程安全问题。另外,为了防止其他对象使用单例类的 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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 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);
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
  • 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));
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

客户端使用链式调用,一步一步的把对象构建出来。

适用场景:

  • 分阶段、分步骤的方法更适合多次运算结果类创建场景。比如创建一个类实例的参数并不会一次准备好,有些参数可能需要调用多个服务运算后才能拿得到,这时,我们可以根据已知参数,预先对类进行创建,待后续的参数准备好了后,再设置。
  • 不关心特定类型的建造者的具体算法实现。比如,我们并不关心StringBuilder的具体代码实现,只关心它提供了字符串拼接功能。

使用建造者模式能更方便地帮助我们按需进行对象的实例化,避免写很多不同参数的构造函数,同时还能解决同一类型参数只能写一个构造函数的弊端。

最后,实际项目中,为了简化编码,通常可以直接使用lombok@Builder 注解实现类自身的建造者模式

# 3、抽象工厂模式

定义:

抽象工厂模式围绕一个超级工厂创建其他工厂,又称为其他工厂的工厂。是一种创建型设计模式,它能创建一系列相关的对象,而无需指定其具体类。

抽象工厂模式的关键点:如何找到正确的抽象。

对于软件调用者来说,他们更关心软件提供了什么功能。至于内部如何实现的,他们并不关心。另外,考虑到安全问题,一般内部具体的实现细节通常会隐藏掉。

我们以电视、冰箱、洗衣机等家用电器生产为例,很多厂商像HaierSony小米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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

AbstractFactory是抽象工厂类,能够创建电视、洗衣机、冰箱抽象产品;而HaierFactoryXiaomiFactory 是具体的工厂,负责生产具体的产品。当我们要生产具体的产品时,只需要告诉AbstractFactory即可。

解决问题:

  • 对于不同产品系列有比较多共性特征时,可以使用抽象工厂模式,有助于提升组件的复用性。
  • 当需要提升代码的扩展性并降低维护成本时,把对象的创建和使用过程分开,能有效地将代码统一到一个级别上。

适用场景:

  • 解决跨平台兼容性的问题。当一个应用程序需要支持Windows、Mac、Linux等多套操作系统。
  • 电商的商品、订单、物流系统,需要根据区域政策、用户的购买习惯,差异化处理
  • 不同的数据库产品,JDBC 就是对于数据库增删改查建立的抽象工厂类,无论使用什么类型的数据库,只要具体的数据库组件能够支持 JDBC,就能对数据库进行读写操作。

# 4、工厂方法模式

工厂方法模式与抽象工厂模式类似。工厂方法模式因为只围绕着一类接口来进行对象的创建与使用,使用场景更加单一,项目中更常见些。

定义:

定义一个创建对象的接口,让其子类自己决定实例化哪一个类,工厂模式使其创建过程延迟到子类进行。

核心点:封装对象创建的过程,提升创建对象方法的可复用性。

工厂方法模式包含三个关键角色:抽象产品、具体产品、工厂类。

定义一个抽象产品接口ITVHaierTVXiaomiTV是具体产品类,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);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

工厂方法模式是围绕着特定的抽象产品(接口)来封装对象的创建过程,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);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

执行结果:

开始克隆《微观技术》对象
a的对象引用:course.p14.p5.APrototype@7cc355be
b的对象引用:course.p14.p5.APrototype@6e8cf4c6
1
2
3

打印出两个对象的地址,发现不相同,在内存中为两个对象。

Cloneable 接口本身是空方法,调用的 clone() 方法其实是 Object.clone() 方法

优点:

  • 性能优良。不用重新初始化对象,而是动态地获取对象运行时的状态。
  • 可以摆脱构造函数的约束。

特别注意:

clone()浅复制,也就是基本类型数据,会给你重新复制一份新的。但是引用类型(对象中包含对象),他就不会重新复制份新的。引用类型如:bean实例引用、集合等一些引用类型。

如何解决?

你需要在执行完super.clone() 获得浅复制对象后,再手动对其中的全局变量重新构造对象并赋值。当然,经过这个过程,得到的对象我们称之为深复制

适用场景:

  • 反序列化,比如 fastjson的JSON.parseObject() ,将字符串转变为对象
  • 每次创建新对象资源损耗较大
  • 对象中的属性非常多,通过get和set方法创建对象,复制黏贴非常痛苦

加餐:

Spring 框架中提供了一个工具类,BeanUtils.copyProperties 可以方便的完成对象属性的拷贝,其实也是浅复制,只能对基本类型数据对象引用拷贝。使用时特别要注意,如果全局变量有对象类型,原型对象和克隆的对象会二次修改,要特殊处理,采用深复制,否则会引发安全问题。

# 6、适配器模式

我们都知道美国的电压是110V,而中国是220V,如果你去要美国旅行时,一定要记得带电源适配器,将不同国家使用的电源电流标准转化为适合我们自己电器的标准,否则很容易烧坏电子设备。

定义:

将类的接口转换为客户期望的另一个接口,适配器可以让不兼容的两个类一起协同工作。核心点在于转换!

核心思路:

在原有的接口或类的外层封装一个新的适配器层,以实现扩展对象结构的效果,并且这种扩展可以无限扩展下去。

  • Adaptee:源接口,需要适配的接口
  • Target:目标接口,暴露出去的接口
  • Adapter:适配器,将源接口适配成目标接口

适用场景:

  • 原有接口无法修改时,又必须快速兼容部分新功能
  • 需要依赖外部系统时,一般会单独封装防腐层,降低外部系统的突发风险带来的影响
  • 适配不同数据格式,不同接口协议转换
  • 旧接口过渡升级

案例:

比如查物流信息,由于物流公司的系统都是各自独立,在编程语言和交互方式上有很大差异,需要针对不同的物流公司做单独适配,同时结合不同公司的系统性能,配置不同的响应超时时间

适配器模式号称为“最好用打补丁模式”,就是因为只要是一个接口,都可以用它来进行适配。

# 写在最后

设计模式很多人都学习过,但项目实战时总是晕晕乎乎,原因在于没有了解其核心是什么,底层逻辑是什么,《设计模式:可复用面向对象的基础》有讲过,

在设计中思考什么应该变化,并封装会发生变化的概念。

软件架构的精髓:找到变化,封装变化。

业务千变万化,没有固定的编码答案,千万不要硬套设计模式。无论选择哪一种设计模式,尽量要能满足SOLID原则,自我review是否满足业务的持续扩展性。有句话说的好,“不论白猫黑猫,能抓老鼠就是好猫。”

# 参考资料

上次更新: 2023/3/9