なぜ現代言語(Go、Rust)は継承よりもコンポジションを好むのか
Olivia Novak
Dev Intern · Leapcell

なぜ現代の言語(Go、Rust)は継承よりもコンポジションを好むのか?
Javaには、_「正当な理由がない限り、継承を使用しないでください」_というガイドラインがあります。しかし、Javaは継承を厳密に制限していません。まだ自由に使用できます。ただし、Goは異なります。Goはコンポジションのみを許可します。
では、なぜ現代の言語はコンポジションを推進するのでしょうか?実際、新しい言語だけではありません。ベテランのJavaでさえ、Effective Java_の項目16で「継承よりもコンポジションを優先する」_と述べています。継承はコードを再利用する強力な方法ですが、最適な方法ではない可能性があります。
したがって、これを慎重に検討する価値があります。特定のコードに深く入り込むことなく、概念について話しましょう。このトピックは、次の視点から探求できると思います。
- 継承とコンポジションの特性
- 継承は実際にはどのような問題を引き起こすのか?
- コンポジションはこれらの問題をどのように解決するのか?
現実世界の例を検証し、継承を放棄し、インターフェイスとコンポジションを受け入れることで、設計を段階的に最適化しましょう。この記事を読むと、継承とコンポジションの両方について新たな理解が得られるでしょう。
継承とコンポジションの特性
ここでは定義について詳しく説明しません。継承とコンポジションは、オブジェクト指向プログラミングにおけるコードの再利用のための2つの一般的な手法です。どちらも再利用を可能にしますが、それぞれに長所と短所があります。両方について簡単に説明しましょう。
継承
利点:
- コードの再利用:親クラスで定義されたプロパティとメソッドは、サブクラスで直接使用できます。
- 継承チェーンによる拡張性:サブクラスは、祖先からのすべてのプロパティとメソッドを継承できるため、スケーラビリティと保守性が向上します。
- 継承とコンポジションの両方がポリモーフィズムをサポートできます。ポリモーフィズムでは、同じメソッドが異なるサブクラスで異なる動作をします。
短所:
- 親クラスの変更はサブクラスに影響します。親クラスが変更された場合、すべてのサブクラスをそれに応じて変更する必要がある場合があり、メンテナンスコストが増加します。
- 強い結合:サブクラスは親クラスに強く結合されているため、コードの柔軟性と移植性が低下します。
次に、コンポジションを見てみましょう。継承と比較して、コンポジションには次の特性があります。
コンポジション
利点:
- 結合の削減:オブジェクト間の関係は緩やかです。1つのオブジェクトを変更しても、他のオブジェクトには影響しません。
- 柔軟な設計:必要に応じて、さまざまなオブジェクトを組み合わせて使用できます。
- インターフェイスの分離:コンポジションにより、関心の分離が可能になり、異なる機能モジュールを独立して実装できるため、コードの再利用性が向上します。
短所:
- コード量の増加:継承と比較して、コンポジションでは、さまざまな組み合わせを実装するためにより多くのコードが必要になる場合があります。
- より複雑な相互作用:コンポジションでは、オブジェクトの相互作用により、より複雑なインターフェイス定義と実装が必要になる場合があり、複雑さが増します。
継承は実際にはどのような問題を引き起こし、どのように最適化できるのか?
1. 初期の問題
車両のクラスを設計するとします。オブジェクト指向の考え方に基づいて、「車両」の一般的な概念をBaseCar
クラスに抽象化します。このクラスには、デフォルトのrun()
動作があります。車やトラックなど、すべてのタイプの車両は、この抽象クラスから継承できます。
public class BaseCar { //... 他のプロパティとメソッドは省略... public void run() { /*...*/ } } // 車 public class Car extends AbstractCar { }
ただし、「車両」オブジェクトの理解と要件に基づいて、車両は走行できるだけでなく、タイヤとエンジンを修理できる必要もあります。したがって、AbstractCar
は次のようになります。
public class BaseCar { //... 他のプロパティとメソッドは省略... public void run() { /* 実行中... */ } public void repaireTire() { /* タイヤの修理中... */ } public void repaireEngine() { /* エンジンの修理中... */ } }
次に、自転車クラスを実装する必要があります。ただし、自転車にはエンジンがありません。どうすればよいでしょうか?
public class Bicycle extends BaseCar { //... 他のプロパティとメソッドは省略... public void repaireEngine() { throw new UnSupportedMethodException("エンジンがありません!"); } }
一見すると、上記のロジックで問題が解決されているように見えますが、実際には、大きな混乱を招く可能性があります。この設計には3つの大きな問題があります。
まず、可能なすべての動作を基本クラスに追加し続けると—たとえば、自動運転、パノラマサンルーフ、サンルーフ機能—すべてを基本クラスに詰め込む必要があります。これにより、再利用が改善されますが、すべてのサブクラスの機能も変更され、複雑さが増します。これは避けたいことです。
2番目に、無関係な機能を無関係なオブジェクト(自転車のエンジン修理など)に公開するのは問題があります。自転車クラスは、エンジン修理メソッドをまったく処理する必要はありません。
3番目に、将来の拡張性はどうでしょうか?人も飛行機も「走る」(ある意味で)モデル化したい場合はどうでしょうか?この設計は拡張性が低く、柔軟性がありません。
では、上記の問題にどのように対処すればよいでしょうか?お察しのことと思いますが、インターフェースです。インターフェースは動作の定義に重点を置いていますが、抽象クラスは通常、タイプの共通の基本動作を定義します。ここでは抽象クラスを使用すると、実際には複雑さが増しています。
2. インターフェースを使用した最適化
上記の問題を解決するために、特定のオブジェクトを無視して、代わりに走行、エンジンの修理、タイヤの修理という動作だけに焦点を当てましょう。これらの動作をIRun
、IEngine
、ITire
というインターフェースとして定義できます。
public interface IRun { void run(); } public interface IEngine { void repaireEngine(); } public interface ITire { void repaireTire(); }
次に、Car
クラスを実装するときに、3つのインターフェースすべて(IRun
、IEngine
、ITire
)を実装します。Bicycle
の場合、IRun
とITire
のみを実装します。Person
の場合、IRun
のみを実装する必要があります。
public class Car implements IRun, IEngine, ITire { //... 他のプロパティとメソッドは省略... @Override public void run() { /* 実行中... */ } @Override public void repaireEngine() { /* エンジンの修理中... */ } @Override public void repaireTire() { /* タイヤの修理中... */ } } public class Bicycle implements IRun, ITire { //... 他のプロパティとメソッドは省略... @Override public void run() { /* 実行中... */ } @Override public void repaireTire() { /* タイヤの修理中... */ } } public class Person implements IRun { //... 他のプロパティとメソッドは省略... @Override public void run() { /* 実行中... */ } }
これで、より柔軟になるのではありませんか?これで、GoやRustなどの現代の言語が継承と抽象クラスを放棄し、代わりに動作の抽象化のためにインターフェースを維持している理由を理解し始めるはずです。
ただし、まだ1つの問題があります。すべてのオブジェクトがrun()
、repaireEngine()
、repaireTire()
などを手動で実装する必要があるように見えます。それは面倒ではないですか?コードの再利用はどうでしょうか?
お待ちください— コンポジションがステージに登場しようとしています。
3. コンポジションを使用した最適化
上記の問題を解決するために、最初にインターフェースを実装し、次にコンポジションと委任を使用して再利用を実現できます。コードは次のようになります。
public class CarRunEnable implements IRun { @Override public void run() { /* 車が走る... */ } } public class PersonRunEnable implements IRun { @Override public void run() { /* 人が走る... */ } } // その他の実装は省略:EngineEnable / TireEnable
次に、コンポジションを使用してオブジェクトクラスを定義します—各クラスは動作クラスをフィールドとして含み、インターフェイスメソッドの呼び出しをそれらのフィールドに委任します。
public class Car implements IRun, IEngine, ITire { private CarRunEnable runEnable = new CarRunEnable(); // コンポジション private EngineEnable engineEnable = new EngineEnable(); // コンポジション private TireEnable tireEnable = new TireEnable(); // コンポジション //... 他のプロパティとメソッドは省略... @Override public void run() { runEnable.run(); } @Override public void repaireEngine() { engineEnable.repaireEngine(); } @Override public void repaireTire() { tireEnable.repaireTire(); } }
Bicycle
クラスとPerson
クラスを見てみましょう。
public class Bicycle implements IRun, ITire { private CarRunEnable runEnable = new CarRunEnable(); // コンポジション private TireEnable tireEnable = new TireEnable(); // コンポジション //... 他のプロパティとメソッドは省略... @Override public void run() { runEnable.run(); } @Override public void repaireTire() { tireEnable.repaireTire(); } } public class Person implements IRun { private PersonRunEnable runEnable = new PersonRunEnable(); // コンポジション //... 他のプロパティとメソッドは省略... @Override public void run() { runEnable.run(); } }
上記のコードを見ると、ロジックがはるかにクリーンで満足のいくように感じませんか?
新しい機能を追加したいですか?どうぞ—他のクラスには影響しません。
結合が大幅に削減され、凝集度は継承の場合と変わりません。これが、人々が高凝集、低結合と呼ぶものです。
唯一の欠点は、コードの総量が増加したことです。
それでは、元の質問に戻りましょう。
なぜ現代の言語(RustやGoなど)はコンポジションを受け入れ、継承を放棄するのでしょうか?
これで、おそらく答えが得られたでしょう。
Leapcellは、Rustプロジェクトをホストするためのトップの選択肢です。
Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、またはRustで開発します。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い—リクエストも料金もありません。
比類のないコスト効率
- アイドル料金なしの従量課金制。
- 例:25ドルで平均応答時間60ミリ秒で694万リクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI / CDパイプラインとGitOps統合。
- 実用的な洞察のためのリアルタイムのメトリックとロギング。
簡単なスケーラビリティと高パフォーマンス
- 簡単な自動スケーリングにより、高い同時実行性を処理します。
- 運用上のオーバーヘッドはゼロ—構築に集中するだけです。
詳細については、ドキュメントを参照してください。
Xでフォローしてください:@LeapcellHQ