From d5de65fdb1802cdf498d65d93397f290813c377c Mon Sep 17 00:00:00 2001 From: muqiuhan Date: Tue, 9 Sep 2025 06:17:00 +0000 Subject: deploy: 9665097f0fa0dae9f92123fac54d26c0818758a5 --- .../index.html" | 1 - .../index.html" | 1 - .../index.html" | 6 ++- .../index.html" | 6 --- .../index.html" | 14 +++++-- .../index.html" | 32 +++++++++------ 2025/03/25/Scala-3-Capture-Checking/index.html | 16 +++----- .../index.html" | 5 +-- .../index.html" | 45 +++++++++++----------- 9 files changed, 64 insertions(+), 62 deletions(-) (limited to '2025/03') diff --git "a/2025/03/13/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\344\270\255\347\232\204\342\200\234\350\201\232\345\220\210\346\240\271\342\200\235/index.html" "b/2025/03/13/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\344\270\255\347\232\204\342\200\234\350\201\232\345\220\210\346\240\271\342\200\235/index.html" index 35e8670e..4f96ab6d 100644 --- "a/2025/03/13/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\344\270\255\347\232\204\342\200\234\350\201\232\345\220\210\346\240\271\342\200\235/index.html" +++ "b/2025/03/13/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\344\270\255\347\232\204\342\200\234\350\201\232\345\220\210\346\240\271\342\200\235/index.html" @@ -207,7 +207,6 @@

用 F# 来描述,以订单管理为例,大概写一下:

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 OrderStatus = 
| New
| Shipped
| Delivered
| Cancelled

type OrderItem (productName: string, price: float, quantity: int) =
do
if quantity <= 0 then
failwith "Quantity must be positive"

member public this.ProductName = productName
member public this.Price = price
member public this.Quantity = quantity
member public this.TotalPrice () = price * quantity

type Order (id: int, customerName: string) =
let mutable status = OrderStatus.New
let mutable orderItems = []

member public this.Id = id
member public this.CustomerName = customerName
member public this.Status = status
member public this.OrderItems = orderItems

member public this.AddItem (item: OrderItem, price: float, quantity: int) =
if quantity <= 0 then
failwith "Quantity must be positive"

orderItems <- orderItems @ [OrderItem(item.ProductName, price, quantity)]

member public this.ChangeStatus (status: OrderStatus) =
this.Status <- status

member public this.TotalPrice () =
orderItems |> List.sumBy (fun item -> item.TotalPrice())

member public this.GetTotalPrice () =
orderItems |> List.sumBy (fun item -> item.TotalPrice())
-

在这个例子中,Order 是聚合根,它通过 AddItem 方法来添加订单项,保证每个订单项符合业务规则。同时,聚合根 Order 还负责订单状态的管理,例如通过 ChangeStatus 方法来更新订单状态。OrderItem 是聚合内的一个实体,表示订单项,它通过 GetTotalPrice 方法来计算每个订单项的总价。外部系统只能通过 Order 聚合根来访问和操作订单项,而不能直接访问或修改 OrderItem

diff --git "a/2025/03/14/DDD-\344\270\255\347\232\204-Ubiquitous-Languages/index.html" "b/2025/03/14/DDD-\344\270\255\347\232\204-Ubiquitous-Languages/index.html" index 3f99a42a..d72575f9 100644 --- "a/2025/03/14/DDD-\344\270\255\347\232\204-Ubiquitous-Languages/index.html" +++ "b/2025/03/14/DDD-\344\270\255\347\232\204-Ubiquitous-Languages/index.html" @@ -209,7 +209,6 @@

假设正在开发一个电子商务系统,业务专家使用“订单”来描述用户购买的商品集合。团队可以在代码中使用“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
type Order = {
items: OrderItem list
customer: Customer
orderDate: DateTime
}

type OrderItem = {
product: Product
quantity: int
}

type Customer = {
name: string
}

type Product = {
name: string
price: float
}

let addItem (order: Order) (item: OrderItem) =
{ order with items = order.items @ [item] }

let getTotalAmount (order: Order) =
let total = 0.0
for item in order.items do
total <- total + item.price
total

