web-dev-qa-db-ja.com

グローバルステートを使用するための「オープンクローズドプリンシプル」と「少ないカップリング」の根拠はありますか?

次のように簡略化できるフレームワークを使用して、ユーザーログイン機能を備えたモバイルアプリを作成しているとします。

class UserData{
    static token="";
    static name="";
    static balance=0;
}

class Main{
    constructor(){
        this.loginPage=new LoginPage();
        this.welcomePage=new WelcomePage();
        .
        .
        .
        if(notLogin){
            this.loginPage.show();
        }
    }
}

userDataを変更するためのログインページ

class LoginPage{
    onLoginResponse(response){
        UserData.token=response.token;
        UserData.name=response.name;
        UserData.balance=response.balance;
    }
}

他のページ:

class WelcomePage{
    showAboutDialog(){ 
      new AboutDialog().show();
    }
}

一般的なダイアログは他のページで使用できます:

class AboutDialog{
}

ある日、AboutDialogはサーバーに情報を送信するためにUserData.tokenを必要とするため、AboutDataにUserData.tokenを追加します。

class AboutDialog{
    onSubmit(){
      HttpUtils.get("(some url)/?token="+UserData.token");
    }
}

しかし、 なぜグローバル状態が悪なのか? によれば、グローバル状態を使用すべきではないことがわかっているので、依存関係注入を使用するようにコードを変更します。

class UserData{
    constructor(){
      this.token="";
      this.name="";
      this.balance=0;
    }
}

class Main{
    constructor(){
        this.userData=new UserData();
        this.loginPage=new LoginPage(this.userData);
        this.welcomePage=new WelcomePage(this.userData);
        .
        .
        .
    }
}

class LoginPage{
    constructor(userData){
      this.userData=userData;
    }
    onLoginResponse(response){
        this.userData.token=response.token;
        this.userData.name=response.name;
        this.userData.balance=response.balance;
    }
}

class AboutDialog{
    constructor(userData){
      this.userData=userData;
    }

    onSubmit(){
      HttpUtils.get("(some url)/?token="+this.userData.token");
    }
}

welcomePageを変更すると問題が発生します。

class WelcomePage{
    constructor(userData){
      this.userData=userData;
    }
    showAboutDialog(){ 
      new AboutDialog(this.userData).show();
    }
}

グローバル状態では、WelcomePageは変更する必要がなく、UserDataにも依存しませんが、グローバル状態のないもの(または「依存性注入バージョン」と言う)は変更する必要があります。依存性注入は「オープンクローズの原則」に違反していませんか?グローバル状態を許可しないと、ここでより多くの結合が発生しますか?

それにもかかわらず、この場合、MainからAboutDialogへのルートは比較的単純です。Main-> WelcomePage-> AboutPageの場合、クラスにUserDataを渡すためにさらに中間が必要な場合(例:Main-> SomePage-> SomeSubPage->) SomeDialog-> SomeService ...)?

また、「依存性注入」は他の理由で維持するのが難しいこともわかりました。

  1. より多くのコードが含まれています(例:追加のコンストラクターとデータを渡すためのクラスプロパティ)

  2. クラスがUserDataを必要としない場合、それらのコンストラクターとクラスプロパティを中間(eg:WelcomePage)から削除するのを忘れる可能性があり、中間でUserDataを誤って変更してしまいます。誤ってデータ

そして、グローバルステートを許可しない他の理由は、ここでは当てはまりません。

  1. 同時実行の問題:このアプリにはマルチスレッドコードを含めないでください(私の知る限り)

