参考:https://github.com/mohuishou/go-design-pattern

参考课程:极客时间《设计模式之美》
资源:https://github.com/ggw2021/design-pattern-books

KISS 原则(Keep It Simple and Stupid,也常被译为 “保持简单愚蠢”)是设计、工程、管理等领域广泛遵循的重要原则,核心思想是让事物保持简单易懂,避免过度复杂。它强调在解决问题或创造产品时,最简单的方案往往是最有效的。

KISS 原则的起源与核心内涵

  • 起源:普遍认为该原则由美国海军工程师凯利・约翰逊(Kelly Johnson)提出。他在设计军用飞机时要求团队:“任何设计都必须简单到让新手在紧急情况下也能轻松操作”,因为复杂的系统在高压环境下容易出错。
  • 核心内涵
    并非倡导 “愚蠢”,而是反对不必要的复杂。即 “用最简单的方式解决问题,避免冗余的步骤、概念或结构”。过度复杂会导致理解困难、执行低效、出错率高,而简单的方案更易维护、推广和优化。

KISS 原则的应用场景

  1. 产品设计与开发
    • 例如:苹果产品的极简界面,通过减少按钮和功能层级,让用户无需学习即可操作;微信的初期版本仅保留核心的聊天和社交功能,避免功能堆砌。
    • 反面案例:早期某些功能机的菜单层级多达五六层,用户查找功能时极为繁琐,违背了 KISS 原则。
  2. 编程与技术领域
    • 程序员常说 “代码越简单,bug 越少”。复杂的逻辑嵌套、冗余的代码会增加维护难度,而模块化、简洁的代码更易迭代。
  3. 沟通与管理
    • 汇报工作时,用简洁的语言和图表传递核心信息,比冗长的 PPT 更有效;管理流程中,减少不必要的审批环节,能提升团队效率。
  4. 日常生活
    • 例如:整理房间时,保留常用物品、精简冗余物品,能让空间更易用;制定计划时,聚焦核心目标而非罗列琐事,更易执行。

为什么 KISS 原则有效?

  • 降低认知负荷:人类的注意力和理解能力有限,简单的事物更易被接受和记忆。
  • 减少出错概率:复杂系统中,一个环节的失误可能引发连锁反应,而简单系统的容错性更强。
  • 提升效率:无论是设计、执行还是维护,简单的方案往往更省时省力。

注意:简单≠简陋

KISS 原则并非追求 “粗制滥造”,而是在满足核心需求的前提下,去除不必要的复杂。例如:一款优秀的工具,既能解决问题,又不会让用户为多余的功能付费或学习 —— 这才是 “简单” 的真正价值。

正如爱因斯坦所说:“凡事应该力求简单,直到不能再简单为止”(Everything should be made as simple as possible, but not simpler.),KISS 原则的本质,正是这种 “恰到好处的简单”。

一、设计原则与思想:面向对象

1.1 golang中的面向对象

按照严格的定义,很多语言都不能算得上面向对象编程语言,但按照不严格的定义来讲,现在流行的大部分编程语言都是面向对象编程语言。

解释:以go为例,go没有继承的概念,却可以实现面向对象的开发风格效果,所以会反映到上面这句话。其实归根到底这是面向对象编程这种编程范式或编程风格没有一个统一的定义

我对golang的面向对象的理解如下:

(一)封装

  1. 实现结构体
  2. 实现结构体方法

(二)抽象

定义接口


(三)继承

嵌套/组合结构体


(四)多态

结构体实现接口方法

使用方式一:直接NewFun()的时候返回接口类型(推荐,更好的屏蔽细节)
使用方式二:NewFun()的时候返回的是结构体类型,最后参数是接口类型


总结c++(使用抽象类实现接口效果), java(原生接口语法), golang(原生接口语法), 这些静态语言的面向对象玩法都差不多。python, js这样的动态语言略微不同,这是由动态语言特性带来的,例如不需要显式的抽象和多态,直接借助duck-typing的特性可以轻松实现。

duck-typing来隐式实现抽象和多态的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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):
recorder.record()

def demo():
logger = Logger()
db = DB()
test(logger)
test(db)




1.2 三大特性?四大特性?(pending)

目前对于面向对象特性的总结存在分歧

观点一:三大特性

  1. 封装
  2. 继承
  3. 多态

观点二:四大特性

  1. 继承
  2. 抽象
  3. 继承
  4. 多态

为什么会有这种分歧呢?抽象为什么可以排除在面向对象编程特性之外呢?





1.3 看似面向对象,实则面向过程?

(一)滥用getter、setter方法 (顺手加上 或者 IDE插件自动生成)

非常不推荐的。它违反了面向对象编程的封装特性,相当于将面向对象编程风格退化成了面向过程编程风格。

推荐做法:非必要不添加


(二)滥用全局变量和全局方法


(三)定义数据和方法分离的类

传统的MVC结构分为Model层、Controller层、View层这三层。不过,在做前后端分离之后,三层结构在后端开发中,会稍微有些调整,被分为Controller层、Service层、Repository层。Controller层负责暴露接口给前端调用,Service层负责核心业务逻辑,Repository层负责数据读写。而在每一层中,我们又会定义相应的VO(View Object)、BO(Business Object)、Entity。一般情况下,VO、BO、Entity中只会定义数据,不会定义方法,所有操作这些数据的业务逻辑都定义在对应的Controller类、Service类、Repository类中。这就是典型的面向过程的编程风格。

实际上,这种开发模式叫作基于贫血模型的开发模式,也是我们现在非常常用的一种Web项目的开发模式。看到这里,你内心里应该有很多疑惑吧?既然这种开发模式明显违背面向对象的编程风格,为什么大部分Web项目都是基于这种开发模式来开发呢?

贫血模型与充血模型(觉得抽象可以看下一个例子)

在面向对象编程和软件架构中,“贫血模型”(Anemic Domain Model)和 “充血模型”(Rich Domain Model)是两种对立的领域模型设计风格,核心区别在于数据与业务逻辑的封装方式。这两个概念由计算机科学家马丁・福勒(Martin Fowler)提出,用于描述业务对象(如 BO、Domain 类)的设计模式。

一、贫血模型(Anemic Domain Model)

定义:贫血模型是指业务对象仅包含数据(成员变量)和简单的 get/set 方法,不包含任何业务逻辑,所有业务逻辑都被转移到独立的服务类(如 Service 类)中。

