https://time.geekbang.org/column/intro/100039001

设计模式学习导读 (3讲)
01 | 为什么说每个程序员都要尽早地学习并掌握设计模式相关知识?
02 | 从哪些维度评判代码质量的好坏?如何具备写出高质量代码的能力?
03 | 面向对象、设计原则、设计模式、编程规范、重构,这五者有何关系?
设计原则与思想:面向对象 (11讲)
04 | 理论一:当谈论面向对象的时候,我们到底在谈论什么?
05 | 理论二:封装、抽象、继承、多态分别可以解决哪些编程问题?
https://time.geekbang.org/column/article/161114
封装
1. 关于封装特性封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式来访问内部信息或者数据。它需要编程语言提供权限访问控制语法来支持,例如 Java 中的 private、protected、public 关键字。封装特性存在的意义,一方面是保护数据不被随意修改,提高代码的可维护性;另一方面是仅暴露有限的必要接口,提高类的易用性。
抽象
2. 关于抽象特性封装主要讲如何隐藏信息、保护数据,那抽象就是讲如何隐藏方法的具体实现,让使用者只需要关心方法提供了哪些功能,不需要知道这些功能是如何实现的。抽象可以通过接口类或者抽象类来实现,但也并不需要特殊的语法机制来支持。抽象存在的意义,一方面是提高代码的可扩展性、维护性,修改实现不需要改变定义,减少代码的改动范围;另一方面,它也是处理复杂系统的有效手段,能有效地过滤掉不必要关注的信息。
继承
3. 关于继承特性继承是用来表示类之间的 is-a 关系,分为两种模式:单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类。为了实现继承这个特性,编程语言需要提供特殊的语法机制来支持。继承主要是用来解决代码复用的问题。
多态
4. 关于多态特性多态是指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。多态这种特性也需要编程语言提供特殊的语法机制来实现,比如继承、接口类、duck-typing。多态可以提高代码的扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础
class Logger:
def record(self):
print("I write a log into file.")
class DB:
def record(self):
print("I insert data into db.")
def test(recorder):
#2个没有关系的类,只要实现了相同的方法,就可以通过test调用对应类实例的同名方法,在没有继承的情况下,实现多态
recorder.record()
def demo():
logger = Logger()
db = DB()
test(logger)
test(db)
if __name__ == "__main__":
demo()
06 | 理论三:面向对象相比面向过程有哪些优势?面向过程真的过时了吗?
07 | 理论四:哪些代码设计看似是面向对象,实际是面向过程的?
08 | 理论五:接口vs抽象类的区别?如何用普通的类模拟抽象类和接口?
09 | 理论六:为什么基于接口而非实现编程?有必要为每个类都定义接口吗?
10 | 理论七:为何说要多用组合少用继承?如何决定该用组合还是继承?
11 | 实战一(上):业务开发常用的基于贫血模型的MVC架构违背OOP吗?
12 | 实战一(下):如何利用基于充血模型的DDD开发一个虚拟钱包系统?
13 | 实战二(上):如何对接口鉴权这样一个功能开发做面向对象分析?
14 | 实战二(下):如何利用面向对象设计和编程开发接口鉴权功能?
设计原则与思想:设计原则 (12讲)
15 | 理论一:对于单一职责原则,如何判定某个类的职责是否够“单一”?
16 | 理论二:如何做到“对扩展开放、修改关闭”?扩展和修改各指什么?
17 | 理论三:里式替换(LSP)跟多态有何区别?哪些代码违背了LSP?
18 | 理论四:接口隔离原则有哪三种应用?原则中的“接口”该如何理解?
19 | 理论五:控制反转、依赖反转、依赖注入,这三者有何区别和联系?
20 | 理论六:我为何说KISS、YAGNI原则看似简单,却经常被用错?
21 | 理论七:重复的代码就一定违背DRY吗?如何提高代码的复用性?
22 | 理论八:如何用迪米特法则(LOD)实现“高内聚、松耦合”?
23 | 实战一(上):针对业务系统的开发,如何做需求分析和设计?
24 | 实战一(下):如何实现一个遵从设计原则的积分兑换系统?
25 | 实战二(上):针对非业务的通用框架开发,如何做需求分析和设计?
26 | 实战二(下):如何实现一个支持各种统计规则的性能计数器?
设计原则与思想:规范与重构 (7讲)
27 | 理论一:什么情况下要重构?到底重构什么?又该如何重构?
28 | 理论二:为了保证重构不出错,有哪些非常能落地的技术手段?
29 | 理论三:什么是代码的可测试性?如何写出可测试性好的代码?
30 | 理论四:如何通过封装、抽象、模块化、中间层等解耦代码?
31 | 理论五:让你最快速地改善代码质量的20条编程规范(上)
https://time.geekbang.org/column/article/188622
32 | 理论五:让你最快速地改善代码质量的20条编程规范(中)
https://time.geekbang.org/column/article/188857
33 | 理论五:让你最快速地改善代码质量的20条编程规范(下)
https://time.geekbang.org/column/article/188882
34 | 实战一(上):通过一段ID生成器代码,学习如何发现代码质量问题
35 | 实战一(下):手把手带你将ID生成器代码从“能用”重构为“好用”
36 | 实战二(上):程序出错该返回啥?NULL、异常、错误码、空对象?
37 | 实战二(下):重构ID生成器项目中各函数的异常处理代码
设计原则与思想:总结课 (3讲)
38 | 总结回顾面向对象、设计原则、编程规范、重构技巧等知识点
39 | 运用学过的设计原则和思想完善之前讲的性能计数器项目(上)
40 | 运用学过的设计原则和思想完善之前讲的性能计数器项目(下)
设计模式与范式:创建型 (7讲)
41 | 单例模式(上):为什么说支持懒加载的双重检测不比饿汉式更优?
使用场景:
1、处理资源访问冲突
2、表示全局唯一类
业务上需要将一些数据保持全局唯一,比如:
配置信息
IdGenerator,当要求生成id必须全局唯一时(分布式系统id生成器)
为什么会出现多线程访问共享资源是的竞争问题?

