阅读视图

发现新文章,点击刷新页面。
🔲 ⭐

设计模式系列——责任链模式

开个坑补齐设计模式系列笔记, 顺带回顾Spring源码中的设计模式运用…

模式

责任链模式(Chain of Responsibility Pattern)是一种行为设计模式,它允许多个对象有机会处理同一个请求,从而避免请求的发送者与多个接收者之间的耦合, 通常用于请求有多个阶段。该模式通过将这些对象连成一条链,并沿着链传递请求,直到有一个对象处理请求为止。如果链的末端没有对象处理请求,整个请求将被丢弃或默认处理。

未加粗的是责任链复杂程度上去之后可选的角色

  1. 抽象处理者(Handler)
    • 定义:一个接口或抽象类,通常包含一个方法来处理请求以及一个方法来设置下一个处理者。它规定了所有具体处理者都必须实现的基本操作,如处理请求或将请求传递给链中的下一个处理者。
    • 角色接口或抽象类 作为处理链的核心定义,保证每个处理者都具有处理请求的能力。
  • 基础处理者 (Base Handler): 可选
    • 定义:一个接口或部分实现类,它实现了抽象处理者的部分功能或是抽象处理者的拓展。
    • 角色复用和扩展 提供公共的逻辑实现,减少子类的重复代码,增强代码的可复用性。
  1. 具体处理者(ConcreteHandlers)
    • 定义:每个实现类,实现了处理某种请求的逻辑。当它无法处理请求时,会将请求传递给链中的下一个处理者。
    • 角色链的节点实现 实现自己的处理逻辑来参与责任链的请求处理。
  • 链的管理者(Chain Manager):可选
    1. 定义:通常是一个Chain类,实现创建和管理处理者的顺序、组装责任链,并维护链的整体状态。
    2. 角色:构建和维护责任链 确保处理者按照正确的顺序处理请求,同时可以动态地添加、移除或调整链中的处理者。
  • 链的构造器(Chain Creator):可选
    1. 定义:通常是一个工厂类或构造器,负责初始化责任链的结构并返回链的起始处理者。它可以根据具体的业务需求,创建不同类型的责任链。
    2. 角色:生成责任链实例 确保处理者按照正确的顺序处理请求,同时可以动态地添加、移除或调整链中的处理者。
  1. 客户类(Client)
    • 定义:客户类是发起请求的对象,它通常会自行或调用链的构造器创建并设置责任链,然后向责任链的第一个处理者提交请求。
    • 角色责任链的创建者 请求的发起者
责任链模式结构图
责任链模式结构图

场景

公司报销流程就像一条责任链,每个环节都根据条件决定是否继续处理:

  1. 填写申请:你提交的报销单是链上的第一个环节,检查所有必要的字段和金额,确保格式正确。如果不符合条件,它会被退回。
  2. 审批环节:提交后的申请进入审批环节(链路)。每一级上司根据公司政策比如费用上限或是否有票据判断是否继续处理。如果符合条件或该审批者无权审批,申请会被传递到下一个环节。
  3. 财务审核:通过所有审批后,财务部门再进行一次核查,确保链路都是true且符合财务规定。如果审核通过,申请继续流向资金发放环节 (实际的处理方法)。
  4. 资金发放:经过所有环节的条件检查和批准,资金会发放到你的账户中。

每个环节都有自己的条件判断,每个节点只处理自己能处理的部分,不符合条件的节点将被跳过,确保整个流程高效顺畅。

案例

简单点

先来个简单点的,虚拟一个简单的过滤器链实现和实际的使用,用来处理HTTP请求。

抽象处理者

接口定义
1
2
3
4
5
6
7
8
interface Handler : Comparable<Handler> {
var index: Int // 责任链顺序

fun setNext(handler: Handler): Handler // 下一个
fun handleRequest(request: Request) // 处理方法,处理同一种入参

override fun compareTo(other: Handler): Int = this.index - other.index
}

基础处理者

基础实现
1
2
3
4
5
6
7
8
9
10
11
12
13
// 基础处理者,提供处理链传递逻辑,并实现index
abstract class BaseHandler(override var index: Int) : Handler {
private var nextHandler: Handler? = null

override fun setNext(handler: Handler): Handler {
nextHandler = handler
return handler
}

override fun handleRequest(request: Request) {
nextHandler?.handleRequest(request)
}
}

具体处理者

具体实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 具体处理者1:处理认证逻辑
class AuthenticationHandler(index: Int) : BaseHandler(index) {
override fun handleRequest(request: Request) {
if ("ROLE_ADMIN" == request?.role) {
super.handleRequest(request)
} else {
response.WriteError("认证失败")
}
}
}

// 具体处理者2:处理日志记录逻辑
class LoggingHandler(index: Int) : BaseHandler(index) {
override fun handleRequest(request: Request) {
Logger.info("登录:${request.username}")
super.handleRequest(request)
}
}

链管理者

链管理者
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
// 链的管理者:负责组装和管理责任链
class ChainManager {
private val handlers: MutableList<Handler> = mutableListOf()
// 添加处理器到链中,指定顺序
fun addHandler(handler: Handler): ChainManager {
handlers.add(handler)
return this
}
// 从链中移除处理器
fun removeHandler(handler: Handler): ChainManager {
handlers.remove(handler)
return this
}
// 构建责任链
fun buildChain(): Handler? {
if (handlers.isEmpty()) {
throw IllegalArgumentException("空链")
}
// 根据index字段排序
handlers.sort()
// 链接处理器
for (i in 0 until handlers.size - 1) {
handlers[i].setNext(handlers[i + 1])
}
return handlers.firstOrNull()
}
}

链构建者

链构建者
1
2
3
4
5
6
7
8
9
// 链的创建者:工厂类,用于创建责任链
object ChainCreator {
fun createDefaultChain(): Handler? {
val manager = ChainManager()
manager.addHandler(LoggingHandler(2)) // 指定顺序
.addHandler(AuthenticationHandler(1))
return manager.buildChain()
}
}

使用

这里假设将我们虚拟的责任链加入到请求拦截器,真实这里应该是基于HandlerInterceptor实现的处理者。

run
1
2
3
4
5
6
7
8
9
10
11
@Configuration
class WebConfig : WebMvcConfigurer {

override fun addInterceptors(registry: InterceptorRegistry) {
// 创建责任链
val chain = ChainCreator.createDefaultChain()

// 注册责任链的第一个处理者
registry.addInterceptor(chain)
}
}

Spring Web

在 Spring Web 框架中,责任链模式被广泛应用于请求处理的各个阶段,尤其是在处理请求拦截和映射时。

看 Spring Web 案例代码之前,现梳理一遍请求到链路末端处理到返回的逻辑,比如启动并访问 OrderController @GetMapping("/order/list") public ResponseEntity<ResponseRow> list() 方法。

  1. Spring Boot 启动时,SpringApplication.run() 方法会被调用。这一方法负责引导和启动 Spring 应用程序上下文,加载所有的配置类和 Bean。
  2. 理所当然 DispatcherServlet.java 作为核心组件会被通过自动配置机制自动注册并初始化,当作 Spring MVC 的前端控制器,用于接收和处理所有的 HTTP 请求。
  3. DispatcherServlet 初始化时会调用 initHandlerMappings() 方法来加载所有的HandlerMapping 实现类储存到私有属性 private List<HandlerMapping> handlerMappings,并且等待处理即将到来的请求。
  4. HandlerMapping 类负责将请求映射到适当的处理器(例如 @Controller 中的方法)。
  5. 用户发出 /order/list 请求时,DispatcherServlet 作为前端控制器接收 HTTP 请求,在 doDispatch() 方法中遍历 handlerMappings 列表并调用每个 HandlerMapping 实现类的 getHandler() 方法,尝试找到合适的 HandlerExecutionChain 来处理这个请求。
  6. 我们举例是 @GetMapping("/order/list") 所以会用RequestMappingHandlerMapping识别出 OrderController 中相应的处理方法,并返回一个 HandlerExecutionChain。这个 HandlerExecutionChain 包含了处理该请求的控制器方法(如 OrderController.list())以及相关的拦截器链。
  7. 当然拦截器(多个 HandlerInterceptor 以及可能是异步的 AsyncHandlerInterceptorWebRequestHandlerInterceptorAdapter )可能会执行一些通用逻辑,如权限验证、日志记录等,都是按既定顺序执行。如果所有的拦截器都允许请求通过,将调用实际的处理器方法(如 OrderController.list())。
  8. 调用实际的处理器方法后还会继续调用拦截器的 postHandle() 方法,在生成响应之前对结果进一步处理。最后,无论请求是否成功,afterCompletion() 方法都会被调用,此为清理操作。

关系图

DispatcherServlet.java
DispatcherServlet.java
RequestMappingHandlerMapping.java
RequestMappingHandlerMapping.java
抽象处理者、基础处理者、具体处理者
抽象处理者、基础处理者、具体处理者

HandlerInterceptor

抽象处理者 网络请求处理的不同阶段拦截器

public interface HandlerInterceptor {        default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {            return true;        }//调用"Controller"方法之前            default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {        }//调用"Controller"方法之后渲染"ModelAndView"之前        default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {        }//渲染"ModelAndView"之后 }  

AsyncHandlerInterceptor

基础处理者 继承了 HandlerInterceptor 接口,并添加了处理异步请求的方法。

public interface AsyncHandlerInterceptor extends HandlerInterceptor {        default void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {    }  }  

WebRequestHandlerInterceptorAdapter

具体处理者 实现了 AsyncHandlerInterceptor 接口。并将 WebRequestInterceptor 转换为 HandlerInterceptor。它将 WebRequestInterceptor 的处理逻辑适配为 HTTP 请求处理逻辑,并处理异步请求的逻辑。

public class WebRequestHandlerInterceptorAdapter implements AsyncHandlerInterceptor {        private final WebRequestInterceptor requestInterceptor;            public WebRequestHandlerInterceptorAdapter(WebRequestInterceptor requestInterceptor) {            Assert.notNull(requestInterceptor, "WebRequestInterceptor must not be null");            this.requestInterceptor = requestInterceptor;        }            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {            this.requestInterceptor.preHandle(new DispatcherServletWebRequest(request, response));            return true;        }            public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {            this.requestInterceptor.postHandle(new DispatcherServletWebRequest(request, response), modelAndView != null && !modelAndView.wasCleared() ? modelAndView.getModelMap() : null);        }            public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {            this.requestInterceptor.afterCompletion(new DispatcherServletWebRequest(request, response), ex);        }            public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) {            WebRequestInterceptor var5 = this.requestInterceptor;            if (var5 instanceof AsyncWebRequestInterceptor asyncInterceptor) {                DispatcherServletWebRequest webRequest = new DispatcherServletWebRequest(request, response);                asyncInterceptor.afterConcurrentHandlingStarted(webRequest);            }        }  }  

DispatcherServlet

客户端 该调度程序负责配置和执行拦截器链。Spring MVC 的前端控制器,用于接收和处理所有的 HTTP 请求。

这条链路客户端到处理器中间其实还有两个部分,稍后就有介绍
HandlerMapping:用于查找与请求匹配的处理器(Controller),并返回HandlerExecutionChain
HandlerExecutionChain:包含处理器和一系列拦截器(处理器)(HandlerInterceptor)。它负责在请求处理过程中依次调用链中的拦截器。

public class DispatcherServlet extends FrameworkServlet {      ...    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {        HandlerExecutionChain mappedHandler = null;        ...        // 获取处理链          mappedHandler = this.getHandler(processedRequest);        ...    }    ...    @Nullable    protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {            if (this.handlerMappings != null) {                Iterator var2 = this.handlerMappings.iterator();                        while(var2.hasNext()) {                    HandlerMapping mapping = (HandlerMapping)var2.next();                    HandlerExecutionChain handler = mapping.getHandler(request);                    if (handler != null) {                        return handler;                    }                }            }                    return null;        }      ...}    

HandlerExecutionChain

处理链管理者 Spring Web中需要一个角色,将处理器和拦截器链组织在一起,按照顺序处理请求。

它是一个管理处理器和拦截器链的组件。在处理请求时,它负责管理和调用所有的拦截器方法,并最终调用实际的处理器(例如 Controller)。

HandlerMapping

链的建立者 Spring Web中需要一个角色,负责从请求中确定合适的处理器,并将处理器与拦截器链组合起来,形成一个完整的处理链(也就是HandlerExecutionChain)。

它可以被视为该条责任链的启动点或“链的建立者”。它确定了请求将由哪个处理器处理,并且可能为处理器配置了一些拦截器。

总结

优点

  • 业务解耦:每个节点独立可以随时新增或删除,不仅不会影响事务请求的业务代码,还不会影响其它责任节点。
  • 责任单一:责任链每个节点处理的对象是同一种,但是每个节点处理的事务不一样。
  • 动态组合:节点之间有强秩序,但是可以根据不同业务动态重新组合成一个新链路。

缺点