特点:

  • 数据与逻辑分离:对象只 “装数据”,不 “做事情”,业务逻辑集中在 Service 层。
  • 违背面向对象封装性:面向对象的核心是 “数据与行为绑定”,而贫血模型更像 “结构化编程” 的延续(将数据和函数分开)。
  • 常见于传统分层架构:例如在基于 MVC 的传统开发中,BO(业务对象)通常是贫血模型,Service 类负责所有业务处理。

举例:

一个 “订单” 贫血模型可能长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 贫血模型的订单BO
public class OrderBO {
private Long orderId;
private BigDecimal amount;
private String status;

// 只有get/set方法,无业务逻辑
public Long getOrderId() { return orderId; }
public void setOrderId(Long orderId) { this.orderId = orderId; }
// ...其他get/set方法
}

// 业务逻辑全部在Service中
public class OrderService {
// 计算订单金额(业务逻辑)
public BigDecimal calculateAmount(OrderBO order) { ... }
// 变更订单状态(业务逻辑)
public void updateStatus(OrderBO order, String newStatus) { ... }
}

二、充血模型(Rich Domain Model)

定义:充血模型是指业务对象既包含数据,又包含与自身相关的业务逻辑,业务逻辑被 “封装” 在对象内部,服务类(Service)仅负责协调和编排,不处理具体业务细节。

特点:

  • 数据与逻辑绑定:对象既 “装数据”,也 “做事情”,符合面向对象的 “封装” 特性(对象自己管理自己的状态和行为)。
  • 服务类简化:Service 层不再包含复杂业务逻辑,主要负责调用领域对象的方法、处理跨领域协作或外部依赖(如数据库、第三方服务)。
  • 常见于 DDD(领域驱动设计):在 DDD 中,Domain 类(领域对象)通常是充血模型,是业务逻辑的核心载体。

举例:

同一个 “订单” 的充血模型可能长这样:

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
// 充血模型的订单Domain类
public class Order {
private Long orderId;
private BigDecimal amount;
private String status;

// 包含业务逻辑:计算金额
public BigDecimal calculateAmount() {
// 逻辑内置于对象中
return this.amount.multiply(/* 折扣规则 */);
}

// 包含业务逻辑:变更状态(自带校验)
public void updateStatus(String newStatus) {
if (!isValidStatus(newStatus)) {
throw new IllegalArgumentException("无效状态");
}
this.status = newStatus;
}

// 私有辅助方法(封装细节)
private boolean isValidStatus(String status) { ... }

// get方法保留(可能不提供set方法,避免外部随意修改)
public Long getOrderId() { return orderId; }
}

// Service层仅负责协调,不处理具体业务
public class OrderService {
public void processOrder(Order order) {
// 调用领域对象的方法完成业务
BigDecimal finalAmount = order.calculateAmount();
order.updateStatus("已处理");
// 处理跨领域协作或持久化
}
}

三、为什么叫 “贫血” 和 “充血”?

这两个名字是一种形象的比喻:

  • 贫血:形容对象 “没有灵魂”,只有空壳数据,缺乏业务行为能力,如同 “贫血” 的人没有活力。
  • 充血:形容对象 “充满活力”,不仅有数据,还有处理自身业务的能力,如同 “充血” 的组织有旺盛的功能。

四、两种模型的适用场景

  • 贫血模型:适合简单业务系统,开发速度快,易于理解(尤其是团队熟悉结构化思维时),但业务复杂后会导致 Service 类臃肿、逻辑分散。
  • 充血模型:适合复杂业务系统(如金融、电商核心业务),通过封装提高代码复用性和可维护性,更符合面向对象设计,但对开发者的业务理解和面向对象功底要求较高。

总之,两种模型没有绝对优劣,选择取决于业务复杂度、团队技术栈和开发习惯。DDD 推崇充血模型,正是因为它能更好地应对复杂业务领域的设计挑战。

贫血模型与充血模型处理http请求逻辑

我们可以通过一个 “用户下单” 的 HTTP 请求场景,分别用贫血模型充血模型的处理流程来对比,直观理解两种模式的差异。

场景说明

假设我们有一个电商系统,客户端发送一个创建订单的 HTTP 请求:

1
2
3
4
5
6
7
8
POST /api/orders
Content-Type: application/json

{
"userId": 1001,
"productIds": [101, 102], // 商品ID列表
"couponId": 501 // 优惠券ID(可选)
}

系统需要完成的业务逻辑:

  1. 校验商品库存是否充足;
  2. 根据商品价格和优惠券计算最终订单金额;
  3. 生成订单并保存到数据库;
  4. 返回订单 ID 和最终金额。

一、贫血模型的处理流程

在贫血模型中,数据(OrderBO)和业务逻辑(OrderService)完全分离,流程如下:

  1. Controller 层接收请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
public class OrderController {
@Autowired
private OrderService orderService;

@PostMapping("/api/orders")
public OrderResponse createOrder(@RequestBody OrderRequest request) {
// 1. 将请求参数转换为贫血模型的BO(仅含数据)
OrderBO orderBO = new OrderBO();
orderBO.setUserId(request.getUserId());
orderBO.setProductIds(request.getProductIds());
orderBO.setCouponId(request.getCouponId());

// 2. 调用Service处理所有业务逻辑
OrderBO resultBO = orderService.createOrder(orderBO);

// 3. 转换为响应返回
return new OrderResponse(resultBO.getOrderId(), resultBO.getFinalAmount());
}
}
  1. 贫血模型的 OrderBO(仅含数据)
1
2
3
4
5
6
7
8
9
10
11
// 贫血模型:只有数据和get/set,无任何业务逻辑
public class OrderBO {
private Long orderId;
private Long userId;
private List<Long> productIds;
private Long couponId;
private BigDecimal finalAmount;

// 仅get/set方法,无业务逻辑
// ...省略get/set
}
  1. Service 层集中处理所有业务逻辑
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
@Service
public class OrderService {
@Autowired
private ProductRepository productRepo;
@Autowired
private CouponRepository couponRepo;
@Autowired
private OrderRepository orderRepo;

public OrderBO createOrder(OrderBO orderBO) {
// 1. 校验商品库存(业务逻辑1)
for (Long productId : orderBO.getProductIds()) {
ProductPO product = productRepo.findById(productId);
if (product.getStock() <= 0) {
throw new RuntimeException("商品" + productId + "库存不足");
}
}

// 2. 计算订单金额(业务逻辑2)
BigDecimal total = BigDecimal.ZERO;
for (Long productId : orderBO.getProductIds()) {
total = total.add(productRepo.findById(productId).getPrice());
}
// 应用优惠券折扣
if (orderBO.getCouponId() != null) {
CouponPO coupon = couponRepo.findById(orderBO.getCouponId());
total = total.subtract(coupon.getDiscount());
}
orderBO.setFinalAmount(total);

// 3. 生成订单并保存(业务逻辑3)
orderBO.setOrderId(generateOrderId());
orderRepo.save(convertBOToPO(orderBO));

return orderBO;
}

// 其他辅助方法(如ID生成、BO转PO等)
// ...
}