  2. パフォーマンス:データフィールドの割り当てにかかる時間はそれほど長くないはずです

だから私の質問は、このケースは依存性注入の代わりにグローバル状態を許可する正当な理由ですか?

6
aacceeggiikk

この例のOCPの誤りは、WelcomePageを「変更できないように閉じられている」再利用可能なブラックボックスライブラリに入れ、記述された方法でその動作を変更しようとすると、かなり明確になります。そのライブラリのコードを変更することなく。

WelcomePageのコードにはステートメントnew AboutDialog()が含まれているため、ライブラリにはAboutDialogのソースコードも含まれている必要があります(またはそのソースコードを含む他のライブラリに依存しています)。 isnot変更に対して閉じられています(UserData.tokenは、UserDataがグローバルであるかグローバルでないかに関係なく、コードの変更が必要です。 つまり、WelcomePageは最初からOCPをフォローしていませんでした!!。 OCPをフォローするには、原則に従っていない他のコンポーネントと密に結合されていないコンポーネントが必要です。また、ここでグローバル状態を使用してもこれは変更されないため、OCPをグローバル状態を正当化するための引数として使用することはできません。

これに対する解決策は、インターフェイスIAboutDialogを提供し、このタイプのオブジェクトを構築時にWelcomePageに挿入することです。その後、WelcomePageをブラックボックスライブラリの一部にすることができます。OCPに従って、ライブラリのコードを変更せずに、後で別の実装で元のAboutDialogを交換することができます。したがって、依存性注入を正しく使用することは正確にWelcomePageをOCPに準拠させるものです。

カップリングに関する質問:WelcomePageAboutDialogに依存し、AboutDialogが一部のグローバル変数に依存する場合、WelcomePageは同じグローバル変数にも推移的に依存します。したがって、有効なカップリングは同じままで、グローバルバリアントまたは非グローバルバリアントのカップリングは増減しません。結合は確かに非グローバルバリアントではもっと見えるですが、それは別物です。

最後に、「文字通りの質問の背後にある質問」に答えようとする何かを追加しましょう。ここで確認したことは、グローバルな状態を導入すると、他の場所のコードへの変更が少ない中央の場所で変更できる場合があることです(何もありません)。 OCPで行う!)確かに答えは次のとおりです。はい、一部のexceptionalのケースでは、これがグローバル変数を導入する理由になり得ますが、これについては非常に注意し、何をしているのかを確認する必要があります。隠れた依存関係のリスクは、安全な努力に本当に値するということです。

あなたの例では、グローバル変数を使用すると、異なるユーザーデータを持つWelcomeFormAboutBoxのインスタンスを同時に持つことがプログラムに禁止されます。これは実際、特定のコンテキストで必要なものを正確に示している場合もあれば、後で別の方法でそれが必要であることがわかった場合に、実際のメンテナンスの頭痛の種となる場合もあります。したがって、このアプローチには注意が必要です。

7
Doc Brown

マルチスレッド環境は、共有の可変状態があなたを惨めにするのに必要ではありません。

変更可能な状態を共有すると、タイミングについて心配する必要があります。タイミングが気になります。

何かがあなたがそれを作ったときのそれと同じものであることを知っているほうがずっといいです。

これが理由です:

class LoginPage{
    constructor(userData){
      this.userData=userData;
    }
    ...
}

私を悩まします。 loginPageは、公開されているuserDataへの参照を保持します。 userDataは、何かが書き込まれるたびに変更できます。ああ。つまり、loginPageは、誰かがuserDataに書き込むたびに変更される可能性があります。それは紛らわしいです。

私はむしろこれを見たいです:

class LoginPage{
    constructor(userData){
        this.token=userData.token;
        this.name=userData.name;
        this.balance=userData.balance;
    }
    ...
}

これでloginPageは、カプセル化できる独自の状態になります。データを隠し、変更されないようにすることができます。また、userDataの別のコピーを起動し、準備ができたら、境界に投げつけることもできます。しかし、userDataに触れたすべてのものがまだ混乱していることを心配する必要はありません。

これは防御コピーと呼ばれます。余分な作業のように見えるかもしれませんが、デバッガーで奇妙な値を凝視し、いつ、どのようにしてこのようになったのか疑問に思っているときは、それは神からの贈り物です。

あなたがこれを見て、それがひどくつながっていると考えるなら。すべてのオブジェクト間でこれを実行する必要はありません。あなたは絶対的に正しいです。これがDTOでのみ行う理由です。 UserDataをデータ転送オブジェクトとして扱います。これらは、ゲッターとセッター(ほとんどがデバッグ用)でラップされた単なるデータ構造です。理想的には、あなたのプログラムはDTOの周りだけにあるのではありません。重要な境界を越えるためにDTOを保存します。 GUI、DB、FileSystem、Networkなどのように、プログラム内では状態を共有しません。メソッドを移動して、必要な状態で生きます。適切なオブジェクト間でメッセージをポリモーフィックに渡します。むしろ、お互いに何をすべきかを互いに教え合う質問をします。

それを行うと、カップリングがはるかに少なくなります。国家は、物事を行うように伝えることができる1つの場所に住んでいます。

Dependency Injectionを実行する場合は、それが無駄であると判断する前に最後まで実行してください。途中で行われたDIは、より大きな混乱です。

4
candied_orange

「依存性注入」を使用した提案されたソリューションはおそらくグローバルな状態であるため、状況は実際にすでに知っているものよりも「さらに悪い」ものです。はい、明示的に状態を渡しますが、どこでもデータと同じ状態を使用するため、わずかに優れています。

言い換えると、グローバル状態の問題は本質的にunmaintainableであるということです。予測できない、または透過的でない方法で他の場所で使用/変更される可能性のあるものを使用/変更します。この問題は、同じUserDataインスタンスを渡すことで解決されますか?それが解決されたと主張するのはかなり難しいと思います。

特にオブジェクト指向(オブジェクト指向を使用したいと思います)は、状態をまったくグローバルにできないこと、さらにはオブジェクトに対してローカルでなければならないことを提案することで、この問題を解決します。つまりオブジェクトには状態およびその状態を使用するすべての操作があります。

つまり、オブジェクトを渡すことができますが、データを渡すことはできません。そのため、UserDataの代わりにUserを使用することができ、そのユーザーには、HTTPリクエストの作成や、準備されたHTTPリクエストへのトークンの追加など、実行したいすべての操作を含めることができます。等.

つまりnoであり、これはグローバルな状態の有効なケースではありません。さらに、提案されたソリューションはそれに関連する問題を実際に回避するには十分ではありません。

1

これは他の回答の上にある単なる回答であり、おそらくあなたの質問には答えません。

この例でのグローバル変数の利点:

  • WelcomePageは、そのコンポーネントのいずれかにユーザーデータなどの特定の変数が必要かどうかを知る必要はありません。

依存性注入の利点:

  • 次のようなグローバル変数の問題を解決します:
    • 変数は、それを使用するコードのAPIの一部ではない(これにより、ユニットテストが容易になり、コードがより純粋になるなどの問題が解決されます)
    • ユーザーデータは、単一のグローバル変数ではなく、アプリケーションのさまざまなスコープで変更できます。 「データリーク」[1]と呼ばれるものを防ぐことが非常に重要で、特定の状況で非常に役立ちます。

これら2つのオプションの間に何かありますか?

(Reactで現在頻繁に使用されている)1つのソリューションは、コンテキスト管理システムです。コンテキスト変数はグローバル変数に似ていますが、値が特定のスコープ/レイヤーに設定され、外側のスコープに自動的に影響しない点が異なります。これらは通常、変数を使用するコードのAPIの一部になりますが、例のWelcomePageなどの中間のレイヤーには含まれません。

変数はすべての関数とクラスに自動的に渡され、定義が保証されていないためオプションである必要があります(たとえば、他のコードを使用せずにWelcomePageだけを実行した場合、WelcomePageはユーザーデータ変数を定義しません)。

Java、そしてすべてのプログラミング言語には、組み込みのコンテキストシステムはありません。あなたはいつもあなた自身のコンテキストシステムのためにシングルトンを作成することができます、あるいはあなたがそれをあなたのすべての関数を通して通過させて幸せならあなたもそれをするかもしれません。すでにそこにコンテキストシステムライブラリがあるかもしれませんが、これはJava固有の答えではありません。


[1]「データリーク」-同じ変数を共有してはならないアプリケーションの部分でデータが利用可能または取得された場合(通常、単一のグローバル変数を使用したことが原因)。これを防ぐには、アプリケーションに個別のレイヤーまたはスコープを作成する機能を追加する必要があります。これの私の良い例は、Webページのサーバー側レンダリングとクライアントハイドレーションです。これが完了すると、クライアントとサーバーは同じコードを実行します。ユーザーデータのグローバル変数がある場合、サーバー全体が1つのユーザーデータ変数にアクセスするため、サーバーはそれを他のクライアントにリークする可能性があります。階層化/スコーピングシステムはこれを防ぎます。

0
David Callanan