  • 性能缺陷:节点之间有强秩序,大概率会走完全部链路,必影响耗时。
  • 死循环:链路过长开发人员不熟悉整个流程一样难以新增节点,容易出现链路闭环。即新增的F节点可能下一步需要B, 但是要在C之前,便出现 B->D->C->F->B。当然这个问题在开发阶段能处理。
🔲 ☆

为什么 Go 社区强调避免不必要的抽象?—— 借用海德格尔哲学寻找“正确”的答案

本文永久链接 – https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction

大家好,我是Tony Bai。

“Go 的哲学强调避免不必要的抽象。”

这句话我们听过无数次。当你试图引入 ORM、泛型 Map/Reduce 、接口或者复杂的设计模式时,往往会收到这样的反馈。这句话本身没有错,但难点在于:到底什么是“不必要”的?

函数是抽象吗?汇编是抽象吗?如果不加定义地“避免抽象”,我们最终只能对着硅片大喊大叫。

在 GopherCon UK 2025 上,John Cinnamond 做了一场与众不同的演讲。他没有展示任何炫酷的并发模式,而是搬出了马丁·海德格尔(Martin Heidegger)和伊曼努尔·康德(Immanuel Kant),试图用哲学的视角,为我们解开关于 Go 抽象的终极困惑。

注:海德格尔与《存在与时间》

马丁·海德格尔(Martin Heidegger)是 20 世纪最重要的哲学家之一。他在 1927 年的巨著《存在与时间》(Being and Time) 中,深入探讨了人(此在)如何与世界互动。John Cinnamond 在演讲中引用的核心概念——“上手状态” (Ready-to-hand)“在手状态” (Present-at-hand),正是海德格尔用来描述我们与工具(如锤子)之间关系的术语。这套理论极好地解释了为什么优秀的工具(或代码抽象)应该是“透明”的,而糟糕的工具则会强行占据我们的注意力。

img{512x368}

我们都在使用的“必要”抽象

首先,让我们承认一个事实:编程本身就是建立在无数层抽象之上的。

  • 泛型:这是对类型的抽象。虽然 Go 曾长期拒绝它,但在技术上它是必要的,否则我们将充斥着重复代码。
  • 接口:这是对行为的抽象。io.Reader 让我们不必关心数据来自文件还是网络。
  • 函数:这是对指令序列的抽象。没有它,我们只能写长长的 main 函数。
  • 汇编语言:这是对机器码的抽象。

所以,当我们说“避免不必要的抽象”时,我们真正想表达的其实是——避免“不恰当” (Inappropriate) 的抽象

那么,如何判断一个抽象是否“恰当”?

何为抽象?—— 一场有目的的“细节隐藏”

在深入探讨“正确”的抽象之前,我们必须先回到最基本的定义。John Cinnamond 在演讲中给出了一个精炼而深刻的定义:

“抽象是一种表示 (Representation),但它是一种刻意移除被表示事物某些细节的表示。”

让我们拆解这个定义:

  1. 抽象是一种“表示”,而非事物本身
    它不是代码的实体,而是代码的地图或模型。例如,一辆模型汽车是真实汽车的表示,但 Gopher 吉祥物是地鼠的抽象——它刻意省略了真实地鼠的所有细节,只保留了核心特征。

  1. 抽象是“有目的的”细节移除
    这与仅仅是“不精确”或“粗糙”不同。抽象是有意为之的,它不试图精确描绘所有方面,而是只关注某个特定维度

  1. 抽象在编程中具有动态性
    • 不确定引用 (Indefinite Reference):一个抽象(如 io.Reader)通常可以指代许多不同的具体实现。
    • 开放引用 (Open Reference):抽象的内容或它所指代的事物可以随着时间而改变。

为什么要刻意移除细节?John 总结了几个核心动机:

  • 避免重复代码:将重复的逻辑提取到抽象中。
  • 统一不同的实现:允许以统一的方式处理本质上不同的数据结构(如所有实现了 Read 方法的类型)。
  • 推迟细节:隐藏那些当下不重要、或开发者不关心的细节(例如,你坐火车参会,不需要知道每节车厢的编号)。
  • 揭示领域概念:用抽象来更好地表达业务领域中的核心概念。
  • 驾驭复杂性:这是最核心的理由——没有抽象,我们无法在大脑中一次性处理所有细节,也就无法解决复杂的问题。

但请记住,并非所有抽象都是一样的。John 将它们分为三类:

  1. 基于“它是如何工作的” (How it works)
    这是为了代码复用而提取的抽象。例如,你发现两处代码都在做“检查用户是否是管理员”的逻辑,于是将其提取为一个函数。这种抽象关注的是内部机制。 (这类抽象通常比较脆弱,一旦实现细节变化,抽象可能就会失效。)

  2. 基于“它做了什么” (What it does)
    这是 Go 语言中接口(Interface)最典型的用法。例如 io.Reader,我们不关心它是文件还是网络连接,我们只关心它能“读取字节”。这是一种行为抽象。

  3. 基于“它是什么” (What it is)
    这是基于领域模型的抽象。例如一个 User 结构体,它代表了系统中的一个实体。这种抽象关注的是本质属性。

在现实中,好的抽象往往是这三者的混合体,但在设计时,明确你是在抽象“行为”还是“实现”,对于判断抽象的质量至关重要。

理解了抽象的本质,我们可能会觉得:既然抽象能驾驭复杂性,那是不是越多越好?

且慢。在急于评判一个抽象是否“恰当”之前,我们必须先意识到一个常被技术人员忽略的现实:抽象不仅存在于代码中,更存在于人与人的互动里。 这将我们引向了一个更现实的考量维度。

抽象的代价 —— 代码是写给人看的

John 提醒我们,软件开发本质上是一项社会活动 (Social Activity)

“除非你是为了自己写着玩,否则你的代码总是写给别人看的。团队是一个微型社会,它有自己的习俗、信仰和‘传说’(Lore)。”

引入一个新的抽象,本质上是在向这个微型社会引入一种新的文化或规则。这意味着:

  1. 你需要支付“社会成本”:如果这个抽象与团队现有的习惯(Lore)相悖——比如在一个从未用过函数式编程的 Go 团队里强推 Monad——你将遭遇巨大的阻力。
  2. 团队的保守性:成熟的团队往往趋于保守,改变既定习惯需要巨大的能量。你不能仅仅因为一个抽象在理论上很美就引入它,你必须证明它的收益足以覆盖它带来的社会摩擦成本
  3. 认知负担是共享的:一个抽象对你来说可能很清晰,但如果它让队友感到困惑,那就是在消耗团队的整体智力资源。

因此,当我们评判一个抽象是否“恰当”时,不能只看代码本身,还必须看它是否“合群”。这正是我们接下来要引入海德格尔哲学的现实基础。

锤子哲学 —— “上手状态” vs. “在手状态”

John 引用了海德格尔在《存在与时间》中的一个著名概念:Ready-to-hand (上手状态)Present-at-hand (在手状态)

  • 上手状态 (Ready-to-hand):当你熟练使用一把锤子钉钉子时,你的注意力完全在钉钉子这件事上,锤子本身在你意识中是“透明”的。你感觉不到它的存在,它只是你身体的延伸。
  • 在手状态 (Present-at-hand):当锤子突然坏了(比如锤头掉了),或者你拿到一把设计奇特的陌生工具时,你的注意力被迫从“钉钉子”转移到了“锤子”本身。你开始审视它的构造、重量和用法。

这对代码意味着什么?

  • 好的抽象是“上手状态”的:比如 for 循环。作为经验丰富的开发者,你使用它时是在思考“我要遍历数据”,而不是“这个循环语法是怎么编译的”。它透明、顺手,让你专注于解决问题。

  • 坏的抽象是“在手状态”的:比如一个复杂的、过度设计的 ORM 或者一个晦涩的 Monad 库。当你使用它时,你的思维被迫中断,你需要停下来思考:“这个函数到底在干什么?这个参数是什么意思?”

如果一个抽象让你频繁地从“解决业务问题”中抽离出来去思考“工具本身”,那么它很可能是一个坏的抽象

注:通过学习和实践,在手状态 (Present-at-hand)的抽象可以转换为 上手状态 (Ready-to-hand)的抽象。

真理的检验 —— “本质真理” vs. “巧合真理”

接着,John 又搬出了康德关于真理的分类,引导我们思考抽象的持久性

  • 分析真理 (Analytic Truth):由定义决定的真理。比如“所有单身汉都没结婚”。在代码中,这就像 unnecessary abstractions are unnecessary,虽然正确但没啥用。
  • 综合真理 (Synthetic Truth):由外部事实决定的真理。比如“外面在下雨”。它的真假取决于环境,随时可能变。
  • 本质真理 (Essential Truth):虽然不是由定义决定,但反映了世界的本质规律。比如“物质由原子构成”。

这对抽象意味着什么?

当你提取一个抽象时,问问自己:它代表的是代码的“本质真理”,还是仅仅是一个“巧合”?

举个例子:你有一段过滤商品的代码,可以按“价格”过滤,也可以按“库存”过滤。你提取了一个 Filter(Product) bool 的抽象。

  • 如果未来所有的过滤需求(如颜色、大小)都能用这个签名解决,那么你发现了一个本质真理。这个抽象是稳固的。
  • 但如果突然来了一个需求:“过滤掉重复的商品”,这个需求需要知道所有商品的状态,而不仅仅是单个商品。原本的 Filter(Product) bool 签名瞬间失效。

如果你提取的抽象仅仅是因为几段代码“长得像”(巧合),而不是因为它们“本质上是一回事”,那么当需求变更时,这个抽象就会崩塌,变成一种负担。

由此可见,好的抽象不是被创造出来的,而是被发现(Recognized)出来的。它们是对代码中某种本质结构的捕捉。

实战指南 —— 如何引入抽象?

最后,John 给出了一个评估抽象是否“恰当”的五步清单:

  1. 明确收益 (Benefit):你到底是为了解决重复、隐藏细节,还是仅仅因为觉得它“很酷”?
  2. 考虑社会成本 (Social Cost):编程是社会活动。这个抽象符合团队的习惯吗?引入它是否需要消耗大量的团队认知成本?(比如在 Go 里强推 Monad等函数式编程的范式)。
  3. 是否处于“上手状态” (Ready-to-hand):它能融入开发者的直觉吗?还是会成为注意力的绊脚石?
  4. 是否本质 (Essential):它是否捕捉到了问题的核心结构,能经得起未来的变化?
  5. 是否涌现 (Emergent):它是你从现有代码中“识别”出来的模式,还是你强加给代码的枷锁?

小结:保持怀疑,但别放弃好奇

Go 社区的“避免不必要的抽象”文化,本质上是对认知负担的防御。我们见过太多为了抽象而抽象的烂代码。但 John 提醒我们,不要因此走向另一个极端——恐惧抽象

正确且必要的抽象是强大的武器,它能让我们驾驭巨大的复杂性。只要我们能像海德格尔审视锤子那样审视我们的代码,区分“上手”与“在手”,区分“本质”与“巧合”,我们就能在 Go 的简约哲学中,找到属于自己的那条“正确”道路。

资料链接:https://www.youtube.com/watch?v=oP_-eHZSaqc


你的“锤子”顺手吗?

用海德格尔的视角审视代码,确实别有一番风味。在你现在的项目中,有哪些抽象是让你感觉“如臂使指”的(上手状态)?又有哪些抽象经常让你
“出戏”,迫使你不得不去研究它内部的构造(在手状态)?

欢迎在评论区分享你的“哲学思考”! 让我们一起寻找那个最本质的代码真理。

如果这篇文章带给你一次思维的“脑暴”,别忘了点个【赞】和【在看】,并转发给那些喜欢深究技术的伙伴!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ⭐

组件通信: EventBus 的原理解析与应用

在开发复杂的单页面应用时,我们经常会遇到一个问题:如何高效地在组件或模块之间进行通信?这里,EventBus(事件总线)就派上了用场。简单来说,EventBus 是一种设计模式,它允许不同组件或模块之间通过事件来通信,而无需直接引用彼此。

EventBus 是传统的组件通信解决方案,下面我们将讲解 EventBus 跨组件通信的原理、实现方式以及该如何使用。

原理解析

EventBus 的核心在于提供一个中央机制,允许不同的组件或模块相互通信,而不必直接引用对方。它是一种典型的发布-订阅(pub-sub)模式,这是一种广泛使用的设计模式,用于解耦发送者和接收者。

在这个模式中,EventBus 充当了一个中介的角色:它允许组件订阅那些它们感兴趣的事件,并在这些事件发生时接收通知。同样,当某个事件发生时,比如用户的一个动作或者数据的变化,EventBus 负责将这一消息广播给所有订阅了该事件的组件。

它基于三个核心操作:注册事件(on(event, callback))、触发事件(emit(event, ...args))、以及移除事件(off(event, callback))。因此,EventBus 的基本代码可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
class EventBus {
on(event, callback) {
// 注册事件监听器
}

emit(event, ...args) {
// 触发事件
}

off(event, callback) {
// 移除事件监听器
}
}

显然,我们需要有一个私有变量来储存用户的函数,此时为类添加 events 属性。events 属性是一个对象映射,其中每个属性表示一个事件名称,对应的值是一个回调函数的数组,这个数组存储了所有订阅了该事件的回调函数。

1
2
3
4
class EventBus {
private events: Record<string, Function[]> = {};
// ...
}

当用户执行订阅事件 on 时,回调函数会被添加到相应事件名称的数组中。这样,同一个事件可以被不同组件或模块订阅,而每个订阅者的回调函数都会被正确地保存在事件队列中。最后,当触发事件 emit 时,事件队列中的每个回调函数都会被执行,实现了事件的触发和通知功能。若已经没有订阅需求,则可以通过 off 移除已经订阅的事件。

代码实现

接下来我们按照前文所述完善我们的代码实现:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
class EventBus {
// 事件存储对象,用于保存不同事件的回调函数
private events: Record<string, Function[]> = {};

/**
* 注册事件监听器
* @param eventName - 事件名称
* @param callback - 回调函数,当事件触发时执行
* @returns this - 返回 EventBus 实例,支持链式调用
*/
public on(eventName: string, callback: Function): this {
// 检查回调函数是否为函数类型
if (typeof callback !== "function") {
throw new Error("EventBus 'on' method expects a callback function.");
}

// 如果事件不存在,创建一个空数组用于存储回调函数
if (!this.events[eventName]) {
this.events[eventName] = [];
}

// 将回调函数添加到事件的回调函数列表中
this.events[eventName].push(callback);

// 支持链式调用
return this;
}

/**
* 触发事件
* @param eventName - 要触发的事件名称
* @param args - 传递给回调函数的参数
* @returns this - 返回 EventBus 实例,支持链式调用
*/
public emit(eventName: string, ...args: any[]): this {
// 获取事件对应的回调函数列表
const callbacks = this.events[eventName];
if (callbacks) {
// 遍历执行每个回调函数,并传递参数
callbacks.forEach((callback) => callback(...args));
}

// 支持链式调用
return this;
}

/**
* 移除事件监听器
* @param event - 要移除的事件名称或事件名称数组
* @param callback - 要移除的回调函数(可选)
* @returns this - 返回 EventBus 实例,支持链式调用
*/
public off(event?: string | string[], callback?: Function): this {
// 清空所有事件监听器
if (!event || (Array.isArray(event) && !event.length)) {
this.events = {};
return this;
}

// 处理事件数组
if (Array.isArray(event)) {
event.forEach((e) => this.off(e, callback));
return this;
}

// 如果没有提供回调函数,则删除该事件的所有监听器
if (!callback) {
delete this.events[event];
return this;
}

// 移除特定的回调函数
const callbacks = this.events[event];
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}

// 支持链式调用
return this;
}
}

当涉及到一次性的事件监听需求时,我们可以进一步扩展 EventBus,以支持一次性事件监听。允许用户在某个事件触发后,自动移除事件监听器,以确保回调函数只执行一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
class EventBus {
// other code ...
public once(eventName: string, callback: Function): this {
const onceWrapper = (...args: any[]) => {
this.off(eventName, onceWrapper);
callback(...args);
};

this.on(eventName, onceWrapper);

return this;
}
}

使用方式

我们将类的封装到 event-bus.ts 中,通过模块的来管理:

1
2
3
export class EventBus {
// ...
}

我们现在已经封装好了一个类,若我们像使用则需要实例化。此处再文件内直接实例化一个类:

1
2
// 创建 EventBus 实例并导出
export const eventBus = new EventBus();

这样使用时可以提供两种方式:

  1. 引入已经实例化的 eventBus

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import { eventBus } from './event-bus';

    // 订阅事件
    eventBus.on('eventName', callback);

    // 触发事件
    eventBus.emit('eventName', data);

    // 移除事件
    eventBus.off('eventName', callback);
  2. 需要多个独立的事件总线实例时,或者希望在不同模块或组件之间使用不同的事件总线时,可以选择额外实例化 eventBus。这样做的目的可能是为了隔离命名的冲突、组件与模块逻辑隔离等原因。

    1
    2
    3
    4
    5
    6
    // events.ts
    import { EventBus } from './event-bus';

    // 创建独立的事件总线实例
    export const eventBusA = new EventBus();
    export const eventBusB = new EventBus();
    1
    2
    3
    4
    5
    6
    7
    8
    9
    import {eventBusA, eventBusB} from './events'

    // 在不同模块或组件中使用不同的事件总线
    eventBusA.on('eventA', callbackA);
    eventBusB.on('eventB', callbackB);

    // 触发不同事件总线上的事件
    eventBusA.emit('eventA', dataA);
    eventBusB.emit('eventB', dataB);

以下是 CodeSandbox 的演示代码:

总结

在本文中,我们深入探讨了 EventBus 的原理,了解了它是如何工作的。我们学习了它的核心操作。除了本文所提及的实现方式,有时候在生产项目中,为了确保代码的可靠性,我们可以考虑使用成熟的第三方库,例如 mitttiny-emitter

这些库已经经过广泛的测试和使用,可以提供稳定和可靠的 EventBus 功能。

🔲 ☆

Promise实现的Scheduler

class Scheduler {
  constructor() {
    super();
    this.queue = [];
    this.maxCount = 2; // 最大并发数
    this.runningCount = 0; // 当前正在运行的任务数
  }

  add(promiseCreator) {
    this.queue.push(promiseCreator);
  }

  start() {
    for (let i = 0; i < this.maxCount; i++) {
      this.run();
    }
  }

  run() {
    if (this.queue.length === 0 || this.runningCount >= this.maxCount) {
      return;
    }

    this.runningCount++;
    const promiseCreator = this.queue.shift();
    const promise = promiseCreator();
    promise
      .then(() => {
        this.runningCount--;
        this.run();
      })
      .catch(() => {
        this.runningCount--;
        this.run();
      });
  }
}

const timeout = (time) => {
  return new Promise((resolve) => {
    setTimeout(resolve, time);
  });
};

const scheduler = new Scheduler();

const addTask = (time, order) => {
  scheduler.add(() => {
    return timeout(time).then(() => console.log(order));
  });
};

addTask(1000, 1);
addTask(500, 2);
addTask(300, 3);
addTask(400, 4);

scheduler.start();

使用 async、await 实现

class Scheduler {
  constructor(maxCount) {
    super()
    this.maxCount = maxCount;
    this.queue = [];
    this.runningCount = 0;
  }

  add(fn) {
    this.queue.push(fn);
  }

  async run() {
    while (this.runningCount < this.maxCount && this.queue.length > 0) {
      this.runningCount++;
      const fn = this.queue.shift();
      await fn();
      this.runningCount--;
    }
  }