核心特点:所有业务逻辑都在 Service 中,OrderBO 仅作为 “数据容器” 传递,自身不做任何决策。


二、充血模型(DDD 模式)的处理流程

在充血模型中,数据(Order 领域对象)和业务逻辑被封装在一起,Service 仅负责协调,流程如下:

  1. Controller 层接收请求(与贫血模型类似,但传递的是领域对象)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
public class OrderController {
@Autowired
private OrderService orderService;

@PostMapping("/api/orders")
public OrderResponse createOrder(@RequestBody OrderRequest request) {
// 1. 直接创建领域对象(充血模型,含数据和行为)
Order order = new Order();
order.setUserId(request.getUserId());
order.setProductIds(request.getProductIds());
order.setCouponId(request.getCouponId());

// 2. 调用Service协调处理(Service逻辑简化)
Order resultOrder = orderService.createOrder(order);

// 3. 转换为响应返回
return new OrderResponse(resultOrder.getOrderId(), resultOrder.getFinalAmount());
}
}
  1. 充血模型的 Order 领域对象(含数据和业务逻辑)
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
// 充血模型:数据+业务逻辑封装在一起
public class Order {
private Long orderId;
private Long userId;
private List<Long> productIds;
private Long couponId;
private BigDecimal finalAmount;

// 业务逻辑1:校验商品库存(自己处理与自身相关的逻辑)
public void validateStock(ProductRepository productRepo) {
for (Long productId : productIds) {
ProductPO product = productRepo.findById(productId);
if (product.getStock() <= 0) {
throw new RuntimeException("商品" + productId + "库存不足");
}
}
}

// 业务逻辑2:计算最终金额(自己管理金额计算规则)
public void calculateFinalAmount(ProductRepository productRepo, CouponRepository couponRepo) {
BigDecimal total = BigDecimal.ZERO;
for (Long productId : productIds) {
total = total.add(productRepo.findById(productId).getPrice());
}
// 应用优惠券
if (couponId != null) {
CouponPO coupon = couponRepo.findById(couponId);
total = total.subtract(coupon.getDiscount());
}
this.finalAmount = total;
}

// 业务逻辑3:生成订单ID(自己管理ID生成规则)
public void generateOrderId() {
this.orderId = System.currentTimeMillis() + RandomUtils.nextInt(1000);
}

// get方法保留(可能限制set方法,避免外部随意修改状态)
// ...省略get方法
}
  1. Service 层仅负责协调(不再处理具体业务逻辑)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class OrderService {
@Autowired
private ProductRepository productRepo;
@Autowired
private CouponRepository couponRepo;
@Autowired
private OrderRepository orderRepo;

public Order createOrder(Order order) {
// 1. 调用领域对象自身的方法处理业务逻辑
order.validateStock(productRepo); // 校验库存(Order自己实现)
order.calculateFinalAmount(productRepo, couponRepo); // 计算金额(Order自己实现)
order.generateOrderId(); // 生成ID(Order自己实现)

// 2. 仅负责跨依赖协调(如保存数据库)
orderRepo.save(convertDomainToPO(order));

return order;
}
}

核心特点:Order 领域对象自己 “做主”,包含与自身相关的业务逻辑(校验库存、计算金额等),Service 仅负责调用领域对象的方法并处理外部依赖(如数据库操作)。


两种模式的核心差异对比

维度 贫血模型 充血模型(DDD)
业务逻辑存放位置 集中在 Service 类中 封装在领域对象(如 Order)中
对象角色 仅作为数据容器(“哑巴对象”) 既是数据载体,也是业务逻辑执行者
Service 层职责 处理所有业务逻辑,代码可能臃肿 仅协调领域对象,逻辑简单
面向对象特性 违背封装(数据与行为分离) 符合封装(数据与行为绑定)

通过 HTTP 请求的处理流程可以看出:贫血模型更像 “procedural(过程式)” 编程,而充血模型更符合 “object-oriented(面向对象)” 编程的初衷。在复杂业务场景中,充血模型能让代码更易维护(业务逻辑跟着数据走),而简单场景下贫血模型更直观。


(三)啥都往同一个接口里面塞,完全不拆分

在go中,最典型的就是对数据库操作抽象出来的一个接口,随着业务越来越复杂,这个接口越来越大,最后也就只起到一个封装的效果,多态式完全不可能的了(定义一个新的结构能够实现这么多方法基本不可能,这是我现在接受业务中代码设计的一个问题)

还是建议按照功能模块拆分一下的。





1.4 多用组合少用继承

在面向对象编程中,有一条非常经典的设计原则,那就是:组合优于继承,多用组合少用继承。

go天然就没有继承的概念,只能组合~


1.为什么不推荐使用继承?

继承是面向对象的四大特性之一,用来表示类之间的is-a关系,可以解决代码复用的问题。虽然继承有诸多作用,但继承层次过深、过复杂,也会影响到代码的可维护性。在这种情况下,我们应该尽量少用,甚至不用继承。

2.组合相比继承有哪些优势?

继承主要有三个作用:表示is-a关系,支持多态特性,代码复用。而这三个作用都可以通过组合、接口、委托三个技术手段来达成。除此之外,利用组合还能解决层次过深、过复杂的继承关系影响代码可维护性的问题。

3.如何判断该用组合还是继承?

尽管我们鼓励多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。在实际的项目开发中,我们还是要根据具体的情况,来选择该用继承还是组合。如果类之间的继承结构稳定,层次比较浅,关系不复杂,我们就可以大胆地使用继承。反之,我们就尽量使用组合来替代继承。除此之外,还有一些设计模式、特殊的应用场景,会固定使用继承或者组合。





1.5 业务开发常用的基于贫血模型的MVC架构违背OOP吗?(摘抄)

很多业务系统都是基于MVC三层架构来开发的。实际上,更确切点讲,这是一种基于贫血模型的MVC三层架构开发模式。

