はじめに
最近、「良いコード/悪いコードで学ぶ設計入門」を読んでいるので、本書の中で自分が疑問に感じたポイントをQ&A形式で振り返ってみたいと思います。
この記事では、各セクションの簡単な説明の後に、[Q]で疑問に感じたこと、[A]で調べてわかったこと、そして[NOTE]で覚えておきたいことをまとめています。
なお、サンプルコードにJavaを使用していますが、言語を問わず広く有効な設計手法なので、他言語でも応用できる内容になっています。
第9~12章については、すでに別の記事にまとめているので、こちらもぜひご覧ください!
それでは、第13〜15章を振り返りながら、一緒に「良いコード/悪いコード」について学んでいきましょう!
必ず自身のクラスのインスタンス変数を使うこと
メソッドは、必ず自身のクラスのインスタンス変数を使うよう設計しましょう。
また、完全性を保証するようにインスタンス変数を操作するメソッドを設計しましょう。
以下のように、ほかのクラスのインスタンス変数を変更するメソッド構造にしてはいけません。
class ActorManager {
// ゲームキャラの位置を移動する。
void shift(Location location, int shiftX, int shiftY) {
location.x += shiftX;
location.y += shiftY;
}
}
値がどこで変更されているのか把握が難しくなったり、重複コードが実装されやすくなります。
ほかのクラスのインスタンス変数を変更するメソッドを書きたくなった場合、変更したいインスタンス変数を持つクラスに変更メソッドを実装しましょう。
[Q]
「メソッドは、必ず自身のクラスのインスタンス変数を使うよう設計しましょう。」って、以下のようなケースもダメなの?
class SellingCommission {
private static final float SELLING_COMMISSION_RATE = 0.05f;
final int amount;
SellingCommission(final SellingPrice sellingPrice) {
amount = (int)(sellingPrice.amount * SELLING_COMMISSION_RATE);
}
}
[A]
「メソッドは、必ず自身のクラスのインスタンス変数を使うよう設計しましょう。」というルールの本質は、他クラスのインスタンス変数を直接変更すると、どこでどんな変更を行ったか見えにくくなり、コードの保守性を低下させるという点にあります。
つまり、逆に他クラスのインスタンス変数を「参照」するだけなら、問題ありません。
尋ねるな、命じろ
以下のように、あるクラスがよそのクラスの状態を判断したり、状態に応じてよその値を変更したりする、「よそのクラスを気にしたりいじったりするメソッド構造」は、不正な値が混入しやすく、保守や変更が難しい構造です。
class OrderService {
void process(Order order) {
if (order.isShipped()) {
System.out.println("すでに発送済みです。");
} else {
order.ship();
}
}
}
class Order {
private boolean shipped;
boolean isShipped() {
return shipped;
}
void ship() {
shipped = true;
}
}
メソッドの呼び出し側で複雑な処理をさせるのではなく、「尋ねるな、命じろ」の考えのもと、呼び出されるメソッドの側で複雑な制御をするよう設計しましょう。
class OrderService {
void process(Order order) {
order.shipIfNotShipped();
}
}
class Order {
private boolean shipped;
void shipIfNotShipped() {
if (shipped) {
System.out.println("すでに発送済みです。");
return;
}
shipped = true;
System.out.println("発送しました。");
}
}
[NOTE]
インスタンス変数の値を取得するメソッドである getter
、値をセットするメソッドである setter
はこの「よそのクラスを気にしたりいじったりするメソッド構造」に陥りやすい代表例です。
public class Person {
private String name;
// getter
public String getName() {
return name;
}
// setter
public void setName(String newName) {
name = newName;
}
}
このように getter
/ setter
を用意すると値を勝手に出し入れ可能になり、不正値の混入や重複コードなど、さまざまな弊害が発生しやすくなります。
モデリングの考え方とあるべき構造
動作原理やしくみを簡単に理解・説明するために、物事の特徴や関係性を図式化したものをモデル、モデルをつくる活動をモデリングと呼びます。
ここで、商品はどういうモデルになるでしょうか。
商品にはさまざまな付帯要素(情報)がありますが、これらすべてを盛り込むと、モデルの目的がわからなくなります。
モデルは「特定の目的達成のために最低限考慮が必要な要素を備えたもの」でなければいけません。
注文時に、商品モデルに最低限必須の要素を考えると、商品ID、商品名、売値、在庫数が挙げられます。
配送時はどうでしょうか。
配送では売値や在庫数は必要ありませんが、商品を梱包するために、商品のサイズや重量といった要素が必要です。
注文と配送では達成目的が違います。
つまり、目的それぞれで商品モデルが違うのです。
[NOTE]
現実世界での物理的な存在と、情報システム上のモデルが1対1になるとは限らず、1対多の関係になるケースがあることが大きな特徴です。
例えば、情報システムにおける「商品」という概念は、入庫品、予約品、注文品、配送品といった複数のクラスから構成されます。
おわりに
今回は、「良いコード/悪いコードで学ぶ設計入門」の第13~15章を題材に、自分が疑問に感じたポイントを紹介しました。
コードの保守性や拡張性を高めるためには、クラス設計の原則を理解し、他クラスの状態をむやみに操作しないようにしましょう!
なお、続きとなる第16~18章の記事も公開しているので、ぜひあわせてご覧ください!