  async start() {
    while (this.queue.length > 0) {
      await this.run();
    }
  }
}

const timeout = (time) => {
  return new Promise((resolve) => {
    setTimeout(resolve, time);
  });
};

const scheduler = new Scheduler(2);

const addTask = (time, order) => {
  scheduler.add(async () => {
    await timeout(time);
    console.log(order);
  });
};

addTask(1000, 1);
addTask(500, 2);
addTask(300, 3);
addTask(400, 4);

scheduler.start();
🔲 ⭐

Go 设计模式之 Options-Pattern

Options Pattern 主要是使用于装配属性,让我们先来看看传统的属性装配方案。

结构体

type House struct {    Material     string    HasFireplace bool    Floors       int}

通过构造函数(Constructor)装配属性

// NewHouse("concrete", 5, true)func NewHouse(m string, f int, fp bool) *House {    return &House {        Material: m,        HasFireplace: fp,        Floors: f,    }}

可以看到此时通过一个自定义的构造函数装配属性,此时需装配的属性需要一次性全部填入,且构造函数的入参有顺序性,必须按照函数定义的顺序传入参数。另外,当需装配的属性过于多时,此时构造函数也会越来越冗长。

通过 func 作为参数传入构造函数

type HouseOption func(*House)func WithConcrete() HouseOption {    return func(h *House) {        h.Material = "concrete"    }}func WithoutFireplace() HouseOption {    return func(h *House) {        h.HasFireplace = false    }}func WithFloors(floors int) HouseOption {    return func(h *House) {        h.Floors = floors    }}func NewHouse(opts ...HouseOption) *House {    const (        defaultFloors       = 2        defaultHasFireplace = true        defaultMaterial     = "wood"    )    h := &House{        Material:     defaultMaterial,        HasFireplace: defaultHasFireplace,        Floors:       defaultFloors,    }    // Loop through each option    for _, opt := range opts {        // Call the option giving the instantiated        // *House as the argument        opt(h)    }    // return the modified house instance    return h}// build House with optionsh := NewHouse(  WithConcrete(),  WithoutFireplace(),  WithFloors(3),)

将 func 作为参数传入,一是方便了装配属性的复杂配置,二是不需要固定顺序的构造参数传入,三其实这样的实现方式也可以作为一个属性装配的切面,可以暗搓搓整点活儿。

这就是 Options Patter 了。

参考资料:

🔲 ⭐

设计模式(2)-工厂模式图文介绍

工厂模式

工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

模拟需求①

假设现有一个口罩生产工厂,可以生产防霾口罩、医用一次性口罩、N95口罩
客户可以通过口罩直营店根据自己的需求下单购买口罩
使用代码实现这一流程

传统实现方式

根据给出的需求,结合面向对象思想,大概有以下几个类

  • BaseMask 抽象口罩类
  • HazeMask 防霾口罩类
  • MedicalMask 医用口罩类
  • N95Mask N95口罩类
  • MaskStore 直营店类
  • Client 客户类

简单类图如下:

实现代码

HazeMask、MedicalMask、N95Mask继承自BaseMask,分别实现prepare方法,并调用setName方法设置name属性

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

public abstract class BaseMask {
protected String name;
public abstract void prepare();
public void processing(){
System.out.println(name+"开始加工...");
}
public void bale(){
System.out.println(name+"打包完成...");
}
public void setName(String name) {
this.name = name;
}
}

MaskStore类,实现了口罩直营店根据用户需求进行下单的流程

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
public class MaskStore {
public void order() {
BaseMask mask = null;
int maskType;
do {
maskType = getType();
if (1 == maskType){
mask = new HazeMask();
}else if (2 == maskType){
mask = new MedicalMask();
}else if (3 == maskType){
mask = new N95Mask();
}else {
System.out.println("不支持的产品类型");
break;
}
mask.prepare();
mask.processing();
mask.bale();
} while (true);
}

/**接收用户要下单的产品类型
* 1:防霾口罩
* 2:医用口罩
* 3:n95口罩
* */
private int getType() {
try {
BufferedReader typeReader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("输入需要下单的类型: ");
return Integer.parseInt(typeReader.readLine());
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
}

Client的实现就相对简单,模拟用户下单操作,直接调用直营店暴露的下单order方法

优缺点分析

根据场景需求我们有了如上的代码方案,其中涉及到的类和方法都比较好理解,核心主要是通过用户需要下单的type来进行产品的创建,但优缺点需要细细捋一捋

优点:思路清晰,便于理解
缺点:违反开闭原则,也就是扩展性差,如果添加一个新的口罩类型,涉及到的修改点过多

举个栗子:
如果这时候添加一个新的口罩类型,那所有的口罩直营店类中的代码都需要同步修改

这时候有一种解决方案:将根据类型创建产品的方法单独封装起来,当有新产品加入时,只需要修改单独封装过的这部分代码,而调用方可以做到无感知接入,这种方式也叫做简单工厂模式。但他并不属于23种设计模式,简单工厂仅仅指一种创建类的解决方案

简单工厂模式

相对于传统方案中多出一个简单工厂类SimpleMaskFactory,同时对MaskStore进行了重构,简单类图如下:

代码实现

与传统方案不同的是,之前的口罩产品创建是在MaskStore中,使用简单工厂模式后,将创建口罩产品的工作封装到了SimpleMaskFactory中

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

public class SimpleMaskFactory {

public BaseMask createMask(int maskType) {
BaseMask mask = null;
if (1 == maskType) {
mask = new HazeMask();
} else if (2 == maskType) {
mask = new MedicalMask();
} else if (3 == maskType) {
mask = new N95Mask();
}
return mask;
}
}

MaskStore只需要持有工厂类和需要下单的产品类型,发起下单操作即可

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
public class MaskStore {
private SimpleMaskFactory factory;

public MaskStore(SimpleMaskFactory factory) {
this.factory = factory;
}

public void order() {
BaseMask mask = null;
int maskType;
do {
maskType = getType();
mask = factory.createMask(maskType);
if (!Objects.isNull(mask)){
mask.prepare();
mask.processing();
mask.bale();
}else {
System.out.println("不支持的产品类型...");
break;
}
} while (true);
}

private int getType() {
try {
BufferedReader typeReader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("----------------");
System.out.println("输入需要下单的类型: ");
return Integer.parseInt(typeReader.readLine());
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
}

Client客户端的调用也更加方便

1
2
3
4
5
public class Client {
public static void main(String[] args) {
new MaskStore(new SimpleMaskFactory()).order();
}
}

模拟需求②

假设现有多个口罩生产工厂,大致分为杭州制造和上海制造,可以生产防霾口罩、医用一次性口罩,
客户可以通过自己的需求下单购买某个地址制造的某一种口罩
使用代码实现这一流程

此时的需求不仅有地域区分,同时还有种类区分,这种场景该如何处理呢?

  • 方案1
    • 使用简单工厂模式,根据地域创建不同的工厂类,通过不同的工厂类来进行不同的产品创建
    • 扩展性差,可维护性差
  • 方案2
    • 使用工厂方法模式,将创建产品的方法抽象化,创建对象的操作交给子类自己来完成,即将对象实例化推迟到子类

工厂方法模式

与简单工厂模式所不同,工厂方法模式将定义一个创建对象的抽象方法,根据实际需求整理到所涉及的类有

  • BaseMask 抽象的口罩类
  • HangzhouHazeMask 杭州制造-防霾口罩
  • HangzhouMedicalMask 杭州制造-医用口罩
  • ShanghaiHazeMask 上海制造-防霾口罩
  • ShanghaiMedicalMask 上海制造-医用口罩
  • BaseMaskFactory 抽象口罩工厂类,定义了一个创建对象的抽象方法,将对象创建延缓到子类进行
  • HangzhouMaskFactory 杭州制造工厂类
  • ShanghaiMaskFactory 上海制造工厂类
  • Client 客户类

简单的类图如下:

代码实现

HangzhouHazeMask、HangzhouMedicalMask、ShanghaiHazeMask、ShanghaiMedicalMask继承自BaseMask,分别实现prepare方法,并调用setName方法设置name属性

HangzhouMaskFactory、ShanghaiMaskFactory继承自BaseMaskFactory类,重写了抽象方法createMask方法实现自己的对象创建逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

public abstract class BaseMaskFactory {
//抽象方法,子类自己实现对象的创建
abstract BaseMask createMask(int maskType);

public BaseMaskFactory() {
BaseMask mask = null;
int maskType;
do {
//1:防霾口罩 2:医用口罩
maskType = getType();
mask = createMask(maskType);
if (!Objects.isNull(mask)) {
mask.prepare();
mask.processing();
mask.bale();
}else {
System.out.println("不支持的产品类型...");
break;
}
} while (true);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13

public class HangzhouMaskFactory extends BaseMaskFactory{
@Override
BaseMask createMask(int maskType) {
BaseMask mask = null;
if (1==maskType){
mask = new HangzhouHazeMask();
}else if (2==maskType){
mask = new HangzhouMedicalMask();
}
return mask;
}
}

此时的客户调用,可以有选择性的指定某一地区来进行下单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client {

public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请选择要购买的产品产地,1:杭州,2:上海");
int location = Integer.parseInt(scanner.nextLine());
if (1 == location) {
new HangzhouMaskFactory();
} else if (2 == location) {
new ShanghaiMaskFactory();
} else {
System.out.println("暂无该地区产品");
}
}
}

模拟需求③

假设现有两种产品要进行生产:口罩和酒精,并且有杭州和上海两个工厂都可以生产这两种产品
客户可以通过自己的需求下单购买某个地址制造的某一种产品
使用代码实现这一流程

这次的需求不同以往,产品类型出现了多种,即一个工厂可以生产多种不同类型的产品,这种涉及到多个产品簇,比较推荐使用抽象工厂模式

抽象工厂模式

抽象工厂模式是一种为访问类提供一个创建一组相关或相互依赖对象的接口,
且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。

抽象工厂模式是工厂方法模式的升级版本,工厂方法模式中一个工厂只生产一种产品,而在抽象工厂模式中,一个工厂生产多种产品,并且存在多个工厂

抽象工厂模式中有这两个概念

  • 产品等级:产品等级可以理解为同一类产品属于一个等级,比如防霾口罩、与医用外科口罩都属于口罩类,属于一个产品等级,但口罩和酒精明显不是一个产品等级
  • 产品族:同一个具体工厂所生产的位于不同产品等级的所有产品称作一个产品族。比如杭州工厂生产的杭州口罩和酒精就属于一个产品族

上面的需求用抽象工厂模式的思路得到的简单类图如下:

代码实现

其中HangzhouMask、ShanghaiMask都继承自BaseMask,HangzhouAlcohol、ShanghaiAlcohol继承自BaseAlcohol

通过定义抽象工厂接口AbstractMaskFactory,定义创建产品的方法,交由子类工厂进行实现。这里的产品创建方法可以覆盖到所有的产品等级

1
2
3
4
5

public interface AbstractFactory {
BaseMask createMask();
BaseAlcohol createAlcohol();
}
1
2
3
4
5
6
7
8
9
10
11
12
public class HangzhouFactory implements AbstractFactory{

@Override
public BaseMask createMask() {
return new HangzhouHazeMask();
}

@Override
public BaseAlcohol createAlcohol() {
return new HangzhouAlcohol();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13

public class ShanghaiFactory implements AbstractFactory{

@Override
public BaseMask createMask() {
return new ShanghaiHazeMask();
}

@Override
public BaseAlcohol createAlcohol() {
return new ShanghaiAlcohol();
}
}

创建了工厂类后,客户可以通过某一工厂进行指定产品的下单操作,这些逻辑封装在了Store类中

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
public class Store {
private AbstractFactory factory;

public Store(AbstractFactory factory) {
this.factory = factory;
}

public void orderMask() {
BaseMask mask = null;
mask = factory.createMask();
if (!Objects.isNull(mask)) {
mask.prepare();
mask.processing();
mask.bale();
} else {
System.out.println("不支持的产品类型...");
}
}

public void orderAlcohol() {
BaseAlcohol alcohol = null;
alcohol = factory.createAlcohol();
if (!Objects.isNull(alcohol)) {
alcohol.prepare();
alcohol.processing();
alcohol.bale();
} else {
System.out.println("不支持的产品类型...");
}
}
}

3种工厂模式的总结

本文一共提到了三种工厂模式,简单工厂模式、工厂方法模式、抽象工厂模式,也根据模拟场景对其进行了简单的说明

从上面的介绍中可以简单做下总结

  • 简单工厂模式
    • 实现对象的创建和对象的使用分离,将对象的创建交给专门的工厂类负责
    • 工厂类不够灵活,增加新的具体产品需要修改工厂类的判断逻辑代码
    • 而且产品较多时,工厂方法代码逻辑将会非常复杂
  • 工厂方法模式
    • 定义一个抽象的核心工厂类,并定义创建产品对象的接口,创建具体产品实例的工作延迟到其工厂子类去完成
    • 系统需要新增一个产品是,无需修改现有系统代码,只需要添加一个具体产品类和其对应的工厂子类
    • 系统的扩展性变得很好,符合面向对象编程的开闭原则
  • 抽象工厂模式
    • 工厂模式的升级版,工厂方法模式中一个工厂负责生产一类产品,而抽象工厂模式中一个工厂可以生产多种产品
    • 扩展性更强,无论是增加工厂,还是增加产品,抽象工厂模式都比工厂方法模式更为便捷

关于工厂方法模式和抽象工厂模式的几点区别如下:

  • 工厂方法模式利用继承,抽象工厂模式利用组合
  • 工厂方法模式产生一个对象,抽象工厂模式产生一族对象
  • 工厂方法模式利用子类创造对象,抽象工厂模式利用接口的实现创造对象

常见的工厂模式的运用

  • JDK中Calendar的getlnstance方法
  • JDBC中的Connection对象的获取
  • Spring中IOC容器创建管理bean对象
  • 反射中Class对象的newlnstance方法
🔲 ☆

设计模式(1)-带你了解3类8种单例模式

单例模式的分类

  • 饿汉式
    • 静态常量
    • 静态代码块
  • 懒汉式
    • 线程不安全
    • 线程安全,同步方法
    • 线程安全,同步代码块
    • 双重检查锁
    • 静态内部类
  • 枚举

饿汉式

饿汉式,单例模式的一种类型,对于这个名字可以假想成:

有一天小明买了菜回到家,由于他特别饿,于是就把所有菜都用掉做了满满一桌子菜,而直到最后吃饱,仍然有一些菜从来没尝过,而且由于做的菜太多导致的燃气也用完了。

这里的菜就是我们要使用的对象,而小明就是单例类,燃气就是系统内存。在调用方准备使用对象前,就把所有的对象都实例化好,以供随时调用,但如果实例化工作量过大可能导致内存浪费

饿汉式-静态常量(⭐慎用)

这是最简单的单例模式,主要有以下几点核心思路

  • 私有构造方法
  • 私有静态常量,类加载时初始化常量对象
  • 公有对象获取方法

示例代码如下

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
public class SingletonType01 {
public static void main(String[] args) {
Singleton01 instance1 = Singleton01.getInstance();
Singleton01 instance2 = Singleton01.getInstance();

System.out.println("instance1 == instance2 "+(instance1==instance2));
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}

class Singleton01 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton01() {
}
/**
* 在类加载时创建私有的静态变量
*/
private final static Singleton01 INSTANCE = new Singleton01();

/**
* 对外提供获取对象的静态方法,
* 外部调用,类名.方法名 Singleton.getInstance()
* @return 返回单例对象
*/
public static Singleton01 getInstance() {
return INSTANCE;
}
}

示例代码本机执行结果:

1
2
3
instance1 == instance2 true
491044090
491044090

主方法中对于两次获取到的对象进行了对比,可以看到两者为同一对象,且hashcode相同

优点:

  • 写法简单,在类装载的时候完成实例化,避免线程同步问题

缺点:

  • 在类装载时就实例化,那可能这个对象从始至终都没有被用到,无形中造成资源浪费,没有懒加载效果

这种单例模式,可以使用,并且无需考虑多线程问题,但是存在内存浪费问题

饿汉式-静态代码块(⭐慎用)

饿汉式静态代码块的实现与静态常量基本类似,唯一不同就是对象的实例化从静态变量转移到了静态代码块中,但其都是在类加载是执行的

代码如下

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

/**
*
* @author larsCheng
*/
public class SingletonType02 {
public static void main(String[] args) {
Singleton02 instance1 = Singleton02.getInstance();
Singleton02 instance2 = Singleton02.getInstance();

System.out.println("instance1 == instance2 : "+(instance1==instance2));
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}

class Singleton02 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton02() {
}
/**
* 静态私有变量
*/
private static Singleton02 INSTANCE;

/**
* 将对象的实例化放在了静态代码块中,同样也是类加载时被执行
*/
static {
INSTANCE = new Singleton02();
}
/**
* 对外提供获取对象的静态方法,
* 外部调用,类名.方法名 Singleton.getInstance()
* @return 返回单例对象
*/
public static Singleton02 getInstance() {
return INSTANCE;
}
}

示例代码本机执行结果:

1
2
3
instance1 == instance2 : true
491044090
491044090

可以看出同样是单例对象的效果,所有与饿汉式静态常量写法相比较,其优缺点也一样,都会造成内存浪费

懒汉式

前面提到的两种单例模式都是饿汉式,即无论用不用这个对象,他对会被实例化。

这里要提到的是另一种单例模式-懒汉式,即对象只有在需要使用的时候才进行实例化,同样可以想象成一个小场景

有一天小李特别饿,但是他很懒,不想做饭就到餐馆吃饭,看了菜单从里面选择点了一份牛肉拉面,后厨师傅马上给他做好,小李吃饱后就开心的回家了

虽然描述的比较抽象,小李是是对象使用方,菜单上的每一个菜是一个单例类,后厨师傅是JVM。

当你选定一个对象了之后才会为你立即创建,而不是提前把所有的对象都实例化好。这样实现了懒加载的效果

懒汉式-线程不安全(👎👎👎不可使用)

懒汉式的简易版本,这一实现方式虽然做到了懒加载,但是存在线程安全问题

示例代码如下:

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

public class SingletonType03 {
public static void main(String[] args) {
Singleton03 instance1 = Singleton03.getInstance();
Singleton03 instance2 = Singleton03.getInstance();

System.out.println("instance1 == instance2 : " + (instance1 == instance2));
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}

class Singleton03 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton03() {
}

/**
* 静态私有变量
*/
private static Singleton03 INSTANCE;


/**
* 对外提供获取对象的静态方法,此处存在线程安全问题
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回单例对象
*/
public static Singleton03 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton03();
}
return INSTANCE;
}
}

示例代码本机执行结果:

1
2
3
instance1 == instance2 : true
491044090
491044090

简单的执行测试结果看似乎并无问题,做到了延迟加载(懒加载),并且实现了单例模式

但是!!!这一切都是单线程的前提下,一旦为多线程环境,在getInstance方法中会有严重的线程安全问题

分析:

  • 假设有两个线程A、B
  • A线程先到,判断INSTANCE为空,进入if内,准备进行对象初始化
  • 此时B线程也到达if判断,发现INSTANCE仍为空(A还未完成对象实例化),B也进入if内。

这种情况下,待A、B执行完后,得到的将是两个对象。这就完全违背了单例模式的初衷!!

所以通常情况下,不推荐使用这种懒汉式的单例模式。因为绝大多数的应用场景都为多线程环境。

而在多线程环境下,这种实现方式完全不算单例模式的范畴,因为它会产生多个对象实例

懒汉式 - 同步方法(👎不推荐)

针对于线程不安全问题,对应则有线程安全的解决方案

即在getInstance方法上加入synchronized关键字,将其改造成同步方法,解决在多线程环境下的线程不安全问题

示例代码:

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
class Singleton04 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton04() {
}

/**
* 静态私有变量
*/
private static Singleton04 INSTANCE;

/**
* 对外提供获取对象的静态方法,加入同步关键字,解决线程同步问题
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回单例对象
*/
public static synchronized Singleton04 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton04();
}
return INSTANCE;
}
}

如上,虽然解决了线程不安全问题,但是随之而来的是效率问题

分析:

  • 每次调用getInstance方法都需要进行线程同步
  • 实际上造成多个对象被实例化的仅仅只是方法中代码片段

所以总的来说,虽然解决的线程安全问题,但是由于效率不加,且有优化方案,故此种方式也不建议使用

针对同步方法带来的效率问题,有改进方案,但有一种错误的改进方案这里有必要提一下

同步方法改造为同步代码块,尝试减少同步的代码,来提高效率,示例代码如下:

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
class Singleton04ErrorSolution {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton04ErrorSolution() {
}

/**
* 静态私有变量
*/
private static Singleton04ErrorSolution INSTANCE;

/**
* 对外提供获取对象的静态方法,对造成线程安全问题的代码块进行同步
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回单例对象
*/
public static Singleton04ErrorSolution getInstance() {
if (INSTANCE == null) {
synchronized (Singleton04ErrorSolution.class) {
INSTANCE = new Singleton04ErrorSolution();
}
}
return INSTANCE;
}
}

如上代码的本意是将同步方法细化到同步代码块,来进行效率优化,但是这样的改动起到了相反的效果

分析:

  • 对实例化对象的代码片段进行同步,假设A、B两线程执行getInstance方法
  • A线程判断INSTANCE为空后进入if内,准备执行同步代码块,此时B线程也判断INSTANCE为空,也进入了if内部,等待A线程执行完毕
  • A线程执行完同步代码块后,实例化了一个对象,此时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
25
26
27
28

class Singleton05 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton05() {
}

/**
* 静态私有变量
*/
private static Singleton05 INSTANCE;

/**
* 对外提供获取对象的静态方法,加入同步关键字,解决线程同步问题
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回单例对象
*/
public static Singleton05 getInstance() {
synchronized (Singleton05.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton05();
}
}
return INSTANCE;
}
}

从getInstance方法可以看到,使用了同步代码块的方式,并且同步的是if判断和实例化部分的代码

虽然达到了线程安全,但是基本上和同步方法的效率没什么区别,依旧每个线程进来后,都需要等待执行同步代码块。

这种方案只是为了和上面的错误同步代码块方式进行对比。真实业务中也不推荐使用这种方式!!!

双重检查锁(👍推荐使用)

想要实现懒加载,同时保证线程安全,同时提高效率。那么一起来看看双重检查锁的实现方式:

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
class Singleton06 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton06() {
}

/**
* 静态私有变量
* 声明volatile,防止指令重排,导致的空对象异常
*/
private static volatile Singleton06 INSTANCE;

/**
* 对外提供获取对象的静态方法,使用双重检查锁机制,保证同步代码块中的实例化代码只会被执行一次
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回单例对象
*/
public static Singleton06 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton06.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton06();
}
}
}
return INSTANCE;
}
}

