テスト可能なコードの書き方

画像








あなたがプログラマー(またはより悪いアーキテクト)である場合、このような簡単な質問に答えることができます:テスト不可能なコードを書く方法は? あなたはそれについて考えましたか? テストされていないコードを実現するための少なくとも3つの方法にほとんど名前を付けられない場合は、この記事が役立ちます。



多くの人が言うでしょう:なぜ私はテストされていないコードを書くかを知る必要があるのですか、あなたは私に悪いことを教えたいですか? 私は答えます:テストされていないコードの典型的なパターンを知っていれば、そうであれば、プロジェクトでそれらを簡単に見ることができます。 そして、ご存知のように、問題の認識はすでに治療への道の半分です。 この記事は、そのような治療が実際にどのように行われるかについても答えています。 猫をお願いします。



この記事では特定のプログラミング言語に焦点を当てることはせず、以下はすべての手続き言語に関連しています。 しかし、私にとっては、この記事は動的なインタープリター言語でプログラムする人にとって特に有用であり、型付きコンパイル言語の開発に本格的な経験はなかったようです。 このカテゴリの開発者のコ​​ードで、以下に説明するパターンに最も頻繁に気づきました。 例は、Java、C#、およびTypeScriptに似た擬似コードになります。



この記事では、テストの書き方の問題については触れていません。 このテーマに関する多くの記事があります。 コードを簡単かつ美しくテストできるようにするためのコードの書き方の問題を検討し、結果として得られるテストはシンプルで保守可能になります。 さらに、テストという用語は美しく、クリーンな単体テストを意味し、さまざまなハックなしで、依存関係をオンザフライで変更するための追加の「マジック」ライブラリ、およびテストの読み取りとサポートを困難にするその他のことなく書かれています。 それは、言葉の最高の意味でのテストです。



ちょっとした哲学。 テストを書くべきですか? 私の意見:開発するプロジェクトを作成する場合は、テストだけが必要です。 テストが直接的な機能を実行するという事実(コードが要件に準拠しているかどうかを確認できる)に加えて、副作用としてクラス設計を「まっすぐに」します。 これは、「曲がった」デザインのクラスではテストを記述できないためです。したがって、テスト容易性を実現するには、クラスをリファクタリングする必要があります。 または、コードの前にテストが記述されている場合、クラス設計はすぐに生まれます。 テスト対象のコードは再利用可能です。テストで再利用できるためです。 また、再利用性は適切なクラス設計の重要な基準です。 また、テストは、必ずしもではありませんが、おそらく、アプリケーション全体のアーキテクチャを改善します。 誰かがよく指摘したように、クラス設計は、このクラスを単体テストでテストできる程度にしか優れていません。



テストされたコードの主なキラーのリスト:







知識マイニング



知識の抽出は、メソッドが1つの引数セットを必要とし、それらを直接使用せず、他のオブジェクトの検索でこれらの引数を「選択」し始めるときに発生します。 典型的なシナリオ:



メソッドまたはコンストラクターには、実際に動作するために必要なものが必要です。 彼は、あらゆる種類の仕事をしながら、これらすべてを取得/計算する方法を気にする必要はありません。 作業には、最低限必要な引数のセットが必要です。



人生の例:店で購入代金を支払うように求められたら、何をしますか:財布を渡し、レジ係自身がそこからお金を受け取るようにしますか、それともお金を与えますか? 類推は明らかだと思います-レジ係は、計算方法を入力するために、ウォレットのクラスではなく、クラスのお金を必要とするべきです。

いくつかの例を見てみましょう。