虽然这种开发模式已经成为标准的Web项目的开发模式,但它却违反了面向对象编程风格,是一种彻彻底底的面向过程的编程风格,因此而被有些人称为反模式(anti-pattern)。特别是领域驱动设计(Domain Driven Design,简称DDD)盛行之后,这种基于贫血模型的传统的开发模式就更加被人诟病。而基于充血模型的DDD开发模式越来越被人提倡。所以,我打算用两节课的时间,结合一个虚拟钱包系统的开发案例,带你彻底弄清楚这两种开发模式。

考虑到你有可能不太了解我刚刚提到的这几个概念,所以,在正式进入实战项目的讲解之前,我先带你搞清楚下面几个问题:

  • 什么是贫血模型?什么是充血模型?
  • 为什么说基于贫血模型的传统开发模式违反OOP?
  • 基于贫血模型的传统开发模式既然违反OOP,那又为什么如此流行?
  • 什么情况下我们应该考虑使用基于充血模型的DDD开发模式?

好了,让我们带着这些问题,正式开始今天的学习吧!


什么是基于贫血模型的传统开发模式?

我相信,对于大部分的后端开发工程师来说,MVC三层架构都不会陌生。不过,为了统一我们之间对MVC的认识,我还是带你一块来回顾一下,什么是MVC三层架构。

MVC三层架构中的M表示Model,V表示View,C表示Controller。它将整个项目分为三层:展示层、逻辑层、数据层。MVC三层开发架构是一个比较笼统的分层方式,落实到具体的开发层面,很多项目也并不会100%遵从MVC固定的分层方式,而是会根据具体的项目需求,做适当的调整。

mvc科普

bilibili视频链接

模型:处理数据验证、逻辑、持久性

控制器:在模型和视图之间传递数据

视图:用于处理怎样显示信息

img

img

比如,现在很多Web或者App项目都是前后端分离的,后端负责暴露接口给前端调用。这种情况下,我们一般就将后端项目分为Repository层、Service层、Controller层。其中,Repository层负责数据访问,Service层负责业务逻辑,Controller层负责暴露接口(也就是model层细分了一下,然后去掉view层)。当然,这只是其中一种分层和命名方式。不同的项目、不同的团队,可能会对此有所调整。不过,万变不离其宗,只要是依赖数据库开发的Web项目,基本的分层思路都大差不差。

刚刚我们回顾了MVC三层开发架构。现在,我们再来看一下,什么是贫血模型?

实际上,你可能一直都在用贫血模型做开发,只是自己不知道而已。不夸张地讲,据我了解,目前几乎所有的业务后端系统,都是基于贫血模型的。我举一个简单的例子来给你解释一下。

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
////////// Controller+VO(View Object) //////////
public class UserController {
private UserService userService; //通过构造函数或者IOC框架注入

public UserVo getUserById(Long userId) {
UserBo userBo = userService.getUserById(userId);
UserVo userVo = [...convert userBo to userVo...];
return userVo;
}
}

public class UserVo {//省略其他属性、get/set/construct方法
private Long id;
private String name;
private String cellphone;
}

////////// Service+BO(Business Object) //////////
public class UserService {
private UserRepository userRepository; //通过构造函数或者IOC框架注入

public UserBo getUserById(Long userId) {
UserEntity userEntity = userRepository.getUserById(userId);
UserBo userBo = [...convert userEntity to userBo...];
return userBo;
}
}

public class UserBo {//省略其他属性、get/set/construct方法
private Long id;
private String name;
private String cellphone;
}