首先先来看看该方案于前几种的不同点

  • 使用synchronized关键字实现同步代码块
  • 同步前同步后两次判断
  • 使用了volatile关键字

分析

在getInstance方法中使用了Double-Check概念,配合同步代码块,保证线程安全。简单分析下其流程

  • A、B、C 3个线程执行getInstance方法
  • A、B线程都通过了第一个if判断,A线程抢到了锁,开始执行同步代码块中的逻辑,B等待
  • A通过了第二个if判断,进行了INSTANCE的实例化操作,A完成操作,释放锁
  • B开始执行同步代码块内容,B未通过第二个if(此时的INSTANCE不为空),直接返回INSTANCE对象,B释放锁
  • 此时C开始执行getInstance方法,C未通过第一个if,直接返回INSTANCE对象

从上面分析过程中可以看到,无论有多少个线程,实例化代码只会被执行一次,意味着只会创建一个对象。

volatile

但是在整个流程中有一个小小的隐患

INSTANCE = new Singleton06();它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:

①第一步:给 INSTANCE 分配内存空间;
②第二步:调用 Singleton06 的构造函数等,来初始化 INSTANCE;
③第三步:将 Singleton06 对象指向分配的内存空间(执行完这步 INSTANCE 就不是 null 了)。

这里的理想执行顺序是 1->2->3,实际在Jvm中执行顺序有可能是1->3->2,也有可能是 1->2->3。
这种现象被称作指令重排也就是说第 2 步和第 3 步的顺序是不能保证的,这就导致了隐患的产生。

在线程A执行INSTANCE = new Singleton06();是,JVM中的执行顺序是1->3->2,先进行分配内存再初始化INSTANCE,若在刚完成内存分配时,线程C开始执行第一个if判断,发现INSTANCE不为空,直接返回INSTANCE对象,此时的INSTANCE明显会出现问题。

在Java内存模型中,volatile 关键字作用可以是保证可见性且禁止指令重排。从而避免由于指令重排导致的异常隐患。

关于 volatile关键字和指令重排相关 可以参考此处

总结

双重检测锁的单例实现方案,可以实现延迟加载,同时线程安全并且效率高,在实际场景中是推荐使用的!

静态内部类(👍推荐使用)

除了双重检查锁被推荐使用外,静态内部类实现单例模式也是被推荐使用的一种
示例代码如下:

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

class Singleton07 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton07() {
}

/**
* 提供一个静态内部类,类中声明一个类型为 Singleton07 的静态属性 INSTANCE
*/
private static class SingletonInstance {
private static final Singleton07 INSTANCE = new Singleton07();
}

/**
* 对外提供获取对象的静态方法,
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回静态内部类的静态属性
*/
public static Singleton07 getInstance() {
return SingletonInstance.INSTANCE;
}
}

分析

  • 该方案采用了类装载机制来保证初始化实例时只有一个线程,从而保证了线程安全
  • 单例类Singleton07被装载时,静态内部类SingletonInstance是不会实例化的,只有调用getInstance方法时才会触发静态内部类SingletonInstance的装载,从而执行实例化代码
  • 并且静态内部类的静态属性只会在第一次加载类的时候被初始化,所以做到了懒加载

结论

保证了线程安全,使用静态内部类的特点实现懒加载,并且有较高效率,推荐使用

枚举(👍推荐使用)

那么这么多的实现方案,Java中有没有一个公认的最佳枚举实现方案呢,当然有啊,通过枚举来实现
代码如下:

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
public class SingletonType08 {
public static void main(String[] args) {
String connection1 = Singleton08.INSTANCE.getConnection();
String connection2 = Singleton08.INSTANCE.getConnection();

System.out.println("connection1 == connection2 : " + (connection1 == connection2));
System.out.println(connection2.hashCode());
System.out.println(connection2.hashCode());
}
}

enum Singleton08 {
/***/
INSTANCE;

/**资源对象,此处以字符串示例*/
private String connection = null;

/**
* 在私有构造中实例化单例对象
*/
Singleton08() {
//模拟实例化过程
this.connection = "127.0.0.1";
}

/**
* 对外提供获取资源对象的静态方法
*/
public String getConnection() {
return connection;
}
}

如上代码是通过枚举来实现单例对象的创建

enum有且仅有private的构造器,防止外部的额外构造,这恰好和单例模式吻合,也为保证单例性做了一个铺垫。
枚举类型是线程安全的,并且只会装载一次。

枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,它保证线程安全,并防止外部反序列化的破坏。

🔲 ⭐

【译】用依赖注入解耦你的代码

用依赖注入解耦你的代码

无需第三方框架