对象级别的锁

重点回顾好了,今天的内容到此就讲完了。我们来总结回顾一下,你需要掌握的重点内容。
1. 单例的定义单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者叫实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
2. 单例的用处从业务概念上,有些数据在系统中只应该保存一份,就比较适合设计为单例类。比如,系统的配置信息类。除此之外,我们还可以使用单例解决资源访问冲突的问题。
3. 单例的实现单例有下面几种经典的实现方式。饿汉式饿汉式的实现方式,在类加载的期间,就已经将 instance 静态实例初始化好了,所以,instance 实例的创建是线程安全的。不过,这样的实现方式不支持延迟加载实例。懒汉式懒汉式相对于饿汉式的优势是支持延迟加载。这种实现方式会导致频繁加锁、释放锁,以及并发度低等问题,频繁的调用会产生性能瓶颈。双重检测双重检测实现方式既支持延迟加载、又支持高并发的单例实现方式。只要 instance 被创建之后,再调用 getInstance() 函数都不会进入到加锁逻辑中。所以,这种实现方式解决了懒汉式并发度低的问题。静态内部类利用 Java 的静态内部类来实现单例。这种实现方式,既支持延迟加载,也支持高并发,实现起来也比双重检测简单。枚举最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。
42 | 单例模式(中):我为什么不推荐使用单例模式?又有何替代方案?
3. 单例对代码的扩展性不友好我们知道,单例类只能有一个对象实例。如果未来某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。你可能会说,会有这样的需求吗?既然单例类大部分情况下都用来表示全局类,怎么会需要两个或者多个实例呢?实际上,这样的需求并不少见。我们拿数据库连接池来举例解释一下。在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便我们控制对数据库连接资源的消耗。所以,我们把数据库连接池类设计成了单例类。但之后我们发现,系统中有些 SQL 语句运行得非常慢。这些 SQL 语句在执行的时候,长时间占用数据库连接资源,导致其他 SQL 请求无法响应。
为了解决这个问题,我们希望将慢 SQL 与其他 SQL 隔离开来执行。为了实现这样的目的,我们可以在系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。
使用场景:
1、全局唯一类,如配置文件
2、共享资源写入时,在多线程并发的情况下,不能保证顺序执行,导致的问题
43 | 单例模式(下):如何设计实现一个集群环境下的分布式单例模式?
1. 如何理解单例模式的唯一性?单例类中对象的唯一性的作用范围是“进程唯一”的。“进程唯一”指的是进程内唯一,进程间不唯一;“线程唯一”指的是线程内唯一,线程间可以不唯一。实际上,“进程唯一”就意味着线程内、线程间都唯一,这也是“进程唯一”和“线程唯一”的区别之处。“集群唯一”指的是进程内唯一、进程间也唯一。
2. 如何实现线程唯一的单例?我们通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 并发工具类,可以更加轻松地实现线程唯一单例。
3. 如何实现集群环境下的单例?我们需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。为了保证任何时刻在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,需要显式地将对象从内存中删除,并且释放对对象的加锁。
4. 如何实现一个多例模式?“单例”指的是一个类只能创建一个对象。对应地,“多例”指的就是一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。多例的实现也比较简单,通过一个 Map 来存储对象类型和对象之间的对应关系,来控制对象的个数。课堂讨论在文章中,我们讲到单例唯一性的作用范围是进程,实际上,
对于 Java 语言来说,单例类对象的唯一性的作用范围并非进程,而是类加载器(Class Loader),你能自己研究并解释一下为什么吗?
使用场景:
1、分布式id生成器,需要保证集群环境下(多进程)环境下的生产唯一id
2、拓展思路,有类似的场景需求时可以参考
44 | 工厂模式(上):我为什么说没事不要随便用工厂模式创建对象?
基于这个设计思想,当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,我们推荐使用工厂方法模式,
将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂。而使用简单工厂模式,将所有的创建逻辑都放到一个工厂类中,会导致这个工厂类变得很复杂。
package main.java.designpattern.creational.factory1.others;
import java.awt.*;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
#这种方式获取对象可以减少类的创建
public class ShapeFactory {
final static Map<String, Supplier<Shape>> map = new HashMap<>();
static {
map.put("CIRCLE", Circle::new);
map.put("RECTANGLE", Rectangle::new);
}
public static Shape getShape(String shapeType){
Supplier<Shape> supplier = map.get(shapeType.toUpperCase());
if(supplier != null) {
return supplier.get();
}
throw new IllegalArgumentException("No such shape " + shapeType.toUpperCase());
}
}
public class Circle implements Shape, java.io.Serializable {
public Circle() {
}
@Override
public Rectangle getBounds() {
return null;
}
@Override
public Rectangle2D getBounds2D() {
return null;
}
@Override
public boolean contains(double x, double y) {
return false;
}
@Override
public boolean contains(Point2D p) {
return false;
}
@Override
public boolean intersects(double x, double y, double w, double h) {
return false;
}
@Override
public boolean intersects(Rectangle2D r) {
return false;
}
@Override
public boolean contains(double x, double y, double w, double h) {
return false;
}
@Override
public boolean contains(Rectangle2D r) {
return false;
}
@Override
public PathIterator getPathIterator(AffineTransform at) {
return null;
}
@Override
public PathIterator getPathIterator(AffineTransform at, double flatness) {
return null;
}
}
使用场景:
1、简单工厂,对象创建过程简单,使用该方式,创建对象,摒弃if-else的创建方式:
main.java.designpattern.creational.factory1.RuleConfigParserFactory2#createParser(String name)
2、工厂方法,创建对象的过程比较复杂,获取可能依赖外部类的情况下,将对象的创建和使用分离,通过工厂方法单独封装对象的创建
IRuleConfigParserFactory parserFactory = RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);
45 | 工厂模式(下):如何设计实现一个Dependency Injection框架?
总结一下,一个简单的 DI 容器的核心功能一般有三个:配置解析、对象创建和对象生命周期管理。
总结:
1、理解DI容器的核心功能:
解析配置文件、注解等bean的定义方式
根据bean的相关信息(BeanDefinitionReader)创建对象,保存到存储对象的map中
bean对象的生命周期管理
46 | 建造者模式:详解构造函数、set方法、建造者模式三种对象创建方式
至此,我们仍然没有用到建造者模式,通过构造函数设置必填项,通过 set() 方法设置可选配置项,就能实现我们的设计需求。如果我们把问题的难度再加大点,比如,还需要解决下面这三个问题,那现在的设计思路就不能满足了。
使用场景:
1、必填参数过多,导致参数列表过长
我们刚刚讲到,name 是必填的,所以,我们把它放到构造函数中,强制创建对象的时候就设置。如果必填的配置项有很多,把这些必填配置项都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。
2、对参数需要做依赖、条件等约束的复杂校验
如果我们把必填项也通过 set() 方法设置,那校验这些必填项是否已经填写的逻辑就无处安放了。除此之外,假设配置项之间有一定的依赖关系,比如,如果用户设置了 maxTotal、maxIdle、minIdle 其中一个,就必须显式地设置另外两个;或者配置项之间有一定的约束条件,比如,maxIdle 和 minIdle 要小于等于 maxTotal。如果我们继续使用现在的设计思路,那这些配置项之间的依赖关系或者约束条件的校验逻辑就无处安放了。
3、创建对象后属性不允许修改
如果我们希望 ResourcePoolConfig 类对象是不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值。要实现这个功能,我们就不能在 ResourcePoolConfig 类中暴露 set() 方法。
为了解决这些问题,建造者模式就派上用场了。
// 方式三:建造者模式
ResourcePoolConfig2 config2 = new ResourcePoolConfig2.Builder()
.setName("dbconnectionpool")
.setMaxTotal(10)
.setMaxIdle(8)
.setMinIdle(0)
.build();
使用建造者模式创建对象,还能避免对象存在无效状态
Rectangle r = new Rectange(); // r is invalid
r.setWidth(2); // r is invalid
r.setHeight(3); // r is valid
为了避免这种无效状态的存在,我们就需要使用构造函数一次性初始化好所有的成员变量。如果构造函数参数过多,我们就需要考虑使用建造者模式,先设置建造者的变量,然后再一次性地创建对象,让对象一直处于有效状态。
与工厂模式有何区别?
从上面的讲解中,我们可以看出,建造者模式是让建造者类来负责对象的创建工作。上一节课中讲到的工厂模式,是由工厂类来负责对象创建的工作。那它们之间有什么区别呢?
实际上,工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。
建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。
网上有一个经典的例子很好地解释了两者的区别。顾客走进一家餐馆点餐,我们利用工厂模式,根据用户不同的选择,来制作不同的食物,比如披萨、汉堡、沙拉。对于披萨来说,用户又有各种配料可以定制,比如奶酪、西红柿、起司,我们通过建造者模式根据用户选择的不同配料来制作披萨。实际上,我们也不要太学院派,非得把工厂模式、建造者模式分得那么清楚,我们需要知道的是,每个模式为什么这么设计,能解决什么问题。只有了解了这些最本质的东西,我们才能不生搬硬套,才能灵活应用,甚至可以混用各种模式创造出新的模式,来解决特定场景的问题。
47 | 原型模式:如何最快速地clone一个HashMap散列表?
设计模式与范式:结构型 (6讲)
public class MetricsCollectorProxy {
private MetricsCollector metricsCollector;
public MetricsCollectorProxy() {
this.metricsCollector = new MetricsCollector();
}
public Object createProxy(Object proxiedObject) {
Class<?>[] interfaces = proxiedObject.getClass().getInterfaces();
DynamicProxyHandler handler = new DynamicProxyHandler(proxiedObject);
return Proxy.newProxyInstance(proxiedObject.getClass().getClassLoader(), interfaces, handler);
}
private class DynamicProxyHandler implements InvocationHandler {
private Object proxiedObject;
public DynamicProxyHandler(Object proxiedObject) {
this.proxiedObject = proxiedObject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long startTimestamp = System.currentTimeMillis();
Object result = method.invoke(proxiedObject, args);
long endTimeStamp = System.currentTimeMillis();
long responseTime = endTimeStamp - startTimestamp;
String apiName = proxiedObject.getClass().getName() + ":" + method.getName();
RequestInfo requestInfo = new RequestInfo(apiName, responseTime, startTimestamp);
metricsCollector.recordRequest(requestInfo);
return result;
}
}
}
//MetricsCollectorProxy使用举例
MetricsCollectorProxy proxy = new MetricsCollectorProxy();
IUserController userController = (IUserController) proxy.createProxy(new UserController());

