Featured image of post コードメカニック【Value Object】支援者番号が、そのまま寄付額になっていた〜裸の数値に、意味の型を着せる〜

コードメカニック【Value Object】支援者番号が、そのまま寄付額になっていた〜裸の数値に、意味の型を着せる〜

寄付額の引数に支援者IDを渡しても、両方intでコンパイルが通り、桁違いの受領証明書が届いた。意味の違う値を別の型に分けて取り違えをコンパイルで止め、散らばったメール検証を生成の一点に集約する整備記録。

支援者番号が、寄付額の欄に座っていた

土曜の午後だった。いつも使っているコワーキングスペースは、週末で人がまばらで、僕のキーボードの音だけが響いていた。コーヒーは、とっくに冷めていた。

僕は、小さな寄付プラットフォームを、ひとりで作って、ひとりで運用している。NPOが継続寄付——支援者からの毎月の寄付——を受け付けるための仕組みだ。設計も、コードも、サーバーの面倒も、ぜんぶ僕ひとり。レビューしてくれる人は、いない。

その日、運営してもらっているNPOから、一通の問い合わせが転送されてきた。

「支援者さまから連絡です。『受領証明書の寄付額が ¥80,423 になっている。私が毎月続けているのは ¥5,000 です』とのことで……」

80,423。その数字を見て、背筋が冷たくなった。打ち間違いなんかじゃない。それは、ちゃんと意味のある数字だった。その支援者の、支援者ID。寄付額の欄に、IDが座っていた。

受領証明書は、確定申告で寄付金控除を受けるための、正式な書類だ。桁が違えば、支援者を税務の話にまで巻き込んでしまう。あわててDBを見ると、同じ「寄付額=支援者ID」の確定済み証明書が、先月分に何件もあった。

エラーログは、一件も無い。panic も出ていない。確定処理は、毎月、ちゃんと「成功」していた。ただ、金額が、静かに、IDに化けていた。

原因は、すぐに分かった。先月、Donation まわりを整理したとき、受領証明書を作る FinalizeMonthly の呼び出しで、寄付額を渡すつもりが、コピペした d.SupporterID を直し忘れていた。d.Amount に直すべき場所を。

もう直した。たった1行、d.SupporterIDd.Amount に書き換えるだけ。これで、今日の証明書は正しくなる。でも——書き換えながら、指先が冷たくなった。直したのは、挿し間違えた1本だけだ。挿さる口は、intint のまま、何も変わっていない。明日の僕が、また同じ口に、同じ手で、IDを挿せる。

自分で書いて、自分でテストして、それでも、気づけなかった。型が同じだと、こうなるのか。intint を渡して、何が悪い、とコンパイラは言う。じゃあ僕は、これから一生、引数を指で押さえて確かめるのか。

チームはない。レビューしてくれる人も、いない。ただ、月に一度行く個人開発者のもくもく会で、一度だけ隣り合った人が、「コードに詰まったら、この人に頼るといい」と、連絡先だけ残していった。藁にもすがる気持ちで、連絡した。

連絡から一時間ほどで、その人は来た。手ぶらに見えたが、薄いノートPCを一台だけ提げている。名乗りも前置きもなく、「席、空いてますか」と僕の隣を指した。腰を下ろすと、もう画面に目を落としている。僕は、いつのまにかこの人を、親方と呼んでいた。

「取り違えなんです」と僕は切り出した。「寄付額を渡す引数に、支援者IDを渡していました。コピペの、直し忘れで。もう直したんですけど——コンパイルも、テストも、何も言わなかった。自分で書いて、自分でテストして、それでも気づけなかったんです」

親方は、症状ではなく、別のことを聞いた。「その引数、両方とも int ですか」

「……はい。支援者IDも、寄付額も、int です」

「その関数を、呼んでいる側ごと、見せてください」

開いた画面には、引き継ぎでも何でもない、自分で書いたコードがあった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
type Donation struct {
	SupporterID int
	Amount      int // 円
	Email       string
}

