エンジニアがよく聞く用語メモ
What is this?
エンジニアとして働いててよく聞くけど実はどういうものかよくわかってないものをかき集めてみた
メモ
依存性の注入
クラス間を疎結合に保つための考え方。 イメージがつきにくければ、「他クラスのインスタンスをコンストラクタやメソッドの引数として受け取り、具体的な実装は他クラスに出してしまう」と考えれば良い。 これはいわゆる関心の分離に相当するもので、クラスの責任分解を考える時に重要な概念である。
この考え方を導入すると、他クラスに移譲したロジックに関しては置き換えが可能になるため、テストがとてもしやすくなる。
依存性逆転の原則
スローガン的に「具体が抽象に依存する」という説明がなされることが多い。 これもパッと聞くとよくわからないが、
具体→ きっちり実装されてるソースコード 抽象→ インターフェースのように実装の中身は定義されてなくて単体では使えないソースコード
と置き換えればわかりやすい。 つまるところ、 あらかじめ設定されたルールに則って具体的な実装を作り込むようにしましょうね、ということ。
例えば以下のような感じ。(例はChatGPTにて出力してみた) 依存性の逆転が考慮されてない状態
class MySQLDatabase { public void save(String data) { // Save the data to a MySQL database } } class HighLevelModule { private MySQLDatabase database; public HighLevelModule() { this.database = new MySQLDatabase(); } public void process(String data) { // Some high-level processing this.database.save(data); } }
これを依存性の逆転を考慮した状態にすると、以下になる。
interface Database { void save(String data); } class MySQLDatabase implements Database { public void save(String data) { // Save the data to a MySQL database } } class HighLevelModule { private Database database; public HighLevelModule(Database database) { this.database = database; } public void process(String data) { // Some high-level processing this.database.save(data); } }
前者の場合、HighLevelModule(ビジネスロジックが反映されたアプリケーションの挙動を表すコード)が、MySQLDatabaseクラスのインスタンスを内部で生成している、すなわちMySQLDatabaseクラスに依存してしまっている。
これを、後者はDatabaseインターフェースを用意し、引数としてDatabseインターフェースをimplementsしているインスタンスを受け取ることにより具体的なデータベースハンドリング処理をもたないようにしている。 Databaseインターフェースをimprementsしたclassであればなんでも使える。Databaseインターフェースをimplementsしているならsaveメソッドが必ず実装されているためである。
今回は依存性注入によって依存解決を図っているが、これに限らず依存性逆転時に依存関係を解決する手段は存在する。 例えばファクトリーパターンが良い例。
interface Database { void save(String data); } class MySQLDatabase implements Database { public void save(String data) { // Save data to MySQL database } } class PostgreSQLDatabase implements Database { public void save(String data) { // Save data to PostgreSQL database } }
class DatabaseFactory { static Database getDatabase(String type) { if (type.equalsIgnoreCase("MySQL")) { return new MySQLDatabase(); } else if (type.equalsIgnoreCase("PostgreSQL")) { return new PostgreSQLDatabase(); } else { throw new IllegalArgumentException("Invalid database type"); } } }
class HighLevelModule { private Database database; public HighLevelModule(String databaseType) { this.database = DatabaseFactory.getDatabase(databaseType); } public void process(String data) { // Some high-level processing this.database.save(data); } }
こう見ると簡単な話で、Databaseインターフェースをimplementsしているクラスが複数存在し使い分けたいのであればファクトリーパターンを使いましょう、というだけ。
リスコフの置換原則
これもお堅い説明が多いが、要するに
スーパークラスを使ってる部分をサブクラスで置き換えられるようにすべき
ということ。 これだけ覚えておけばいい。
トレイト
機能の集合体(基本的にメソッド。定数を持っててもいいんじゃないかと思う)。
クラスの継承とトレイトのミックスインは似てるようで異なる。 本質的な違いは以下なので、ここだけ押さえておくと良い。
- トレイトのミックスイン→ has-a(can-do)関係を表す。
- クラスの継承→ is-a関係を表す。
トレイトのミックスインはアビリティを表す。「何ができるか、どんな能力を持ってるのか?」を表すための手段なので、複数のトレイトをミックスインすることが可能。 太郎さんはマラソンもできるし、お店の経営もできる、というのはごく自然だと思う。 そして、この場合マラソン::run()と、お店::run()のように、どの文脈で使われるメソッドなのか明示することで問題なくメソッドを利用できる。
逆に、クラスの継承はアイデンティティを表す。「あなたはどういう存在か?」を表すための手段なので、多重継承のようにアイデンティティが揺らぐような実装は多重継承可能な言語であってもやめた方が良い。
テンプレートメソッドパターン
読んで字の如く、テンプレートとなるメソッドが用意されてるパターン。 親クラスの方で大まかな挙動は示しつつ、具体的な処理の部分はサブクラスに任せる構成を取る。 この結果、基本的な考え方が同じ部分は再実装しなくて済む。
abstract class Recipe { // テンプレートメソッド:料理の一連の流れ。料理のテンプレが定義されてる final void cook() { prepareIngredients(); cookFood(); serve(); } void prepareIngredients() { System.out.println("材料を準備します"); } // サブクラスで具体的な調理手順を書く abstract void cookFood(); void serve() { System.out.println("皿に盛り付けます"); } }
class Omelette extends Recipe { @Override void cookFood() { System.out.println("卵と塩を混ぜて、フライパンで焼きます"); } }
class Curry extends Recipe { @Override void cookFood() { System.out.println("野菜と肉を切って、鍋で煮ます"); } }
public class Main { public static void main(String[] args) { Recipe omelette = new Omelette(); System.out.println("オムレツを作ります:"); omelette.cook(); //abstractクラスのテンプレメソッドを呼び出してる System.out.println("\nカレーを作ります:"); Recipe curry = new Curry(); curry.cook(); //abstractクラスのテンプレメソッドを呼び出してる } }
この例では、料理のテンプレートを親クラスで用意しつつ、具体的な料理のステップをどうするのかは、カレーやオムレツといった子クラスたちに実装を持たせている。 これにより、子クラスが関心を払うべき点のみに集中し、抽象度の高い部分は意識しなくて良くなっている。
逆にいうと、親クラスに必要以上にロジックを詰め込みすぎて肥大化、いわゆる神クラス化する懸念もあるため、その点は気をつけないとダメ。 親クラスは基本的に具体によりすぎないように注意しつつ、単一責任の原則を守りながら共通処理を提供するという役割にのみ集中させる。 そういう意味ではチームで開発する時にはかなり注意を払う必要があるし、エンハンス開発が活発なチームの場合は抽象度が適切かどうかをエンハンスのたびに見直す意識が必要。
ストラテジーパターン
これも理解が難しそうな考え方だけど、要は
「誰が」「何をする」を分けて考えること
と覚えておけば大体イメージがつくようになる。 「誰」の部分と「何を」の部分の変更が互いに影響を及ぼさないように切り分けをする。
「誰が」を表すクラス内にストラテジーを受け取るコンポジションを用意する。
public interface AttackStrategy { void attack(); }
public class SwordAttack implements AttackStrategy { public void attack() { System.out.println("剣で斬りつける!"); } } public class BowAttack implements AttackStrategy { public void attack() { System.out.println("弓で矢を放つ!"); } }
public class Character { private AttackStrategy attackStrategy; // これがコンポジション。ストラテジーパターンの肝。 public void setAttackStrategy(AttackStrategy attackStrategy) { this.attackStrategy = attackStrategy; } public void attack() { attackStrategy.attack(); } }
public class Main { public static void main(String[] args) { Character warrior = new Character(); // 剣で攻撃する戦略をセット warrior.setAttackStrategy(new SwordAttack()); warrior.attack(); // "剣で斬りつける!"と表示 // 弓で攻撃する戦略をセット warrior.setAttackStrategy(new BowAttack()); warrior.attack(); // "弓で矢を放つ!"と表示 } }
ドラクエのようなターン制バトルを想像するとわかりやすいかと思ったので上記の例を出してみた。 上は「何をする」を柔軟に切り替えられる例だが、AttackStrategyをコンポジションとして持ってるクラスなら誰でもこのストラテジーを使えるので、「誰が」の部分にストラテジーが依存はしてないこともわかると思う。