Skip to content

Commit

Permalink
doc:case study in clojure
Browse files Browse the repository at this point in the history
  • Loading branch information
MarsonShine committed Nov 13, 2024
1 parent f01caf5 commit 362c76c
Show file tree
Hide file tree
Showing 3 changed files with 289 additions and 2 deletions.
281 changes: 280 additions & 1 deletion FunctionalDesign/06-Case Study.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,282 @@ water 的行为是什么?我们询问模型设计师,他们告诉我们,

![](asserts/17.3.png)

好了深呼吸记住我们在这里是玩个游戏在像 Wa-Tor 这样简单的应用中我不会如此严格地对这些文件进行分区事实上我很可能会把整个程序写在一个文件里让复杂性自生自灭但我们假装这是一个多百万行的企业级应用程序所以我们会认真处理所有这些源代码依赖对吧

因此我们解决这个问题的方法是回到类似于旧的 C 语言中的声明和实现机制参见图 17.4

![](asserts/17.4.png)

通过将 `water` 分成两部分使其对 `fish` 的依赖在 `water-imp` 并确保 `water-imp` 依赖于 `water` 而不是相反遵循 DIP),循环依赖被打破了我还出于一致性分离了 `fish` `shark`[^12]。我可能很快也得分离 `animal`[^13]。

现在代码看起来像这样:

```clojure
(ns wator.world
(:require [wator
[water :as water]]))

(defn make [w h]
(let [locs (for [x (range w) y (range h)] [x y])
loc-water (interleave locs (repeat (water/make)))
cells (apply hash-map loc-water)]
{::cells cells
::bounds [w h]}))

(defn set-cell [world loc cell]
(assoc-in world [::cells loc] cell))

(defn get-cell [world loc]
(get-in world [::cells loc]))

; . . .
------------------------------------------
(ns wator.cell)

(defmulti tick ::type)

------------------------------------------
(ns wator.water
(:require [wator
[cell :as cell]]))

(defn make [] {::cell/type ::water})

(defn is? [cell]
(= ::water (::cell/type cell)))

------------------------------------------
(ns wator.water-imp
(:require [wator
[cell :as cell]
[water :as water]
[fish :as fish]
[config :as config]]))

(defmethod cell/tick ::water/water [water]
(if (> (rand) config/water-evolution-rate)
(fish/make)
water))

------------------------------------------
(ns wator.animal
(:require [wator
[world :as world]
[cell :as cell]
[water :as water]]))

(defmulti move (fn [animal & args] (::cell/type animal)))

(defmulti reproduce (fn [animal & args] (::cell/type animal)))

(defn tick [animal]
)

(defn do-move [animal loc world]
(let [neighbors (world/neighbors world loc)
destinations (filter #(water/is?
(world/get-cell world %))
neighbors)
new-location (rand-nth destinations)]
[new-location animal]))

------------------------------------------
(ns wator.fish
(:require [wator
[cell :as cell]]))
(defn make [] {::cell/type ::fish})

------------------------------------------
(ns wator.fish-imp
(:require [wator
[cell :as cell]
[animal :as animal]
[fish :as fish]]))

(defmethod cell/tick ::fish/fish [fish]
(animal/tick fish)
)

(defmethod animal/move ::fish/fish [fish loc world]
(animal/do-move fish loc world))

(defmethod animal/reproduce ::fish/fish [fish]
)
```

鲨鱼目前还不相关,所以我没有展示它。

分离 `water` 和 `fish` 的标准很容易看出来。任何引用直接类型层次之外的文件的函数都放在 `imp` 文件中。特别注意命名空间和命名空间关键字。例如,注意在 `fish-imp` 中的 `defmethod` 仍会根据 `::fish/fish` 进行分发。

以防你以为我忘记了,以下是当前的测试:

```clojure
(ns wator.core-spec
(:require [speclj.core :refer :all]
[wator
[cell :as cell]
[water :as water]
[water-imp]
[animal :as animal]
[fish :as fish]
[fish-imp]
[world :as world]]))
(describe "Wator"
(with-stubs)
(context "Water"
(it "usually remains water"
(with-redefs [rand (stub :rand {:return 0.0})]
(let [water (water/make)
evolved (cell/tick water)]
(should= ::water/water (::cell/type evolved)))))

(it "occasionally evolves into a fish"
(with-redefs [rand (stub :rand {:return 1.0})]
(let [water (water/make)
evolved (cell/tick water)]
(should= ::fish/fish (::cell/type evolved))))))

(context "world"
(it "creates a world full of water cells"
(let [world (world/make 2 2)
cells (::world/cells world)
positions (set (keys cells))]
(should= #{[0 0] [0 1]
[1 0] [1 1]} positions)
(should (every? #(= ::water/water (::cell/type %))
(vals cells)))))

(it "makes neighbors"
(let [world (world/make 5 5)]
(should= [[0 0] [0 1] [0 2]
[1 0] [1 2]
[2 0] [2 1] [2 2]]
(world/neighbors world [1 1]))
(should= [[4 4] [4 0] [4 1]
[0 4] [0 1]
[1 4] [1 0] [1 1]]
(world/neighbors world [0 0]))
(should= [[3 3] [3 4] [3 0]
[4 3] [4 0]
[0 3] [0 4] [0 0]]
(world/neighbors world [4 4]))))

(context "animal"
(it "moves"
(let [fish (fish/make)

world (-> (world/make 3 3)
(world/set-cell [1 1] fish))
[loc cell] (animal/move fish [1 1] world)]
(should= cell fish)
(should (#{[0 0] [0 1] [0 2]
[1 0] [1 2]
[2 0] [2 1] [2 2]}
loc))))))
```