type Receipt struct {
	SupporterID int
	Amount      int
}

// 寄付を確定して受領証明書を作る
func FinalizeMonthly(supporterID int, amount int, email string) (Receipt, error) {
	if !strings.Contains(email, "@") {
		return Receipt{}, errors.New("donation: メールアドレスが不正です")
	}
	if amount <= 0 {
		return Receipt{}, errors.New("donation: 寄付額は正の数で")
	}
	return Receipt{SupporterID: supporterID, Amount: amount}, nil
}

そして、事故った呼び出し——直す前の姿——を見せた。

1
2
3
4
// 月次バッチ。意図は FinalizeMonthly(d.SupporterID, d.Amount, d.Email)
//   d.SupporterID をコピペして、第2引数を d.Amount に直し忘れた
r, err := FinalizeMonthly(d.SupporterID, d.SupporterID, d.Email)
//                                       ^^^^^^^^^^^^^ 寄付額の枠に、支援者ID

「なんで、コンパイルが通るんですか」と僕は聞いた。「寄付額の引数に、IDを渡してるのに」

amountint だからです」と親方は言った。「支援者IDも int。コンパイラには、同じ int にしか見えない。意味が違うのに、型が同じ。だから、見分けられない」

意味が違うのに、型が同じ。その言葉が、妙に引っかかった。


直したのは一本。口の形は、そのままだ

親方は何も言わず、僕のキーボードを借りた。事故った呼び出しを、短く書き直す。Receipt を、そのまま表示する。

1
2
3
d := Donation{SupporterID: 80423, Amount: 5000, Email: "yamada@example.com"}
r, err := FinalizeMonthly(d.SupporterID, d.SupporterID, d.Email) // 取り違え
fmt.Println(err, r.Amount)

走らせた。出力は、これだった。

1
<nil> 80423

「エラーは nil」と親方は言った。「処理は、成功しています。なのに、寄付額は 80423——支援者IDそのものだ」

ぞっとした。「ほんとだ……。成功してる。¥80,423 の証明書が、ちゃんと『正常に』作られた。これが、支援者さんに届いたんだ」

抽象的だった「事故」が、目の前で、<nil> 80423 という一行になっていた。

親方は、僕がもう直した1行を確認した。

1
2
r, err := FinalizeMonthly(d.SupporterID, d.Amount, d.Email) // 寄付額を正しく渡す
// 出力: <nil> 5000

「直りました」と僕は言った。半分は安堵で、半分は——諦めだった。「¥5,000 になった。でも、これ、また誰かが——僕が——やりますよね。intint を渡すだけだから」

「ここは塞がりました」と親方は言った。「今日の証明書は、もう正しい。でも——口の形は、変えていません」

親方は、FinalizeMonthly のシグネチャを、指でなぞった。「supporterID intamount int。意味の違う二つが、同じ int の口で並んでいる。あなたが直したのは、挿し間違えた一本だけだ。口の形がこのままなら、同じ部品が、また挿さる。今日のは、ミスじゃない。この口が、いつか必ず起こすことを、たまたま今日、引いただけです」

痛いところだった。それでも、僕はまだ食い下がった。「でも、金額もIDも、ただの数字じゃないですか。int で持つのが、普通でしょう」

「数字に見えるのは、人間がそう読むからです」と親方は言った。「コンパイラには、ただの int だ。『これは寄付額』『これは支援者ID』という区別を、型のどこにも書いていない。書いていないものは、守れない」


同じ形の口には、何でも挿さる

親方は、FinalizeMonthly の三つの引数を、順に指した。「supporterIDamountemail。この関数の口は、三つ。でも、形は二種類しかない。int の口が二つと、string の口が一つ」

int の口には、int なら何でも挿さる」と親方は続けた。「支援者IDでも、寄付額でも、注文番号でも、年齢でも。口の形が同じだから、中身が何かは、問われない。——燃料ラインの継手と、冷却水のラインが、同じ形をしていたら。整備士は、いつか、燃料の口に冷却水を繋ぐ。気づかないまま」

