web-dev-qa-db-ja.com

オブジェクトの構築:コン​​ストラクターに渡されたパラメーターを公開または非表示にする必要がありますか?

私はそれについてあまり考えなくても、機械的にやるだけの癖があります。

コンストラクターがいくつかのパラメーターを待機しているときはいつでも、これは、必要に応じて、後で呼び出し元のコードが利用できる公開情報であると見なします。

例えば:

public class FooRepository : IFooRepository
{
    public FooRepository(IDbConnection dbConnection)
    {
        DbConnection = dbConnection ?? throw new ArgumentNullException(nameof(dbConnection));
    }

    public IDbConnection DbConnection { get; }
}

FooRepositoryオブジェクトをインスタンス化した呼び出しコードはIDbConnectionオブジェクトを渡しているため、後でこの情報にアクセスする権利がありますが、変更することはできません(set on DbConnectionプロパティ)

dbConnectionパラメータは明示的に、または依存性注入によって渡すことができますが、問題ありません。 FooRepositoryはこのような詳細を認識してはなりません。

しかし、昨日、同僚とピアプログラミングをしているとき、私が書いたクラスは、最小限の有用な情報だけを公開するべきだと彼は私に言いました。彼は、開発者がオブジェクトの内部状態を分析したり、いじったりするべきではないと述べました。

私は彼に全く同意しません。時間を無駄にしすぎないようにするために、各パラメータを数分考えて、これを公開するのが良いアイデアかどうかを判断したくありません。私の意見では、最初に新しいクラスを作成するとき、私たちが単に考えることができないいくつかのユースケースがあります。

クラスが最終的にNugetパッケージに含まれるかどうかは、実際には重要ではありません。クラスのユーザーが、オブジェクトのインスタンス化時に明示的に渡した情報や、依存関係注入フレームワークから簡単に取得できる情報にアクセスできないように制限したくありません。

誰かが私にここで良い習慣と考えられていることを説明できますか?

各パラメーターを公開するのが理にかなっているのかどうか、本当に考えるべきでしょうか?それとも、時間を無駄にせずに本能的に適用できるデザインパターンはありますか?

テーマに関するあらゆるリソースを歓迎します。

22
Jérôme MEVEL

FooRepositoryオブジェクトをインスタンス化した呼び出しコードはIDbConnectionオブジェクトを渡しているため、後でこの情報にアクセスする権利があります

factory pattern のようなものを扱っている場合、これは当てはまりません。オブジェクトのインスタンス化子は、オブジェクトのハンドラーではありません。オブジェクトの構築は抽象化する必要がある実装の詳細であるため、ファクトリパターンは非常に頻繁に明確に存在します。

これは、ファクトリーパターンだけでなく、より多くのケースに当てはまります。基本的に、少なくとも1回は渡されるすべてのオブジェクトに適用されます。

しかし、それを変更することはできません(DbConnectionプロパティには設定されていません)

これは参照型には当てはまりません。参照されている which オブジェクトを変更することはできませんが、その内容は変更できます。例えば:

public class Foo
{
    public string Name { get; set; }
}

public class Baz
{
    public Foo Foo { get; } // allegedly: "can't modify it anymore"

    public Baz(Foo foo)
    {
        this.Foo = foo;
    }
}

var myFoo = new Foo() { Name = "Hello" };
var myBaz = new Baz(myFoo);

あなたの主張に従って、myBaz.Fooは変更できなくなりました。しかし、このコードは完全に合法です:

myBaz.Foo.Name = "a completely different name";

そして、それはあなたが取るリスクです。

私が書いたクラスは、最小限の有用な情報だけを公開するべきだと彼は私に言った。

これを公開するかどうかを判断するために、各パラメータについて数分考えたくありません。

これらの2つは完全には従いません。これについて考える必要はありません。現在のようにprivateではなくpublicにデフォルト設定する必要があります。それを公開する正当な理由がない限り、公開しないでください。

private(DTOプロパティなど)から始めるべきではない場合があるので、これは単純化しすぎですが、これをまだ評価できない場合は、privateではなくpublicをデフォルトに設定することをお勧めします。

私の意見では、最初に新しいクラスを作成するときに、私たちが単純に考えることができないいくつかのユースケースがあります。

私の意見では、これはクラスの責任と既存のコードベースにどのように適合するかを完全に理解していないことを示しています。