49 | 桥接模式:如何实现支持不同类型和渠道的消息推送系统?

50 | 装饰器模式:通过剖析Java IO类库源码学习装饰器模式


51 | 适配器模式:代理、适配器、桥接、装饰,这四个模式有何区别?
那在实际的开发中,什么情况下才会出现接口不兼容呢?我总结下了下面这样 5 种场景:
封装有缺陷的接口、设计统一多个类的接口、设计替换依赖的外部系统、兼容老版本接口、适配不同格式的数据
代理、桥接、装饰器、适配器 4 种设计模式的区别代理、桥接、装饰器、适配器,这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似。笼统来说,它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。尽管代码结构相似,但这 4 种设计模式的用意完全不同,也就是说要解决的问题、应用场景不同,这也是它们的主要区别。这里我就简单说一下它们之间的区别。
代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。
52 | 门面模式:如何设计合理的接口粒度以兼顾接口的易用性和通用性?
门面模式的应用场景举例在 GoF 给出的定义中提到,“门面模式让子系统更加易用”,实际上,它除了解决易用性问题之外,还能解决其他很多方面的问题。关于这一点,我总结罗列了 3 个常用的应用场景,你可以参考一下,举一反三地借鉴到自己的项目中。除此之外,我还要强调一下,门面模式定义中的“子系统(subsystem)”也可以有多种理解方式。它既可以是一个完整的系统,也可以是更细粒度的类或者模块。关于这一点,在下面的讲解中也会有体现。
1. 解决易用性问题
门面模式可以用来封装系统的底层实现,隐藏系统的复杂性,提供一组更加简单易用、更高层的接口。比如,Linux 系统调用函数就可以看作一种“门面”。它是 Linux 操作系统暴露给开发者的一组“特殊”的编程接口,它封装了底层更基础的 Linux 内核调用。再比如,Linux 的 Shell 命令,实际上也可以看作一种门面模式的应用。它继续封装系统调用,提供更加友好、简单的命令,让我们可以直接通过执行命令来跟操作系统交互。我们前面也多次讲过,设计原则、思想、模式很多都是相通的,是同一个道理不同角度的表述。实际上,从隐藏实现复杂性,提供更易用接口这个意图来看,门面模式有点类似之前讲到的迪米特法则(最少知识原则)和接口隔离原则:两个有交互的系统,只暴露有限的必要的接口。除此之外,门面模式还有点类似之前提到封装、抽象的设计思想,提供更抽象的接口,封装底层实现细节。
2. 解决性能问题关于利用门面模式解决性能问题这一点,刚刚我们已经讲过了。我们通过将多个接口调用替换为一个门面接口调用,减少网络通信成本,提高 App 客户端的响应速度。所以,关于这点,我就不再举例说明了。我们来讨论一下这样一个问题:从代码实现的角度来看,该如何组织门面接口和非门面接口?如果门面接口不多,我们完全可以将它跟非门面接口放到一块,也不需要特殊标记,当作普通接口来用即可。如果门面接口很多,我们可以在已有的接口之上,再重新抽象出一层,专门放置门面接口,从类、包的命名上跟原来的接口层做区分。如果门面接口特别多,并且很多都是跨多个子系统的,我们可以将门面接口放到一个新的子系统中。3. 解决分布式事务问题
3. 解决分布式事务问题
关于利用门面模式来解决分布式事务问题,我们通过一个例子来解释一下。在一个金融系统中,有两个业务领域模型,用户和钱包。这两个业务领域模型都对外暴露了一系列接口,比如用户的增删改查接口、钱包的增删改查接口。
假设有这样一个业务场景:在用户注册的时候,我们不仅会创建用户(在数据库 User 表中),还会给用户创建一个钱包(在数据库的 Wallet 表中)。对于这样一个简单的业务需求,我们可以通过依次调用用户的创建接口和钱包的创建接口来完成。但是,用户注册需要支持事务,也就是说,创建用户和钱包的两个操作,要么都成功,要么都失败,不能一个成功、一个失败。要支持两个接口调用在一个事务中执行,是比较难实现的,这涉及分布式事务问题。虽然我们可以通过引入分布式事务框架或者事后补偿的机制来解决,但代码实现都比较复杂。
而最简单的解决方案是,利用数据库事务或者 Spring 框架提供的事务(如果是 Java 语言的话),在一个事务中,执行创建用户和创建钱包这两个 SQL 操作。这就要求两个 SQL 操作要在一个接口中完成,所以,我们可以借鉴门面模式的思想,再设计一个包裹这两个操作的新接口,让新接口在一个事务中执行两个 SQL 操作。
53 | 组合模式:如何设计实现支持递归遍历的文件系统目录树结构?
54 | 享元模式(上):如何利用享元模式优化文本编辑器的内存占用?
55 | 享元模式(下):剖析享元模式在Java Integer、String中的应用
设计模式与范式:行为型 (18讲)
56 | 观察者模式(上):详解各种应用场景下观察者模式的不同实现方式
我们常把 23 种经典的设计模式分为三类:创建型、结构型、行为型。前面我们已经学习了创建型和结构型,从今天起,我们开始学习行为型设计模式。
我们知道,创建型设计模式主要解决“对象的创建”问题,结构型设计模式主要解决“类或对象的组合或组装”问题,那行为型设计模式主要解决的就是“类或对象之间的交互”问题。
实际上,设计模式要干的事情就是解耦。
创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦,具体到观察者模式,它是将观察者和被观察者代码解耦。
57 | 观察者模式(下):如何实现一个异步非阻塞的EventBus框架?