口の形。僕は、その言葉を、頭の中で転がした。

「これは Primitive Obsession(プリミティブ・オブセッション)です」と親方は言った。プリミティブ・オブセッション——金額や支援者IDのような「意味のある概念」を、専用の型ではなく、裸の intstring(プリミティブ型)のまま持ってしまう癖のことだ。

intstring も、言語が最初から持っている、いちばん素の型だ」と親方は言った。「素のままだと、速い。考えなくていい。だから、つい、何でもそれで済ませる。済ませた瞬間は、ラクだ。ツケは、あとで来る」

言われて、自分のコードを見返した。支援者IDも、寄付額も、int。メールアドレスは string。寄付の参照番号も、たしか int で持っていた。ぜんぶ、素の型だった。早く動かしたくて、何も考えずに intstring を並べてきた。意味は、僕の頭の中にしかなかった。コードのどこにも、「これは寄付額だ」とは、書いていない。

親方は、メールアドレスに目を移した。「メールアドレスも、同じです。string の口には、どんな文字列でも挿さる。yamada@example.com も、a@b も、空っぽの文字列も。だから——」と、コードの別の場所を開いた。

僕のコードには、メールアドレスを確かめる処理が、三箇所に散っていた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 1) 新規支援者の登録
func Register(email string) (int, error) {
	if email == "" || !strings.Contains(email, "@") { // 検証その1
		return 0, errors.New("register: メールアドレスが不正です")
	}
	return nextID(), nil // nextID は採番のスタブ。本筋ではないので中身は省略
}

// 2) 受領証明書の確定(前掲 FinalizeMonthly の中)
//    if !strings.Contains(email, "@") { ... }          // 検証その2(その1と微妙に違う)

// 3) サンクスメール送信
var thanksEmailRe = regexp.MustCompile(`^.+@.+\..+$`)

func SendThanks(email string, amount int) error {
	if !thanksEmailRe.MatchString(email) { // 検証その3(正規表現・他と食い違う)
		return errors.New("thanks: メールアドレスが不正です")
	}
	// ...(送信)
}

「同じ『メールアドレスが正しいか』の確認が、三箇所に、別々のやり方で書いてある」と親方は言った。「RegisterContains@ を見るだけ。SendThanks は正規表現。だから、a@b のような文字列は、登録は通るのに、サンクスメールでは弾かれる。同じ値が、場所によって、正しかったり、正しくなかったりする」

「……コピペしたんです」と僕は白状した。「登録のときに書いて、確定のときにまたコピーして、サンクスのときは正規表現の方がいいかと思って、書き直して。少しずつ、違ってる」

そこで、思い当たることがあった。

「……そういえば、先々月。別の支援者から『受領証明書が届かない』と問い合わせが、ありました。登録は、できてる。でも、メールが戻ってきてた。アドレスが yamada@example——ドットから先が、抜けてたんです。Register@ チェックは、それを通した。システムには、ちゃんと入っていた。なのに、送ると弾かれる。誰も気づかないまま、あの人にも、書類が届いていなかった」

口にして、はじめて、その二つが繋がった。寄付額の取り違えも、届かなかった証明書も、根は同じだった。裸の値に、意味が乗っていない。intstring も、ただの素の型のまま、何の意味も持たずに、そこにあった。

「同じ口径のソケットが並んでいれば、そりゃあ間違えて差し込むさ」親方は、僕の頭上にある配管を指さすように言った。「燃料の給油口と、ウォッシャー液の注入口がまったく同じ形状で、隣り合わせに並んでいるようなものだ。人間がどれだけ気をつけたって、いつか必ず差し間違える」

同じ型の口だから、何でも挿さる。僕のコードは、そんな危ういソケットだらけの配管だったのだ。おまけに、流れる液体の安全チェックは、下流のいたるところでバラバラに行われていた。

