はじめに
最近、「良いコード/悪いコードで学ぶ設計入門」を読んでいるので、本書の中で自分が疑問に感じたポイントをQ&A形式で振り返ってみたいと思います。
この記事では、各セクションの簡単な説明の後に、[Q]で疑問に感じたこと、[A]で調べてわかったことをまとめています。
なお、サンプルコードにJavaを使用していますが、言語を問わず広く有効な設計手法なので、他言語でも応用できる内容になっています。
それでは、第1〜4章を振り返りながら、一緒に「良いコード/悪いコード」について学んでいきましょう!
技術駆動命名
技術駆動命名とは、型名を表す Int
、メモリ制御を表す Memory
や Flag
など、プログラミング用語やコンピュータ用語のような技術用語由来の命名のことを指します。
例えば、以下のような技術駆動命名を用いたコードは、ビジネス的に何を解決したいクラスやメソッドなのかわからないので、悪いコードとされています。
class MemoryStateManager {
void changeIntValue01(int changeValue) {
intValue01 -= changeValue;
if (intValue01 < 0) {
intValue01 = 0;
updateState02Flag();
}
}
...
}
[Q]
技術駆動命名って絶対にやっちゃダメなの?
例えば、Int
型を String
型に変換する intToString
というメソッド名もダメなの?
[A]
汎用ツールやアプリケーションと外部の世界(ファイル、DB、API、ネットワークなど)をつなぐインフラ層のコードでは、intToString
のような命名でもOKです。
命名の良し悪しは、「どの層で使われるか」によって変わります。
データクラス
データクラスとは、データを保持するだけのクラスです。
次の例では、税込み金額と消費税率を public
なインスタンス変数として保持しています。
// 契約金額
public class ContractAmount {
public int amountIncludingTax; // 税込み金額
public BigDecimal salesTaxRate; // 消費税率
}
しかし、当然ながら金額を扱うサービスにおいては、データの入れ物だけでなく、税込み金額を計算するロジックも必要になります。
あまり設計を考えないと、以下のように計算ロジックをデータクラスと別のクラスに実装してしまい、消費税関係で仕様変更が発生した場合に、いろいろなファイルを修正することになってしまいます。
// 契約を管理するクラス
public class ContractManager {
public ContractAmount contractAmount;
// 税込み金額を計算する。
public int calculateAmountIncludingTax(int amountExcludingTax, BigDecimal salesTaxRate) {
BigDecimal multiplier = salesTaxRate.add(new BigDecimal("1.0"));
BigDecimal amountIncludingTax = multiplier.multiply(new BigDecimal(amountExcludingTax));
return amountIncludingTax.intValue();
}
...
}
[Q]
データとロジックは、ひとつのクラスにまとめるべきってこと?
[A]
はい、カプセル化(データとそのデータを操作するロジックをひとつにまとめること)を意識して実装しましょう。
バラバラに実装されていると、既存の機能に気づかず似た処理を重複して書いてしまったり、仕様変更のたびに複数ファイルを修正しなくてはいけません。
また、関連するすべてのコードを探し出すのに膨大な時間を要するので、可読性・保守性ともに低下してしまいます。
不変で思わぬ動作を防ぐ
インスタンス変数の上書きは、理解を難しくします。
例えば、以下の Money
クラスでは、金額値を表す amount
というインスタンス変数を持っています。
class Money {
int amount; // 金額値
Currency currency; // 通貨単位
// 中略
void add(int other) {
amount += other;
}
}
一見よさそうですが、以下のように他のクラスから値を直接変更できてしまうので、いつ変更されたのか、今の値がどうなっているのかをいちいち気にしなければなりません。
money.amount = originalPrice;
// 中略
if (specialServiceAdded) {
money.add(additionalServiceFee);
// 中略
if (seasonOffApplied) {
money.amount = seasonPrice();
}
}
そこで、インスタンス変数に final
修飾子を付けて再代入できないようにすることで、外部からの変更を防ぎます。
それに伴い、add
メソッドも amount
を変更できなくなるので、インスタンス変数の中身を変更するのではなく、変更値を持った Money
クラスのインスタンスを、また新たに生成します。
これで不変にしつつ、変更できるようになりました。
class Money {
final int amount;
final Currency currency;
// 中略
Money add(int other) {
int added = amount + other;
return new Money(added, currency);
}
}
[Q]
これって「いつ変更されたのか、今の値がどうなっているのかをいちいち気にしなければなりません。」という問題の解決になってるんだっけ?
[A]
はい、money.amount = originalPrice;
のように値を直接書き換えられず、add
メソッドを通して新しいインスタンスを返さなくてはいけないので、「ここで変更された」と明確になります。
また、元のインスタンス変数は不変なので、他の場所で勝手に書き換わっていたという心配がありません。
メソッド引数やローカル変数にも final
を付け不変にする
先ほどの例では、インスタンス変数に final
を付けて不変にしました。
class Money {
final int amount;
final Currency currency;
// 中略
Money add(int other) {
int added = amount + other;
return new Money(added, currency);
}
}
しかし、今のままだとメソッド引数 other
やローカル変数 added
を後から変更できてしまいます。
Money add(int other) {
other = 100;
int added = amount + other;
added = 100;
return new Money(added, currency);
}
途中で値が変化すると、どう変化したのか追うのが難しくなりますし、バグの原因にもなります。
したがって、メソッド引数やローカル変数にも final
を付けて、より頑健なメソッド構造にしましょう。
Money add(final int other) {
other = 100; // コンパイルエラーになる
final int added = amount + other;
added = 100; // コンパイルエラーになる
return new Money(added, currency);
}
[Q]
このような final
を使った不変な設計って、PHPでも実現できるの?
class Money {
final int amount;
final Currency currency;
Money(final int amount, final Currency currency) {
this.amount = amount;
this.currency = currency;
}
Money add(final int other) {
final int added = amount + other;
return new Money(added, currency);
}
}
[A]
PHPでは、インスタンス変数に readonly
を使うことで再代入を防げます。
ただし、Javaの final
のようにメソッド引数やローカル変数を不変にする仕組みはありません。
class Money {
public readonly int $amount;
public readonly Currency $currency;
public function __construct(int $amount, Currency $currency) {
$this->amount = $amount;
$this->currency = $currency;
}
public function add(int $other): self {
$added = $this->amount + $other;
return new self($added, $this->currency);
}
}
おわりに
今回は、「良いコード/悪いコードで学ぶ設計入門」の第1~4章を題材に、自分が疑問に感じたポイントを紹介しました。
設計は「なんとなく書く」から「なぜそう書くか」にシフトすることで、コードの質もチームの生産性も大きく変わってきますよね。
こうした視点をさらに深めていくために、続きとなる記事も公開しているので、ぜひあわせてご覧ください!