目次
第一章:小さくまとめてわかりやすくする ※本記事
第二章:場合分けのロジックを整理する
第三章:業務ロジックをわかりやすく整理する
はじめに
この記事は増田 亨さんの著書「現場で役立つシステム設計の原則」を読んでの感想と備忘録です
自分用のメモのため、書籍の内容を網羅したものではありません
書籍のタイトルの通り、まさに現場ですぐにでも役立つ内容でしたので是非読んでみてください
特に新卒エンジニアに早いうちに読んで欲しい。先輩達のクソコードに毒される前に
第一章の感想
すぐにでも試せる強力なテクニックがいくつか紹介されていた
第一章のポイントは以下の2点だと思う
- オブジェクトの状態を不変にして振る舞いを追いやすくする
- データと振る舞いをまとめて業務上の意味を持たせる
どちらもすぐに取り掛かることができる
ただ、2. についてはどのようなクラスに切り分ければいいか?を判断するために業務の理解が必要なため、時間がかかりそうではある
キーワード
- 説明用の変数の導入
- リファクタリング
- 破壊的代入
- メソッドの抽出
- ドメインオブジェクト
- 値オブジェクト
- コレクションオブジェクト(ファーストクラスコレクション)
第一章:小さくまとめてわかりやすくする
なぜソフトウェアの変更は大変なのか
以下は現場あるある
- どこを変えればいいのかわからない…え、ここも修正必要だったの?」
- 「副作用が追いきれない…全部テストしよ…え、そこ壊れてたの?」
大体クラスが大きい + ロジックが散らばってる + 結合が密すぎなせい
(感想)
なので、高凝集/疎結合にしていきましょうね、というのが主旨かな?
プログラムの変更が楽になる書き方
わかりやすい名前を使う
// 変数名のつけ方
int unitPrice; // 良い例。意味のある単語を使う
int up; // 悪い例。略称を使う。パッと見何のことだかわからない
int a; // 論外。1文字の変数。何も意味がわからない
(感想)
当然のように取り組むべきことである
現代はほとんどの場合コードの文字数を減らす意味はない
たまに「変数名が長くなりすぎちゃうのが嫌」とか言われるけど、それは多分形容詞が多すぎるから
その場合は一言二言で表せる業務用語がある可能性が高い
長いメソッドは「段落」に分けて読みやすくする
// 悪い例。段落が無くてステップがわかりづらい
int price = quantity * unitPrice;
if ( price < 3000 )
price += 500; // 送料
price = price * taxRate();
// 良い例。段落が分かれて以下の3ステップであることがわかりやすい
// 1. ベース価格の計算
// 2. 送料の加算
// 3. 税額の加算
int price = quantity * unitPrice;
if ( price < 3000 )
price += 500; // 送料
price = price * taxRate();
(感想)
サンプルコードはシンプルなので効果が分かりづらいけど、実際の現場でも意識して処理のまとまりで段落を分けて読みやすくする
よくあるパターンとして、同じ主旨の処理が他の処理を跨いでいると可読性が著しく低下するので避けるべき
// 悪い例。そしてよく起こる例。
// 例えば送料に税をかけたくないからという理由で送料に関する処理で税額の処理を挟んでしまう
int price = quantity * unitPrice;
boolean needsPostage = price < 3000;
price = price * taxRate();
if ( needsPostage )
price += 500; // 送料
目的ごとに変数を用意する
// 悪い例。変数 priceの意味が変わって分かりにくい + いつどこで変更されたかわからない
int price = quantity * unitPrice; // price = 数量 x 単価の計算結果
if ( price < 3000 )
price += 500; // price = 今までの計算結果に送料を足した結果
price = price * taxRate(); // 今までの計算結果に税率を適用した結果
// 良い例。目的ごとに変数が用意されてわかりやすい + いつどこで変更されたかすぐわかる
int basePrice = quantity * unitPrice;
int shippingCost = 0;
if ( basePrice < 3000 )
shippingCost = 500; // 送料
int itemPrice = (basePrice + shippingCost) * taxRate();
(感想)
手軽に出来て良い
おそらく本質は値をイミュータブルにしたい、ということなのだと思う
javascriptで「letやvar使わずにconst使いましょうね」というのと同じ理由
そういう意味では shippingCost をif文の中で代入しているのはちょっと危険…
メソッドとして独立させる
段落をメソッドに抽出する
(感想)
実際よくやるけどどこまでやるか結構迷う
ユニットテストのスコープが小さくなるので良い
あとメソッド名考えるのが結構大変。大変だけどロジックの意味を理解して言語化できるという副次効果があるのでやる価値は高い
たまにやりすぎてメソッドからメソッドを呼び、逆に可読性が低いコードが生まれることがあるけどどうすれば良いのだろう…
異なるクラスの重複したコードをなくす
やり方:
- それぞれのクラスで該当するコード(段落)をメソッドに抽出する
- 2つのクラスに参照関係がある場合
- 参照する側のメソッド呼び出しを参照される側のメソッド呼び出しに置き換える
- 2つのクラスに参照関係がない場合
- 共通のメソッドの置き場として別のクラスを作成する
- 元の2つのクラスのメソッド呼び出しを新しく作ったクラスの共通メソッド呼び出しに置き換える
(感想)
実践する難易度は実はちょっと高い
そもそも参照関係があるのが問題だったり、
メソッドのインタフェースの都合で別のクラスにメソッドを抜き出せなかったなどで
壁に当たることがある
これについては気軽にやる前にクラス設計しましょうね、という感じ
狭い感心事に特化したクラスにする
例えば送料に関することは送料クラスにまとめる
逆に送料に関係しないことは送料クラスには実装しない
(感想)
難易度は高いがやるべきこと
どのような感心事があるのか?を知るためにビジネスサイドの人の力を借りたくなる
現場としてはビジネスサイドと十分に話ができず、不明なまま進むこともある
「最初から完璧はありえない」の精神でわかる範囲で実践すべき
メソッドは短く、クラスは小さく
(感想)せやな
小さなクラスでわかりやすく安全に
データとロジック
プログラムはデータとロジックの組み合わせ
基本データ型の落とし穴
intなど、基本データ型は用途に合わない値を取ることができる
例えばintを数量として使う場合でも マイナス21億〜プラス21億 という現実離れした値を取りうる
値の範囲を制限してプログラムをわかりやすく安全にする
例えば「数量は1〜100で十分」なのであれば
class Quantity {
static final int MIN = 1;
static final int MAX = 100;
int value;
Quantity(int value) {
// コンストラクタでvalueをチェック。1〜100じゃないならエラー
// ...
}
}
(感想)
データと振る舞いを人まとまりにするメリットが大きい
コードを読んだ時に業務上の意味がわかる
ただ単に値のチェックをしましょうね、という話ではない
「値」を扱うための専用のクラスを作る
値とロジックを組み合わせたクラスを作る。これを値オブジェクト(Value Object)という
値とロジックを1つのクラスにまとめることで「業務でやりたいこと」と「プログラムでやっていること」を一致させていく
(感想)
値オブジェクト!
データとロジックが分かれているが故に変更が追えない、実装意図がわからないということは多々あるので積極的に使いたい
値オブジェクトは不変にする
変数の値の上書きは危険
コードが複雑になり、思わぬ副作用の原因となる
値オブジェクトも別の値が必要になったら別のオブジェクトを生成するべき
(感想)
前節の「目的ごとに変数を用意する」と同じ考え方
setterを用意すべきではない、はなるほどと思った
型を使ってコードをわかりやすく安全にする
基本データ型だけで書いたプログラムはバグを生みやすい
例えば同じintでも業務上の意味は異なるので同じように処理されるとバグになる
// 悪い例。金額(unitPrice)と数量(quantity) 両方をintで定義している
int amount(int unitPrice, int quantity) {
if ( quantity < 5 ) {
return unitPrice * quantity + 500; // 数量が4個以下なら送料がかかる
}
return unitPrice * quantity; 送料無料
}
int unitPrice = 100;
int quantity = 4;
int amount = amount(quantity, unitPrice); // 引数を逆にしちゃった!でもエラーは出ない
// 良い例。全ての値で専用の型を使っている
Money amount(Money unitPrice, Quantity quantity) {
...
}
Money unitPrice = new Money(100);
Quantity quantity = new Quantity(4);
Money amount = amount(quantity, unitPrice); // 引数を逆にしちゃった!コンパイルエラーで気付ける
(感想)
いわゆる型安全というやつだろうか
複雑さを閉じ込める
配列やコレクションはコードを複雑にする
配列やコレクションを扱うコードは複雑になりがち
- ループ処理
- 要素数の変化
- 個々の要素の状態の変化
- 件数に関する処理
なので、配列やコレクションを扱う専用のクラスを作って扱うべし
コレクション型を扱うコードの整理
コレクション型には基本操作(要素の追加、削除など)があるが、大体の場合業務にはもっと複雑な処理が必要
プログラムの各所でコレクションが操作されるため、コレクションの状態の把握は難しい
コレクション型を扱うロジックを専用クラスに閉じ込める
専用のクラスを作ることで要素数のチェックやループなどの複雑な処理を閉じ込め、使う側が簡単になる
// 悪い例。コレクション型をそのまま使う
List<Customer> customers = new ArrayList<>();
customers.add(customer);
// 良い例。コレクション型のインスタンス変数を1つだけ持つ専用クラスを作る
class Customers {
List<Customer> customers;
void add(Customer customer) { ... }
void removeIfExist(Customer customer) { ... }
int count() {...}
Customers importCustomers() { ... }
}
(感想)
この方法は知らなかった、なるほど!
コレクションのループ処理は各所に分散してワケワカランになりがちなので、とても良さそう
実際だとメソッド名はもっと業務に関わる言葉になりそう
.add() の代わりに .invite() や .register() など
コレクションオブジェクトを安定させる
コレクションを返す必要がある場合、参照をそのまま返さず
- 新しく作ったコレクションを返す
- 不変にしたコレクションを返す
この場合でも個々の要素の変更は出来てしまうので要素は変更ができない値オブジェクトにしておくと安心
// 悪い例。参照をそのまま返す
class Customers {
List<Customer> customers;
...
List<Customer> add(Customer customer) {
customers.add(customer);
return customers; // 参照をそのまま返す
}
}
List<Customer> currentCustomers = customers.add(newCustomer); // 参照を取得
currentCustomers.remove(newCustomer); // customersインスタンスの中身も変わってしまう
// 良い例。同じ型のコレクションオブジェクトを作って返す
class Customers {
List<Customer> customers;
...
Customers add(Customer customer) {
List<Customer> newCustomers = new ArrayList<>(customers);
newCustomers.add(customer)
return new Customers(newCustomers); // 新しいインスタンスを返す
}
}
Customers currentCustomers = customers.add(newCustomer); // 新しいインスタンスを取得
currentCustomers.remove(newCustomer); // customersインスタンスの中身は変わらない
// 良い例。参照を不変にして返す
class Customers {
List<Customer> customers;
...
List<Customer> getList() {
return Collection.unmodifiableList(customers); // 参照を不変にして返す
}
}
Customers currentCustomers = customers.getList(); // 参照を取得
currentCustomers.add(newCustomer); // 例外発生
(感想)
覚えておきたいコレクションの安全な扱い方
値オブジェクトと組み合わせるとより安全になる
逆に言うと List<int> みたいなコードを書くのは悪い例になりそう
コレクションオブジェクトは業務の感心事
業務の感心事とプログラミング単位であるクラスが1対1で一致していれば業務ルールが変わった時にプログラムの変更箇所を特定しやすくなる
(感想)
「業務の感心事とクラスを1対1で対応させる」
このキャッチーなフレーズを声に出して伝えていきたい
コメント