Primitive Obsession in software architecture: Multiple modules (“FinalizeMonthly”, “Register”, “SendThanks”) share generic “int” and “string” type sockets, leading to potential parameter swapping and duplicate validation checks across different functions


裸の数値に、意味の型を着せる

「裸の値に、意味の型を着せます」と親方は言った。「寄付額は寄付額の型、支援者IDは支援者IDの型。口の形を、概念ごとに分ける」

まず、二つの型を作った。

1
2
3
4
// 土台はどちらも int64 でも、別の型
// (事故のときの int を、金額が大きくなっても困らないよう int64 に広げた。土台の広さは自由)
type SupporterID int64
type Money       int64 // 円(最小単位)

type SupporterID int64……」と僕は読んだ。「これ、ただ int64 に別名を付けただけに見えます。中身は同じ int64 なのに、意味、あるんですか」

「別名じゃない。別の型です」と親方は言った。「SupporterIDMoney は、中身がどちらも int64 でも、Goにとっては別物。Money を入れる口に、SupporterID は挿さらない」

type SupporterID int64 は、int64 を土台にした、新しい型を作る書き方だ。SupporterID という名前が付いた瞬間、それは「ただの int64」ではなく「支援者IDという意味を持った型」になる。土台が同じでも、Goは、名前の違う型を、別物として扱う。僕は、頭の中でそう言い直した。

親方は、関数の口も、構造体のフィールドも、その型に変えた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type Donation struct {
	SupporterID SupporterID
	Amount      Money
	Email       Email // ← この型は、このあと作る
}

type Receipt struct {
	SupporterID SupporterID
	Amount      Money
	Email       Email
}

// 引数の型が、概念ごとに分かれた
func FinalizeMonthly(supporterID SupporterID, amount Money, email Email) (Receipt, error) {
	if amount <= 0 {
		return Receipt{}, errors.New("donation: 寄付額は正の数で")
	}
	return Receipt{SupporterID: supporterID, Amount: amount, Email: email}, nil
}

そして親方は、今日の取り違えを、もう一度、わざと書いた。d.SupporterID を、amount の口に。

1
2
// d.SupporterID は SupporterID 型。amount の口は Money 型
FinalizeMonthly(d.SupporterID, d.SupporterID, email)

ビルドした。止まった。

1
cannot use d.SupporterID (variable of int64 type SupporterID) as Money value in argument to FinalizeMonthly

「さっきは、これが通った」と親方は言った。引っぱりはしなかった。「今は、通らない。Money の口に、SupporterID は挿さらないからです。今日の事故は、もう、コンパイルの時点で止まる」

「口の形を、分けたから」と僕は言った。「違う部品は、物理的に、挿さらない。型が、見張ってくれてる」

「ただし」と親方は付け加えた。「数字を直に書いた FinalizeMonthly(1, 1000, email) は、通ります」

「え」

1000 は、まだ型のついていない、ただの数だからです。Money にも、SupporterID にも収まる。型が止めるのは、別の型の『変数』を取り違えて渡す——今日のような——事故であって、最初から 1000 と打ち込む打ち間違いまでは、見ない。型は、万能じゃない。今日の事故を、構造で止める。それだけです」

正直だな、と思った。直してくれた本人が、その限界まで、先に言う。


散らばった検証が、ひとつの口に集まる

「メールアドレスは、もう一段やります」と親方は言った。「正しいものしか作れない型にする」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 検証付きの Email。フィールド v は小文字始まり=外から触れない
type Email struct {
	v string
}

var emailRe = regexp.MustCompile(`^[^@\s]+@[^@\s]+\.[^@\s]+$`)

// Email を作る、唯一の入口。ここで一度だけ検証する
func NewEmail(s string) (Email, error) {
	s = strings.TrimSpace(s)
	if !emailRe.MatchString(s) {
		return Email{}, errors.New("donation: 不正なメールアドレス")
	}
	return Email{v: s}, nil
}

