8.架构设计系列:常用设计模式的实践
架构设计系列文章
- 架构设计系列:什么是架构设计
- 架构设计系列:几个常用的架构设计原则
- 架构设计系列:高并发系统的设计目标
- 架构设计系列:如何设计可扩展架构
- 架构设计系列:如何设计高性能架构
- 架构设计系列:如何设计高可用架构
- 架构设计系列:如何应对软件变化
一、什么是设计模式
作为一名开发人员,在软件开发过程中,我们通常会面临一些不断重复发生的问题,而设计模式就是这些问题的通用解决方案,这些解决方案是众多开发人员经过相当长的一段时间的试验和错误所总结出来的。
那么,设计模式到底是什么呢?
设计模式是一套经过验证、被反复使用的,用于解决特定场景、重复出现地特定问题的解决方案。
简单来说,设计模式是在软件开发过程中,用于解决特定问题的解决方案。
二、为什么要使用设计模式
在介绍为什么使用设计模式之前,我们先来看看在软件开发中,设计模式的两个主要用途。
- 代码设计的最佳实践
设计模式已经经历了很长一段时间的发展,它们是软件开发过程中面临的一般问题的最佳解决方案。学习这些模式有助于经验不足的开发人员,通过一种简单快捷的方式来写出更优雅的代码。
- 开发人员的标准术语
设计模式提供了一个标准的术语系统,且具体到特定的情景。例如,单例设计模式意味着使用单个对象,这样所有熟悉单例设计模式的开发人员都能使用单个对象,并且可以通过这种方式告诉对方,程序使用的是单例模式。
基于上面的两个主要用途,我们来提炼下为什么要使用设计模式的原因。
- 提高代码的可重用性
- 提高代码的可维护性
- 提高代码的可扩展性
- 保证代码的可靠性
三、如何正确使用设计模式
诚然设计模式是一系列通用的技术性解决方案,然而我们需要明白,技术是为了业务服务的。
在实际开发过程中,我们不要为了设计模式而设计,这只会对开发有害。
因此,设计模式的正确打开方式是,以满足业务需要即可。
对于如何才能用好设计模式,我觉得需要遵守以下几个原则:
- 需求驱动
开发的最终目的是需求的实现,因此设计模式需要为需求服务。
- 避免过度设计
引入设计模式是为了简化问题,当我们觉得问题变得更加复杂的时候,说明简单的问题已经被复杂化,需要评估问题本身是否需要设计模式。
- 在编程中领悟模式
软件开发是一项实践性工作,没有不会下棋的围棋大师,也没有不会编程的架构师。掌握设计模式需要理论积累以及实践积累,在实践过程中进行“渐悟”。
切记,设计模式要活学活用,不要生搬硬套。
四、设计模式的具体应用
下面我们一起来看看几个常用设计模式的具体应用,感兴趣的小伙伴,可以点击下面的链接找到对应的代码实现。
模板方法模式(Template Pattern)
- 核心思想:定义一个行为的骨架,将一些具体步骤推迟到子类中实现。
- 说明
- 什么是模板方法?行为的结构和顺序,被称为“模板方法” 。
- 模板方法模式,可以在不改变行为结构的情况下,重定义该行为的某些步骤。
- 优点:
- 1、封装不变部分,扩展可变部分。
- 2、提取公共代码,便于维护。
- 3、行为由父类控制,子类实现。
- 缺点:
- 每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。
- 适用场景
- 当一个业务有多种行为,并且不同行为的处理步骤基本相同,这些步骤既有共性的地方,也有差异性的地方时,此时可以用「模板方法模式」治之。
- 典型案例
- 案例1:下单流程、退款流程
- 案例2:l2cache 中 CacheService接口 的实现
public interface CacheService<K, R> {
default Cache getNativeL2cache() {
throw new L2CacheException("未实现方法CacheService.getNativeL2cache(),请检查代码");
}
// ---------------------------------------------------
// 第一部分:业务逻辑,业务开发时,仅仅只需实现如下几个方法
// 变化的部分,不同业务实现逻辑不同
// ---------------------------------------------------
String getCacheName();
String buildCacheKey(K key);
R queryData(K key);
Map<K, R> queryDataList(List<K> keyList);
// ---------------------------------------------------
// 第二部分:缓存操作,将缓存操作封装为默认方法实现,简化业务开发
// 不变的部分,下面的方法都是模板方法
// ---------------------------------------------------
default R get(K key) {
return (R) this.getNativeL2cache().getIfPresent(this.buildCacheKey(key));
}
// 省略其他模板方法 ... ...
default Map<K, R> batchGetOrLoad(List<K> keyList) {
return this.getNativeL2cache().batchGetOrLoad(keyList, k -> this.buildCacheKey(k), notHitCacheKeyList -> this.queryDataList(notHitCacheKeyList));
}
default Map<K, R> batchReload(List<K> keyList) {
Map<K, R> value = this.queryDataList(keyList);
this.getNativeL2cache().batchPut(value, k -> this.buildCacheKey(k));
return value;
}
}
策略模式(Strategy Pattern)
- 核心思想:对一类行为进行封装,使得它们可以互相替换。
- 说明
- 什么是策略?策略就是某种行为的具体实现。
- 策略模式,可以在不修改使用方的情况下,独立灵活的修改或新增策略。
- 优点:
- 1、策略可以自由切换。
- 2、避免使用多重条件判断。
- 3、扩展性良好。
- 缺点:
- 1、策略类会增多。
- 2、所有策略类都需要对外暴露。
- 适用场景
- 当一个业务有多种行为,并且可以动态从多个行为中进行选择,不同行为之间可以相互替换,此时可以用「策略模式」治之。
- 典型案例
- 案例1:支付,可以用微信支付,也可用支付宝支付,还可以是其他第三方支付等等
- 案例2:l2cache 中 Cache模块 的多种策略实现
- 【策略接口】缓存策略接口:com.github.jesse.l2cache.Cache
- 【策略实现】混合缓存:CompositeCache.java
- 【策略实现】一级缓存(本地缓存):CaffeineCache.java
- 【策略实现】二级缓存(分布式缓存):RedissonRBucketCache.java
单例模式(Singleton Pattern)
- 核心思想:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
- 说明
- 什么是单例?单例就是指系统全局,一个类只有一个实例。
- 使用单例模式可能遇到哪些问题?主要有三个问题:线程安全、反射攻击、序列化攻击,这也是我们在使用单例模式时需要特别注意的地方。
- 单例模式有几种实现方式?共有7种实现方式,Demo案例见:singleton-pattern 。
- 优点:
- 避免频繁的创建和销毁实例,节省系统资源。
- 缺点:
- 与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎样来实例化。
- 适用场景
- 当一个全局使用的类被频繁创建和销毁,并且创建对象需要消耗很多资源时,此时可以用「单例模式」治之。
- 典型案例
- 案例1:l2cache 中基于容器的单例模式:com.github.jesse.l2cache.content.CacheSupport
- 案例2:l2cache 中基于双重检查锁的单例模式:com.github.jesse.l2cache.schedule.NullValueClearSupport
构建者模式(Builder Pattern)
- 核心思想:将一个复杂对象的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
- 说明
- 建造者模式中 Builder 类使用多个简单的对象一步一步构建成一个复杂的对象。
- 优点:
- 分离构建过程和表示,使得构建过程更加灵活,可以构建不同的表示。
- 可以更好地控制构建过程,隐藏具体构建细节。
- 代码复用性高,可以在不同的构建过程中重复使用相同的建造者。
- 缺点:
- 如果产品的属性较少,建造者模式可能会导致代码冗余。
- 建造者模式增加了系统的类和对象数量。
- 适用场景
- 当一个复杂对象的创建工作,由于需求的变化,导致这个复杂对象的各个部分经常面临剧烈的变化,此时可以用「构建者模式」治之。
- 典型案例
- 案例:l2cache 中构建不同类型的缓存实例: com.github.jesse.l2cache.CacheBuilder
外观模式(Facade Pattern)
- 核心思想:为系统提供一个统一的入口,来隐藏系统的复杂性,简化客户端的使用。
- 说明
- 外观模式,在客户端和系统之间增加一层,这一层将调用顺序、依赖关系等处理好,以此来降低访问系统的复杂度。
- 优点:
- 1、减少系统相互依赖。
- 2、提高灵活性。
- 3、提高了安全性。
- 缺点:
- 不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
- 适用场景
- 当一个系统内部的关系复杂,且客户端不希望知道系统内部的复杂关系,此时可以用「外观模式」治之。
- 典型案例
- 案例:l2cache 中热key探测门面:com.github.jesse.l2cache.hotkey.HotKeyFacade
装饰器模式(Decorator Pattern)
- 核心思想:动态地给一个对象添加一些额外的职责。
- 说明
- 装饰器模式,也称为包装模式(Wrapper Pattern)。
- 装饰器模式,允许向一个现有的对象添加新的功能,同时又不改变其结构。
- 装饰器模式,可以替代继承。
- 优点:
- 装饰类和被装饰类可以独立发展,不会相互耦合。
- 装饰模式可以动态扩展一个实现类的功能。
- 缺点:
- 多层装饰比较复杂。
- 适用场景
- 当我们需要扩展一个类的功能,且希望可以动态增加和撤销功能时,此时可以用「装饰器模式」治之。
- 典型案例
- 案例:l2cache 中加载数据时的多层包装:
SPI机制
- 核心思想:定义一种服务接口,允许外部实现,并动态加载,从而提升系统的可扩展性
- 说明
- SPI,全称为 Service Provider Interface,是一种服务发现机制,也是一种用于实现组件之间解耦的设计模式,它允许在程序运行时通过配置文件等方式动态加载和替换实现。
- SPI 本质上是,将接口实现类的全限定名,配置在配置文件中,并由服务加载器读取配置文件,实现动态加载和切换实现类。
- SPI实际上是,“基于接口的编程+策略模式+配置文件” 组合实现的动态加载机制。
- 优点
- 1、灵活性高,可以简单地添加实现类来扩展功能
- 2、耦合度低,将应用程序和具体实现解耦
- 缺点
- 1、开发工作量,需手动在META-INF/services目录下创建一个以接口全限定名为命名的文件
- 2、安全风险,由于实现类可由外部提供,可能存在恶意实现类的风险
- 适用场景
- 「SPI机制」常用于插件化系统、框架扩展、模块化设计中。
- 典型案例
- 案例:JDK的SPI、Dubbo的SPI、Spring的SPI、SLF4J的SPI等。
- 案例:l2cache 中基于 SPI机制 实现的多种热key探测策略,具体源码见github仓库:l2cache
// 简略的代码
/**
* 扩展点接口标记
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {
/**
* 默认扩展名
*/
String value() default "";
}
/**
* 【接口】热key探测
*/
@SPI("sentinel")
public interface HotkeyService extends Serializable {
void init(CacheConfig.Hotkey hotkey, List<String> cacheNameList);
boolean isHotkey(String cacheName, String key);
}
/**
* 【实现策略】京东热key探测
*/
@Slf4j
public class JdHotkeyService implements HotkeyService {
// 省略其他方法...
@Override
public boolean isHotkey(String cacheName, String key) {
StringBuilder sb = new StringBuilder(cacheName).append(CacheConsts.SPLIT).append(key);
return JdHotKeyStore.isHotKey(sb.toString());
}
}
/**
* 【实现策略】sentinel热key探测
*/
@Slf4j
public class SentinelHotkeyService implements HotkeyService {
// 省略其他方法...
@Override
public boolean isHotkey(String cacheName, String key) {
Entry entry = null;
try {
entry = SphU.entry(cacheName, EntryType.IN, 1, key);
return false;// 返回 false 表示不是热key
} catch (BlockException ex) {
if (log.isDebugEnabled()) {
log.debug("sentinel 识别到热key, resource={}, key={}, rule={}", cacheName, key, ex.getRule().toString());
}
return true;// 返回 true 表示热key
} finally {
if (entry != null) {
entry.exit(1, key);
}
}
}
}
/**
* 【】基于SPI机制的热key策略加载
*/
@Slf4j
@Configuration
@ConditionalOnProperty(name = "l2cache.config.hotkey.type")
public class HotKeyConfiguration {
@Autowired
L2CacheProperties l2CacheProperties;
@Autowired
ApplicationContext context;
@PostConstruct
public void init() {
CacheConfig.Hotkey hotKey = l2CacheProperties.getConfig().getHotKey();
if (ObjectUtil.isEmpty(hotKey.getType())) {
log.error("未配置 hotkey type,不进行初始化");
return;
}
HotkeyService hotkeyService = ServiceLoader.load(HotkeyService.class, hotKey.getType());
if (ObjectUtil.isNull(hotkeyService)) {
log.error("非法的 hotkey type,无匹配的HotkeyService实现类, hotkey type={}", hotKey.getType());
return;
}
hotkeyService.init(hotKey, getAllCacheName());
log.info("Hotkey实例初始化成功, hotkey type={}", hotKey.getType());
}
// 省略其他方法...
}
五、最后
设计模式使代码编写真正工程化,它是软件工程的基石,如同大厦的一块块砖石一样。
每种设计模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,因此在项目中合理地运用设计模式可以完美地解决很多问题,这也是设计模式能被广泛应用的原因。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!