////////// Repository+Entity //////////
public class UserRepository {
public UserEntity getUserById(Long userId) { //... }
}

public class UserEntity {//省略其他属性、get/set/construct方法
private Long id;
private String name;
private String cellphone;
}

我们平时开发Web后端项目的时候,基本上都是这么组织代码的。其中,UserEntity和UserRepository组成了数据访问层,UserBo和UserService组成了业务逻辑层,UserVo和UserController在这里属于接口层。

从代码中,我们可以发现,UserBo是一个纯粹的数据结构,只包含数据,不包含任何业务逻辑。业务逻辑集中在UserService中。我们通过UserService来操作UserBo。换句话说,Service层的数据和业务逻辑,被分割为BO和Service两个类中。像UserBo这样,只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。同理,UserEntity、UserVo都是基于贫血模型设计的。这种贫血模型将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。


什么是基于充血模型的DDD开发模式?

刚刚我们讲了基于贫血模型的传统的开发模式。现在我们再讲一下,另外一种最近更加被推崇的开发模式:基于充血模型的DDD开发模式。

首先,我们先来看一下,什么是充血模型?

在贫血模型中,数据和业务逻辑被分割到不同的类中。充血模型(Rich Domain Model)正好相反,数据和对应的业务逻辑被封装到同一个类中。因此,这种充血模型满足面向对象的封装特性,是典型的面向对象编程风格。

接下来,我们再来看一下,什么是领域驱动设计?

领域驱动设计,即DDD,主要是用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互。领域驱动设计这个概念并不新颖,早在2004年就被提出了,到现在已经有十几年的历史了。不过,它被大众熟知,还是基于另一个概念的兴起,那就是微服务。

我们知道,除了监控、调用链追踪、API网关等服务治理系统的开发之外,微服务还有另外一个更加重要的工作,那就是针对公司的业务,合理地做微服务拆分。而领域驱动设计恰好就是用来指导划分服务的。所以,微服务加速了领域驱动设计的盛行。

不过,我个人觉得,领域驱动设计有点儿类似敏捷开发、SOA、PAAS等概念,听起来很高大上,但实际上只值“五分钱”。即便你没有听说过领域驱动设计,对这个概念一无所知,只要你是在开发业务系统,也或多或少都在使用它。做好领域驱动设计的关键是,看你对自己所做业务的熟悉程度,而并不是对领域驱动设计这个概念本身的掌握程度。即便你对领域驱动搞得再清楚,但是对业务不熟悉,也并不一定能做出合理的领域设计。所以,不要把领域驱动设计当银弹,不要花太多的时间去过度地研究它。

实际上,基于充血模型的DDD开发模式实现的代码,也是按照MVC三层架构分层的。Controller层还是负责暴露接口,Repository层还是负责数据存取,Service层负责核心业务逻辑。它跟基于贫血模型的传统开发模式的区别主要在Service层。

在基于贫血模型的传统开发模式中,Service层包含Service类和BO类两部分,BO是贫血模型(BO, Business Object, 业务对象 是一个核心概念,主要用于承载业务数据和部分业务处理逻辑),只包含数据,不包含具体的业务逻辑。业务逻辑集中在Service类中。在基于充血模型的DDD开发模式中,Service层包含Service类和Domain类两部分。Domain就相当于贫血模型中的BO。不过,Domain与BO的区别在于它是基于充血模型开发的,既包含数据,也包含业务逻辑。而Service类变得非常单薄。总结一下的话就是,基于贫血模型的传统的开发模式,重Service轻BO;基于充血模型的DDD开发模式,轻Service重Domain

基于充血模型的DDD设计模式的概念,今天我们只是简单地介绍了一下。在下一节课中,我会结合具体的项目,通过代码来给你展示,如何基于这种开发模式来开发一个系统。


为什么基于贫血模型的传统开发模式如此受欢迎?

前面我们讲过,基于贫血模型的传统开发模式,将数据与业务逻辑分离,违反了OOP的封装特性,实际上是一种面向过程的编程风格。但是,现在几乎所有的Web项目,都是基于这种贫血模型的开发模式,甚至连Java Spring框架的官方demo,都是按照这种开发模式来编写的。

我们前面也讲过,面向过程编程风格有种种弊端,比如,数据和操作分离之后,数据本身的操作就不受限制了。任何代码都可以随意修改数据。既然基于贫血模型的这种传统开发模式是面向过程编程风格的,那它又为什么会被广大程序员所接受呢?关于这个问题,我总结了下面三点原因。

第一点原因是,大部分情况下,我们开发的系统业务可能都比较简单,简单到就是基于SQL的CRUD操作,所以,我们根本不需要动脑子精心设计充血模型,贫血模型就足以应付这种简单业务的开发工作。除此之外,因为业务比较简单,即便我们使用充血模型,那模型本身包含的业务逻辑也并不会很多,设计出来的领域模型也会比较单薄,跟贫血模型差不多,没有太大意义。

第二点原因是,充血模型的设计要比贫血模型更加有难度。因为充血模型是一种面向对象的编程风格。我们从一开始就要设计好针对数据要暴露哪些操作,定义哪些业务逻辑。而不是像贫血模型那样,我们只需要定义数据,之后有什么功能开发需求,我们就在Service层定义什么操作,不需要事先做太多设计。

第三点原因是,思维已固化,转型有成本。基于贫血模型的传统开发模式经历了这么多年,已经深得人心、习以为常。你随便问一个旁边的大龄同事,基本上他过往参与的所有Web项目应该都是基于这个开发模式的,而且也没有出过啥大问题。如果转向用充血模型、领域驱动设计,那势必有一定的学习成本、转型成本。很多人在没有遇到开发痛点的情况下,是不愿意做这件事情的。


什么项目应该考虑使用基于充血模型的DDD开发模式?

既然基于贫血模型的开发模式已经成为了一种约定俗成的开发习惯,那什么样的项目应该考虑使用基于充血模型的DDD开发模式呢?

刚刚我们讲到,基于贫血模型的传统的开发模式,比较适合业务比较简单的系统开发。相对应的,基于充血模型的DDD开发模式,更适合业务复杂的系统开发。比如,包含各种利息计算模型、还款模型等复杂业务的金融系统。

你可能会有一些疑问,这两种开发模式,落实到代码层面,区别不就是一个将业务逻辑放到Service类中,一个将业务逻辑放到Domain领域模型中吗?为什么基于贫血模型的传统开发模式,就不能应对复杂业务系统的开发?而基于充血模型的DDD开发模式就可以呢?

实际上,除了我们能看到的代码层面的区别之外(一个业务逻辑放到Service层,一个放到领域模型中),还有一个非常重要的区别,那就是两种不同的开发模式会导致不同的开发流程。基于充血模型的DDD开发模式的开发流程,在应对复杂业务系统的开发的时候更加有优势。为什么这么说呢?我们先来回忆一下,我们平时基于贫血模型的传统的开发模式,都是怎么实现一个功能需求的。

不夸张地讲,我们平时的开发,大部分都是SQL驱动(SQL-Driven)的开发模式。我们接到一个后端接口的开发需求的时候,就去看接口需要的数据对应到数据库中,需要哪张表或者哪几张表,然后思考如何编写SQL语句来获取数据。之后就是定义Entity、BO、VO,然后模板式地往对应的Repository、Service、Controller类中添加代码。

业务逻辑包裹在一个大的SQL语句中,而Service层可以做的事情很少。SQL都是针对特定的业务功能编写的,复用性差。当我要开发另一个业务功能的时候,只能重新写个满足新需求的SQL语句,这就可能导致各种长得差不多、区别很小的SQL语句满天飞。

所以,在这个过程中,很少有人会应用领域模型、OOP的概念,也很少有代码复用意识。对于简单业务系统来说,这种开发方式问题不大。但对于复杂业务系统的开发来说,这样的开发方式会让代码越来越混乱,最终导致无法维护。

如果我们在项目中,应用基于充血模型的DDD的开发模式,那对应的开发流程就完全不一样了。在这种开发模式下,我们需要事先理清楚所有的业务,定义领域模型所包含的属性和方法。领域模型相当于可复用的业务中间层。新功能需求的开发,都基于之前定义好的这些领域模型来完成。

我们知道,越复杂的系统,对代码的复用性、易维护性要求就越高,我们就越应该花更多的时间和精力在前期设计上。而基于充血模型的DDD开发模式,正好需要我们前期做大量的业务调研、领域模型设计,所以它更加适合这种复杂系统的开发。


重点回顾

今天的内容到此就讲完了,我们来一起回顾一下,你应该掌握的重点内容。

我们平时做Web项目的业务开发,大部分都是基于贫血模型的MVC三层架构,在专栏中我把它称为传统的开发模式。之所以称之为“传统”,是相对于新兴的基于充血模型的DDD开发模式来说的。基于贫血模型的传统开发模式,是典型的面向过程的编程风格。相反,基于充血模型的DDD开发模式,是典型的面向对象的编程风格。

不过,DDD也并非银弹。对于业务不复杂的系统开发来说,基于贫血模型的传统开发模式简单够用,基于充血模型的DDD开发模式有点大材小用,无法发挥作用。相反,对于业务复杂的系统开发来说,基于充血模型的DDD开发模式,因为前期需要在设计上投入更多时间和精力,来提高代码的复用性和可维护性,所以相比基于贫血模型的开发模式,更加有优势。


课堂讨论

今天课堂讨论的话题有两个。