func (e Email) String() string { return e.v } // 取り出しは、読み取りだけ

NewEmail は、Emailerror を、返してます」と僕は読んだ。「正しければ Email、ダメなら error(Email, error) って、そういう意味か」

「そうです」と親方は言った。

Email の中の v は、小文字で始まっている。Goでは、小文字始まりの名前は、そのパッケージの外からは触れない。だから、別の場所から Email{v: "でたらめ"} と、直接は作れない。Email を手に入れる道は、NewEmail ただ一つ。そして NewEmail は、検証を通った文字列しか、Email にしない。

最後の func (e Email) String() string の、関数名の前にある (e Email) は、レシーバと呼ばれる。この動作を Email という型にぶら下げる書き方で、e が、その Email 自身を指す。String() は、fmt.Println などで文字列にするときに、Goが自動で呼ぶ決まった名前だ。中の v は外から触れないから、この読み取り専用の口だけが、値を取り出す出口になる。

親方は、RegisterFinalizeMonthlySendThanks を、順に開いた。そして、それぞれのメール検証を、消していった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 1) 登録: 検証が消えた。引数が Email になった
func Register(email Email) (SupporterID, error) {
	// email は、もう正しい。確かめ直さない
	return nextID(), nil // あとは採番して返すだけ
}

// 2) 確定: FinalizeMonthly の中の Contains チェックが、消えた(前掲)

// 3) サンクス: 正規表現が、消えた
func SendThanks(email Email, amount Money) error {
	fmt.Printf("ありがとうございます。今月のご寄付 ¥%d を受け取りました。\n", amount)
	return nil
}

「三箇所にあった検証が、消えました」と親方は言った。「NewEmail の、一箇所だけになった。Register も、SendThanks も、もう確かめません。受け取った時点で Email なら、それは正しい。一度 Email になれば、その先は、誰も、検証し直さなくていい」

「さっきの yamada@example」と親方は続けた。「あれは、NewEmail で止まります。入口で。登録のときに弾けば、半端なアドレスは、そもそもシステムに入らない。三箇所のどれかをすり抜ける、ということが、起きなくなる」

画面を見た。三回コピペした、あのちぐはぐな確認が、ぜんぶ、消えていた。string だったときは、メールアドレスを受け取るたびに、「これ、確かめたっけ」と、どこかで不安だった。Email になったら、もう、信じていい。

——先々月の、あの人も。これなら、登録の時点で気づけた。書類が、ちゃんと届いた。

胸の奥が、少し、軽くなった。直したかったのは、たぶん、今日の一件だけじゃなかった。

親方がやったこれには、名前があった。Value Object(バリュー・オブジェクト)——裸の値(intstring)を、意味を持った専用の型でくるみ、不正な値では作れないようにしたものだ。Money のように、型を分けるだけの軽いものから、Email のように、生成のときに検証するものまで、含む。

ひとつ、訊いてみた。「引数に名前を付ける、みたいなやり方でも、取り違えは防げません?」

「呼び出しの一箇所なら、それでもいい」と親方は言った。「でも、型にすれば——構造体のフィールドでも、別の関数の引数でも、どこに置いても、取り違えが挿さらない。意味そのものを、型に持たせるからです」

口の形を分けるというのは、関数の入口だけの話じゃなかった。値そのものに、意味を持たせる。だから、その値が行く先のどこでも、形が合わなければ挿さらない。

「専用の型を作るということは、ソケットの形状そのものを変えることだ」親方は言った。「四角い口には四角いプラグしか挿さらないし、六角形の口には六角形のプラグしか挿さらない。そして、一度安全なフィルタを通した液体だけを、その専用ラインに流す」

型という専用の規格を持った継手が、互いのパイプを正確に繋ぎ、異物の侵入を入口で阻む。それはまるで、プログラムの中に組み込まれた、自動の物理的な安全装置のようだった。