前:テストされていないコード
class DiscountCard { DiscountCard(UserContext userContext) { this.user = userContext.getUser(); this.level = userContext.getLevel(); this.order = userContext.getOrder(); } // ... } //  UserContext userContext = new UserContext(); userContext.setUser(new User("Ivan")); PlanLevel level = new PlanLevel(143, "yearly"); userContext.setLevel(level); Order order = new Order("SuperDeluxe", 100, true); userContext.setOrder(order); DiscountCard discountCard = new DiscountCard(userContext); //  
      
      







DiscountCardはuserContextを直接使用しません。 userContextには初期化のために多数のオブジェクトを含めることができるため、テストを作成する人は、DiscountCardクラスのデバイスを調べて、そこで本当に必要なオブジェクトを理解する必要があります。 そして、これは何か間違ったことをする時間とリスクであり、その結果、テストが不正確になる可能性があります。 本当に必要なものをDiscountCardに要求させましょう:



後:テストコード
 class DiscountCard { DiscountCard(User user, PlanLevel level, Order order) { this.user = user; this.level = level; this.order = order; } // ... } //  User user = new User("Ivan"); PlanLevel level = new PlanLevel(143, "yearly"); Order order = new Order("SuperDeluxe", 100, true); DiscountCard discountCard = new DiscountCard(user, level, order); //  
      
      







この例は、アプリケーションのすべての依存関係を保存する有名なServiceLocatorパターンも意図的に示しています。 そのため、このようなパターンはアンチパターンと見なされます。 それを避けるようにしてください。



知識抽出の別の例を考えてみましょう。



前:テストされていないコード
 class SalesTaxCalculator { TaxTable taxTable; SalesTaxCalculator(TaxTable taxTable) { this.taxTable = taxTable; } float computeSalesTax(User user, Invoice invoice) { //  "user"    Address address = user.getAddress(); float amount = invoice.getSubTotal(); return amount * taxTable.getTaxRate(address); } } //  SalesTaxCalculator calc = new SalesTaxCalculator(new TaxTable()); Address address = new Address(" 23 ..."); //     "user" User user = new User(address, ...); Invoice invoice = new Invoice(1, new ProductX(95.00)); assertEquals(calc.computeSalesTax(user, invoice), 100);
      
      







テストでは、Userクラスを作成する必要がありますが、そこからはアドレスのみが必要です。 Invoiceクラスについても同じことが言えます。 繰り返しますが、テストを書いた人から、SalesTaxCalculatorコードを「スキャン」して、そこで本当に必要なものを見つけ出す必要があります。 面倒な場所。



また、SalesTaxCalculatorは、UserクラスとInvoiceクラスを持たない別のプロジェクトで再利用できません。



後:テストコード
 class SalesTaxCalculator { TaxTable taxTable; SalesTaxCalculator(TaxTable taxTable) { this.taxTable = taxTable; } float computeSalesTax(Address address, float amount) { return amount * taxTable.getTaxRate(address); } } //  SalesTaxCalculator calc = new SalesTaxCalculator(new TaxTable()); Address address = new Address(" 23 ..."); assertEquals(calc.computeSalesTax(address, 95.00), 100);
      
      







これで、クラスは必要なものだけを必要とし、テストは透過的になります。



ビジネスコードの新しいオペレーター



おそらく、このパターンには、テストされていないコードを作成するための金メダルを与えることができます。

簡単な例から解析を始めましょう:



前:テストされていないコード
 class House { Kitchen kitchen; Bedroom bedroom = new Bedroom(); House() { this.kitchen = new Kitchen(new Refrigerator()); } // ... } //  House house = new House(); // ,    //    kitchen  bedroom
      
      







このコードのすべてが悪いです。 Houseメソッドを呼び出すと、キッチンやベッドルームの呼び出しが発生し、それらを制御しないため、テストできません。 データベースと通信するか、ネットワークにリクエストを送信すると、テストは終了します。 ポリモーフィズムの助けを借りて、キッチンやベッドルームを交換することは不可能であり、私たちの家は特定のクラスに厳密に結びついています。 コードをテスト可能にする方法は?



後:テストコード
 class House { Kitchen kitchen; Bedroom bedroom; House(Kitchen kitchen, Bedroom bedroom) { this.kitchen = kitchen; this.bedroom = bedroom; } // ... } //  Kitchen kitchen = new DummyKitchen(null); Bedroom bedroom = new DummyBedroom(); House house = new House (kitchen, bedroom); // ,     
      
      







現在、Houseクラスでは、デザイナーが仕事に必要なものをすべて必要としています。 そして、彼はそれがどこから来たか気にしません。 すべてのビジネスクラスは、そのコンストラクタで依存関係の既製のインスタンスを必要とする必要があります。これが主な原則です。



しかし、読者の中には、「ちょっと待ってください。 コンストラクターですべての依存関係を要求する必要がある場合、「深い」子クラス(アプリケーション依存関係グラフの深さにあるクラス)を作成するために、このクラスのすべての依存関係を親クラスを通じて転送する必要があります。 そして、これは、「上位」クラス(依存関係グラフの先頭に近いクラス)の設計者が、あらゆるもののクラスターに変わるという事実につながります。 例として、これは、 新しいHouseを作成するクラスがキッチンとベッドルームのコンストラクター自体を要求する必要があることを意味します。 しかし、なぜ彼はそれらについて知る必要がありますか? これはナンセンスです。」



この推論にのみ1つの間違いがあります。 上記の原則に従って、Houseを作成するクラスはまったく作成すべきではありません。 結局のところ、原則によれば、彼自身が自分に応じて下院を要求するべきです。 しかし、最後に、Houseクラスはどこから来るのでしょうか? アプリケーションの開始時に作成されます(ハウスのライフタイムがアプリケーションのライフタイムと一致する場合)または工場によって作成されます(ハウスのライフタイムがアプリケーションのライフタイムより短い場合):



クラスハウスの作成
 class HouseFactory { House build() { Kitchen kitchen = new Kitchen(new Refrigerator()); Bedroom bedroom = new Bedroom(); return new House (kitchen, bedroom); } }
      
      







しかし、再び反対する人もいます。「では、クラスごとにファクトリを作成し、コード量を2倍に増やしますか? これはナンセンスです。」 実際、1つのファクトリーが同じライフタイムを持つクラスを作成して関連付けます。 また、実際のアプリケーションでは、存続期間の異なる大量のコードはありません。 たとえば、さまざまなライフタイムで分類されたオブジェクトのタイプがWebアプリケーションに存在することを考えてみましょう。



一般的なWebアプリケーションには4つのファクトリーが十分にあることがわかります。 したがって、それらを恐れてはいけません(そして結論として、工場がまったく書けない方法が示されます)。

したがって、2つのグループのコードを区別できます。



ビジネスコード-ビジネスロジックを反映するコード。顧客の要件が変化すると変更されるコード。 これはコードの主要なカテゴリであり、これがコードを記述している理由です。



バインディングコード-ビジネスコードが相互に作用し、一緒にバインドできるようにするコード。 ボンディングコードにはビジネスロジックは含まれません。 これには、工場、アプリケーションの起動ポイント、およびその他のタイポイントが含まれます。 リンクコードには多くの新しい演算子があります。



コードを2つのグループに分割するこのアプローチにより、アプリケーションロジックとアプリケーションのオブジェクトグラフの作成を混在させることを回避できます。 したがって、常にnew演算子を使用するときは、コードのグループで使用するかどうかを検討してください。



例外:


new演算子をビジネスコードで使用して、動作を含まないストレージオブジェクト(HashMap、Arrayなど)を作成できます。



次の例の別の一般的なケースを検討してください。



前:テストされていないコード
 class DocumentActions { Network network; DocumentModel documentModel; DocumentActions(Network network, DocumentModel documentModel) { //    this.network = network; this.documentModel = documentModel; } changeTextStyle(int textOffset, TextStyle style) { Revision revision = new Revision(this.network, this.documentModel, textOffset); revision.updateTextStyle(style); revision.apply(); } insertParagraph(int textOffset, ParagraphProps props, ParagraphStyle style) { Revision revision = new Revision(this.network, this.documentModel, textOffset); revision.addParagraph(props); revision.updateParagraphStyle(style); revision.apply(); } //      new Revision } //  //        Network network = new Network(...); DocumentModel documentModel = new DocumentModel(...); DocumentActions docActions = new DocumentActions(network, documentModel); //      docActions
      
      







各メソッドのDocumentActionsクラスはRevisionクラスを作成し、このクラスの実装をmokeeに置き換える機能のテストを奪います。 さらに悪いことに、DocumentActionsでは、使用しないコンストラクターのクラスが必要です。 しかし、Revisionを作成するために、事前に知られていない3番目の引数textOffsetを毎回与える必要がある場合はどうでしょうか? 終了:リビジョンを作成する方法の知識を引き継ぐクラスを作成します。 そして、これは工場に過ぎません:



後:テストコード
 class DocumentActions { RevisionFactory revisionFactory; DocumentActions(RevisionFactory revisionFactory) { this.revisionFactory = revisionFactory; } changeTextStyle(int textOffset, TextStyle style) { Revision revision = this.revisionFactory.build(textOffset); revision.updateTextStyle(style); revision.apply(); } insertParagraph(int textOffset, ParagraphProps props, ParagraphStyle style) { Revision revision = this.revisionFactory.build(textOffset); revision.addParagraph(props); revision.updateParagraphStyle(style); revision.apply(); } //      this.revisionFactory.build } class RevisionFactory { Network network; DocumentModel documentModel; RevisionFactory(Network network, DocumentModel documentModel) { this.network = network; this.documentModel = documentModel; } Revision build(int textOffset) { return new Revision(this.network, this.documentModel, textOffset); } } //  class MyMockRevision extends Revision { //    } class MyMockRevisionFactory extends RevisionFactory { public Revision revision; Revision build(int textOffset) { this.revision = new MyMockRevision(this.network, this.documentModel, textOffset); return this.revision; } } RevisionFactory revisionFactory = new MyMockRevisionFactory(null, null); DocumentActions docActions = new DocumentActions(revisionFactory); //   //    Revision  revisionFactory.revision
      
      







工場は、リビジョンを作成するためにNetworkとDocumentModelが絶対に必要であることを知っています。 したがって、彼女自身がこれらのクラスを自分で必要とします。 また、Revisionを作成するためのすべての動的パラメーター(textOffset)は、ファクトリビルドメソッドの引数として必要になります。



将来、Revisionクラスがコンストラクターに別の定数引数を必要とする場合、RevisionFactoryをわずかに修正することは難しくなく、DocumentActionsクラスはまったく変更されません。



グローバル変数とシングルトーン



グローバル変数は悪であることに誰もが同意すると思います。 しかし、本質的にはすべてが同じグローバル変数であるにもかかわらず、多くの人はシングルトンに問題はありません。 では、なぜシングルトンが悪いのでしょうか? 以下に2つの理由を示します(ただし、いずれでも十分です)。



1)シングルトーンを使用する場合の問題は、コードに緊密な接続性が導入されることです。 シングルトンを作成するとき、クラスはシングルトンが提供する単一の実装のみで機能すると言います。 交換する機会はありません。 テストの性質上、実装を別の実装に置き換える機能が必要なため、クラスをシングルトンから分離してテストすることは困難です。 この設計を変更するまで、クライアントをテストするために適切に動作するためにシングルトンに頼らなければなりません。



2)シングルトンが存在すると、「見えない」依存関係が作成されるため、クラスの依存関係が嘘になります。 クラスの実際の依存関係を理解するには、コンストラクター/メソッドの依存関係のリストを見るだけでなく、そのコードを完全に読む必要があります。 そして遅かれ早かれ、これはテストが隠されたグローバルな状態を介して互いに影響を及ぼし始めるという事実につながります。



両方の問題をすぐに説明する実践の例:Webアプリケーションでは、ユーザーアクションをログに記録する必要が生じました。 このために、多くのクラスで使用されるシングルトンが作成されました。 シングルトンがjQueryを使用してDOMから詳細情報を取得したことに言及します。 アプリケーションのノードバージョンでクラスの一部を再利用する必要があるまで、すべてがうまくいきました。 このバージョンは、テスト中に定期的に落ち始めました。 シングルトンロガーを使用するクラスがこのバージョンになり、ノードにDOMがないことが判明しました。 これらのクラスは「見えない」依存関係(理由2)を使用していました。その結果、この状況が発生しました。 ロガーを別のロガーに置き換える機会はありませんでした(理由1)。そのため、一部のクラスをやり直す必要がありました。



「意図的にシングルトンを使用して、アプリケーション全体のクラスのインスタンスを1つだけにする」と言う人もいます。 ただし、シングルトンを作成する場合、アプリケーション全体ではなく、コード実行スペース全体(Javaのjvm、rhinoまたはV8のjavascript)に固有のクラスインスタンスを作成します。 ほとんどの場合、コード実行スペース内にアプリケーションは1つしかなく、これらのスペースは一致していることがわかります。 しかし、これはテストの場合ではありません。 各テストは、他のテストと同じ実行スペースで実行されるアプリケーションの一部です。



ミニアプリケーションの多くは、単一のかけがえのないシングルトーンが存在する1つの実行スペースに住み始めます。 これは、テストが相互に影響を及ぼし始めたときにシングルトンの副作用が現れる場所です。



さらに、おそらくいつかは、1つのコード実行スペースにアプリケーションまたはその一部の複数のコピーが必要になるでしょう。 その場合、アプリケーションスペースはコード実行スペースと等しくなりません。 そして、シングルトンを取り除く以外に何もすることはありません。



この例を考えてみましょう。PhoneAccountクラスをテストするように指示されます。PhoneAccountクラスは、電話アカウントの操作を担当します。 考え直すことなく、次のように書きます。



試行1
 PhoneAccount phoneAccount = new PhoneAccount('79008001020'); phoneAccount.addMoney(100); expect(phoneAccount.getBalance()).toBe(100);
      
      







テストを単独で実行し、実行時にnullアクセスエラーを取得します。 何が悪かったのですか? このクラスを書いた同僚に尋ねます。 彼は長い間考え、シングルトンクラスPhoneAccountTransactionProcessorを初期化する必要があることを思い出します。 あなたは思う:「その前にどのように推測すべきだったのか?」 次に、追加します:



試行2
 PhoneAccountTransactionProcessor.init(...); PhoneAccount phoneAccount = new PhoneAccount('79008001020'); phoneAccount.addMoney(100); expect(phoneAccount.getBalance()).toBe(100);
      
      







ただし、起動時に、nullアクセスエラーが発生します。 困惑して、あなたは再び同僚に尋ねます:「私は何を間違っているのですか?」 「トランザクションキュー-AccountTransactionQueueを初期化しましたか?」という答えを受け取ります。 少しイライラしてコードを追加しましたが、これで問題は終わりではなく、「経験のある」同僚から離れないように頼むことができます。



試行3
 AccountTransactionQueue.start(...); PhoneAccountTransactionProcessor.init(...); PhoneAccount phoneAccount = new PhoneAccount('79008001020'); phoneAccount.addMoney(100); expect(phoneAccount.getBalance()).toBe(100);
      
      







そしてまた間違い。 しかし、質問する時間がある前に、同僚が「トランザクションデータベースに接続しなかった!」 さらに、深呼吸をしてください:



試行4
 TransactionsDataBase.connect(...) AccountTransactionQueue.start(...); PhoneAccountTransactionProcessor.init(...); PhoneAccount phoneAccount = new PhoneAccount('79008001020'); phoneAccount.addMoney(100); expect(phoneAccount.getBalance()).toBe(100);
      
      







最後に、テストに合格します。



あなたはすでにこのコードの何が問題なのか、ある種の黒魔術を理解していると思います。 3つのクラスは依存関係にありますが、初期化の順序はまさにそれである必要がありますが、コードはその順序を正確に指示しません。



しかし、シングルトーンがなく、各クラスがコンストラクターで動作するために必要なすべてを必要とする場合はどうでしょうか?



正しいコード
 TransactionsDataBase db = new TransactionsDataBase(...); AccountTransactionQueue transactionQueue; transactionQueue = new AccountTransactionQueue(db); PhoneAccountTransactionProcessor transactionProcessor; transactionProcessor = PhoneAccountTransactionProcessor(transactionQueue); PhoneAccount phoneAccount = new PhoneAccount('79008001020', transactionProcessor); phoneAccount.addMoney(100); expect(phoneAccount.getBalance()).toBe(100);
      
      







今では、魔法もクラス間のコミュニケーションの隠れたチャンネルもありません。 チームの新しいメンバーは、すべての依存関係をすぐに確認できるため、このようなテストを独自に作成できます。 コード自体が初期化の順序を指示するため、別の方法で実行することはできません。 実際のアプリケーションでは、クラスの初期化で何十行もある可能性があるため、これは大きなプラスです。 そしてもう1つの重要な結論:このようなコードでは、クラスの代わりにnullを渡す(可能な場合)か、クラスをmochに置き換えることができます。 シングルトンコードでは、これは不可能でした。



別の例:



前:テストされていないコード
 class LoginService { private static LoginService instance; private LoginService() {}; static LoginService getInstance() { if (instance == null) { instance = new RealLoginService(); } return instance; } //     //     ,   ! static setForTest(LoginService testDouble) { instance = testDouble; } //    //     ,   ! static resetForTest() { instance = null; } // ... } //    class AdminDashboard { boolean isAuthenticatedAdminUser(User user) { LoginService loginService = LoginService.getInstance(); return loginService.isAuthenticatedAdmin(user); } } //  AdminDashboard adminDashboard = new AdminDashboard() assertTrue(adminDashboard.isAuthenticatedAdminUser(user)); //    LoginService,    //  
      
      







シングルトンを修正します。



後:テストコード
 class LoginService { // ... } //    class AdminDashboard { AdminDashboard(LoginService loginService) { this.loginService = loginService; } boolean isAuthenticatedAdminUser(User user) { return this.loginService.isAuthenticatedAdmin(user); } } //  AdminDashboard adminDashboard = new AdminDashboard(new MockLoginService()); assertTrue(adminDashboard.isAuthenticatedAdminUser(user));
      
      







LoginServiceを簡単かつ簡単に置き換えることができ、「テスト専用」のメソッドは必要ありません。



シングルトンを引き続き使用できる場合の例外:




また、一部のシステムオブジェクトには、 math.randomnew Date()などの非表示のグローバル変数があります。 そのようなオブジェクトを使用するクラスをテストする場合は、それらを置き換えるために独自のラッパーを作成する必要があります。



ベテランのコンストラクター



ベテランコンストラクターは、クラスのフィールドを初期化する以外のことを行うコンストラクターです。 厳密に言えば、デザイナーはクラスのフィールドを初期化することのみに責任があり、その他の作業は単一責任原則に違反します。 コンストラクターでのこのような「余分な」作業は、テストがテストするクラスに依存性スタブを渡すことができないため、テストを困難にします。 熟練したデザイナーの典型的なパターンは次のとおりです。





最後の3つのパターンは、設計者だけでなく、上記の一般的なケースで考慮された特徴的なものです。



コンストラクター引数の初期化



前:テストされていないコード
 class Metro { TicketPrices ticketPrices; Metro(TicketPrices ticketPrices) { this.ticketPrices = ticketPrices; ticketPrices.setCostCalculator(new MoscowCostCalculatorWithVerySlowConstructor()); } } //    TicketPrices ticketPrices = new TicketPrices(); Metro metro = new Metro(ticketPrices); expect(metro.isWork()).toBe(true);
      
      







Metroクラスのコンストラクターはその義務を果たしません-引数を初期化します。 このコードは多くの理由で悪いですが、テスト可能性に興味があります。 初期化が遅い場合、Metroでのすべてのテストに時間がかかります。



後:テストコード
 class Metro { TicketPrices ticketPrices; Metro(TicketPrices ticketPrices) { this.ticketPrices = ticketPrices; } } class TicketPricesFactory { TicketPrices build() { TicketPrices ticketPrices = new TicketPrices(); ticketPrices.setCostCalculator(new VerySlowMoscowCostCalculator()); return ticketPrices; } } // test TicketPrices ticketPrices = new TicketPrices(); ticketPrices.setCostCalculator(null); Metro metro = new Metro(ticketPrices); expect(metro.isWork()).toBe(true);
      
      







これで、テストでは、テストに必要のないクラスを作成できなくなり、ダミーに置き換えられます。 TicketPricesの作成と初期化のロジックは、ファクトリーに移行しました。



条件とサイクル



前:テストされていないコード
 class Car { IEngine engine; Car() { if (FLAG_ENGINE.get()){ this.engine = new V8Engine(); } else { this.engine = new V12Engine(); } } } // test // ,   FLAG_ENGINE    Car car = new Car(); //       
      
      







テストは特定のフラグに関連付けられています。 先験的に、2つのエンジンオプションのみをテストできます。



:
 class Car { IEngine engine; Car(IEngine engine) { this.engine = engine; } } class EngineFactory { IEngine build(boolean isV8) { if (isV8){ return new V8Engine(); } else { return new V12Engine(); } } } //  ar   Car car = new Car(new EngineFactory().build(FLAG_ENGINE.get())); // test IEngine simpleEngine = new SimpleEngine(); Car car = new Car(simpleEngine); //   , simpleEngine    //    
      
      











前:テストされていないコード
 class Voicemail { User user; private List<Call> calls; Voicemail(User user) { this.user = user; } init(Server server) { this.calls = server.getCallsFor(this.user); } //   ,  !!! setCalls(List<Call> calls) { this.calls = calls; } // ... } //  User dummyUser = new DummyUser(); Voicemail voicemail = new Voicemail(dummyUser); voicemail.setCalls(buildListOfTestCalls());
      
      







init, , . init , calls init. , , . . — . . init. , init, initialize, setup , . Voicemail (server.getCallsFor). Voicemail:



:
 class Voicemail { List<Call> calls; Voicemail(List<Call> calls) { this.calls = calls; } // ... } class ProviderGetCalls { List<Call> getCalls(Server server, User user) { return server.getCallsFor(user); } } //  Voicemail voicemail = new Voicemail(buildListOfTestCalls()); //     
      
      









おわりに



, ( new , ), , Dependency Injection .



IoC , — (), . IoC . IoC — .



, IoC ( ), , Dependency Injection. , .



, . ( ), ( «» angular Miško Hevery ), .



All Articles