let getPrice (item: OrderItem) =
item.product.price * item.quantity

let getTotalPrice (order: Order) =
let total = 0.0
for item in order.items do
total <- total + getPrice(item)
total
-

在这个示例中使用了“Order”、“OrderItem”、“Customer”和“Product”等术语,这些术语都是 Ubiquitous Language 的一部分,可以提高代码与业务需求的一致性。

diff --git "a/2025/03/17/\344\272\214\343\200\207\344\272\214\344\272\224\345\271\264\344\270\211\346\234\210\345\215\201\344\270\203\346\227\245/index.html" "b/2025/03/17/\344\272\214\343\200\207\344\272\214\344\272\224\345\271\264\344\270\211\346\234\210\345\215\201\344\270\203\346\227\245/index.html" index 4c617dc1..d4d68cd3 100644 --- "a/2025/03/17/\344\272\214\343\200\207\344\272\214\344\272\224\345\271\264\344\270\211\346\234\210\345\215\201\344\270\203\346\227\245/index.html" +++ "b/2025/03/17/\344\272\214\343\200\207\344\272\214\344\272\224\345\271\264\344\270\211\346\234\210\345\215\201\344\270\203\346\227\245/index.html" @@ -301,8 +301,10 @@ mjx-container[display="true"] + br {

它们是因缘聚散吗?

那露珠消散后去了哪里?

此有故彼有,此生故彼灭。

-

当晨光加热露珠表面至 深度时,表层水分子动能突破氢键束缚(键能约 )。
这种相变并非整齐划一的队列解散,而是呈现量子隧穿效应——单个水分子以 量级的涨落,在液态与气态间振荡,直到完全脱离范德华力作用半径(约 )。

-

逃逸的 H2O 分子并非直线升空,而是在空气分子碰撞下进行三维随机游走。
根据爱因斯坦-斯托克斯方程,其扩散系数 (25℃标准大气压)。这意味着单个水分子在1秒内将形成半径约 的概率云,与数十亿同伴共同形成水汽。

+

当晨光加热露珠表面至 深度时,表层水分子动能突破氢键束缚(键能约 )。
+这种相变并非整齐划一的队列解散,而是呈现量子隧穿效应——单个水分子以 量级的涨落,在液态与气态间振荡,直到完全脱离范德华力作用半径(约 )。

+

逃逸的 H2O 分子并非直线升空,而是在空气分子碰撞下进行三维随机游走。
+根据爱因斯坦-斯托克斯方程,其扩散系数 (25℃标准大气压)。这意味着单个水分子在1秒内将形成半径约 的概率云,与数十亿同伴共同形成水汽。

水中捞月,伸手时,涟漪碎了三千世界。

人类的眉睫处有十方虚空,三藏经书不过指月之指,

看见儿时门前溪水倒流,看见婴孩啼哭时眼底星河闪烁。

diff --git "a/2025/03/19/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\344\270\255\350\201\232\345\220\210\346\240\271\346\214\201\344\271\205\345\214\226\345\222\214\344\272\213\344\273\266\345\217\221\345\270\203\345\217\257\350\203\275\345\257\274\350\207\264\346\225\260\346\215\256\344\270\215\344\270\200\350\207\264\351\227\256\351\242\230/index.html" "b/2025/03/19/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\344\270\255\350\201\232\345\220\210\346\240\271\346\214\201\344\271\205\345\214\226\345\222\214\344\272\213\344\273\266\345\217\221\345\270\203\345\217\257\350\203\275\345\257\274\350\207\264\346\225\260\346\215\256\344\270\215\344\270\200\350\207\264\351\227\256\351\242\230/index.html" index b15e30ac..f2210beb 100644 --- "a/2025/03/19/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\344\270\255\350\201\232\345\220\210\346\240\271\346\214\201\344\271\205\345\214\226\345\222\214\344\272\213\344\273\266\345\217\221\345\270\203\345\217\257\350\203\275\345\257\274\350\207\264\346\225\260\346\215\256\344\270\215\344\270\200\350\207\264\351\227\256\351\242\230/index.html" +++ "b/2025/03/19/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\344\270\255\350\201\232\345\220\210\346\240\271\346\214\201\344\271\205\345\214\226\345\222\214\344\272\213\344\273\266\345\217\221\345\270\203\345\217\257\350\203\275\345\257\274\350\207\264\346\225\260\346\215\256\344\270\215\344\270\200\350\207\264\351\227\256\351\242\230/index.html" @@ -194,7 +194,6 @@

使用领域事件的一种直接做法是:在 应用服务 (Application Service) 中产生事件并发布出去。例如,对于“用户昵称更新”的场景来讲,对应的应用服务 UserCommandService 实现如下:

1
2
3
4
5
6
7
8
9
10
member public this.UpdateMyName (command: UpdateUsernameCommand) (user: User) =
let user = userRepository.GetById user.Id
let oldName = user.Username
let newName = command.Username

user.UpdateUsername newName
|> userRepository.Save

UsernameChangeEvent (user.Id, newName, oldName)
|> eventPublisher.Publish
-

这里,在更新了用户姓名之后,即刻调用事件发布器 eventPublisher.Publish 将事件发送到消息队列中。虽然这种方式比较流行,但它至少存在两个问题:

  1. 领域事件本应属于领域模型的一部分,也即应该从领域模型中产生,而这里却在应用服务中产生
  2. @@ -202,24 +201,19 @@

对于第1个问题,可以采用“从领域模型中返回领域事件”的方式:

1
2
3
4
5
6
7
8
9
member public this.UpdateMyName (command: UpdateUsernameCommand) (user: User) =
let user = userRepository.GetById user.Id
let oldName = user.Username
let newName = command.Username

user.UpdateUsername newName // UpdateUsername 中构建 UsernameChangeEvent
|> fun user event ->
userRepository.Save user
eventPublisher.Publish event
-

这种方式保证了领域事件是从领域模型中产生,但仍然存在第二个问题。

第二个问题中所谓的“数据一致性”,表示的是将聚合根保存到数据库和将领域事件发布到消息队列之间的一致性。由于数据库和消息队列属于异构的数据源,要保证他们之间的数据一致性需要引入分布式事务。

但是分布式事务通常是比较重量级的,再加上当下的诸多常见消息队列均不支持分布式事务(比如Kafka),因此并不建议使用分布式事务来解决这个问题。

Transactional Outbox 便是一种方案,概括来说,这种方式将一个分布式事务的问题拆解为多个本地事务,并采用“至少一次投递(At Least Once Delivery)”原则保证消息的发布。具体来讲,发布方在与业务数据相同的数据库中为领域事件创建相应的事件发布表(Outbox table),然后在保存业务数据的同时将所产生的事件保存到事件发布表中,由于此时二者都属于同一个数据库的本地事务所管辖,因此保证了“业务操作”与“事件产生”之间的一致性。此时的代码变成了:

1
2
3
4
5
6
7
8
9
member public this.UpdateMyName (command: UpdateUsernameCommand) (user: User) =
let user = userRepository.GetById user.Id
let oldName = user.Username
let newName = command.Username

user.UpdateUsername newName // UpdateUsername 中构建 UsernameChangeEvent
|> fun user event ->
userRepository.Save user
eventStore.Save event // 这儿用 eventStore 代替 eventPublisher 啦
-

应用服务不再将事件直接发布出去,而是将事件保存到数据库中,之后,另一个模块将从数据库中读取事件并发布。

然而,这种方式依然有个缺点:每个需要产生领域事件的场景都需要应用服务先后调用repository.Save()和eventStore.Save(),导致了代码重复。解决方法也很简单——在聚合根中临时保存领域事件,然后在资源库中同时保存聚合根和领域事件到数据库。

在这种方式下,首先需要在聚合根的基类中完成与领域事件相关的各种设施,包括创建临时性的事件容器events以及通用的事件产生方法RaiseEvent():

1
2
3
4
5
6
7
8
9
[<AbstractClass>]
type IAggregateRoot =
...
let events = Collections.Generic.List<DomainEvent> ()

member private this.RaiseEvent (event: DomainEvent) =
events.Add event

...
-

在聚合根基类AggregateRoot中,events字段用于临时保存聚合根中所产生的所有事件,各实际的聚合根类通过调用RaiseEvent()向events中添加事件。比如,对于“用户修改昵称”而言,User实现如下:

1
2
3
4
5
6
7
8
member public this.UpdateUsername (name: string, user: User) =
if this.Username = name then
()
else
let oldName = this.Username
this.Username <- name
UsernameChangeEvent (user.Id, name, oldName)
|> this.RaiseEvent
-

这里,聚合根 User 不再返回领域事件,而是将领域事件通过AggregateRoot.RaiseEvent()暂时性地保存到自身的events中。之后在保存User时,资源库的公共基类BaseRepository的Save()方法同时完成对聚合根和领域事件的持久化:

1
2
3
4
5
6
7
8
9
member public this.Save<AR: when AR :> AggrateRoot> (it: AR) =
match it with
| null -> failwith "..."
| it when it.Events |> isEmpty |> not ->
this.SaveEvents it.Events
this.CleanEvents ()
| _ -> ()

db.Save it
-

在Save()方法中,首先获取到聚合根中的所有领域事件,然后通过SaveEvents()方法将它们保存到发布事件表中,最后通过db.Save it保存聚合根。需要注意的是,在这种方式下,AggregateRoot中的events字段是不能被持久化的,因为需要保证每次从数据库中加载出聚合根时events都是空的,为此在SaveEvents()保存了领域事件后,立即调用it.clearEvents()将所有的领域事件清空掉,以免领域事件随着聚合根一道被持久化到数据库中。

到目前为止,对领域事件的处理都还没有涉及到与任何消息中间件相关的内容,也即事件的产生是一个完全独立于消息队列的关注点,此时不用关心领域事件之后将以何种形式发布出去,Kafka 也好,RabbitMQ 也罢。除了关注点分离的好处外,这种解耦也使得系统在有可能切换消息中间件时更加的简单。

对于“在应用服务中通过eventPublisher.Publish()直接发布事件”而言,事件的产生和发布是同时完成的;但是对于“在聚合根中临时性保存领域事件”的方式来说,它只解决了事件的产生问题,并未解决事件的发布问题,事件的发布方应该采用“发射后不管(Fire And Forget)”的原则,即发布方无需了解消费方是如何处理领域事件的,甚至都不需要知道事件被哪些消费方所消费。

diff --git "a/2025/03/20/TDD-\345\222\214-DDD-\347\232\204\344\270\200\344\272\233\345\260\217\346\203\263\346\263\225/index.html" "b/2025/03/20/TDD-\345\222\214-DDD-\347\232\204\344\270\200\344\272\233\345\260\217\346\203\263\346\263\225/index.html" index 8c1575ec..e7adc5c0 100644 --- "a/2025/03/20/TDD-\345\222\214-DDD-\347\232\204\344\270\200\344\272\233\345\260\217\346\203\263\346\263\225/index.html" +++ "b/2025/03/20/TDD-\345\222\214-DDD-\347\232\204\344\270\200\344\272\233\345\260\217\346\203\263\346\263\225/index.html" @@ -193,25 +193,31 @@

Test-Driven Development (TDD) 和 Domain-Driven Design (DDD) 是两种不同的软件开发方法论,各自有其独特的优缺点和应用场景。

-

Test-Driven Development (TDD)

优点:

    +

    Test-Driven Development (TDD)

    +

    优点:

    +
    1. 提高代码质量:通过编写测试来驱动开发,可以确保代码的正确性和可靠性。
    2. 减少缺陷:早期发现和修复缺陷,减少后期的调试和维护成本。
    3. 促进可维护性:代码更加模块化和可测试,便于后续的维护和扩展。
    4. 文档化:测试代码本身就是一种文档,描述了系统的行为和期望。
    5. 提高开发速度:虽然初期可能会慢一些,但长期来看,由于减少了重构和调试的时间,开发速度会提高。
    -

    缺点:

      +

      缺点:

      +
      1. 初期投入大:需要在开发前编写测试,初期投入的时间和精力较大。
      2. 学习曲线陡峭:对新手开发者来说,学习和掌握TDD需要一定的时间。
      3. 过度依赖测试:可能导致过度依赖单元测试,忽略了系统的整体性能和用户体验。
      -

      Domain-Driven Design (DDD)

      优点:

        +

        Domain-Driven Design (DDD)

        +

        优点:

        +
        1. 聚焦领域:通过深入理解业务领域,确保软件系统与业务需求紧密结合。
        2. 模型驱动:建立一个清晰的领域模型,帮助开发者和业务专家共同理解系统。
        3. 可维护性:通过明确的领域模型和边界,系统结构更加清晰,便于维护和扩展。
        4. 沟通桥梁:提供了一种通用的语言(Ubiquitous Language),促进开发团队和业务团队之间的沟通。
        -

        缺点:

          +

          缺点:

          +
          1. 复杂性高:DDD方法论较为复杂,需要深入理解领域模型和设计模式。
          2. 初期投入大:需要大量的时间和精力进行领域分析和建模。
          3. 适用范围有限:对于简单的系统或小型项目,DDD可能显得过于复杂和冗余。
          4. diff --git "a/2025/03/20/\350\202\272\345\212\237\350\203\275\346\243\200\346\237\245\346\225\260\345\200\274/index.html" "b/2025/03/20/\350\202\272\345\212\237\350\203\275\346\243\200\346\237\245\346\225\260\345\200\274/index.html" index eea6b710..5733b0ac 100644 --- "a/2025/03/20/\350\202\272\345\212\237\350\203\275\346\243\200\346\237\245\346\225\260\345\200\274/index.html" +++ "b/2025/03/20/\350\202\272\345\212\237\350\203\275\346\243\200\346\237\245\346\225\260\345\200\274/index.html" @@ -193,41 +193,51 @@

肺功能测试是评估患者呼吸系统健康的重要工具。各个指标的及其异常数值可能指示的潜在生理疾病:

-

1. FEV1(第一秒用力呼气量)

-

一、传统手艺:Partial Application

在函数式编程中,Partial Application 是传递依赖的常用方式。例如:

+

一、传统手艺:Partial Application

+

在函数式编程中,Partial Application 是传递依赖的常用方式。例如:

1
2
3
let foo bar baz request = ...
let wired = foo dependency1 dependency2
let response = wired request
- -

优点:

+

优点:

-

缺点:

+

缺点:


-

二、结构化方法:单一环境参数(env)

为解决参数爆炸问题,可将依赖封装为单一环境对象env,并通过接口约束访问权限:

+

二、结构化方法:单一环境参数(env)

+

为解决参数爆炸问题,可将依赖封装为单一环境对象env,并通过接口约束访问权限:

1
2
3
4
5
6
7
8
[<Interface>] type ILog = abstract Logger: ILogger
[<Interface>] type IDb = abstract Database: IDatabase

module Log =
let info (env: #ILog) = env.Logger.Info("Message")

module Db =
let fetchUser (env: #IDb) = env.Database.Query(...)
- -

优点:

+

优点:

-

应用场景:

+

应用场景:

1
2
3
4
5
let changePass env req = task {
let! user = Db.fetchUser env req.UserId
Log.info env "Processing user: %i" user.Id
...
}
-
-

三、Reader Monad

为消除显式的env传递,可引入 Reader Monad,将环境隐式注入计算流程:

+

三、Reader Monad

+

为消除显式的env传递,可引入 Reader Monad,将环境隐式注入计算流程:

1
2
3
4
5
6
7
8
9
10
11
[<Struct>] type Effect<'env, 'out> = Effect of ('env -> 'out)

module Effect =
let run env (Effect fn) = fn env
let bind f effect = Effect (fun env -> run env (f (run env effect)))

type EffectBuilder() =
member __.Bind(e, f) = Effect.bind f e
member __.Return(x) = Effect (fun _ -> x)

let effect = EffectBuilder()
-

然后:

1
2
3
4
5
6
let changePass req = effect {
let! user = Db.fetchUser req.UserId
let! salt = Random.bytes 32
do! Log.info "Password updated for user %i" user.Id
return Ok()
}
- -

优点:

+

优点:

-

缺点:

+

缺点:

-

Refs.