第 9 章: モジュール設計と型による設計¶
9.1 はじめに¶
前章では判別共用体とパターンマッチを導入しました。この章では、FizzBuzz プログラム全体を モジュール分割 し、型による設計(Type-Driven Design) でアーキテクチャを整理します。
9.2 現在のコード構造の課題¶
これまでの実装では、すべてのコードが 1 つのモジュールに集約されています。プログラムの規模が大きくなると以下の課題が生じます。
- 責務の混在: ドメインロジックとアプリケーションロジックが分離されていない
- 再利用性の低下: 特定の機能だけを利用することが困難
- テストの複雑化: 依存関係が絡み合いテストが書きにくい
9.3 モジュール分割¶
Domain モジュール¶
ビジネスルールとデータ型を定義するモジュールです。
module Domain =
// 値オブジェクト
type FizzBuzzValue =
{ Number: int
Value: string }
override this.ToString() = sprintf "%d:%s" this.Number this.Value
let createValue number value = { Number = number; Value = value }
// 判別共用体
type FizzBuzzType =
| Standard
| NumberOnly
| FizzBuzzOnly
// ドメインロジック
let private isFizz number = number % 3 = 0
let private isBuzz number = number % 5 = 0
let private isFizzBuzz number = isFizz number && isBuzz number
let generate (fizzBuzzType: FizzBuzzType) (number: int) : FizzBuzzValue =
match fizzBuzzType with
| Standard ->
if isFizzBuzz number then createValue number "FizzBuzz"
elif isFizz number then createValue number "Fizz"
elif isBuzz number then createValue number "Buzz"
else createValue number (string number)
| NumberOnly ->
createValue number (string number)
| FizzBuzzOnly ->
if number % 15 = 0 then createValue number "FizzBuzz"
else createValue number (string number)
FizzBuzzList 型¶
FizzBuzzValue のコレクションを専用の型でラップする ファーストクラスコレクション を定義します。
type FizzBuzzList =
{ Values: FizzBuzzValue list }
member this.Count = this.Values.Length
member this.Get(index) = this.Values.[index]
member this.Filter(predicate: FizzBuzzValue -> bool) =
{ Values = this.Values |> List.filter predicate }
member this.FindFirst(predicate: FizzBuzzValue -> bool) =
this.Values |> List.tryFind predicate
member this.ToStringValues() =
this.Values |> List.map (fun v -> v.Value)
member this.Add(value: FizzBuzzValue) =
{ Values = this.Values @ [ value ] }
override this.ToString() =
this.Values
|> List.map (fun v -> v.ToString())
|> String.concat ", "
let emptyList = { Values = [] }
let createList (values: FizzBuzzValue list) = { Values = values }
テストを書く¶
module FizzBuzzListTests =
[<Fact>]
let ``空のリストを作成できる`` () =
let list = emptyList
Assert.Equal(0, list.Count)
[<Fact>]
let ``値を追加できる`` () =
let list = emptyList
let newList = list.Add(createValue 1 "1")
Assert.Equal(1, newList.Count)
Assert.Equal(0, list.Count) // 元のリストは不変
[<Fact>]
let ``フィルタリングできる`` () =
let list =
createList
[ createValue 1 "1"
createValue 3 "Fizz"
createValue 5 "Buzz"
createValue 15 "FizzBuzz" ]
let filtered = list.Filter(fun v -> v.Value = "Fizz")
Assert.Equal(1, filtered.Count)
list.Add は新しいリストを返し、元のリストは変更されません。F# のレコード型は不変であるため、すべての操作が新しい値を返します。
Application モジュール¶
ユースケースを実行するモジュールです。Domain モジュールに依存します。
module Application =
open Domain
let executeValue (fizzBuzzType: FizzBuzzType) (number: int) : FizzBuzzValue =
generate fizzBuzzType number
let executeList (fizzBuzzType: FizzBuzzType) (count: int) : FizzBuzzList =
[ 1..count ]
|> List.map (generate fizzBuzzType)
|> createList
FizzBuzz モジュール(公開 API)¶
外部に公開するシンプルな API を提供するモジュールです。
module FizzBuzz =
open Domain
open Application
let generate (number: int) : string =
let value = executeValue Standard number
value.Value
let generateList (count: int) : string list =
let list = executeList Standard count
list.ToStringValues()
テストを書く¶
module ApplicationTests =
[<Fact>]
let ``executeValueで単一値を取得できる`` () =
let result = executeValue Standard 3
Assert.Equal("Fizz", result.Value)
[<Fact>]
let ``executeListでリストを生成できる`` () =
let result = executeList Standard 100
Assert.Equal(100, result.Count)
9.4 依存関係¶
FizzBuzz (公開 API)
↓
Application (ユースケース)
↓
Domain (ドメインモデル)
FizzBuzzモジュールはApplicationとDomainに依存ApplicationモジュールはDomainに依存Domainモジュールは外部に依存しない(純粋なドメインロジック)- 逆方向の依存は存在しない(単方向依存)
9.5 型による設計の利点¶
不正な状態を表現できない¶
判別共用体を使うことで、存在しないタイプを指定することがコンパイル時に防止されます。
// 型安全: コンパイルエラーになる
let result = generate InvalidType 1 // FizzBuzzType に InvalidType は定義されていない
// 型安全: 正しい使い方
let result = generate Standard 1
SOLID 原則の適用¶
| 原則 | 適用内容 |
|---|---|
| SRP | Domain / Application / FizzBuzz の責務分離 |
| OCP | 判別共用体にケースを追加して拡張 |
| LSP | FizzBuzzType の各ケースは同じ generate 関数で処理 |
| ISP | 各モジュールは必要な関数のみ公開 |
| DIP | Application は Domain の型に依存(抽象に依存) |
9.6 まとめ¶
この章では以下を実現しました。
| モジュール | 責務 | 含まれる型 |
|---|---|---|
Domain |
ドメインモデルとビジネスルール | FizzBuzzValue, FizzBuzzType, FizzBuzzList |
Application |
ユースケースの実行 | executeValue, executeList |
FizzBuzz |
公開 API | generate, generateList |
第 3 部を通じて、F# のレコード型、判別共用体、モジュールシステムを使った関数型アプローチによる設計を学びました。次の第 4 部では、F# の高度な関数型プログラミング機能(高階関数、関数合成、エラーハンドリング)を活用します。