Value Object pattern resolving Primitive Obsession: custom shapes for SupporterID (square), Money (circle), and Email (hexagon) prevent argument swapping, and the “NewEmail” factory function acts as a single centralized validation valve


試運転——ひとりでも、型が見張ってくれる

直したコードに、テストを書いた。といっても、古いコードを消したわけじゃない。事故当時の、裸の int のままの版は before/ という別のパッケージに、型を着せた新しい版は after/ に置いて、両方を残してある。まず、Before——before/ に残した、事故をそのまま記録するテストだ。ここは昔のまま int なので、さっき After で見た「取り違えはコンパイルで止まる」とは関係なく、取り違えは、通って、走る。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 取り違え(amount の枠に SupporterID)が、両方 int なので
// エラーも出ずに「成功」してしまうことを、記録する
func TestFinalizeMonthly_SwapCompilesAndSucceeds(t *testing.T) {
	d := Donation{SupporterID: 80423, Amount: 5000, Email: "yamada@example.com"}

	r, err := FinalizeMonthly(d.SupporterID, d.SupporterID, d.Email)
	if err != nil {
		t.Fatalf("取り違えても処理は成功してしまう想定だが error: %v", err)
	}
	if r.Amount != 80423 {
		t.Fatalf("事故の再現: 寄付額に支援者IDが入る: got Amount=%d, want 80423", r.Amount)
	}
}

このテストは、バグが「直った」ことではなく、バグが「起きる」ことを、確かめている。errnilAmount80423。先月、本番で起きていたことが、そのままテストになった。これが、二度と同じ事故を起こさないための、記録になる。メール検証の食い違い——yamada@example が、Register は通り、SendThanks は弾く——も、同じように記録した。

After は、逆向きのことを確かめる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func TestFinalizeMonthly_CorrectReceipt(t *testing.T) {
	email, err := NewEmail("yamada@example.com")
	if err != nil {
		t.Fatalf("NewEmail: %v", err)
	}
	r, err := FinalizeMonthly(SupporterID(80423), Money(5000), email)
	if err != nil {
		t.Fatalf("FinalizeMonthly: %v", err)
	}
	if r.Amount != Money(5000) {
		t.Fatalf("寄付額は 5000 のはず: got Amount=%d", r.Amount)
	}
}

func TestNewEmail_RejectsInvalid(t *testing.T) {
	for _, s := range []string{"yamada@example", "no-at", "", "a@b@c"} {
		if _, err := NewEmail(s); err == nil {
			t.Fatalf("NewEmail は %q を弾くはず", s)
		}
	}
}

テストの中の SupporterID(80423)Money(5000) は、ただの数に、その型を着せる書き方だ。さっき親方が「リテラルの 1000 は、まだ型がついていない」と言っていた、あの素の数に、ここでは「これは支援者IDだ」「これは金額だ」と、はっきり型を着せている。

取り違えが「コンパイルを通らない」ことは、テストには書けない。書いた瞬間、ビルドが落ちるからだ。だから、それは、さっき親方が見せてくれた、あのコンパイルエラーで確かめた。テストに残せるのは、通るコードだけ。通らないことの証明は、コンパイラがやってくれる。

走らせた。

1
2
3
$ go test ./...
ok  	code-mechanic/primitive-obsession-value-object/after   0.004s
ok  	code-mechanic/primitive-obsession-value-object/before  0.003s

ひとつ、確かめておきたいことがあった。あの応急処置——d.SupporterIDd.Amount に直した、あの一行——も、今日の取り違えは、ちゃんと防ぐ。型にした After も、防ぐ。「今日の事故は起きない」という結果だけ見れば、二つは同じだ。

違うのは、次の取り違えを、どう防ぐかだった。応急のほうは、僕が、その一行を、正しく書けているあいだだけ、正しい。次に同じ口へIDを挿せば、また通る。型にしたほうは、口の形そのものが違うから、挿そうとした瞬間に、コンパイラが止める。直したのは、同じ一件。変えたのは、次の一件が、黙って通れるかどうか、だった。