実際、それはあなたが質問で述べているようなものです:あなたはそれについて考えたくないです。しかし、あなたは本当にすべきです。あなたの例として、データベース接続を公開するリポジトリの目的は何でしょうか?ここですぐに良い習慣のルールに違反しない答えは考えられません、あなたはできますか?
データベース接続を公開することは、リポジトリの目的の一部ではありません。それは、永続データストアへのアクセスを提供することに関するすべてです。

一部には、これは時間の経過とともに発生する経験の問題です。既存のプロパティ/メソッドのアクセス修飾子を変更する必要があるときはいつでも、前の選択が適切でなかった理由を学ぶ時です。それを十分に行うと、最初の設計での公共契約の判断が向上します。

私の意見では、最初に新しいクラスを作成するときに、私たちが単純に考えることができないいくつかのユースケースがあります。

忘れないでください [〜#〜] ocp [〜#〜] "ソフトウェアエンティティ(クラス、モジュール、関数など)は拡張用に開いている必要がありますが、閉じています変更用」

時間の経過とともにクラスの内部を変更する必要があることを本質的に説明している場合は、OCPに直交するスタンスをとっています。

これは、内部が変更できないということではありません。バグが見つかったか、重大な変更が実装されています。しかし、それはできる限りそれを避けるように努めるべきであることを意味します。既存の(多くの場合、中心的な)ロジックの変更は、バグの最も一般的な原因です。

クラスが最終的にNugetパッケージに含まれるかどうかは、実際には重要ではありません。

それは本当に重要です。ライブラリが同じソリューションファイルでのみ使用されている場合、ニーズに合わせて非常にすばやく変更でき、シンプルなビルドでライブラリが機能していることを確認できます。

しかし、ヌゲは問題を複雑にします。 yout Nugetパッケージで公開されているクラスのコントラクトを変更すると、すべてのNugetコンシューマーが重大な変更に対処する必要があります。
個人的な経験から、この問題はNugetサーバーが who がNugetパッケージを消費したという記録を保持しないことによりさらに悪化し、すべてのユーザーを把握するのが難しくなります消費者は、破壊的な変更がまもなくリリースされることを事前に警告します。

デフォルトでprivateを作成し、それらを選択的に公開した場合、ここでの問題は少なくなります。既存のパーツを変更せずにコントラクトに追加しても、既存のコードは壊れません。
デフォルトでpublicに設定した場合に発生する、コントラクトからの削除は、現在コントラクトから削除するものに依存するコードを破壊する可能性が常にあります。

それを公開することが理にかなっている場合、各パラメータについて本当に考える必要がありますか?

はい。しかし、それはあなたがそうすることを理解しているほど複雑ではありません。特定のクラスが公開する必要があるかどうかを理解することは、クラスごとに1回考える必要があるものです。このクラスの目的は何ですか?このクラスをコンシューマーが使用するにはどうすればよいですか?

その後、開発するすべてのプロパティ/メソッドをクラスの目的に簡単に一致させることができます。これは、新しい評価ではなく、すでに行った決定を適用するだけです。

それとも、時間を無駄にせずに本能的に適用できるデザインパターンはありますか?

すべてのクラスでインターフェースを使用していて、インターフェースベースの依存性注入を使用している場合、クラスのコントラクト(インターフェース内のもの)を実装(インターフェース内のものではない)から分離する方法を理解するのに役立ちます。

例えば:

public interface ISodaVendingMachine
{
    Soda GetDrink();
}

public class RegularVendingMachine : ISodaVendingMachine
{
    private Drinks drinks;

    public RegularVendingMachine(Drinks drinks)
    {
        this.drinks = drinks;
    }

    public Soda GetDrink()
    {
        return this.drinks.TakeOne();
    }
}

public class ConjuringVendingMachine : ISodaVendingMachine
{
    private PhilosophersStone philosophersStone;

    public ConjuringVendingMachine(PhilosophersStone philosophersStone)
    {
        this.philosophersStone = philosophersStone;
    }

    public Soda GetDrink()
    {
        return philosophersStone.PerformIncantation("Drinkum givum");
    }
}

各自動販売機の内部はそれらに任されています。彼らがどのように消費者に飲み物にアクセスして分配するかは問題ではありません。消費者にとって、それは無関係な実装の詳細です。顧客は知りたくありません ソーセージの作り方

公共契約で重要なのは、それが消費者に飲み物を分配することであり、したがって、ISodaVendingMachineインターフェースは、その目的のために特別に構築されています。

インターフェースが、それが保証するように設計されたもの以外の鎮静について気にしていないことに注意してください。

そのインターフェースがある場合、そのインターフェースの一部ではないクラス内のすべてがprivateである可能性が高いことがすでにわかります。これは、コントラクトではなく、実装の詳細であるためです。

47
Flater

あなたの同僚は正しいです。内部状態はデフォルトでカプセル化され、正当な理由がある場合にのみ公開されます。疑わしいときは隠してください。

それを公開することが理にかなっている場合、私は各パラメーターについて本当に考える必要がありますか?それとも、時間を無駄にせずに本能的に適用できるデザインパターンはありますか?

デフォルトでそれらをすべて非表示にするだけです。これが最も簡単です。一部のプロパティを公開する必要があるかどうかは明らかです。

一般に、オブジェクトはそれ自体の問題を解決するために、必要なだけ表面を公開する必要があります。これにより、偶発的な結合が回避され、コード全体がよりモジュール化され、変更が容易になります。

これは、データまたは機能へのアクセスを可能な限り制限する、より一般的なパターンの一部です。

  • 変数のスコープはできる限り小さくする必要があります(ローカルはグローバルよりも優れており、内部スコープは外部スコープより優れています)
  • 可能な限り制約されたメンバーへのアクセス(これがメンバーのデフォルトがprivateである理由です)。
  • 可能な限り小さいインターフェース
  • オブジェクトの寿命は可能な限り短い。
  • 不変オブジェクトは、可変オブジェクトよりも優れています。

それはすべて、コードの特定のセクションが影響を与える可能性のあるもの、およびコードに影響を与える可能性のあるものの数を減らすことになります。依存関係と相互作用が少ないほど、バグも少なくなり、左右を壊すことなくコードを変更して新しい機能を追加するのが簡単になります。

クラスのユーザーがオブジェクトをインスタンス化するときに明示的に渡した情報にアクセスすることを制限したくない

私はあなたが思う可能性があるそこに行くアンチパターンを持っている。クラスのユーザーが情報を渡した場合、それはすでに情報を持っている必要がありますよね?では、渡したばかりのオブジェクトから再度取得する必要があるのはなぜでしょうか。

あなたの例では、リポジトリが使用する接続は実装の詳細です。リポジトリの全体的な考え方は、基盤となるストレージを気にすることなくリポジトリを使用できるということです。したがって、リポジトリから接続をフェッチする必要があることは、設計に別の問題があることを示唆しています。

9
JacquesB

そもそもなぜリポジトリを作成するのか考えてみてください。クライアントがデータの格納方法を知る必要がないように、永続化の概念を抽象化しようとしています。これを行うのは、永続化メカニズムに対する将来の変更からクライアントを保護できるためです。

リポジトリからDbConnectionを公開すると、公開インターフェースの一部になります。 「私は常にデータベース、特にIDbConnectionを使用します」と言っているため、実装を(たとえば、インメモリまたはファイルベースに)切り替えることが困難になっています。

本当にDbConnectionを公開する必要がある場合は、最初に、汎用の永続化メソッドのみを含むインターフェースIFooRepositoryと、接続を追加で公開する具体的な実装DbFooRepositoryを作成する必要があります。一般に、クライアントがIFooRepositoryインターフェースのみに依存するようにして、データベースに結合されないようにしますが、特定のクライアントがデータベースに直接アクセスする必要がある場合は、代わりにDbFooRepositoryを使用できます。 。

私の意見では、最初に新しいクラスを作成するときに、私たちが単に考えることができないいくつかのユースケースがあります。

[〜#〜] yagni [〜#〜] 。必要なときに追加する方が簡単です(そうすることはできません)。 「念のために」誰かが将来それを必要とする可能性があるので、一連の詳細を公開しないでください。特にNuGetパッケージを公開する場合は、公開APIを作成すると削除できないことに注意してください。

6
casablanca

クラスにしたいことを行うために、必要なことを公開する必要があります。それ以外のものはせいぜい金メッキであり、さらに悪いことに、無関係な詳細で消費者を圧倒しています。後者の例として、FooRepositoryのユーザーが使用しているロガーを知る必要があることはほとんどありませんが、それは多くのリポジトリに注入されるものです。

公開情報は興味深い情報ではなく、役立つ情報でもありません。必要な情報である必要があります。ユーザーが情報を要求する理由が思いつかない場合は、クラスがないとクラスは使用できないと言ってから省略します。クラスを開発する際に、クラスの一部の側面を公開する必要がある理由が考えられる場合がありますが、それは問題ありません。ただし、クラスがビルダーであっても、共有する必要のない多くの詳細が含まれている可能性があります。共有しすぎないでください。

0
jmoreno