  1. 你做经历的项目中,有哪些是基于贫血模型的传统的开发模式?有哪些是基于充血模型的DDD开发模式呢?请简单对比一下两者的优劣。
  2. 对于我们举的例子中,UserEntity、UserBo、UserVo包含的字段都差不多,是否可以合并为一个类呢?

欢迎在留言区写下你的答案,和同学一起交流和分享。如果有收获,也欢迎你把这篇文章分享给你的朋友。





二、设计原则与思想:设计原则





三、设计原则与思想:规范与重构




四、设计模式与范式:创建型

创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。

4.1 单例模式

有人觉得"饿汉式"方式不好,因为不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。不过,我个人并不认同这样的观点。

如果初始化耗时长,那我们最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。

如果实例占用资源多,按照fail-fast的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错(比如Java中的 PermGen Space OOM),我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。

(一)饿汉式

“饿” 体现了 “迫切、提前准备” 的状态 —— 就像一个饿了很久的人,会提前把食物准备好,不等别人索要就已经就绪。

代码实现

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

// Singleton 饿汉式单例
type Singleton struct{}

var singleton *Singleton

func init() {
singleton = &Singleton{}
}

// GetInstance 获取实例
func GetInstance() *Singleton {
return singleton
}

(二)懒汉式

“懒” 体现了 “拖延、按需行动” 的状态 —— 就像一个懒惰的人,不到万不得已不会主动做事,只有当别人明确要求时才动手。

代码实现

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

import "sync"

var (
lazySingleton *Singleton
once sync.Once
)

// GetLazyInstance 懒汉式
func GetLazyInstance() *Singleton {
once.Do(func() {
lazySingleton = &Singleton{}
})
return lazySingleton
}

每个coroutine一个单例

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
package main

import (
"context"
"fmt"
"sync"
"time"
)

// 单例结构
type Singleton struct {
ID string // 用于标识不同实例
}

// 私有类型,用作上下文键,避免冲突
type singletonKey struct{}

// 将单例实例绑定到上下文中(如果不存在)
func WithSingleton(ctx context.Context) context.Context {
// 检查上下文中是否已有实例
if _, ok := ctx.Value(singletonKey{}).(*Singleton); ok {
return ctx // 已有实例,直接返回
}

// 创建新实例并绑定到上下文中
return context.WithValue(ctx, singletonKey{}, &Singleton{
ID: generateUniqueID(), // 生成唯一ID用于标识实例
})
}

// 从上下文中获取单例实例
func GetSingleton(ctx context.Context) *Singleton {
if s, ok := ctx.Value(singletonKey{}).(*Singleton); ok {
return s
}
panic("singleton not found in context") // 或者返回 nil,根据需求决定
}

// 生成唯一ID(简化实现)
func generateUniqueID() string {
return fmt.Sprintf("%d", time.Now().UnixNano())
}

func main() {
var wg sync.WaitGroup
baseCtx := context.Background()

// 启动多个goroutine测试
for i := 0; i < 3; i++ {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()

// 为每个goroutine创建独立上下文
ctx := WithSingleton(baseCtx)

// 同一个goroutine多次获取应该得到相同实例
s1 := GetSingleton(ctx)
s2 := GetSingleton(ctx)

fmt.Printf("Goroutine %d: s1.ID=%s, s2.ID=%s, same instance: %v\n",
goroutineID, s1.ID, s2.ID, s1 == s2)

// 验证上下文唯一性:再次调用 WithSingleton 不会创建新实例
ctx = WithSingleton(ctx)
s3 := GetSingleton(ctx)
fmt.Printf("Goroutine %d after re-wrap: s3.ID=%s, same instance: %v\n",
goroutineID, s3.ID, s1 == s3)
}(i)
}

wg.Wait()
}

尽管单例是一个很常用的设计模式,在实际的开发中,我们也确实经常用到它,但是,有些人认为单例是一种反模式(anti-pattern),并不推荐使用。所以,今天,我就针对这个说法详细地讲讲这几个问题:单例这种设计模式存在哪些问题?为什么会被称为反模式?如果不用单例,该如何表示全局唯一类?有何替代的解决方案?

单例存在哪些问题?

  1. 单例对OOP特性的支持不友好
    一旦你选择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性
  2. 单例会隐藏类之间的依赖关系
    因为直接使用即可
  3. 单例对代码的扩展性不友好
  4. 单例对代码的可测试性不友好
  5. 单例不支持有参数的构造函数

有何替代解决方案?

看着没啥好办法,接着用吧。

多例模式

“单例”指的是一个类只能创建一个对象。对应地,“多例”指的就是一个类可以创建多个对象,但是个数是有限制的,比如只能创建3个对象。多例的实现也比较简单,通过一个Map来存储对象类型和对象之间的对应关系,来控制对象的个数。





4.2 工厂模式

一般情况下,工厂模式分为三种更加细分的类型:简单工厂、工厂方法和抽象工厂。不过,在GoF的《设计模式》一书中,它将简单工厂模式看作是工厂方法模式的一种特例,所以工厂模式只被分成了工厂方法和抽象工厂两类。实际上,前面一种分类方法更加常见。

重点是搞清楚应用场景:什么时候该用工厂模式?相对于直接new来创建对象,用工厂模式来创建究竟有什么好处呢

4.2.1 工厂模式

简单工厂(Simple Factory)

由于 Go 本身是没有构造函数的,一般而言我们采用 NewName 的方式创建对象/接口,当它返回的是接口的时候,其实就是简单工厂模式

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
package factory

import (
"encoding/json"
"fmt"
"gopkg.in/yaml.v3"
)

// IRuleConfigParser 配置解析器接口
type IRuleConfigParser interface {
Parse(data []byte) error
}

// JsonRuleConfigParser JSON配置解析器
type JsonRuleConfigParser struct{}

// Parse 解析JSON数据
func (j JsonRuleConfigParser) Parse(data []byte) error {
var result interface{}
return json.Unmarshal(data, &result)
}

// YamlRuleConfigParser YAML配置解析器
type YamlRuleConfigParser struct{}

// Parse 解析YAML数据
func (y YamlRuleConfigParser) Parse(data []byte) error {
var result interface{}
return yaml.Unmarshal(data, &result)
}

// NewIRuleConfigParser 创建配置解析器的工厂方法
func NewIRuleConfigParser(t string) (IRuleConfigParser, error) {
switch t {
case "json":
return JsonRuleConfigParser{}, nil
case "yaml":
return YamlRuleConfigParser{}, nil
default:
return nil, fmt.Errorf("unsupported parser type: %s", t)
}
}



工厂方法

当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂。

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
// 产品接口
type IRuleConfigParser interface {
Parse(data []byte) error
}

// 具体产品
type jsonRuleConfigParser struct{}
func (j jsonRuleConfigParser) Parse(data []byte) error { /* 解析JSON */ }