CopyOnWriteArraySet,顾名思义,在写入数据的时候,会创建一个新的 set,并且将原始数据 clone 到新的 set 中,在新的 set 中写入数据完成之后,再用新的 set 替换老的 set。这样就能保证在写入数据的时候,不影响数据的读取操作,以此来解决读写并发问题。
除此之外,CopyOnWriteSet 还通过加锁的方式,避免了并发写冲突。具体的作用你可以去查看一下 CopyOnWriteSet 类的源码,一目了然。
58 | 模板模式(上):剖析模板模式在JDK、Servlet、JUnit等中的应用
模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。在模板模式经典的实现中,模板方法定义为 final,可以避免被子类重写。需要子类重写的方法定义为 abstract,可以强迫子类去实现。不过,在实际项目开发中,模板模式的实现比较灵活,以上两点都不是必须的。
模版方法类
public abstract class TestCase extends Assert implements Test {
/**
* 模板方法
*
* @throws Throwable
*/
public void runBare() throws Throwable {
Throwable exception = null;
// 扩展点
setUp();
try {
//扩展点 运行真正的测试代码
runTest();
} catch (Throwable running) {
exception = running;
} finally {
try {
// 扩展点
tearDown();
} catch (Throwable tearingDown) {
if (exception == null) {
exception = tearingDown;
}
}
}
if (exception != null) {
throw exception;
}
}
protected abstract void runTest();
/**
* Sets up the fixture, for example, open a network connection.
* This method is called before a test is executed.
*/
protected void setUp() throws Exception {
}
/**
* Tears down the fixture, for example, close a network connection.
* This method is called after a test is executed.
*/
protected void tearDown() throws Exception {
}
}
实现模仿类,自定义扩展点
package main.java.designpattern.behavioral.template;
public class MyTestCase extends TestCase {
/**
* 扩展点
*/
@Override
protected void runTest() {
System.out.println("MyTestCase runTest");
}
/**
* 扩展点
*/
@Override
protected void setUp() throws Exception {
System.out.println("MyTestCase setUp");
// super.setUp();
}
/**
* 扩展点
*/
@Override
protected void tearDown() throws Exception {
System.out.println("MyTestCase tearDown");
// super.tearDown();
}
}
59 | 模板模式(下):模板模式与Callback回调函数有何区别和联系?
@FunctionalInterface
public interface StatementCallback<T> {
/**
* Gets called by {@code JdbcTemplate.execute} with an active JDBC
* Statement. Does not need to care about closing the Statement or the
* Connection, or about handling transactions: this will all be handled
* by Spring's JdbcTemplate.
* <p><b>NOTE:</b> Any ResultSets opened should be closed in finally blocks
* within the callback implementation. Spring will close the Statement
* object after the callback returned, but this does not necessarily imply
* that the ResultSet resources will be closed: the Statement objects might
* get pooled by the connection pool, with {@code close} calls only
* returning the object to the pool but not physically closing the resources.
* <p>If called without a thread-bound JDBC transaction (initiated by
* DataSourceTransactionManager), the code will simply get executed on the
* JDBC connection with its transactional semantics. If JdbcTemplate is
* configured to use a JTA-aware DataSource, the JDBC connection and thus
* the callback code will be transactional if a JTA transaction is active.
* <p>Allows for returning a result object created within the callback, i.e.
* a domain object or a collection of domain objects. Note that there's
* special support for single step actions: see JdbcTemplate.queryForObject etc.
* A thrown RuntimeException is treated as application exception, it gets
* propagated to the caller of the template.
* @param stmt active JDBC Statement
* @return a result object, or {@code null} if none
* @throws SQLException if thrown by a JDBC method, to be auto-converted
* to a DataAccessException by an SQLExceptionTranslator
* @throws DataAccessException in case of custom exceptions
* @see JdbcTemplate#queryForObject(String, Class)
* @see JdbcTemplate#queryForRowSet(String)
*/
@Nullable
T doInStatement(Statement stmt) throws SQLException, DataAccessException;
}
60 | 策略模式(上):如何避免冗长的if-else/switch分支判断代码?
使用场景:
当出现冗长的if-else分支判断时,就可以考虑使用策略模式,例如:
使用getDiscountStrategy2替换getDiscountStrategy3是if-else的方式
// 策略的创建
public class DiscountStrategyFactory {
/*静态存储创建DiscountStrategy的map*/
private static final Map<OrderType, DiscountStrategy> strategies = new HashMap<>();
/*动态存储创建DiscountStrategy的map*/
private static final Map<OrderType, Class<?>> strategies2 = new HashMap<>();
static {
strategies.put(OrderType.NORMAL, new NormalDiscountStrategy());
strategies.put(OrderType.GROUPON, new GrouponDiscountStrategy());
strategies.put(OrderType.PROMOTION, new PromotionDiscountStrategy());
}
static {
strategies2.put(OrderType.NORMAL, NormalDiscountStrategy.class);
strategies2.put(OrderType.GROUPON, GrouponDiscountStrategy.class);
strategies2.put(OrderType.PROMOTION, PromotionDiscountStrategy.class);
}
/**
* 反射方式动态创建DiscountStrategy对象
*
* @param orderType
* @return
*/
public static DiscountStrategy getDiscountStratepy2(OrderType orderType) {
try {
return (DiscountStrategy) strategies2.get(orderType).getConstructor().newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return null;
}
public static DiscountStrategy getDiscountStrategy3(OrderType orderType) {
if (orderType.equals(OrderType.NORMAL)) {
return new NormalDiscountStrategy();
} else if (orderType.equals(OrderType.GROUPON)) {
return new GrouponDiscountStrategy();
} else if (orderType.equals(OrderType.PROMOTION)) {
return new PromotionDiscountStrategy();
}
return null;
}
}
61 | 策略模式(下):如何实现一个支持给不同大小文件排序的小程序?
设计原则和思想其实比设计模式更加普适和重要,掌握了代码的设计原则和思想,我们甚至可以自己创造出来新的设计模式。
实际上,拆分是应对类或者函数代码过多、应对代码复杂性的一个常用手段。按照这个解决思路,我们对代码进行重构。重构之后的代码如下所示:
对于 Java 语言来说,我们可以通过反射来避免对策略工厂类的修改。具体是这么做的:我们通过一个配置文件或者自定义的 annotation 来标注都有哪些策略类;策略工厂类读取配置文件或者搜索被 annotation 标注的策略类,然后通过反射动态地加载这些策略类、创建策略对象;当我们新添加一个策略的时候,只需要将这个新添加的策略类添加到配置文件或者用 annotation 标注即可。
62 | 职责链模式(上):如何实现可灵活扩展算法的敏感信息过滤框架?
63 | 职责链模式(下):框架中常用的过滤器、拦截器是如何实现的?
64 | 状态模式:游戏、工作流引擎中常用的状态机是如何实现的?
65 | 迭代器模式(上):相比直接遍历集合数据,使用迭代器有哪些优势?
66 | 迭代器模式(中):遍历集合的同时,为什么不能增删集合元素?
67 | 迭代器模式(下):如何设计实现一个支持“快照”功能的iterator?
68 | 访问者模式(上):手把手带你还原访问者模式诞生的思维过程
69 | 访问者模式(下):为什么支持双分派的语言不需要访问者模式?
70 | 备忘录模式:对于大对象的备份和恢复,如何优化内存和时间的消耗?
71 | 命令模式:如何利用命令模式实现一个手游后端架构?
72 | 解释器模式:如何设计实现一个自定义接口告警规则功能?
73 | 中介模式:什么时候用中介模式?什么时候用观察者模式?
设计模式与范式:总结课 (2讲)
74 | 总结回顾23种经典设计模式的原理、背后的思想、应用场景等
75 | 在实际的项目开发中,如何避免过度设计?又如何避免设计不足?
开源与项目实战:开源实战 (5讲)
76 | 开源实战一(上):通过剖析Java JDK源码学习灵活应用设计模式
77 | 开源实战一(下):通过剖析Java JDK源码学习灵活应用设计模式
78 | 开源实战二(上):从Unix开源开发学习应对大型复杂项目开发
79 | 开源实战二(中):从Unix开源开发学习应对大型复杂项目开发
80 | 开源实战二(下):从Unix开源开发学习应对大型复杂项目开发
开源与项目实战:项目实战 (9讲)
90 | 项目实战一:设计实现一个支持各种算法的限流框架(分析)
这里提到的公共服务平台,有点类似现在比较火的“中台”或“微服务”。不过,为了减少部署、维护多个微服务的成本,我们把所有公共的功能,放到一个项目中开发,放到一个应用中部署。只不过,我们要未雨绸缪,事先按照领域模型,将代码的模块化做好,等到真的有哪个模块的接口调用过于集中,性能出现瓶颈的时候,我们再把它拆分出来,设计成独立的微服务来开发和部署。
需求背景
对于公共服务平台来说,接口请求来自很多不同的系统(后面统称为调用方),比如各种金融产品的后端系统。在系统上线一段时间里,我们遇到了很多问题。比如,因为调用方代码 bug 、不正确地使用服务(比如启动 Job 来调用接口获取数据)、业务上面的突发流量(比如促销活动),导致来自某个调用方的接口请求数突增,过度争用服务的线程资源,而来自其他调用方的接口请求,因此来不及响应而排队等待,导致接口请求的响应时间大幅增加,甚至出现超时。
限流框架主要包含两部分功能:配置限流规则和提供编程接口(RateLimiter 类)验证请求是否被限流。不过,作为通用的框架,除了功能性需求之外,非功能性需求也非常重要,有时候会决定一个框架的成败,比如,框架的易用性、扩展性、灵活性、性能、容错性等。
易用性方面,我们希望限流规则的配置、编程接口的使用都很简单。我们希望提供各种不同的限流算法,比如基于内存的单机限流算法、基于 Redis 的分布式限流算法,能够让使用者自由选择。除此之外,因为大部分项目都是基于 Spring 开发的,我们还希望限流框架能否非常方便地集成到使用 Spring 框架的项目中。
扩展性、灵活性方面,我们希望能够灵活地扩展各种限流算法。同时,我们还希望支持不同格式(JSON、YAML、XML 等格式)、不同数据源(本地文件配置或 Zookeeper 集中配置等)的限流规则的配置方式。
性能方面,因为每个接口请求都要被检查是否限流,这或多或少会增加接口请求的响应时间。而对于响应时间比较敏感的接口服务来说,我们要让限流框架尽可能低延迟,尽可能减少对接口请求本身响应时间的影响。
容错性方面,接入限流框架是为了提高系统的可用性、稳定性,不能因为限流框架的异常,反过来影响到服务本身的可用性。所以,限流框架要有高度的容错性。比如,分布式限流算法依赖集中存储器 Redis。如果 Redis 挂掉了,限流逻辑无法正常运行,这个时候业务接口也要能正常服务才行。
91 | 项目实战一:设计实现一个支持各种算法的限流框架(设计)
重点回顾好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。我们将这个限流框架划分为限流规则、限流算法、限流模式、集成使用者这四个模块来分析讲解。除了功能方面的设计之外,我们重点讲了如何满足易用、灵活、易扩展、低延迟、高容错这些非功能性需求。
针对限流规则,大部分 Java 程序员已经习惯了 Spring 的配置方式。基于最小惊奇原则,在限流框架中,我们也延续 Spring 的配置方式,支持 XML、YAML、Properties 等几种配置文件格式。同时,借鉴 Spring 的约定优于配置设计原则,限流框架用户只需要将配置文件按照约定来命名,并且放置到约定的路径下,框架就能按照约定自动查找和加载配置文件。除此之外,为了提高框架的兼容性、易用性,除了本地文件的配置方式之外,我们还希望兼容从其他数据源获取配置的方式,比如 Zookeeper 或者自研的配置中心。
针对限流算法,尽管固定时间窗口限流算法没法做到让流量很平滑,但大部分情况下,它已经够用了。默认情况下,框架使用固定时间窗口限流算法做限流。不过,考虑到框架的扩展性,我们需要预先做好设计,预留好扩展点,方便今后扩展其他限流算法。除此之外,为了提高框架的易用性、灵活性,我们将其他几种常用的限流算法也在框架中实现出来,供框架用户根据自己的业务场景自由选择。
针对限流模式,因为分布式限流基于外部存储 Redis,网络通信成本较高,框架的高容错和低延迟的设计,主要是针对基于 Redis 的分布式限流模式。不能因为 Redis 的异常,影响到集成框架的应用的可用性和稳定性。不能因为 Redis 访问超时,导致接口访问超时。
针对集成使用,我们希望框架低侵入,跟业务代码松耦合。应用集成框架的代码,尽可能集中、不分散,这样删除、替换起来就容易很多。除此之外,为了将框架的易用性做到极致,我们借鉴 MyBatis-Spring 类库,设计实现一个 RateLimiter-Spring 类库,方便集成了 Spring 框架的应用集成限流框架。
92 | 项目实战一:设计实现一个支持各种算法的限流框架(实现)
最小原型代码上节课我们讲到,项目实战中的实现等于面向对象设计加实现。而面向对象设计与实现一般可以分为四个步骤:划分职责识别类、定义属性和方法、定义类之间的交互关系、组装类并提供执行入口。在第 14 讲中,我还带你用这个方法,设计和实现了一个接口鉴权框架。如果你印象不深刻了,可以回过头去再看下。不过,我们前面也讲到,在平时的工作中,大部分程序员都是边写代码边做设计,边思考边重构,并不会严格地按照步骤,先做完类的设计再去写代码。而且,如果想一下子就把类设计得很好、很合理,也是比较难的。过度追求完美主义,只会导致迟迟下不了手,连第一行代码也敲不出来。
所以,我的习惯是,先完全不考虑设计和代码质量,先把功能完成,先把基本的流程走通,哪怕所有的代码都写在一个类中也无所谓。然后,我们再针对这个 MVP 代码(最小原型代码)做优化重构,比如,将代码中比较独立的代码块抽离出来,定义成独立的类或函数。
// 重构前:
com.xzg.ratelimiter
--RateLimiter
com.xzg.ratelimiter.rule
--ApiLimit
--RuleConfig
--RateLimitRule
com.xzg.ratelimiter.alg
--RateLimitAlg
// 重构后:
com.xzg.ratelimiter
--RateLimiter(有所修改)
com.xzg.ratelimiter.rule
--ApiLimit(不变)
--RuleConfig(不变)
--RateLimitRule(抽象接口)
--TrieRateLimitRule(实现类,就是重构前的RateLimitRule)
com.xzg.ratelimiter.rule.parser
--RuleConfigParser(抽象接口)
--YamlRuleConfigParser(Yaml格式配置文件解析类)
--JsonRuleConfigParser(Json格式配置文件解析类)
com.xzg.ratelimiter.rule.datasource
--RuleConfigSource(抽象接口)
--FileRuleConfigSource(基于本地文件的配置类)
com.xzg.ratelimiter.alg
--RateLimitAlg(抽象接口)
--FixedTimeWinRateLimitAlg(实现类,就是重构前的RateLimitAlg)
93 | 项目实战二:设计实现一个通用的接口幂等框架(分析)
94 | 项目实战二:设计实现一个通用的接口幂等框架(设计)
95 | 项目实战二:设计实现一个通用的接口幂等框架(实现)
96 | 项目实战三:设计实现一个支持自定义规则的灰度发布组件(分析)
97 | 项目实战三:设计实现一个支持自定义规则的灰度发布组件(设计)
98 | 项目实战三:设计实现一个支持自定义规则的灰度发布组件(实现)
不定期加餐 (3讲)

本文详细介绍了设计模式的概念,包括面向对象的四大特性——封装、抽象、继承、多态,以及它们在代码设计中的作用。文章通过一系列理论课程和实战案例,深入探讨了设计原则、设计模式、编程规范和重构的重要性,旨在帮助程序员提升代码质量和可维护性。通过学习,读者能够理解何时使用单例、工厂、建造者等模式,以及如何在实际项目中运用设计模式解决复杂问题。
1556

被折叠的 条评论
为什么被折叠?



