ソフトウェア開発は素晴らしいですが…私たちはそれが感情的なジェットコースターになる可能性があることに同意できると思います。最初は、すべてが素晴らしいです。数時間ではないにしても、数日で次々と新しい機能を追加します。あなたは幸運な連勝をしています!
数か月早送りすると、開発速度が遅くなります。以前ほど頑張っていないからですか?あんまり。さらに数か月早送りすると、開発速度がさらに遅くなります。このプロジェクトに取り組むことはもはや面白くなく、ドラッグになっています。
しかし、それはさらに悪化します。アプリケーションで複数のエラーを発見し始めます。多くの場合、1つのバグを解決すると、2つの新しいバグが作成されます。この時点で、あなたは歌い始めることができます:
コード内の99の小さなバグ。 99の小さな間違い。 1つ取って、パッチを当てて、
…コード内の127個の小さなエラー。
今、このプロジェクトに取り組むことについてどう思いますか?あなたが私のようなら、あなたはおそらくあなたのモチベーションを失い始めます。このアプリケーションの開発は複雑です。既存のコードを変更するたびに、予測できない結果が生じる可能性があるためです。
この経験はソフトウェアの世界では一般的であり、多くのプログラマーがソースコードを捨ててすべてを書き直したいと思う理由を説明するかもしれません。
では、この問題の理由は何ですか?
主な原因は複雑さの増大です。私の経験から、全体的な複雑さの最大の原因は、ソフトウェアプロジェクトの大部分で、すべてが接続されているという事実です。各クラスには依存関係があるため、メールを送信するクラスのコードを変更すると、ユーザーは突然登録できなくなります。何故ですか?登録コードはメールを送信するコードに依存しているためです。これで、エラーを発生させずに何も変更することはできません。すべての依存関係を追跡することは不可能です。
だからあなたはそれを持っています。私たちの問題の本当の原因は、コードが持つすべての依存関係から来る複雑さを増すことです。
面白いことに、この問題は何年も前から知られています。これは、「大きな粘土のボール」と呼ばれる一般的なアンチパターンです。私は、複数の異なる会社で何年にもわたって取り組んできたほぼすべてのプロジェクトで、そのようなアーキテクチャを見てきました。
では、このアンチパターンは正確には何ですか?簡単に言えば、各アイテムが他のアイテムに依存しているとき、あなたは大きな粘土のボールを手に入れます。以下に、人気のあるオープンソースのApacheHadoopプロジェクトの依存関係のグラフを示します。粘土の大きなボール(または、糸の大きなボール)を視覚化するには、円を描き、その中にプロジェクトクラスを均等に配置します。相互に依存するクラスの各ペアの間に線を引くだけです。これで、問題の原因を確認できます。
C ++に含める
それで私は自分自身に質問をしました:プロジェクトの始めのように複雑さを減らしてそれでも楽しむことは可能でしょうか?正直なところ、削除することはできません みんな 複雑さ。新しい機能を追加したい場合は、常にコードの複雑さを増す必要があります。ただし、複雑さは移動して広がる可能性があります。
機械産業について考えてみてください。小さな機械工場が機械を作るとき、標準的なアイテムのセットを購入し、いくつかのカスタムアイテムを作成し、それらを組み合わせます。彼らはそれらのコンポーネントを完全に別々に作り、すべてを最後に組み立てることができ、ほんの数回の修正を行うだけです。これはどのように可能ですか?彼らは、ボルトのサイズなどの業界標準と、取り付け穴のサイズやボルト間の距離などの初期決定に基づいて、各アイテムがどのように適合するかを知っています。
上記の各アイテムは、最終製品やその他の部品に関する知識を持たない独立した会社から提供されます。各モジュラーアイテムが仕様どおりに製造されている限り、計画どおりに最終的なデバイスを作成できます。
それをソフトウェア業界で再現できますか?
きっとできます!インターフェイスを使用し、制御原理を逆にすることによって。最良の部分は、このアプローチが任意のオブジェクト指向言語で使用できるという事実です:Java、C#、Swift、TypeScript、JavaScript、PHP-リストは延々と続きます。この方法を適用するために、特別なフレームワークは必要ありません。あなたはただいくつかの簡単なルールに固執し、規律を保つ必要があります。
制御の逆転について最初に聞いたとき、私はすぐに解決策を見つけたことに気づきました。これは、既存の依存関係を取得し、インターフェースを使用してそれらを反転させるという概念です。インターフェイスは単純なメソッド宣言です。それらは具体的な実装を提供しません。その結果、それらを接続する方法に関する2つの要素間の合意として使用できます。必要に応じて、モジュラーコネクタとして使用できます。 1つの要素がインターフェースを提供し、別の要素が実装を提供する限り、それらはお互いについて何も知らなくても一緒に作業できます。素晴らしいです。
簡単な例で、システムを分離してモジュラーコードを作成する方法を見てみましょう。次の図は、単純なJavaアプリケーションとして実装されています。あなたはこれでそれらを見つけることができます GitHubリポジトリ 。
1つのクラスMain
、3つのサービス、および1つのクラスUtil
で構成される非常に単純なアプリケーションがあるとします。これらの要素は、複数の方法で相互に依存しています。以下に、「大きな泥だんご」アプローチを使用した実装を示します。クラスはお互いを呼び出すだけです。それらは密接に関連しており、他のアイテムに触れずに1つのアイテムを引き出すことはできません。このスタイルで作成されたアプリケーションを使用すると、最初はすぐに成長できます。このスタイルは、簡単にプレイできるため、概念実証プロジェクトに適していると思います。ただし、メンテナンスでさえ危険であり、変更を加えると予測できないエラーが発生する可能性があるため、本番環境に対応したソリューションには適していません。下の図は、この大きな粘土の球の構造を示しています。
より良いアプローチを探すために、依存性注入と呼ばれる手法を使用できます。この方法では、すべてのコンポーネントをインターフェイス間で使用する必要があることを前提としています。アイテムをドッキング解除するという主張を読みましたが、実際にそれを実行しますか?いいえ。下の図を見てください。
現在の状況と大きな泥だんごの唯一の違いは、クラスを直接呼び出すのではなく、インターフェイスを介して呼び出すという事実です。分離要素を互いにわずかに改善します。たとえば、再利用したい場合Servicio A
別のプロジェクトでは、Servicio A
、Interfaz A
、およびInterfaz B
を取り出すことでこれを行うことができます。およびInterface Útil
。ご覧のとおり、Servicio A
それはまだ他の要素に依存しています。その結果、ある場所でコードを変更したり、別の場所で動作を台無しにしたりするのにまだ問題があります。それでも、Servicio B
を変更すると問題が発生します。 e Interfaz 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
メソッドの実装から完全に独立していることに注意してください。コンソール、物理プリンター、または派手なユーザーインターフェイスに印刷できます。要素はその実装に依存しません。この抽象化により、この要素はさまざまなアプリケーションで簡単に再利用できます。
次に、App
のメインクラスを見てみましょう。リスナーを実装し、具体的な実装とともに要素をアセンブルします。これで使用を開始できます。
こちらのJavaScriptでこの例を実行することもできます
大規模なアプリケーションでの要素パターンの使用を見てみましょう。小さなプロジェクトでそれを披露することは1つのことです。もう1つは、それを現実の世界に適用することです。
私が使用したいフルスタックWebアプリの構造は、次のようになります。
src ├── client │ ├── app │ └── elements │ └── server ├── app └── elements
ソースコードフォルダで、最初にクライアントファイルとサーバーファイルを分割します。ブラウザとバックエンドサーバーの2つの異なる環境で実行されるため、これを行うのは合理的なことです。
次に、各レイヤーのコードをアプリとアイテムと呼ばれるフォルダーに分割します。アイテムは個別のコンポーネントを持つフォルダーで構成され、アプリケーションフォルダーはすべてのアイテムを接続してすべてのビジネスロジックを格納します。
このようにして、要素を異なるプロジェクト間で再利用できますが、アプリケーション固有の複雑さはすべて1つのフォルダーにカプセル化され、多くの場合、単純な要素呼び出しに削減されます。
実践が常に理論に勝ると信じる場合は、Node.jsとTypeScriptで作成された実際の例を見てみましょう。
これは非常に単純なWebアプリケーションであり、より高度なソリューションの開始点として使用できます。これは要素アーキテクチャに従い、広範囲に構造的な要素パターンを使用します。
ハイライトから、メインページがアイテムとして区別されていることがわかります。このページには独自のビューが含まれています。したがって、たとえば再利用したい場合は、フォルダ全体をコピーして別のプロジェクトにドロップするだけです。すべてを接続するだけで、準備完了です。
これは、今日から独自のアプリケーションにアイテムを導入できることを示す基本的な例です。独立したコンポーネントを区別し、それらのロジックを分離し始めることができます。現在作業しているコードがどれほど乱雑であるかは関係ありません。
この新しいツールセットを使用すると、保守が容易なコードをより簡単に開発できることを願っています。実際に要素パターンを使用する前に、すべての要点を簡単に説明しましょう。
さまざまなコンポーネント間の依存関係により、ソフトウェアで多くの問題が発生します。
ある場所で変更を加えることにより、別の場所で予測できない動作を導入できます。
3つの一般的なアーキテクチャアプローチは次のとおりです。
粘土の大きなボール。迅速な開発には最適ですが、安定した生産目的にはそれほど適していません。
依存性注入。それはあなたが避けるべき半分の解決策です。
要素アーキテクチャ。このソリューションを使用すると、独立したコンポーネントを作成して、他のプロジェクトで再利用できます。安定した製品リリースのために保守可能で明るいです。
基本的な要素パターンは、すべてのメインメソッドを持つメインクラスと、外界との通信を可能にするシンプルなインターフェイスであるリスナーで構成されます。
フルスタック要素アーキテクチャを実現するために、フロントエンドは最初にバックエンドコードから分離されます。次に、アプリとアイテム用にそれぞれにフォルダーを作成します。 itemsフォルダーはすべてのスタンドアロンアイテムで構成され、applicationsフォルダーはすべてを接続します。
これで、自分のアイテムの作成と共有を開始できます。長期的には、メンテナンスが容易な製品の作成に役立ちます。頑張って、あなたが作成したものを教えてください!
また、コードを時期尚早に最適化していることに気付いた場合は、 _時期尚早の最適化の呪いを回避する方法_ 私のApeeScapeパートナーのKevinBlochから。