依存関係のない真のモジュラーコードの作成
公開: 2022-03-11ソフトウェアの開発は素晴らしいですが…私たちは皆、それが少し感情的なローラーコースターになる可能性があることに同意できると思います。 最初は、すべてが素晴らしいです。 数時間ではなくても数日で新しい機能を次々に追加します。 あなたは順調です!
数か月早送りすると、開発速度が低下します。 以前ほど頑張っていないからですか? あまり。 さらに数か月早送りすると、開発速度がさらに低下します。 このプロジェクトに取り組むことはもはや面白くなく、ドラッグになっています。
悪化する。 アプリケーションで複数のバグを発見し始めます。 多くの場合、1つのバグを解決すると、2つの新しいバグが作成されます。 この時点で、歌い始めることができます:
コード内の99の小さなバグ。 99の小さなバグ。 1つを降ろし、パッチを当てて、
…コードに127個の小さなバグがあります。
このプロジェクトに今取り組んでいることについてどう思いますか? あなたが私のようなら、あなたはおそらくあなたのモチベーションを失い始めます。 既存のコードに変更を加えるたびに予測できない結果が生じる可能性があるため、このアプリケーションを開発するのは面倒です。
この経験はソフトウェアの世界では一般的であり、多くのプログラマーがソースコードを捨ててすべてを書き直したい理由を説明できます。
ソフトウェア開発が時間の経過とともに遅くなる理由
では、この問題の理由は何ですか?
主な原因は複雑さの増大です。 私の経験から、全体的な複雑さの最大の原因は、ソフトウェアプロジェクトの大部分で、すべてが接続されているという事実です。 各クラスには依存関係があるため、メールを送信するクラスのコードを変更すると、ユーザーは突然登録できなくなります。 何故ですか? 登録コードはメールを送信するコードに依存しているためです。 これで、バグを導入せずに何も変更することはできません。 すべての依存関係を追跡することは不可能です。
これで完了です。 私たちの問題の本当の原因は、私たちのコードが持っているすべての依存関係から来る複雑さを上げることです。
大きな泥だんごとそれを減らす方法
面白いことに、この問題は何年も前から知られています。 これは、「大きな泥だんご」と呼ばれる一般的なアンチパターンです。 私は、複数の異なる会社で何年にもわたって取り組んだほとんどすべてのプロジェクトで、そのタイプのアーキテクチャを見てきました。
では、このアンチパターンとは正確には何ですか? 簡単に言えば、各要素が他の要素と依存関係にある場合、大きな泥だんごが発生します。 以下に、有名なオープンソースプロジェクトであるApacheHadoopの依存関係のグラフを示します。 大きな泥だんご(つまり、大きな泥だんご)を視覚化するには、円を描き、プロジェクトのクラスをその上に均等に配置します。 相互に依存するクラスの各ペアの間に線を引くだけです。 これで、問題の原因を確認できます。
モジュラーコードを使用したソリューション
そこで、私は自分自身に質問をしました。プロジェクトの開始時のように、複雑さを軽減し、それでも楽しむことは可能でしょうか? 正直なところ、すべての複雑さを排除することはできません。 新しい機能を追加する場合は、常にコードの複雑度を上げる必要があります。 それにもかかわらず、複雑さは移動して分離することができます。
他の業界がこの問題をどのように解決しているか
機械産業について考えてみてください。 いくつかの小さな機械店が機械を作成しているとき、彼らは標準的な要素のセットを購入し、いくつかのカスタム要素を作成し、それらをまとめます。 これらのコンポーネントを完全に個別に作成し、最後にすべてを組み立てて、わずかな調整を行うことができます。 これはどのように可能ですか? 彼らは、ボルトのサイズなどの設定された業界標準、および取り付け穴のサイズやそれらの間の距離などの事前の決定によって、各要素がどのように組み合わされるかを知っています。
上記のアセンブリの各要素は、最終製品やその他の部品についてまったく知識がない別の会社から提供されます。 各モジュラーエレメントが仕様に従って製造されている限り、計画どおりに最終的なデバイスを作成できます。
それをソフトウェア業界で再現できますか?
もちろんできるよ! インターフェースと制御の反転を使用する。 最良の部分は、このアプローチが任意のオブジェクト指向言語(Java、C#、Swift、TypeScript、JavaScript、PHP)で使用できるという事実です。リストはどんどん増えています。 この方法を適用するために、特別なフレームワークは必要ありません。 あなたはただいくつかの簡単なルールに固執し、規律を保つ必要があります。
制御の反転はあなたの友達です
制御の反転について最初に聞いたとき、私はすぐに解決策を見つけたことに気づきました。 これは、既存の依存関係を取得し、インターフェイスを使用してそれらを反転させるという概念です。 インターフェイスは、メソッドの単純な宣言です。 それらは具体的な実装を提供しません。 結果として、それらを接続する方法に関する2つの要素間の合意として使用できます。 必要に応じて、モジュラーコネクタとして使用できます。 1つの要素がインターフェースを提供し、別の要素がその実装を提供する限り、それらはお互いについて何も知らなくても連携できます。 すばらしい。
簡単な例で、システムを分離してモジュラーコードを作成する方法を見てみましょう。 以下の図は、単純なJavaアプリケーションとして実装されています。 これらはこのGitHubリポジトリで見つけることができます。
問題
Main
クラス、3つのサービス、および1つのUtil
クラスのみで構成される非常に単純なアプリケーションがあると仮定します。 これらの要素は、複数の方法で相互に依存しています。 以下に、「大きな泥だんご」アプローチを使用した実装を示します。 クラスは単にお互いを呼び出します。 それらは緊密に結合されており、他の要素に触れずに1つの要素を単純に取り出すことはできません。 このスタイルを使用して作成されたアプリケーションを使用すると、最初は急速に成長できます。 このスタイルは、物事を簡単に試すことができるため、概念実証プロジェクトに適していると思います。 それでも、メンテナンスでさえ危険であり、単一の変更で予測できないバグが発生する可能性があるため、本番環境に対応したソリューションには適していません。 下の図は、この大きな泥だんごの構造を示しています。
依存性注入がすべて間違っている理由
より良いアプローチを探すために、依存性注入と呼ばれる手法を使用できます。 この方法は、すべてのコンポーネントがインターフェースを介して使用されることを前提としています。 私はそれが要素を切り離すという主張を読みました、しかしそれは本当にそうですか? いいえ。下の図をご覧ください。
現在の状況と大きな泥だんごの唯一の違いは、クラスを直接呼び出すのではなく、インターフェイスを介して呼び出すという事実です。 これにより、要素の相互分離がわずかに改善されます。 たとえば、 Service A
を別のプロジェクトで再利用したい場合は、 Service A
自体、 Interface A
、 Interface B
、 Interface Util
を削除することで再利用できます。 ご覧のとおり、 Service A
は依然として他の要素に依存しています。 その結果、ある場所でコードを変更したり、別の場所で動作を台無しにしたりする際に、依然として問題が発生します。 それでも、 Service B
とInterface B
を変更すると、それに依存するすべての要素を変更する必要があるという問題が発生します。 このアプローチは何も解決しません。 私の意見では、要素の上にインターフェースのレイヤーを追加するだけです。 依存関係を注入することは絶対にしないでください。代わりに、依存関係を完全に取り除く必要があります。 独立のための万歳!
モジュラーコードのソリューション
私が信じるアプローチは、依存関係の主な問題をすべて解決するものであり、依存関係をまったく使用しないことで解決します。 コンポーネントとそのリスナーを作成します。 リスナーはシンプルなインターフェースです。 現在の要素の外部からメソッドを呼び出す必要がある場合は常に、リスナーにメソッドを追加して、代わりに呼び出すだけです。 この要素は、ファイルの使用、パッケージ内のメソッドの呼び出し、およびメインフレームワークまたはその他の使用済みライブラリによって提供されるクラスの使用のみが許可されています。 以下に、要素アーキテクチャを使用するように変更されたアプリケーションの図を示します。

このアーキテクチャでは、 Main
クラスのみが複数の依存関係を持っていることに注意してください。 すべての要素を相互に接続し、アプリケーションのビジネスロジックをカプセル化します。
一方、サービスは完全に独立した要素です。 これで、このアプリケーションから各サービスを取り出して、別の場所で再利用できます。 彼らは他に何も依存していません。 しかし、待ってください。それは良くなります。動作を変更しない限り、これらのサービスを再度変更する必要はありません。 それらのサービスが本来の目的を果たしている限り、時間の終わりまでそのままにしておくことができます。 それらは、プロのソフトウェアエンジニア、またはgoto
ステートメントを混ぜて調理した史上最悪のスパゲッティコードを初めてコーダーが侵害した場合に作成できます。ロジックがカプセル化されているため、問題ありません。 恐ろしいことかもしれませんが、他のクラスに流出することはありません。 また、プロジェクト内の作業を複数の開発者間で分割することもできます。各開発者は、別の開発者に割り込んだり、他の開発者の存在を知らなくても、独自のコンポーネントで独立して作業できます。
最後に、最後のプロジェクトの開始時と同じように、独立したコードをもう一度書き始めることができます。
要素パターン
繰り返し可能な方法で作成できるように、構造要素のパターンを定義しましょう。
要素の最も単純なバージョンは、メイン要素クラスとリスナーの2つで構成されています。 要素を使用する場合は、リスナーを実装してメインクラスを呼び出す必要があります。 最も単純な構成の図を次に示します。
明らかに、最終的には要素にさらに複雑さを加える必要がありますが、それは簡単に行うことができます。 ロジッククラスがプロジェクト内の他のファイルに依存していないことを確認してください。 この要素では、メインフレームワーク、インポートされたライブラリ、およびその他のファイルのみを使用できます。 画像、ビュー、サウンドなどのアセットファイルに関しては、将来再利用しやすいように、要素内にカプセル化する必要があります。 フォルダ全体を別のプロジェクトにコピーするだけで、そこにあります。
以下に、より高度な要素を示すグラフの例を示します。 使用しているビューで構成されており、他のアプリケーションファイルに依存していないことに注意してください。 依存関係をチェックする簡単な方法を知りたい場合は、インポートセクションをご覧ください。 現在の要素の外部からのファイルはありますか? その場合は、それらを要素に移動するか、リスナーに適切な呼び出しを追加することによって、これらの依存関係を削除する必要があります。
また、Javaで作成された簡単な「HelloWorld」の例も見てみましょう。
public class Main { interface ElementListener { void printOutput(String message); } static class Element { private ElementListener listener; public Element(ElementListener listener) { this.listener = listener; } public void sayHello() { String message = "Hello World of Elements!"; this.listener.printOutput(message); } } static class App { public App() { } public void start() { // Build listener ElementListener elementListener = message -> System.out.println(message); // Assemble element Element element = new Element(elementListener); element.sayHello(); } } public static void main(String[] args) { App app = new App(); app.start(); } }
最初に、出力を出力するメソッドを指定するためにElementListener
を定義します。 要素自体は以下に定義されています。 要素でsayHello
を呼び出すと、 ElementListener
を使用してメッセージを出力するだけです。 この要素は、 printOutput
メソッドの実装から完全に独立していることに注意してください。 コンソール、物理プリンター、または派手なUIに印刷できます。 要素はその実装に依存しません。 この抽象化により、この要素はさまざまなアプリケーションで簡単に再利用できます。
次に、メインのApp
クラスを見てみましょう。 リスナーを実装し、具体的な実装とともに要素をアセンブルします。 これで、使用を開始できます。
こちらのJavaScriptでこの例を実行することもできます
要素アーキテクチャ
大規模なアプリケーションで要素パターンを使用する方法を見てみましょう。 小さなプロジェクトでそれを示すことは1つです。それは、現実の世界に適用することとは別のことです。
私が使用したいフルスタックWebアプリケーションの構造は、次のようになります。
src ├── client │ ├── app │ └── elements │ └── server ├── app └── elements
ソースコードフォルダで、最初にクライアントファイルとサーバーファイルを分割します。 ブラウザとバックエンドサーバーの2つの異なる環境で実行されるため、これを行うのは合理的です。
次に、各レイヤーのコードをアプリと要素と呼ばれるフォルダーに分割します。 要素は独立したコンポーネントを持つフォルダーで構成され、アプリフォルダーはすべての要素を相互に接続し、すべてのビジネスロジックを格納します。
このようにして、要素を異なるプロジェクト間で再利用できますが、アプリケーション固有の複雑さはすべて1つのフォルダーにカプセル化され、多くの場合、要素への単純な呼び出しになります。
実践例
その実践は常に理論に勝ると信じて、Node.jsとTypeScriptで作成された実際の例を見てみましょう。
実際の例
これは非常にシンプルなWebアプリケーションであり、より高度なソリューションの開始点として使用できます。 それは要素アーキテクチャに従うだけでなく、広範囲に構造的な要素パターンを使用します。
ハイライトから、メインページが要素として区別されていることがわかります。 このページには独自のビューが含まれています。 したがって、たとえば、それを再利用したい場合は、フォルダ全体をコピーして別のプロジェクトにドロップするだけです。 すべてを一緒に配線するだけで、準備が整います。
これは、今日から独自のアプリケーションに要素を導入できることを示す基本的な例です。 独立したコンポーネントの区別を開始し、それらのロジックを分離することができます。 現在作業しているコードがどれほど乱雑であるかは関係ありません。
より速く開発し、より頻繁に再利用します!
この新しいツールセットを使用すると、より保守しやすいコードをより簡単に開発できるようになることを願っています。 実際に要素パターンを使用する前に、すべての要点を簡単に要約しましょう。
複数のコンポーネント間の依存関係が原因で、ソフトウェアで多くの問題が発生します。
ある場所で変更を加えることにより、別の場所で予測できない動作を導入できます。
3つの一般的なアーキテクチャアプローチは次のとおりです。
大きな泥だんご。 迅速な開発には最適ですが、安定した生産目的にはそれほど適していません。
依存性注入。 それはあなたが避けるべき中途半端な解決策です。
要素アーキテクチャ。 このソリューションを使用すると、独立したコンポーネントを作成して、他のプロジェクトで再利用できます。 安定した製品リリースのために保守可能で優れています。
基本的な要素パターンは、すべての主要なメソッドを持つメインクラスと、外界との通信を可能にするシンプルなインターフェイスであるリスナーで構成されます。
フルスタック要素アーキテクチャを実現するには、最初にフロントエンドをバックエンドコードから分離します。 次に、アプリと要素のそれぞれにフォルダーを作成します。 要素フォルダーはすべての独立した要素で構成され、アプリフォルダーはすべてを相互に接続します。
これで、独自の要素の作成と共有を開始できます。 長期的には、メンテナンスが容易な製品を作成するのに役立ちます。 頑張って、あなたが作成したものを教えてください!
また、コードを時期尚早に最適化していることに気付いた場合は、仲間のToptalerKevinBlochによる時期尚早な最適化の呪いを回避する方法をお読みください。