type yamlRuleConfigParser struct{}
func (y yamlRuleConfigParser) Parse(data []byte) error { /* 解析YAML */ }

// 抽象工厂接口
type IRuleConfigParserFactory interface {
CreateParser() IRuleConfigParser
}

// 具体工厂类
type jsonRuleConfigParserFactory struct{}
func (j jsonRuleConfigParserFactory) CreateParser() IRuleConfigParser {
return jsonRuleConfigParser{}
}

type yamlRuleConfigParserFactory struct{}
func (y yamlRuleConfigParserFactory) CreateParser() IRuleConfigParser {
return yamlRuleConfigParser{}
}

// 简单工厂(!!可选!!但推荐):封装工厂方法的选择逻辑
func NewIRuleConfigParserFactory(t string) IRuleConfigParserFactory {
switch t {
case "json":
return jsonRuleConfigParserFactory{}
case "yaml":
return yamlRuleConfigParserFactory{}
default:
return nil
}
}

工厂方法 vs 简单工厂

维度 简单工厂 工厂方法
核心类 一个工厂类负责所有产品的创建 一个抽象工厂接口和多个具体工厂类
新增产品 修改工厂类,违反开闭原则 添加新的具体工厂类,符合开闭原则
复杂度 简单,适合创建逻辑简单的场景 复杂,适合创建逻辑复杂的场景

个人感觉:没必要纠结工厂方法,这还不如简单工厂…即没有可读性,还没有使代码的到优化;怎么说的,就好像开发了一半,发现是错的,但是不承认,强行赋予意义。🤡



抽象工厂

假设你正在开发一个跨平台应用,需要支持 Windows、Linux、macOS 三种操作系统,每种系统都有不同的 UI 组件(按钮、文本框、对话框等)。你希望代码能根据当前系统自动创建对应的 UI 组件,同时保持组件之间的一致性(比如 Windows 风格的按钮只能和 Windows 风格的文本框搭配)。

这种场景下,抽象工厂模式就很合适:它定义一个 “工厂” 接口,负责创建一组相关产品(如按钮 + 文本框),而具体实现由子类(如 Windows 工厂、Linux 工厂)完成。

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
package factory

// IRuleConfigParser IRuleConfigParser
type IRuleConfigParser interface {
Parse(data []byte)
}

// jsonRuleConfigParser jsonRuleConfigParser
type jsonRuleConfigParser struct{}

// Parse Parse
func (j jsonRuleConfigParser) Parse(data []byte) {
...
}

// ISystemConfigParser ISystemConfigParser
type ISystemConfigParser interface {
ParseSystem(data []byte)
}

// jsonSystemConfigParser jsonSystemConfigParser
type jsonSystemConfigParser struct{}

// Parse Parse
func (j jsonSystemConfigParser) ParseSystem(data []byte) {
...
}

// IConfigParserFactory 工厂方法接口
type IConfigParserFactory interface {
CreateRuleParser() IRuleConfigParser
CreateSystemParser() ISystemConfigParser
}

type jsonConfigParserFactory struct{}

func (j jsonConfigParserFactory) CreateRuleParser() IRuleConfigParser {
return jsonRuleConfigParser{}
}

func (j jsonConfigParserFactory) CreateSystemParser() ISystemConfigParser {
return jsonSystemConfigParser{}
}




4.2.2 Dependency Injection框架

golang 现有的依赖注入框架:





4.3 建造者模式

其实在 Golang 中对于创建类参数比较多的对象的时候,我们常见的做法是必填参数直接传递,可选参数通过传递可变的方法进行创建。
本文会先实现课程中的建造者模式,然后再实现我们常用的方式。


建造者模式

通过下面可以看到,使用 Go 编写建造者模式的代码其实会很长,这些是它的一个缺点,所以如果不是参数的校验逻辑很复杂的情况下,一般我们在 Go 中不会采用这种方式,而会采用后面的另外一种方式

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
85
86
87
88
89
90
91
92
93
94
95
96
package builder

import "fmt"

const (
defaultMaxTotal = 10
defaultMaxIdle = 9
defaultMinIdle = 1
)

// ResourcePoolConfig resource pool
type ResourcePoolConfig struct {
name string
maxTotal int
maxIdle int
minIdle int
}

// ResourcePoolConfigBuilder 用于构建 ResourcePoolConfig
type ResourcePoolConfigBuilder struct {
name string
maxTotal int
maxIdle int
minIdle int
}

// SetName SetName
func (b *ResourcePoolConfigBuilder) SetName(name string) error {
if name == "" {
return fmt.Errorf("name can not be empty")
}
b.name = name
return nil
}

// SetMinIdle SetMinIdle
func (b *ResourcePoolConfigBuilder) SetMinIdle(minIdle int) error {
if minIdle < 0 {
return fmt.Errorf("max tatal cannot < 0, input: %d", minIdle)
}
b.minIdle = minIdle
return nil
}

// SetMaxIdle SetMaxIdle
func (b *ResourcePoolConfigBuilder) SetMaxIdle(maxIdle int) error {
if maxIdle < 0 {
return fmt.Errorf("max tatal cannot < 0, input: %d", maxIdle)
}
b.maxIdle = maxIdle
return nil
}

// SetMaxTotal SetMaxTotal
func (b *ResourcePoolConfigBuilder) SetMaxTotal(maxTotal int) error {
if maxTotal <= 0 {
return fmt.Errorf("max tatal cannot <= 0, input: %d", maxTotal)
}
b.maxTotal = maxTotal
return nil
}

// Build Build
func (b *ResourcePoolConfigBuilder) Build() (*ResourcePoolConfig, error) {
if b.name == "" {
return nil, fmt.Errorf("name can not be empty")
}

// 设置默认值
if b.minIdle == 0 {
b.minIdle = defaultMinIdle
}

if b.maxIdle == 0 {
b.maxIdle = defaultMaxIdle
}

if b.maxTotal == 0 {
b.maxTotal = defaultMaxTotal
}

if b.maxTotal < b.maxIdle {
return nil, fmt.Errorf("max total(%d) cannot < max idle(%d)", b.maxTotal, b.maxIdle)
}

if b.minIdle > b.maxIdle {
return nil, fmt.Errorf("max idle(%d) cannot < min idle(%d)", b.maxIdle, b.minIdle)
}

return &ResourcePoolConfig{
name: b.name,
maxTotal: b.maxTotal,
maxIdle: b.maxIdle,
minIdle: b.minIdle,
}, nil
}

