title |
---|
软件设计模式系列(第一期) |
作者:Tom哥
公众号:微观技术
博客:https://offercome.cn
人生理念:知道的越多,不知道的越多,努力去学
面对复杂的业务场景,千变万化的客户需求,如何以一变应万变,以最小的开发成本快速落地实现,同时保证系统有着较低的复杂度,能够保证系统后续de持续迭代能力,让系统拥有较高的可扩展性。
这些是一个合格的架构师必须修炼的基础内功,但是如何修炼这门神功???
不要着急,慢慢看下去。学了真本事,拿了阿里、头条的offer,女神还会远吗!❤️💖💘
接下来我们来系统性汇总下,软件架构设计需要知晓的设计模式,主要是提炼精髓、核心设计思路、代码示例、以及应用场景等。
CRUD很多人都会,不懂设计模式也可以开发软件,但是当开发及维护大型软件系统过程中就痛苦不堪,懂了人自然听得懂我在说什么,不懂的人说了你也不会懂。
我将常用的软件设计模式,做了汇总,目录如下:
考虑到内容篇幅较大,为了便于大家阅读,将软件设计模式系列(共23个)拆分成四篇文章,每篇文章讲解六个设计模式,采用不同的颜色区分,便于快速消化记忆
本文是首篇,主要讲解单例模式
、建造者模式
、抽象工厂
、工厂方法
、原型模式
、适配器模式
,共6个设计模式。
定义:
单例模式(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;
}
}
定义:
建造者模式,也称 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
注解实现类自身的建造者模式
。
定义:
抽象工厂模式围绕一个超级工厂创建其他工厂,又称为其他工厂的工厂。是一种创建型设计模式,它能创建一系列相关的对象,而无需指定其具体类。
抽象工厂模式的关键点:如何找到正确的抽象。
对于软件调用者来说,他们更关心软件提供了什么功能。至于内部如何实现的,他们并不关心。另外,考虑到安全问题,一般内部具体的实现细节通常会隐藏掉。
我们以电视、冰箱、洗衣机等家用电器生产为例,很多厂商像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,就能对数据库进行读写操作。
工厂方法模式与抽象工厂模式类似。工厂方法模式因为只围绕着一类接口来进行对象的创建与使用,使用场景更加单一,项目中更常见些。
定义:
定义一个创建对象的接口,让其子类自己决定实例化哪一个类,工厂模式使其创建过程延迟到子类进行。
核心点:封装对象创建的过程,提升创建对象方法的可复用性。
工厂方法模式包含三个关键角色:抽象产品、具体产品、工厂类。
定义一个抽象产品接口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
只需要通过工厂类来创建具体对象实例,然后就可以使用其功能。
工厂方法模式将对象的创建和使用过程分开,降低代码耦合性。
原型模式是创建型模式的一种,其特点在于通过“复制”一个已经存在的实例来返回新的实例,而不是新建实例。被复制的实例就是我们所称的“原型”,这个原型是可定制的。
定义:
使用原型实例指定创建对象的种类,然后通过拷贝这些原型来创建新的对象。
代码示例:
/**
* @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
可以方便的完成对象属性的拷贝,其实也是浅复制
,只能对基本类型数据
、对象引用
拷贝。使用时特别要注意,如果全局变量有对象类型,原型对象和克隆的对象会二次修改,要特殊处理,采用深复制,否则会引发安全问题。
我们都知道美国的电压是110V,而中国是220V,如果你去要美国旅行时,一定要记得带电源适配器,将不同国家使用的电源电流标准转化为适合我们自己电器的标准,否则很容易烧坏电子设备。
定义:
将类的接口转换为客户期望的另一个接口,适配器可以让不兼容的两个类一起协同工作。核心点在于转换!
核心思路:
在原有的接口或类的外层封装一个新的适配器层,以实现扩展对象结构的效果,并且这种扩展可以无限扩展下去。
- Adaptee:源接口,需要适配的接口
- Target:目标接口,暴露出去的接口
- Adapter:适配器,将源接口适配成目标接口
适用场景:
- 原有接口无法修改时,又必须快速兼容部分新功能
- 需要依赖外部系统时,一般会单独封装
防腐层
,降低外部系统的突发风险带来的影响 - 适配不同数据格式,不同接口协议转换
- 旧接口过渡升级
案例:
比如查物流信息,由于物流公司的系统都是各自独立,在编程语言和交互方式上有很大差异,需要针对不同的物流公司做单独适配,同时结合不同公司的系统性能,配置不同的响应超时时间
适配器模式号称为“最好用打补丁模式”,就是因为只要是一个接口,都可以用它来进行适配。
设计模式很多人都学习过,但项目实战时总是晕晕乎乎,原因在于没有了解其核心是什么,底层逻辑是什么,《设计模式:可复用面向对象的基础》有讲过,
在设计中思考什么应该变化,并封装会发生变化的概念。
软件架构的精髓:找到变化,封装变化。
业务千变万化,没有固定的编码答案,千万不要硬套设计模式。无论选择哪一种设计模式,尽量要能满足SOLID
原则,自我review是否满足业务的持续扩展性。有句话说的好,“不论白猫黑猫,能抓老鼠就是好猫。”