看看 `ns` 声明中的 `:require`。注意我们引用了 `imp` 文件,但没有明确地使用它们。引用它们会注册它们所包含的 `defmethod`。

好了,现在我们可以移动 `fish`,我确信 `shark` 也会移动。因此,接下来我们应该尝试一些繁殖。但在此之前,我对 `world` 的类型系统感到(假装)担忧。先设置一下吧:

```clojure
(ns wator.world
(:require [clojure.spec.alpha :as s]
[wator
[cell :as cell]
[water :as water]]))

(s/def ::location (s/tuple int? int?))
(s/def ::cell #(contains? % ::cell/type))
(s/def ::cells (s/map-of ::location ::cell))
(s/def ::bounds ::location)
(s/def ::world (s/keys :req [::cells ::bounds])))

(defn make [w h]
{:post [(s/valid? ::world %)]}
…)
```

好了,现在感觉好多了。那么,我们需要哪些条件来实现繁殖呢?模型设计者说,如果一条 `fish` 在它旁边有一个 `water` 单元格并且它的年龄超过一定值,它就会繁殖。两条子鱼的年龄将被重置为零。否则,`fish` 的 `::age` 随时间增加。

以下是测试:

```clojure
(it "reproduces"
(let [fish (-> (fish/make)
(animal/set-age config/fish-reproduction-age))
world (-> (world/make 3 3)
(world/set-cell [1 1] fish))
[loc1 cell1 loc2 cell2] (animal/reproduce
fish [1 1] world)]
(should= loc1 [1 1])
(should (fish/is? cell1))
(should= 0 (animal/age cell1))
(should (#{[0 0] [0 1] [0 2]
[1 0] [1 2]
[2 0] [2 1] [2 2]}
loc2))
(should (fish/is? cell2))
(should= 0 (animal/age cell2))))

(it "doesn't reproduce if there is no room"
(let [fish (-> (fish/make)
(animal/set-age config/fish-reproduction-age)]
world (-> (world/make 1 1)
(world/set-cell [0 0] fish))
failed (animal/reproduce fish [0 0] world)]
(should-be-nil failed)))

(it "doesn't reproduce if too young"
(let [fish (-> (fish/make)
(animal/set-age
(dec config/fish-reproduction-age)]
world (-> (world/make 3 3)
(world/set-cell [1 1] fish)
failed (animal/reproduce fish [1 1] world)]
(should-be-nil failed)))
```

请注意,如果鱼繁殖了,返回值将包含两个子鱼。但是,如果出现问题,则返回 `nil`。这是因为我认为鱼的高层策略可能类似于这样:

```clojure
(if-let [result (animal/reproduce …)]
result
(animal/move …))
```

无论如何,以下是通过测试的简略代码:

```clojure
(ns wator.animal
(:require [clojure.spec.alpha :as s]
[wator
[world :as world]
[cell :as cell]
[water :as water]
[config :as config]]))

(s/def ::age int?)
(s/def ::animal (s/keys :req [::age]))

(defmulti move (fn [animal & args] (::cell/type a
(defmulti reproduce (fn [animal & args] (::cell/t
(defmulti make-child ::cell/type)

(defn make []
{::age 0})

(defn age [animal]
(::age animal))

(defn set-age [animal age]
(assoc animal ::age age))

;. . .
```

再次注意,我将 `fish/reproduce` 函数推迟到 `animal/do-reproduce`。这样我可以在 `animal` 中指定 `reproduce` 的通用行为,同时允许 `fish` 进行重写或扩展。我不知道这是否有必要[^14],但添加这个逻辑成本不高,并且可以避免在 `shark` 和 `fish` 中重复代码。

[^1]: 哎呀,《科学美国人》,我曾经很熟悉它……
[^2]: https://en.wikipedia.org/wiki/Wa-Tor
[^3]: 我在这里所使用的“高”与“低”层级的定义是“距离 I/O 的远近”。参见 Robert C. Martin 的《架构整洁之道》(Pearson, 2017),第183页。
Expand All @@ -320,4 +596,7 @@ water 的行为是什么?我们询问模型设计师,他们告诉我们,
[^8]: 几乎是函数式的)`(rand)` 调用是不纯的。
[^9]: 这有点像在基类中实现一个方法,并允许子类选择是否覆盖它。
[^10]: 也许这种不满是没有根据的,但毕竟这是一本关于函数式设计的书,所以...
[^11]: 记住 `:cells` 包含一个映射,因此 `update-cell` 函数将接收 `[key val]` 对,并返回 `[key val]` 对。
[^11]: 记住 `:cells` 包含一个映射,因此 `update-cell` 函数将接收 `[key val]` 对,并返回 `[key val]` 对。
[^12]: 其实只是分离了 `fish`。我在图中分离了 `shark` 但没有在代码中实现。YAGNI(你不会需要它)。
[^13]: 未来的 Uncle Bob:……不,还是需要
[^14]: 是的,我知道 "YAGNI"(“你不会需要它”)原则,但规则就是用来打破的。
10 changes: 9 additions & 1 deletion FunctionalDesign/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,12 @@

## [1.函数式基础](01-Functional Basics.md)

## [2.比较分析](Comparative Analysis.md)
## [2.比较分析](Comparative Analysis.md)

## [3.函数式设计](03-Functional Design.md)

## [4.函数式语论](04-Functional Pragmatics.md)

## [5.设计模式](05-Design Patterns.md)

## [6.案例学习](06-Case Study.md)
Binary file added FunctionalDesign/asserts/17.4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 362c76c

Please sign in to comment.