Go 常用的参数传递方法

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
package builder

import "fmt"

// ResourcePoolConfigOption option
type ResourcePoolConfigOption struct {
maxTotal int
maxIdle int
minIdle int
}

// ResourcePoolConfigOptFunc to set option
type ResourcePoolConfigOptFunc func(option *ResourcePoolConfigOption)

// NewResourcePoolConfig NewResourcePoolConfig
func NewResourcePoolConfig(name string, opts ...ResourcePoolConfigOptFunc) (*ResourcePoolConfig, error) {
if name == "" {
return nil, fmt.Errorf("name can not be empty")
}

option := &ResourcePoolConfigOption{
maxTotal: 10,
maxIdle: 9,
minIdle: 1,
}

for _, opt := range opts {
opt(option)
}

if option.maxTotal < 0 || option.maxIdle < 0 || option.minIdle < 0 {
return nil, fmt.Errorf("args err, option: %v", option)
}

if option.maxTotal < option.maxIdle || option.minIdle > option.maxIdle {
return nil, fmt.Errorf("args err, option: %v", option)
}

return &ResourcePoolConfig{
name: name,
maxTotal: option.maxTotal,
maxIdle: option.maxIdle,
minIdle: option.minIdle,
}, nil
}




4.4 原型模式

  • 这个模式在 Java、C++ 这种面向对象的语言不太常用,但是如果大家使用过 javascript 的话就会非常熟悉了,因为 js 本身是基于原型的面向对象语言,所以原型模式在 js 中应用非常广泛。

    解释
    简单说:**原型模式的核心是 “基于已有对象复制出新对象”**,而 JavaScript、Python 这类语言的设计天然支持这种模式,所以用得多。
    
    **为什么 JS、Python 中原型模式用得多?**
    
    1. **语言层面直接支持 “对象克隆”**
        - JS 中,`Object.create(原型对象)` 可以直接基于一个已有对象(原型)创建新对象,新对象自动继承原型的属性和方法。
            例:`const newObj = Object.create(oldObj);` 新对象 `newObj` 会 “克隆” `oldObj` 的特性。
        - Python 中,虽然没有 “原型链”,但可以通过 `copy.copy()`(浅拷贝)、`copy.deepcopy()`(深拷贝)快速复制一个对象,本质就是原型模式的应用。
    2. **动态特性允许 “原型随时修改”**
        - JS 和 Python 都是动态语言,对象的属性 / 方法可以在运行时动态添加或修改。
            比如 JS 中,给原型对象加一个方法,所有基于它创建的新对象都会立刻拥有这个方法,这正是原型模式 “用原型统一管理共性” 的核心思想。
    3. **弱化 “类” 的概念,更依赖 “对象”**
        - Java、C++ 是 “基于类” 的语言:必须先定义类(模板),才能创建对象,对象的复制也依赖类的构造逻辑,原型模式用得少。
        - JS(ES6 前无类)、Python(类是动态的)更倾向于 “基于对象”:直接用现有对象当 “模板”(原型)复制新对象,不需要先定义类,原型模式自然成了常用手段。
    
    **一句话总结**
    
    JS、Python 中,**“拿一个现成对象当原型,复制出相似对象” 的操作太容易了**(语言直接提供 API),而且符合它们 “灵活、动态” 的设计理念,所以原型模式用得多。而 Java、C++ 更依赖 “类” 来创建对象,原型模式就显得没那么必要。
    
  • 接下来会按照一个类似课程中的例子使用深拷贝和浅拷贝结合的方式进行实现

  • 需求: 假设现在数据库中有大量数据,包含了关键词,关键词被搜索的次数等信息,模块 A 为了业务需要

    • 会在启动时加载这部分数据到内存中
    • 并且需要定时更新里面的数据
    • 同时展示给用户的数据每次必须要是相同版本的数据,不能一部分数据来自版本 1 一部分来自版本 2

主要就是一个深拷贝的问题。

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
package prototype

import (
"encoding/json"
"time"
)

// Keyword 搜索关键字
type Keyword struct {
Word *string `json:"word"`
Visit *int `json:"visit"`
UpdatedAt time.Time `json:"updatedAt"`
}

// Clone 这里使用序列化与反序列化的方式深拷贝
func (k *Keyword) Clone() *Keyword {
var newKeyword Keyword
b, err := json.Marshal(k)
if err != nil {
return nil
}
err = json.Unmarshal(b, &newKeyword)
if err != nil {
return nil
}
return &newKeyword
}

// Keywords 关键字 map
type Keywords map[string]*Keyword

// Clone 复制一个新的 keywords
// updatedWords: 需要更新的关键词列表,由于从数据库中获取数据常常是数组的方式
func (words Keywords) Clone(updatedWords []*Keyword) Keywords {
newKeywords := Keywords{}

for k, v := range words {
// 这里是浅拷贝,直接拷贝了地址
newKeywords[k] = v
}

// 替换掉需要更新的字段,这里用的是深拷贝
for _, word := range updatedWords {
if word != nil {
newKeywords[word.Word] = word.Clone()
}
}

return newKeywords
}




五、设计模式与范式:结构型

结构型模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。

5.1 代理模式

代理模式(Proxy Design Pattern)的原理和代码实现都不难掌握。它在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

grpc也使用了这个设计模式。


静态代理

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
package proxy

import (
"log"
"time"
)

// IUser IUser
type IUser interface {
Login(username, password string) error
}

// User 用户
type User struct {
}

// Login 用户登录
func (u *User) Login(username, password string) error {
// 不实现细节
return nil
}

// UserProxy 代理类
type UserProxy struct {
user *User
}

// NewUserProxy NewUserProxy
func NewUserProxy(user *User) *UserProxy {
return &UserProxy{
user: user,
}
}

// Login 登录,和 user 实现相同的接口
func (p *UserProxy) Login(username, password string) error {
// before 这里可能会有一些统计的逻辑
start := time.Now()

// 这里是原有的业务逻辑
if err := p.user.Login(username, password); err != nil {
return err
}

// after 这里可能也有一些监控统计的逻辑
log.Printf("user login cost time: %s", time.Now().Sub(start))

return nil
}

动态代理

动态代理的核心目的之一就是避免 “为每个接口、每个方法手动编写代理逻辑” 的重复劳动,实现 “一次编码,批量增强”。

go不好搞,放弃了





5.2 桥接模式

桥接模式,也叫作桥梁模式,英文是Bridge Design Pattern。这个模式可以说是23种设计模式中最难理解的模式之一。

用不上先忽略。





5.3 装饰器模式