[Icons8 团队](https://unsplash.com/@icons8?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 摄于 [Unsplash](https://unsplash.com/s/photos/ingredients?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)

没有多少组件是能够独立存在而不依赖于其它组件的。除了创建紧密耦合的组件,我们还可以利用依赖注入(DI)来改善 关注点的分离

这篇文章将会脱离第三方框架向你介绍依赖注入的核心概念。所有的示例代码都将使用 Java,但所介绍的一般原则也适用于其它任何语言。


示例:数据处理器

为了让如何使用依赖注入更加形象化,我们将从一个简单的类型开始:

public class DataProcessor {

private final DbManager manager = new SqliteDbManager("db.sqlite");
private final Calculator calculator = new HighPrecisionCalculator(5);

public void processData() {
this.manager.processData();
}

public BigDecimal calc(BigDecimal input) {
return this.calculator.expensiveCalculation(input);
}
}

DataProcessor 有两个依赖项:DbManagerCalculator。直接在我们的类型中创建它们有几个明显的缺点:

  • 调用构造函数时可能发生崩溃
  • 构造函数签名可能会改变
  • 紧密绑定到显式实现类型

是时候改进它了!


依赖注入

《敏捷开发的艺术》 的作者 James Shore 很好地指出

「依赖注入听起来复杂,实际上它的概念却十分简单。」

依赖注入的概念实际上非常简单:为组件提供完成其工作所需的一切。

通常,这意味着通过从外部提供组件的依赖关系来解耦组件,而非直接在组件内创建依赖,让组件间过度耦合。

我们可以通过多种方式为实例提供必要的依赖关系:

  • 构造函数注入
  • 属性注入
  • 方法注入

构造函数注入

构造函数注入,或称基于初始化器的依赖注入,意味着在实例初始化期间提供所有必需的依赖项,将其作为构造函数的参数:

public class DataProcessor {

private final DbManager manager;
private final Calculator calculator;

public DataProcessor(DbManager manager, Calculator calculator) {
this.manager = manager;
this.calculator = calculator;
}

// ...
}

由于这一简单的改变,我们可以弥补大多数最开始的缺点:

  • 易于替换:DbManagerCalculator 不再被具体的实现所束缚,现在可以模拟单元测试了。
  • 已经初始化并且「准备就绪」:我们不必担心依赖项所需要的任何子依赖项(例如,数据库文件名、有效数字(译者注)等),也不必担心它们可在初始化期间发生崩溃的可能性。
  • 强制要求:调用方确切地知道创建 DataProcessor 的所需内容。
  • 不变性:依赖关系始终如初。

尽管构造函数注入是许多依赖注入框架的首选方法,但它也有明显的缺点。其中最大的缺点是:必须在初始化时提供所有依赖项。

有时,我们无法自己初始化一个组件,或者在某个时刻我们无法提供组件的所有依赖关系。或者我们需要使用另外一个构造函数。一旦设置了依赖项,我们就无法再改变它们了。

但是我们可以使用其它注入类型来缓解这些问题。

属性注入

有时,我们无法访问类型实际的初始化方法,只能访问一个已经初始化的实例。或者在初始化时,所需要的依赖关系并不像之后那样明确。

在这些情况下,我们可以使用属性注入而不是依赖于构造函数:

public class DataProcessor {

public DbManager manager = null;
public Calculator calculator = null;

// ...

public void processData() {
// WARNING: Possible NPE
this.manager.processData();
}

public BigDecimal calc(BigDecimal input) {
// WARNING: Possible NPE
return this.calculator.expensiveCalculation(input);
}
}

我们不再需要构造函数了,在初始化后我们可以随时提供依赖项。但这种注入方式也有缺点:易变性

在初始化后,我们不再保证 DataProcessor 是「随时可用」的。能够随意更改依赖关系可能会给我们带来更大的灵活性,但同时也会带来运行时检查过多的缺点。

现在,我们必须在访问依赖项时处理出现 NullPointerException 的可能性。

方法注入

即使我们将依赖项与构造函数注入与/或属性注入分离,我们也仍然只有一个选择。如果在某些情况下我们需要另一个 Calculator 该怎么办呢?

我们不想为第二个 Calculator 类添加额外的属性或构造函数参数,因为将来可能会出现第三个这样的类。而且在每次调用 calc(...) 前更改属性也不可行,并且很可能因为使用错误的属性而导致 bug。

更好的方法是参数化调用方法本身及其依赖项:

public class DataProcessor {

// ...

public BigDecimal calc(Calculator calculator, BigDecimal input) {
return calculator.expensiveCalculation(input);
}
}

现在,calc(...) 的调用者负责提供一个合适的 Calculator 实例,并且 DataProcessor 类与之完全分离。

通过混合使用不同的注入类型来提供一个默认的 Calculator,这样可以获得更大的灵活性:

public class DataProcessor {

// ...

private final Calculator defaultCalculator;

public DataProcessor(Calculator calculator) {
this.defaultCalculator = calculator;
}

// ...

public BigDecimal calc(Calculator calculator, BigDecimal input) {
return Optional.ofNullable(calculator)
.orElse(this.calculator)
.expensiveCalculation(input);
}
}

调用者可以提供另一种类型的 Calculator,但这不是必须的。我们仍然有一个解耦的、随时可用的 DataProcessor,它能够适应特定的场景。

选择哪种注入方式?

每种依赖注入类型都有自己的优点,并没有一种「正确的方法」。具体的选择完全取决于你的实际需求和情况。

构造函数注入

构造函数注入是我的最爱,它也常受依赖注入框架的青睐。

它清楚地告诉我们创建特定组件所需的所有依赖关系,并且这些依赖不是可选的,这些依赖关系在整个组件中应该都是必需的。

属性注入

属性注入更适合可选参数,例如监听或委托。又或是我们无法在初始化时提供依赖关系。

其它编程语言,例如 Swift,大量使用了带属性的 委托模式。因此,使用属性注入将使其它语言的开发人员更熟悉我们的代码。

方法注入

如果在每次调用时依赖项可能不同,那么使用方法注入最好不过了。方法注入进一步解耦组件,它使方法本身持有依赖项,而非整个组件。

请记住,这不是非此即彼。我们可以根据需要自由组合各种注入类型。

控制反转容器

这些简单的依赖注入实现可以覆盖很多用例。依赖注入是很好的解耦工具,但事实上我们仍然需要在某些时候创建依赖项。

但随着应用程序和代码库的增长,我们可能还需要一个更完整的解决方案来简化依赖注入的创建和组装过程。

控制反转(IoC)是 控制流 的抽象原理。依赖注入是控制反转的具体实现之一。

控制反转容器是一种特殊类型的对象,它知道如何实例化和配置其它对象,它也知道如何帮助你执行依赖注入。

有些容器可以通过反射来检测关系,而另一些必须手动配置。有些容器基于运行时,而有些则在编译时生成所需要的所有代码。

比较所有容器的不同之处超出了本文的讨论范围,但是让我通过一个小示例来更好地理解这个概念。

示例: Dagger 2

Dagger 是一个轻量级、编译时进行依赖注入的框架。我们需要创建一个 Module,它就知道如何构建我们的依赖项,稍后我们只要添加 @Inject 注释就可以注入这个 Module

@Module
public class InjectionModule {

@Provides
@Singleton
static DbManager provideManager() {
return manager;
}

@Provides
@Singleton
static Calculator provideCalculator() {
return new HighPrecisionCalculator(5);
}
}

@Singleton 确保只能创建一个依赖项的实例。

要注入依赖项,我们只需要将 @Inject 添加到构造函数、字段或方法中。

public class DataProcessor {

@Inject
DbManager manager;

@Inject
Calculator calculator;

// ...
}

这些仅仅是一些基础知识,乍一看不可能会给人留下深刻的印象。但是控制反转容器和框架不仅解耦了组件,也让创建依赖关系的灵活性得以最大化。

由于提供了高级特性,创建过程的可配置性变得更强,并且支持了使用依赖项的新方法。

高级特性

这些特性在不同类型的控制反转容器和底层语言之间差异很大,比如:

  • 代理模式 和延迟加载。
  • 生命周期(例如:单例模式与每个线程一个实例)。
  • 自动绑定。
  • 单一类型的多种实现。
  • 循环依赖。

这些特性是控制反转容器真正的能力。你可能会认为诸如「循环依赖」这样的特性并非好的主意,确实如此。

但是,如果由于遗留代码或是过去不可更改的错误设计而需要这种奇怪的代码构造,那么我们现在有能力可以这样做。

总结

我们应该根据抽象(例如接口)而不是具体的实现来设计代码,这样可以帮助我们减少代码耦合。

接口必须提供我们代码所需要的唯一信息,我们不能对实际实现情况做任何假设。

「程序应当依赖抽象,而非具体的实现」
—— Robert C. Martin (2000), 《设计原则与设计模式》

依赖注入是通过解耦组件来实现这一点的好办法。它使我们能够编写更简洁明了、更易于维护和重构的代码。

选择三种依赖注入类型中的哪种很大程度上取决于环境和需求,但是我们也可以混合使用三种类型使收益最大化。

控制反转容器有时几乎以一种神奇的方式通过简化组件创建过程来提供另一种便利的布局。

我们应该处处使用它吗?当然不是。

就像其它模式和概念一样,我们应该在适当的时候应用它们,而不是能用则用。

永远不要把自己局限在一种做事的方式上。也许 工厂模式 甚至是广为厌恶的 单例模式 是能够满足你需求的更好的解决方案。


资料


控制反转容器

Java

Kotlin

Swift

C

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

🔲 ⭐

《Head First 设计模式》学习笔记 | 策略模式

前言

我最近在看大名鼎鼎的《Head First 设计模式》。这本「OO 圣经」用 Java 实现各类设计模式,对于我 —— 一个非 Java 爱好者而言,读起来并不过瘾。

有人读完这本书可能会误解设计模式就是设计 Interface,而事实并非如此。在知乎的一个问题《Python 里没有接口,如何写设计模式?》中,vczh 轮子哥是这样回答的:

设计模式搞了那么多东西就是在告诉你如何在各种情况下解耦你的代码,让你的代码在运行时可以互相组合。这就跟兵法一样。难道有了飞机大炮,兵法就没有用了吗?

我觉得这个比喻很好,不同的语言就像不同的兵器,各有各的特点与使用方式,而设计模式就是那套「兵法」,无论你使用何种兵器,不过是「纵横不出方圆,万变不离其宗」。而只看书中一种「兵器」未免太少,不如我们多试几样?

本篇就来看一看第一章「兵法」 —— 策略模式(Strategy Pattern)。

定义

书中对策略模式的定义如下:

策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

下面以书中的「模拟鸭子应用」为例。

继承的弊端

你要设计一个鸭子游戏,游戏里有各种各样的鸭子,它们会游泳(swim()),还会呱呱叫(quack()),每种鸭子拥有不同的外观(display())。

一开始,你可能会设计一个鸭子的超类 Duck,然后让所有不同种类的鸭子继承它:

设计一个鸭子超类(Superclass)

如果此时我们想让鸭子飞起来,就要在超类中增加一个 fly() 方法:

让鸭子飞

此时,鸭子家族来了一只擅于代码调试工作的小黄鸭。

此时,一切都乱套了,这位代码调试工作者会发出「吱吱」的叫声,但却不会飞,然而它却从鸭子超类继承了 quack()fly() 方法。为了让它尊重客观事实,我们需要在小黄鸭类中覆盖超类的 quack()fly() 方法,让它变得不会叫也不会飞。

在小黄鸭中覆盖原有的方法

虽然我们用「覆盖方法」的手段解决了小黄鸭的问题,但未来我们可能还会制造更多奇奇怪怪的鸭子。例如周黑鸭或北京烤鸭,它们显然既不会叫,也不会游泳,还不会飞,这时我们又要为它们重写所有的行为吗?利用继承的方式来为不同种类的鸭子提供行为显然不够灵活。

抽离可变行为

不同的鸭子具有不同的行为,鸭子的行为应当是灵活可变的

设计原则一:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。

因此,利用上述原则,我们把「鸭子的行为」从鸭子类(Duck)中抽离出来。

取出容易变化的行为

实现被抽离的行为

设计原则二:针对接口编程,而不是针对实现编程。

我们将这些被抽离出的行为归类:

  • 所有具体的飞行行为属于飞行策略
  • 所有具体的叫声行为属于叫声策略
  • 所有具体的游泳行为属于游泳策略
  • ……

我们可以利用接口或抽象类代表这些策略,然后让特定的具体行为来实现这些策略中的方法

例如,我们的飞行策略名为 FlyBehavior,我们将它设计为一个抽象类(当然也可以是接口)。然后,我们有两种具体的飞行方式 FlyWithWings(会飞)和 FlyNoWay(不会飞),它们需要实现飞行策略中的 fly() 方法:

整合

此时,我们已经将可变的行为从鸭子超类(Duck)中抽离,并把它们用具体的「行为类」进行表示。我们希望:如果鸭子要执行某个行为,它不需要自己处理,而是将这一行为委托给具体的「行为类」

因此,我们可以在鸭子超类(Duck)中加入「行为类」的实例变量,从而通过这些实例变量来调用具体的行为方法。

Class Duckfly() 方法中,我们可以使用实例 flyBehavior 调用具体的行为方法,从而达成「委托」的目的:

public function fly() 
{
$this->flyBehavior->fly();
}

具体实现

下面来看看不同语言的具体实现:

PHP

PHP 有抽象类也有接口,语法和 Java 比较接近。实现方法中规中矩,和书中的并无二致。只不过这里我把行为接口改成了抽象类。类图如下:

UML 类图关系

具体实现:

<?php
// 飞行行为类
abstract class FlyBehavior
{
abstract public function fly();
}

// 「飞」的具体行为
class FlyWithWings extends FlyBehavior
{
public function fly()
{
echo "会飞\n";
}
}

class FlyNoWay extends FlyBehavior
{
public function fly()
{
echo "不会飞\n";
}
}

// 叫声行为类
abstract class QuackBehavior
{
abstract public function quack();
}

// 「叫」的具体行为
class Quack extends QuackBehavior
{
public function quack()
{
echo "呱呱\n";
}
}

class Squeak extends QuackBehavior
{
public function quack()
{
echo "吱吱\n";
}
}

class MuteQuack extends QuackBehavior
{
public function quack()
{
echo "不会叫\n";
}
}

// 鸭子类
abstract class Duck
{
protected $flyStrategy;
protected $quackStrategy;

public function fly()
{
$this->flyStrategy->fly();
}

public function quack()
{
$this->quackStrategy->quack();
}
}

// 有只小黄鸭
class YellowDuck extends Duck
{
public function __construct($flyStrategy, $quackStrategy)
{
$this->flyStrategy = $flyStrategy;
$this->quackStrategy = $quackStrategy;
}
}

$yellowDuck = new YellowDuck(new FlyNoWay(), new Squeak());
$yellowDuck->fly();
$yellowDuck->quack();

/* Output:
不会飞
吱吱
*/
?>

Python

Python 就没有所谓的抽象类和接口了,当然你也可以通过 abc 模块来实现这些功能。

比较简单的做法是:将具体行为直接定义为函数,在初始化鸭子时通过构造函数传入行为函数,赋值给对应的变量。当执行具体行为时,将直接调用被赋值的变量,这时具体的行为动作就被委托给了传入的行为函数,达到了「委托」的效果。

class Duck:
def __init__(self, fly_strategy, quack_strategy):
self.fly_strategy = fly_strategy
self.quack_strategy = quack_strategy

def fly(self):
self.fly_strategy()

def quack(self):
self.quack_strategy()

def fly_with_wings():
print("会飞")

def fly_no_way():
print("不会飞")

def quack():
print("呱呱")

def squeak():
print("吱吱")

def mute_quack():
print("不会叫")

# 一只会飞也不会叫的小黄鸭
yellow_duck = Duck(fly_no_way, mute_quack)
yellow_duck.fly()
yellow_duck.quack()

# Output:
# 不会飞
# 不会叫

Golang

在 Go 语言中没有 extends 关键字,但可以通过在结构体中内嵌匿名类型的方式实现继承关系。此处,将 FlyBehavior 飞行行为和 QuackBehavior 行为声明为接口。

package main

import "fmt"

// FlyBehavior 飞行行为接口
type FlyBehavior interface {
fly()
}

// QuackBehavior 呱呱叫行为接口
type QuackBehavior interface {
quack()
}

// FlyWithWings 会飞的类
type FlyWithWings struct {
}

func (flyWithWings FlyWithWings) fly() {
fmt.Println("会飞")
}

// FlyWithWings 不会飞的类
type FlyNoWay struct{}

func (flyNoWay FlyNoWay) fly() {
fmt.Println("不会飞")
}

// Quack 呱呱叫
type Quack struct{}

func (quack Quack) quack() {
fmt.Println("呱呱")
}

// Squeak 吱吱叫
type Squeak struct{}

func (squeak Squeak) quack() {
fmt.Println("吱吱")
}

// MuteQuack 不会叫
type MuteQuack struct{}

func (muteQuack MuteQuack) quack() {
fmt.Println("不会叫")
}

// Duck 鸭子类
type Duck struct {
FlyBehavior FlyBehavior
QuackBehavior QuackBehavior
}

func (d *Duck) fly() {
d.FlyBehavior.fly() // 委托给飞行行为
}

func (d *Duck) quack() {
d.QuackBehavior.quack() // 委托给呱呱叫行为
}

func main() {
yellowDuck := Duck{FlyNoWay{}, Squeak{}}
yellowDuck.fly()
yellowDuck.quack()
}

/* Output:
不会飞
吱吱
*/

总结

三种设计原则:

  1. 封装变化
  2. 多用组合,少用继承
  3. 针对接口编程,不针对实现编程

注意此处的「针对接口编程」,书中也有强调:

「针对接口编程」真正的意思是「针对超类型(supertype)编程」。这里所谓的「接口」有多个含义,接口是一个「概念」,也是一种 Java 的 interface 构造。你可以在不涉及 Java interface 的情况下「针对接口编程」,关键就在多态。利用多态,程序可以针对超类型编程,执行时会根据实际状况执行到真正的行为。

因此,你不用拘泥于 interface,你所用的语言就算没有 interface 也能实现设计模式。

🔲 ⭐

设计模式之桥接模式

场景问题

发送消息

现在我们要实现这样一个功能:发送消息。从业务上看,消息又分成普通消息、加急消息和特急消息多种,不同的消息类型,业务功能处理是不一样的,比如加急消息是在消息上添加“加急”字样,而特急消息除了添加特急外,还会做一条催促的记录,多久不完成会继续催促。从发送消息的手段上看,又有系统内短消息、手机短消息、邮件等等。现在要实现这样的发送提示消息的功能,该如何实现呢?

不用模式的解决方案

实现简化版本

先实现一个简单点的版本:消息只是实现发送普通消息,发送的方式先实现系统内短消息和邮件。其它的功能,等这个版本完成过后,再继续添加,这样先把问题简单化,实现起来会容易一点。由于发送普通消息会有两种不同的实现方式,为了让外部能统一操作,因此,把消息设计成接口,然后由两个不同的实现类,分别实现系统内短消息方式和邮件发送消息的方式。此时系统结构如下:
2019-9-2-12-30-41.png

先来看看消息的统一接口,示例代码如下:

1
2
3
4
5
6
7
8
9
10
public interface Message {

/**
* 发送消息
*
* @param message 要发送的消息内容
* @param toUser 消息发送的目的人员
*/
void send(String message, String toUser);
}

再来分别看看两种实现方式,这里只是为了示意,并不会真的去发送Email和站内短消息,先看站内短消息的方式,示例代码如下:

1
2
3
4
5
6
7
public class CommonMessageSMS implements Message {

@Override
public void send(String message, String toUser) {
System.out.println("使用站内短消息的方式,发送消息'" + message + "'给" + toUser);
}
}

同样的,实现以Email的方式发送普通消息,示例代码如下:

1
2
3
4
5
6
7
public class CommonMessageEmail implements Message {

@Override
public void send(String message, String toUser) {
System.out.println("使用Email的方式,发送消息'" + message + "'给" + toUser);
}
}

实现发送加急消息

上面的实现,看起来很简单,对不对。接下来,添加发送加急消息的功能,也有两种发送的方式,同样是站内短消息和Email的方式。
加急消息的实现跟普通消息不同,加急消息会自动在消息上添加加急,然后再发送消息;另外加急消息会提供监控的方法,让客户端可以随时通过这个方法来了解对于加急消息处理的进度,比如:相应的人员是否接收到这个信息,相应的工作是否已经开展等等。因此加急消息需要扩展出一个新的接口,除了基本的发送消息的功能,还需要添加监控的功能,这个时候,系统的结构如图所示:
2019-9-2-12-31-39.png

先看看扩展出来的加急消息的接口,示例代码如下:

1
2
3
4
5
6
7
8
9
10
public interface UrgencyMessage extends Message {

/**
* 监控某消息的处理过程
*
* @param messageId 被监控的消息的编号
* @return 包含监控到的数据对象,这里示意一下,所以用了Object
*/
Object watch(String messageId);
}

相应的实现方式还是发送站内短消息和Email两种,同样需要两个实现类来分别实现这两种方式,先看站内短消息的方式,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UrgencyMessageSMS implements UrgencyMessage {

@Override
public void send(String message, String toUser) {
message = "加急:" + message;
System.out.println("使用站内短消息的方式,发送消息'" + message + "'给" + toUser);
}

@Override
public Object watch(String messageId) {
//获取相应的数据,组织成监控的数据对象,然后返回
return null;
}
}

再看看Emai的方式,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UrgencyMessageEmail implements UrgencyMessage {

@Override
public void send(String message, String toUser) {
message = "加急:" + message;
System.out.println("使用Email的方式,发送消息'" + message + "'给" + toUser);
}

@Override
public Object watch(String messageId) {
//获取相应的数据,组织成监控的数据对象,然后返回
return null;
}
}

事实上,在实现加急消息发送的功能上,可能会使用前面发送不同消息的功能,也就是让实现加急消息处理的对象继承普通消息的相应实现,这里为了让结构简单一点,清晰一点,所以没有这样做。

有何问题

上面这样实现,好像也能满足基本的功能要求,可是这么实现好不好呢?有没有什么问题呢?
我们继续向下来添加功能实现,为了简洁,就不再去进行代码示意了,通过实现的结构示意图就可以看出实现上的问题。

继续添加特急消息的处理

特急消息不需要查看处理进程,只要没有完成,就直接催促,也就是说,对于特急消息,在普通消息的处理基础上,需要添加催促的功能。而特急消息、还有催促的发送方式,相应的实现方式还是发送站内短消息和Email两种,此时系统的结构如图所示:
2019-9-2-12-32-9.png

仔细观察上面的系统结构示意图,会发现一个很明显的问题,那就是:通过这种继承的方式来扩展消息处理,会非常不方便。
你看,实现加急消息处理的时候,必须实现站内短消息和Email两种处理方式,因为业务处理可能不同;在实现特急消息处理的时候,又必须实现站内短消息和Email这两种处理方式。
这意味着,以后每次扩展一下消息处理,都必须要实现这两种处理方式,是不是很痛苦,这还不算完,如果要添加新的实现方式呢?继续向下看吧。

继续添加发送手机消息的处理方式

如果看到上面的实现,你还感觉问题不是很大的话,继续完成功能,添加发送手机消息的处理方式
仔细观察现在的实现,如果要添加一种新的发送消息的方式,是需要在每一种抽象的具体实现里面,都要添加发送手机消息的处理的。也就是说:发送普通消息、加急消息和特急消息的处理,都可以通过手机来发送。这就意味着,需要添加三个实现。此时系统结构如图所示:
2019-9-2-12-32-31.png

这下能体会到这种实现方式的大问题了吧。

小结一下出现的问题

采用通过继承来扩展的实现方式,有个明显的缺点:扩展消息的种类不太容易,不同种类的消息具有不同的业务,也就是有不同的实现,在这种情况下,每个种类的消息,需要实现所有不同的消息发送方式。
更可怕的是,如果要新加入一种消息的发送方式,那么会要求所有的消息种类,都要加入这种新的发送方式的实现。
要是考虑业务功能上再扩展一下呢?比如:要求实现群发消息,也就是一次可以发送多条消息,这就意味着很多地方都得修改,太恐怖了。
那么究竟该如何实现才能既实现功能,又能灵活的扩展呢?

解决方案

桥接模式来解决

用来解决上述问题的一个合理的解决方案,就是使用桥接模式。那么什么是桥接模式呢?
桥接模式定义:

将抽象部分和实现部分分离,使它们都可以独立地变化

应用桥接模式来解决的思路

仔细分析上面的示例,根据示例的功能要求,示例的变化具有两个维度,一个维度是抽象的消息这边,包括普通消息、加急消息和特急消息,这几个抽象的消息本身就具有一定的关系,加急消息和特急消息会扩展普通消息;另一个维度在具体的消息发送方式上,包括站内短消息、Email和手机短信息,这几个方式是平等的,可被切换的方式。这两个维度一共可以组合出9种不同的可能性来。
现在出现问题的根本原因,就在于消息的抽象和实现是混杂在一起的,这就导致了,一个维度的变化,会引起另一个维度进行相应的变化,从而使得程序扩展起来非常困难。
要想解决这个问题,就必须把这两个维度分开,也就是将抽象部分和实现部分分开,让它们相互独立,这样就可以实现独立的变化,使扩展变得简单。
桥接模式通过引入实现的接口,把实现部分从系统中分离出去;那么,抽象这边如何使用具体的实现呢?肯定是面向实现的接口来编程了,为了让抽象这边能够很方便的与实现结合起来,把顶层的抽象接口改成抽象类,在里面持有一个具体的实现部分的实例。
这样一来,对于需要发送消息的客户端而言,就只需要创建相应的消息对象,然后调用这个消息对象的方法就可以了,这个消息对象会调用持有的真正的消息发送方式来把消息发送出去。也就是说客户端只是想要发送消息而已,并不想关心具体如何发送。

模式结构和说明

桥接模式的结构图:
2019-9-2-12-32-50.png

  • Abstraction:抽象部分的接口。通常在这个对象里面,要维护一个实现部分的对象引用,在抽象对象里面的方法,需要调用实现部分的对象来完成。这个对象里面的方法,通常都是跟具体的业务相关的方法。
  • RefinedAbstraction:扩展抽象部分的接口,通常在这些对象里面,定义跟实际业务相关的方法,这些方法的实现通常会使用Abstraction中定义的方法,也可能需要调用实现部分的对象来完成。
  • Implementor:定义实现部分的接口,这个接口不用和Abstraction里面的方法一致,通常是由Implementor接口提供基本的操作,而Abstraction里面定义的是基于这些基本操作的业务方法,也就是说Abstraction定义了基于这些基本操作的较高层次的操作。
  • ConcreteImplementor:真正实现Implementor接口的对象。

桥接模式示例代码

先看看Implementor接口的定义,示例代码如下:

1
2
3
4
public interface Implementor {

void operationImpl();
}

再看看Abstraction接口的定义,注意一点,虽然说是接口定义,但其实是实现成为抽象类。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class Abstraction {

/**
* 持有一个实现部分的对象
*/
protected Implementor impl;

/**
* 构造方法,传入实现部分的对象
*
* @param impl 实现部分的对象
*/
public Abstraction(Implementor impl) {
this.impl = impl;
}

public void operation() {
impl.operationImpl();
}
}

该来看看具体的实现了,示例代码如下:

1
2
3
4
5
6
public class ConcreteImplementorA implements Implementor {

public void operationImpl() {
//真正的实现
}
}

另外一个实现,示例代码如下:

1
2
3
4
5
6
public class ConcreteImplementorB implements Implementor {

public void operationImpl() {
//真正的实现
}
}

最后来看看扩展Abstraction接口的对象实现,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RefinedAbstraction extends Abstraction {

public RefinedAbstraction(Implementor impl) {
super(impl);
}

/**
* 示例操作,实现一定的功能
*/
public void otherOperation() {

//实现一定的功能,可能会使用具体实现部分的实现方法,
//但是本方法更大的可能是使用Abstraction中定义的方法,
//通过组合使用Abstraction中定义的方法来完成更多的功能
}
}

使用桥接模式重写示例

学习了桥接模式的基础知识过后,该来使用桥接模式重写前面的示例了。通过示例,来看看使用桥接模式来实现同样的功能,是否能解决“既能方便的实现功能,又能有很好的扩展性”的问题。
要使用桥接模式来重新实现前面的示例,首要任务就是要把抽象部分和实现部分分离出来,分析要实现的功能,抽象部分就是各个消息的类型所对应的功能,而实现部分就是各种发送消息的方式。
其次要按照桥接模式的结构,给抽象部分和实现部分分别定义接口,然后分别实现它们就可以了。

从简单功能开始

从相对简单的功能开始,先实现普通消息和加急消息的功能,发送方式先实现站内短消息和Email这两种。使用桥接模式来实现这些功能的程序结构如图所示
2019-9-2-12-33-19.png

还是看看代码实现,会更清楚一些。先看看消息发送器接口,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 消息发送器
*
* @author HansChen
*/
public interface MessageSender {

/**
* 发送消息
*
* @param message 要发送的消息内容
* @param toUser 消息发送的目的人员
*/
void send(String message, String toUser);
}

再看看抽象部分定义的接口,示例代码如下:

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
/**
* 抽象的消息对象
*
* @author HansChen
*/
public class AbstractMessageController {

/**
* 持有一个实现部分的对象
*/
MessageSender impl;

/**
* 构造方法,传入实现部分的对象
*
* @param impl 实现部分的对象
*/
AbstractMessageController(MessageSender impl) {
this.impl = impl;
}

/**
* 发送消息,转调实现部分的方法
*
* @param message 要发送的消息内容
* @param toUser 消息发送的目的人员
*/
protected void sendMessage(String message, String toUser) {
impl.send(message, toUser);
}
}

看看如何具体的实现发送消息,先看站内短消息的实现吧,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 以站内短消息的方式发送消息
*
* @author HansChen
*/
public class MessageSenderSMS implements MessageSender {

@Override
public void send(String message, String toUser) {
System.out.println("使用站内短消息的方式,发送消息'" + message + "'给" + toUser);
}
}

再看看Email方式的实现,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 以Email的方式发送消息
*
* @author HansChen
*/
public class MessageSenderEmail implements MessageSender {

@Override
public void send(String message, String toUser) {
System.out.println("使用Email的方式,发送消息'" + message + "'给" + toUser);
}
}

接下来该看看如何扩展抽象的消息接口了,先看普通消息的实现,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class CommonMessageController extends AbstractMessageController {

public CommonMessageController(MessageSender impl) {
super(impl);
}

@Override
public void sendMessage(String message, String toUser) {
//对于普通消息,什么都不干,直接调父类的方法,把消息发送出去就可以了
super.sendMessage(message, toUser);
}
}

再看看加急消息的实现,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class UrgencyMessageController extends AbstractMessageController {

public UrgencyMessageController(MessageSender impl) {
super(impl);
}

@Override
protected void sendMessage(String message, String toUser) {
message = "加急:" + message;
super.sendMessage(message, toUser);
}

/**
* 扩展自己的新功能:监控某消息的处理过程
*
* @param messageId 被监控的消息的编号
* @return 包含监控到的数据对象,这里示意一下,所以用了Object
*/
public Object watch(String messageId) {
//获取相应的数据,组织成监控的数据对象,然后返回
return null;
}
}

添加功能

看了上面的实现,发现使用桥接模式来实现也不是很困难啊,关键得看是否能解决前面提出的问题,那就来添加还未实现的功能看看,添加对特急消息的处理,同时添加一个使用手机发送消息的方式。该怎么实现呢?
很简单,只需要在抽象部分再添加一个特急消息的类,扩展抽象消息就可以把特急消息的处理功能加入到系统中了;对于添加手机发送消息的方式也很简单,在实现部分新增加一个实现类,实现用手机发送消息的方式,也就可以了。
这么简单?好像看起来完全没有了前面所提到的问题。的确如此,采用桥接模式来实现过后,抽象部分和实现部分分离开了,可以相互独立的变化,而不会相互影响。因此在抽象部分添加新的消息处理,对发送消息的实现部分是没有影响的;反过来增加发送消息的方式,对消息处理部分也是没有影响的。

接着看看代码实现,先看看新的特急消息的处理类,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SpecialUrgencyMessageController extends AbstractMessageController {

public SpecialUrgencyMessageController(MessageSender impl) {
super(impl);
}

@Override
protected void sendMessage(String message, String toUser) {
message = "特急:" + message;
super.sendMessage(message, toUser);
}

public void hurry(String messageId) {
//执行催促的业务,发出催促的信息
}
}

再看看使用手机短消息的方式发送消息的实现,示例代码如下:

1
2
3
4
5
6
7
public class MessageSenderMobile implements MessageSender {

@Override
public void send(String message, String toUser) {
System.out.println("使用手机的方式,发送消息'" + message + "'给" + toUser);
}
}

测试一下功能

看了上面的实现,可能会感觉得到,使用桥接模式来实现前面的示例过后,添加新的消息处理,或者是新的消息发送方式是如此简单,可是这样实现,好用吗?写个客户端来测试和体会一下,示例代码如下:

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
public class Client {

public static void main(String[] args) {
//创建具体的实现对象
MessageSender impl = new MessageSenderSMS();

//创建一个普通消息对象
AbstractMessageController controller = new CommonMessageController(impl);
controller.sendMessage("请喝一杯茶", "小李");

//创建一个紧急消息对象
controller = new UrgencyMessageController(impl);
controller.sendMessage("请喝一杯茶", "小李");

//创建一个特急消息对象
controller = new SpecialUrgencyMessageController(impl);
controller.sendMessage("请喝一杯茶", "小李");


//把实现方式切换成手机短消息,然后再实现一遍
impl = new MessageSenderMobile();
controller = new CommonMessageController(impl);
controller.sendMessage("请喝一杯茶", "小李");

controller = new UrgencyMessageController(impl);
controller.sendMessage("请喝一杯茶", "小李");

controller = new SpecialUrgencyMessageController(impl);
controller.sendMessage("请喝一杯茶", "小李");
}
}

运行结果如下:

1
2
3
4
5
6
使用站内短消息的方式,发送消息'请喝一杯茶'给小李
使用站内短消息的方式,发送消息'加急:请喝一杯茶'给小李
使用站内短消息的方式,发送消息'特急:请喝一杯茶'给小李
使用手机的方式,发送消息'请喝一杯茶'给小李
使用手机的方式,发送消息'加急:请喝一杯茶'给小李
使用手机的方式,发送消息'特急:请喝一杯茶'给小李

前面三条是使用的站内短消息,后面三条是使用的手机短消息,正确的实现了预期的功能。看来前面的实现应该是正确的,能够完成功能,且能灵活扩展。

广义桥接-Java中无处不桥接

使用Java编写程序,一个很重要的原则就是“面向接口编程”,说得准确点应该是“面向抽象编程”,由于在Java开发中,更多的使用接口而非抽象类,因此通常就说成“面向接口编程”了。接口把具体的实现和使用接口的客户程序分离开来,从而使得具体的实现和使用接口的客户程序可以分别扩展,而不会相互影响。

桥接模式中的抽象部分持有具体实现部分的接口,最终目的是什么,还不是需要通过调用具体实现部分的接口中的方法,来完成一定的功能,这跟直接使用接口没有什么不同,只是表现形式有点不一样。再说,前面那个使用接口的客户程序也可以持有相应的接口对象,这样从形式上就一样了。

也就是说,从某个角度来讲,桥接模式不过就是对“面向抽象编程”这个设计原则的扩展。正是通过具体实现的接口,把抽象部分和具体的实现分离开来,抽象部分相当于是使用实现部分接口的客户程序,这样抽象部分和实现部分就松散耦合了,从而可以实现相互独立的变化。

这样一来,几乎可以把所有面向抽象编写的程序,都视作是桥接模式的体现,至少算是简化的桥接模式,就算是广义的桥接吧。而Java编程很强调“面向抽象编程”,因此,广义的桥接,在Java中可以说是无处不在。

桥接模式在Android中的应用

如果各位童鞋看到这里仍然对桥接模式还是不太清楚,在这里给大家举个在Android中非常常用的桥接模式栗子:AbsListViewListAdapter之间的桥接模式。童鞋们可以根据这个栗子体会一下桥接模式的好处。

🔲 ☆

设计模式之代理模式

概述

我们执行一个功能的函数时,经常需要在其中写入与功能不是直接相关但很有必要的代码,如日志记录、信息发送、安全和事务支持等,这些枝节性代码虽然是必要的,但它会带来以下麻烦:

  • 枝节性代码游离在功能性代码之外,它下是函数的目的
  • 枝节性代码会造成功能性代码对其它类的依赖,加深类之间的耦合
  • 枝节性代码带来的耦合度会造成功能性代码移植困难,可重用性降低

毫无疑问,枝节性代码和功能性代码需要分开来才能降低耦合程度,我们可以使用代理模式(委托模式)完成这个要求。代理模式的作用是:为其它对象提供一种代理以控制对这个对象的访问。在某些情况下,一 个客户不想直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介作用。

代理模式一般涉及到三个角色:

  • 抽象角色:声明真实对象和代理对象的共同接口
  • 代理角色:代理对象内部包含有真实角色的引用,从而可以操作真实角色,同时代理对象 与真实对象有相同的接口,能在任何时候代替真实对象,代理对象可以在执行真实对 象前后加入特定的逻辑以实现功能的扩展。
  • 真实角色:代理角色所代表的真实对象,是我们最终要引用的对象

常见的代理应用场景有:

  • 远程代理:对一个位于不同的地址空间对象提供一个局域代表对象,如RMI中的stub
  • 虚拟代理:根据需要将一个资源消耗很大或者比较复杂的对象,延迟加载,在真正需要的时候才创建
  • 保护代理:控制对一个对象的访问权限
  • 智能引用:提供比目标对象额外的服务和功能

接下来,我们用代码来说明什么是代理模式

代理模式

UML图

先看看代理模式的结构图:
2019-9-2-12-29-26.png

代码

下面给出一个小栗子说明代理模式,先定义一个抽象角色,也就是一个公共接口,声明一些需要代理的方法,本文定义一个Subject接口,为了简单说明,只是在里面定义一个request方法:

1
2
3
4
public interface Subject {

void request();
}

定义Subject的实现类RealSubject,它是一个真实角色:

1
2
3
4
5
6
7
public class RealSubject implements Subject {

@Override
public void request() {
System.out.print("do real request");
}
}

定义一个代理角色ProxySubject,跟RealSubject一样,它也继承了Subject接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ProxySubject implements Subject {

private RealSubject mSubject;

public ProxySubject() {
mSubject = new RealSubject();
}

@Override
public void request() {
System.out.print("before");
mSubject.request();
System.out.print("after");
}
}

客户端调用代码

1
2
3
4
5
6
7
8
public class Client {

public static void main(String[] args) {

Subject subject = new ProxySubject();
subject.request();
}
}

这样,一个简易的代理模式模型就建立了,客户端在使用过程中,无需关注RealSubject,只需要关注ProxySubject就行了,并且可以在ProxySubject中插入一些非功能信的代码,比如输出Log,统计执行时间等等

远程代理

远程代理,对一个位于不同的地址空间对象提供一个局域代表对象。这样说大家可能比较抽象,不太能理解,但其实童鞋们可能在就接触过了,在Android中,Binder的使用就是典型的远程代理。比如ActivityManager:
2019-9-2-12-29-57.png

在启动Activity的时,会调用ActivityManager的startActivity方法,我们看看Activity是怎么获取的:

1
2
3
4
5
6
7
8
9
10
11
12
static public IActivityManager asInterface(IBinder obj) {
if (obj == null) {
return null;
}
IActivityManager in =
(IActivityManager)obj.queryLocalInterface(descriptor);
if (in != null) {
return in;
}
// 返回代理类
return new ActivityManagerProxy(obj);
}

可以看到,最终是返回了一个ActivityManager的代理类,因为真正的ActivityManager是运行在内核空间的,Android应用无法直接访问得到,那么就可以借助这个ActivityManagerProxy,通过Binder与真正的ActivityManager,也就是ActivityManagerService交互。其中ActivityManagerService和ActivityManagerProxy都实现了同一个接口:IActivityManager。这个就是Android中典型的代理模式的栗子了。至于ActivityManagerService和ActivityManagerProxy是如何通过Binder实现远程调用,这个就是另一个话题Binder的内容了,这里不再做阐述

延迟加载

根据需要将一个资源消耗很大或者比较复杂的对象,延迟加载,在真正需要的时候才创建。假设我们创建RealSubject需要耗费一定的资源,那么,我们可以把创建它延迟到实际调用的时候,优化Client初始化速度,比如,这样修改ProxySubject以达到延迟加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ProxySubject implements Subject {

private RealSubject mSubject;

public ProxySubject() {
}

@Override
public void request() {
// 延时加载
if (mSubject == null) {
mSubject = new RealSubject();
}
mSubject.request();
}
}

Client在实例化ProxySubject的时候,不需消耗资源,而是等到真正调用request的时候,才会加载RealSubject,达到延时加载的效果

保护代理

可以在Proxy类中加入进行权限,验证是否具有执行真实代码的权限,只有权限验证通过了才进行真实对象的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ProxySubject implements Subject {

private RealSubject mSubject;
private User mUser;

public ProxySubject(User user) {
this.mUser = user;
}

@Override
public void request() {
// 验证权限
if (mUser.isLogin()) {
mSubject.request();
}
}
}

额外功能

通过引入代理类,可以方便地在功能性代码前后插入扩展,如Log输出,调用统计等,实现对原代码的无侵入式代码扩展,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ProxySubject implements Subject {

private RealSubject mSubject;

public ProxySubject() {
mSubject = new RealSubject();
}

@Override
public void request() {
System.out.print("Log: before");
mSubject.request();
System.out.print("Log: after");
}
}

静态代理和动态代理

静态代理和动态代理的概念和使用可以参考我另一篇文章:Java动态代理:http://blog.csdn.net/shensky711/article/details/52872249

🔲 ☆

设计模式之工厂模式(Factory)

概述

根据依赖倒置原则,我们知道,我们应优先依赖抽象类而不是具体类。在应用开发过程中,有很多实体类都是非常易变的,依赖它们会带来问题,所以我们更应该依赖于抽象接口,已使我们免受大多数变化的影响。
工厂模式(Factory)允许我们只依赖于抽象接口就能创建出具体对象的实例,所以在开发中,如果具体类是高度易变的,那么该模式就非常有用。

接下来我们就通过代码举例说明什么是工厂模式

简单工厂模式

假设我们现在有个需求:把一段数据用Wi-Fi或者蓝牙发送出去。
需求很简单是吧?刷刷刷就写下了以下实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private String mode; //Wi-Fi|Bluetooth

public void onClick() {
byte[] data = {0x00, 0x01};

if ("Wi-Fi".equals(mode)) {
sendDataByWiFi(data);
} else {
sendDataByBluetooth(data);
}
}

private void sendDataByWiFi(byte[] data) {
// send data via Wi-Fi
}

private void sendDataByBluetooth(byte[] data) {
// send data via Bluetooth
}

但是上面的代码扩展性并不高,违反了开放封闭原则。比如现在又有了个新的需求,需要用zigbee把数据发送出去,就得再新增一个sendDataByZigbee方法了,而且还得修改onClick里面的逻辑。那么比较好的方法是怎么样的呢?

定义一个数据发送器类:

1
2
3
4
5
6
7
8
9
/**
* 数据发送器Sender
*
* @author HansChen
*/
public interface Sender {

void sendData(byte[] data);
}

实现WiFi数据发送:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Sender的实现类,通过Wi-Fi发送数据
*
* @author HansChen
*/
public class WiFiSender implements Sender {

@Override
public void sendData(byte[] data) {
System.out.println("Send data by Wi-Fi");
}
}

实现蓝牙数据发送:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Sender的实现类,通过蓝牙发送数据
*
* @author HansChen
*/
public class BluetoothSender implements Sender {

@Override
public void sendData(byte[] data) {
System.out.println("Send data by Bluetooth");
}
}

这样,原来发送数据的地方就改为了:

1
2
3
4
5
6
7
8
9
10
11
12
13
private String mode; //Wi-Fi|Bluetooth

public void onClick() {
byte[] data = {0x00, 0x01};

Sender sender;
if ("Wi-Fi".equals(mode)) {
sender = new WiFiSender();
} else {
sender = new BluetoothSender();
}
sender.sendData(data);
}

有没有觉得代码优雅了一点?但是随着发送器Sender的实现类越来越多,每增加一个实现类,就需要在onClick里面实例化相应的实现类,能不能用一个单独的类来做这个创造实例的过程呢?这就是我们讲到的工厂。我们新增一个工厂类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 简单工厂类
*
* @author HansChen
*/
public class SimpleFactory {

public static Sender createSender(String mode) {
switch (mode) {
case "Wi-Fi":
return new WiFiSender();
case "Bluetooth":
return new BluetoothSender();
default:
throw new IllegalArgumentException("illegal type: " + mode);
}
}
}

这样一来,怎么实例化数据发送器我们也不用管了,最终代码变为:

1
2
3
4
5
6
7
8
private String mode; //Wi-Fi|Bluetooth

public void onClick() {
byte[] data = {0x00, 0x01};

Sender sender = SimpleFactory.createSender(mode);
sender.sendData(data);
}

好了,到这里我们就完成了简单工厂模式的应用了,下图就是简单工厂模式的结构图:
2019-9-2-11-28-47.png

工厂方法模式

简单工厂模式的优点在于工厂类包含了必要的判断逻辑,根据传入的参数动态实例化相关的类,对于客户端来说,去除了与具体产品的依赖。但是这里还是会有个问题,假设上面例子中新增了一个zigbee发送器,那么一定是需要修改简单工厂类的,也就是说,我们不但对扩展开放了,对修改也开放了,这是不好的。解决的方法是使用工厂方法模式,工厂方法模式是指定义一个用于创建对象的接口,让子类决定实例化哪一个类。下面还是通过代码来说明:

在简单工厂模式的基础上,让我们对工厂类也升级一下,首先定义一个工厂类接口:

1
2
3
4
public interface SenderFactory {

Sender createSender();
}

然后为每一个发送器的实现类各创建一个具体的工厂方法去实现这个接口

定义WiFiSender的工厂类:

1
2
3
4
5
6
7
public class WiFiSenderFactory implements SenderFactory {

@Override
public Sender createSender() {
return new WiFiSender();
}
}

定义BluetoothSender的工厂类:

1
2
3
4
5
6
7
public class BluetoothSenderFactory implements SenderFactory {

@Override
public Sender createSender() {
return new BluetoothSender();
}
}

这样,即使有新的Sender实现类加进来,我们只需要新增相应的工厂类就行了,不需要修改原有的工厂,下图就是工厂方法模式的结构图:
2019-9-2-11-28-17.png

客户端调用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private String mode; //Wi-Fi|Bluetooth

public void onClick() {
byte[] data = {0x00, 0x01};

SenderFactory factory;
if ("Wi-Fi".equals(mode)) {
factory = new WiFiSenderFactory();
} else {
factory = new BluetoothSenderFactory();
}
Sender sender = factory.createSender();
sender.sendData(data);
}

细心的读者可能已经发现了,工厂方法模式实现时,客户端需要决定实例化哪一个工厂类,相比于简单工厂模式,客户端多了一个选择判断的问题,也就是说,工厂方法模式把简单工厂模式的内部逻辑判断移到了客户端!你想要加功能,本来是修改简单工厂类的,现在改为修改客户端。但是这样带来的好处是整个工厂和产品体系都没有“修改”的变化,只有“扩展”的变化,完全符合了开放封闭原则。

总结

简单工厂模式和工厂方法模式都封装了对象的创建,它们使得高层策略模块在创建类的实例时无需依赖于这些类的具体实现。但是两种工厂模式之间又有差异:

  • 简单工厂模式:最大的优点在于工厂类包含了必要的判断逻辑,根据客户端的条件动态地实例化相关的类。但这也是它的缺点,当扩展功能的时候,需要修改工厂方法,违反了开放封闭原则
  • 工厂方法模式:符合开放封闭原则,但这带来的代价是扩展的时候要增加相应的工厂类,增加了开发量,而且需要修改客户端代码
❌