「これで、寄付額に支援者IDは挿さりません」と親方は言った。「メールは、Email になった時点で正しい。あなたが指で押さえて確かめていたことを、これからは、型がやります」

ひとりで書いてきた。レビューしてくれる人は、いない。だから、間違えたら、誰も止めてくれないと思っていた。——違った。型に意味を持たせれば、型が、僕の代わりに、見張ってくれる。ひとりじゃ、なかった。

親方は、Email の定義を最後にもう一度だけ確かめると、ノートPCを閉じた。それを小脇に抱えて、静かに席を立つ。引き止める間も、なかった。

「整備士は、いません」去り際に、親方はそう言った。「だが、新しい値が出てくるたびに、intstring のまま放っておかないこと。意味のあるものには、型を着せる。そうすれば、型が、あなたのレビュアーになる。手が滑った瞬間に、コンパイラが、声を上げてくれます」

返事は、すぐには出てこなかった。代わりに、思った。気をつけます、と言うのは簡単だ。でも、気をつけるのは僕で、僕は、また忘れる。忘れても止まるように、型に、見ていてもらう。それが、ひとりで書く僕の、たぶん一番の備えだ。

最初の問い合わせメールを、もう一度開いた。80,423。あの数字は、もう二度と、寄付額の欄には座れない。座ろうとした瞬間、コンパイラが、止める。

支援者さんに、正しい受領証明書を、送り直そう。今度は、桁を、指で押さえなくていい。


整備記録簿

こんな異音・症状が出たら入れるべき整備(Value Object)まだ様子見でいい
意味の違う値(支援者ID・寄付額・メール等)を、ぜんぶ裸の intstring で持ち、引数で取り違えてもコンパイルが通る
同じ値の検証(メール形式・金額の正負 等)が、複数箇所にコピペで散り、少しずつ食い違っている
「この string は検証済みか」が型から読めず、受け取るたびに確かめ直しているか、確かめ忘れて事故っている
値に制約が一切ない/内部の一時変数で外に出ない/ドメインの語彙として名前が付かない✓(裸のままで十分。Value Object は過剰)

整備手順

  1. ドメインの概念(「寄付額」「支援者ID」「メールアドレス」)を、裸の intstring のまま持っている箇所を洗い出す。
  2. 取り違えると痛い概念は、それぞれ別の型にする(type SupporterID int64type Money int64)。関数の引数も、構造体のフィールドも、その型に変える。取り違えは、コンパイルで止まる。
  3. 検証が要る概念は、非公開フィールドを持つ struct + コンストラクタ(NewEmail(s) (Email, error))にし、生成時に一度だけ検証する。外からは New… 経由でしか作れないので、検証を必ず通る。
  4. 散っていた検証を消し、New… の一箇所に集める。受け取った側は「型になっている=検証済み」と信じる。
  5. 複数通貨など単位が絡むなら、Money に通貨も抱かせて、異なる単位の演算を弾く。ただし、やりすぎない——取り違えると痛いもの、検証が散るものにだけ、型を着せる。

親方より

intstring は、いちばん素の型だ。素のままは、速い。考えなくていい。だから、つい、寄付額も、支援者IDも、メールも、ぜんぶ素のままで並べてしまう。並べた瞬間は、ラクだ。

だが、素の型は、意味を持たない。コンパイラには、寄付額も支援者IDも、同じ int にしか見えない。同じ口には、何でも挿さる。いつか、燃料の口に冷却水が繋がる。気づかないまま。

裸の値に、意味の型を着せろ。意味の違うものは、違う型にしろ。検証が要るものは、正しいものしか作れない型にしろ。そうすれば、取り違えは挿さらないし、検証は、作る一箇所で済む。——ひとりで書いていても、型は、お前の代わりに、見張ってくれる。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。