クラスを設計して、個々のプロパティではなくクラス全体をパラメータとして取得する


30

たとえば、広く共有されているクラスと呼ばれるアプリケーションがあるとしUserます。このクラスは、ユーザー、ID、名前、各モジュールへのアクセスレベル、タイムゾーンなどに関するすべての情報を公開します。

ユーザーデータは明らかにシステム全体で広く参照されていますが、何らかの理由で、このユーザーオブジェクトをそれに依存するクラスに渡すのではなく、個々のプロパティを単に渡すようにシステムが設定されています。

ユーザーIDを必要とするクラスは、userIdパラメーターとしてGUID を必要としますが、ユーザー名も必要な場合があります。そのため、別のパラメーターとして渡されます。場合によっては、これは個々のメソッドに渡されるため、値はクラスレベルでまったく保持されません。

Userクラスから別の情報にアクセスする必要があるたびに、パラメーターを追加して変更する必要があり、新しいオーバーロードの追加が適切でない場合は、メソッドまたはクラスコンストラクターへのすべての参照も変更する必要があります。

ユーザーはほんの一例です。これは、コードで広く実践されています。

これはオープン/クローズの原則に違反していると思うのは正しいですか?既存のクラスを変更するだけでなく、そもそもクラスを設定して、将来的に広範囲の変更が必要になる可能性が非常に高くなりますか?

Userオブジェクトを渡したばかりの場合は、作業しているクラスに少し変更を加えることができます。パラメータを追加する必要がある場合は、クラスへの参照に数十の変更を加える必要があります。

この慣行によって他の原則が破られていますか?おそらく依存関係の逆転?抽象化を参照しているわけではありませんが、ユーザーは1種類しかないため、ユーザーインターフェイスを持つ必要はありません。

基本的な防衛プログラミングの原則など、違反している他の非ソリッドの原則はありますか?

私のコンストラクタは次のようになります:

MyConstructor(GUID userid, String username)

またはこれ:

MyConstructor(User theUser)

投稿編集:

「パスIDまたはオブジェクト?」で質問に回答することが提案されています。これは、どちらの方向に進むかの決定が、この質問の中心にあるSOLID原則に従う試みにどのように影響するかという質問には答えません。


11
@gnat:これは間違いなく重複ではありません。可能な重複は、オブジェクト階層の奥深くに到達するためのメソッドチェーンに関するものです。この質問はそれについて全く質問していないようです。
グレッグブルクハート

2
2番目の形式は、渡されるパラメーターの数が手に負えなくなったときによく使用されます。
ロバートハーヴェイ

12
最初の署名について気に入らないことの1つは、userIdとユーザー名が実際に同じユーザーからのものであるという保証がないことです。これは潜在的なバグであり、ユーザーをどこにでも渡すことで回避できます。しかし、決定は本当に呼び出されたメソッドが引数で何をしているかに依存します。

9
「解析する」という言葉は、それを使用しているコンテキストでは意味がありません。代わりに「合格」という意味ですか?
コンラッドルドルフ

5
およそ何ISOLIDMyConstructor基本的には「私は必要今言うGuidstring」。なぜ提供するインタフェースがないGuidstring、聞かせてUserそのインターフェイスを実装してみましょうMyConstructorそのインタフェースを実装するインスタンスに依存しますか?MyConstructor変更の必要がある場合は、インターフェースを変更します。- プロバイダーではなく、消費者に「属する」インターフェースを考えるのに大いに役立ちました。ですから、「消費者として、私はこれを行うことができるプロバイダーとして」ではなく、「これを行う何かが必要です」と考えてください。
コラク

回答:


31

Userオブジェクト全体をパラメーターとして渡すことには、まったく問題はありません。実際、コードを明確にするのに役立ち、メソッドシグネチャにが必要な場合にメソッドがとる内容をプログラマにわかりやすくすることができますUser

単純なデータ型を渡すことは、本来の意味とは異なる意味を示すまでいいです。この例を考えてみましょう:

public class Foo
{
    public void Bar(int userId)
    {
        // ...
    }
}

そして使用例:

var user = blogPostRepository.Find(32);
var foo = new Foo();

foo.Bar(user.Id);

欠陥を見つけることができますか?コンパイラーはできません。渡される「ユーザーID」は単なる整数です。変数に名前を付けますuserが、blogPostRepositoryオブジェクトからその値を初期化します。おそらくBlogPostオブジェクトではなくUserオブジェクトを返します。それでもコードはコンパイルされ、最終的に実行時エラーが発生します。

次に、この変更された例を検討します。

public class Foo
{
    public void Bar(User user)
    {
        // ...
    }
}

たぶん、Barこの方法は、「ユーザーID」を使用しますが、メソッドのシグネチャが必要ですUserオブジェクトです。前と同じ使用例に戻りますが、「ユーザー」全体を渡すように修正します。

var user = blogPostRepository.Find(32);
var foo = new Foo();

foo.Bar(user);

コンパイラエラーが発生しました。このblogPostRepository.FindメソッドはBlogPostオブジェクトを返します。これを巧妙に「ユーザー」と呼びます。次に、この「ユーザー」をBarメソッドに渡すと、BlogPostを受け入れるメソッドにを渡すことができないため、すぐにコンパイラエラーが発生しますUser

言語の型システムを活用して、正しいコードをより迅速に記述し、実行時ではなくコンパイル時に欠陥を特定しています。

実際、ユーザー情報の変更のために多くのコードをリファクタリングする必要があるのは、他の問題の症状にすぎません。Userオブジェクト全体を渡すと、Userクラスに関する何かが変更されたときにユーザー情報を受け入れるすべてのメソッドシグネチャをリファクタリングする必要がないという利点に加えて、上記の利点が得られます。


6
あなたの推論自体は実際にはフィールドを渡すことを指しているが、フィールドを実際の値の取るに足らないラッパーにすることだと思います。この例では、ユーザーにはタイプUserIDのフィールドがあり、ユーザーIDには単一の整数値フィールドがあります。これで、Barの宣言により、Barはユーザーに関するすべての情報を使用するのではなく、IDのみを使用することがすぐにわかりますが、UserIDから来ていない整数をBarに渡すようなばかげた間違いを犯すことはできません。
イアン

(続き)もちろん、この種のプログラミングスタイルは、特に適切な構文サポートを持たない言語では非常に退屈です(たとえば、「UserID id」で一致させることができるので、Haskellはこのスタイルに適しています) 。
イアン

5
@Ian:独自のタイプスケートでIdをラップすると、OPによって引き起こされた元の問題を回避できます。これは、Userクラスの構造的な変更であり、多くのメソッドシグネチャをリファクタリングする必要があります。Userオブジェクト全体を渡すと、この問題が解決します。
グレッグブルクハート

@Ian:正直に言って、C#で作業していても、IDをラップしてStructでソートして、もう少しわかりやすくしたいと思っています。
グレッグブルクハート

1
「代わりにポインタを渡すことは何の問題もありません。」または、参照する可能性のあるポインターの問題をすべて回避するための参照。
Yay295

17

これはオープン/クローズの原則に違反していると思うのは正しいですか?

いいえ、それはその原則の違反ではありません。その原則はUser、それを使用するコードの他の部分に影響を与えるような方法で変更しないことに関するものです。への変更Userはそのような違反である可能性がありますが、それは無関係です。

この慣行によって他の原則が破られていますか?依存関係の逆転はおそらく?

いいえ。ユーザーオブジェクトの必要な部分だけを各メソッドに注入するという記述は逆で、純粋な依存関係の反転です。

基本的な防衛プログラミングの原則など、違反している他の非ソリッドの原則はありますか?

いいえ。このアプローチは完全に有効なコーディング方法です。それはそのような原則に違反していません。

ただし、依存関係の反転は原則にすぎません。それは破ることのできない法律ではありません。また、純粋なDIはシステムを複雑にする可能性があります。ユーザーオブジェクト全体をメソッドまたはコンストラクターに渡すのではなく、必要なユーザー値をメソッドに注入するだけで問題が発生する場合は、そのようにしないでください。原則と実用主義のバランスを取ることがすべてです。

コメントに対処するには:

チェーンの5レベル下にある新しい値を不必要に解析してから、それらの既存のメソッドの5つすべてへの参照をすべて変更する必要があるという問題があります...

ここでの問題の一部は、「不必要に[pass] ...」コメントに従って、明らかにこのアプローチが好きではないということです。それで十分です。ここには正しい答えはありません。面倒だと思ったら、そのようにしないでください。

ただし、オープン/クローズの原則に関して、厳密に従うと、「...既存の5つのメソッドすべてへのすべての参照を変更...」は、それらのメソッドが変更されるべきときに、それらのメソッドが変更されたことを示します変更が終了しました。ただし、実際には、公開/閉鎖の原則はパブリックAPIには適していますが、アプリの内部にはあまり意味がありません。

...しかし、それが実行可能である限り、その原則を確実に順守する計画には、将来の変更の必要性を減らす戦略が含まれるでしょうか?

しかし、それからあなたはYAGNIの領土迷い込み、それでも原則に直交するでしょう。Fooユーザー名Fooを受け取るメソッドがあり、生年月日も取得する場合は、原則に従って新しいメソッドを追加します。Foo変更されません。繰り返しますが、これはパブリックAPIにとっては良い習慣ですが、内部コードにとってはナンセンスです。

前述のように、それはあらゆる状況のバランスと常識についてです。これらのパラメーターが頻繁に変更される場合は、はい、User直接使用します。記述した大規模な変更からあなたを救います。しかし、頻繁に変更しない場合は、必要なものだけを渡すことも良いアプローチです。


チェーンの5レベル下にある新しい値を不必要に解析してから、すべての参照をこれらの5つの既存のメソッドすべてに変更しなければならないという問題があります。Open / Closedの原則がUserクラスにのみ適用され、現在編集中のクラスには適用されず、他のクラスでも使用されるのはなぜですか?原則は具体的には変更を回避することに関するものであると認識していますが、実行可能な限り、原則を遵守する計画には、将来の変更の必要性を減らすための戦略が含まれますか?
神保

@Jimbo、私はあなたのコメントを試して対処するために私の答えを更新しました。
デビッドアルノ

あなたの貢献に感謝します。ところで。ロバートCマーティンでさえ、オープン/クローズド原則に厳しいルールがあることを受け入れません。必然的に破られるのは経験則です。原則を適用することは、実行可能な限りそれを遵守しようとする試みです。これが、以前「実用的」という言葉を使用した理由です。
神保

ユーザー自体ではなく、ユーザーのパラメーターを渡すことは、依存関係の逆転ではありません。
ジェームズエリスジョーンズ

@ JamesEllis-Jones、依存関係の反転は、依存関係を「尋ねる」から「伝える」に切り替えます。Userインスタンスを渡し、そのオブジェクトをクエリしてパラメーターを取得する場合、依存関係を部分的に反転するだけです。まだいくつか質問があります。真の依存関係の逆転は、100%「教えて、聞かないで」です。しかし、それは複雑な代償を伴います。
デビッドアルノ

10

はい、既存の機能を変更することは、オープン/クローズド原則に違反します。要件を変更するため、変更を閉じる必要があるものを変更しています。(要件が変更されたときに変化しないように)優れたデザインは、物事へのユーザーに渡すことであろうはずですユーザに取り組んでいます。

あなたがに沿って通過する可能性があるためしかし、それは、インタフェース分掌の原則の抵触を実行する可能性がある方法関数がその作業を行うために必要以上の情報。

だから、ほとんどのものと同様に- それは依存します。

ユーザー名だけを使用すると、機能がより柔軟になり、ユーザー名の由来に関係なく、完全に機能するUserオブジェクトを作成する必要なくユーザー名を操作できます。データのソースが変更されると思われる場合、変更に対する回復力を提供します。

ユーザー全体を使用すると、使用法が明確になり、呼び出し元との契約がより強固になります。より多くのユーザーが必要になると思われる場合、変更に対する回復力を提供します。


+1ですが、「もっと多くの情報を伝えることができる」というフレーズについてはわかりません。(ユーザーtheuser)を渡すとき、1つのオブジェクトへの参照である最小限の情報を渡します。参照はより多くの情報を取得するために使用できますが、呼び出し元のコードが取得する必要がないことを意味します。(GUID userid、string username)を渡すと、呼び出されたメソッドは常にUser.find(userid)を呼び出してオブジェクトのパブリックインターフェイスを見つけることができるため、実際には何も隠しません。
-dcorking

5
@dcorking、 " (ユーザー theuser )を渡すと、最小限の情報、1つのオブジェクトへの参照を渡します "。そのオブジェクトに関連する最大の情報、つまりオブジェクト全体を渡します。「呼び出されたメソッドは、常にUser.find(userid) ...を呼び出すことができます。」適切に設計されたシステムでは、問題のメソッドはにアクセスできないため、それは不可能User.find()です。実際にも、そこにはならないことUser.find。ユーザーを見つけることは決して責任ではありませんUser
デビッドアルノ

2
@dcorking-エン 小さい参照を渡すのは技術的な偶然です。全体Userを関数に結合しています。たぶんそれは理にかなっています。ただし、関数はユーザーの名前のみを考慮する必要があります-そして、ユーザーの参加日や住所などの情報を渡すことは不適切です。
テラスティン

@DavidArnoは、おそらくOPに対する明確な答えの鍵です。ユーザーを見つける責任は誰にありますか?ファインダー/工場をクラスから分離するという設計原則に名前はありますか?
-dcorking

1
@dcorkingこれは、単一責任原則の含意の1つと言えます。ユーザーが格納されている場所とIDでユーザーを取得する方法を知ることは、User-classが持つべきではない別個の責任です。そのUserRepositoryようなことを扱っている、またはそれに類似したものがあるかもしれません。
ハルク

3

この設計は、Parameter Object Patternに従います。メソッドのシグネチャに多くのパラメータを含めることから生じる問題を解決します。

これはオープン/クローズの原則に違反していると思うのは正しいですか?

いいえ。このパターンを適用すると、オープン/クローズ原則(OCP)が有効になります。たとえば、の派生クラスはUser、消費クラスで異なる動作を引き起こすパラメーターとして提供できます。

この慣行によって他の原則が破られていますか?

それ起こる可能性があります。固い原則に基づいて説明させてください。

単一責任の原則は、それはあなたが説明したようなデザインを持っている場合(SRP)に違反することができます。

このクラスは、ユーザー、ID、名前、各モジュールへのアクセスレベル、タイムゾーンなどに関するすべての情報を公開します。

問題はすべての情報にあります。Userクラスに多くのプロパティがある場合、消費するクラスの観点から無関係な情報を転送する巨大なデータ転送オブジェクトになります。例:消費クラスの観点から、UserAuthenticationプロパティUser.IdUser.Nameは関連していますが、関連はありませんUser.Timezone

インターフェイス分離原則(ISP)も似推論に違反したが、別の視点を追加しています。例:消費クラスUserManagementでプロパティUser.Nameを分割する必要がUser.LastNameありUser.FirstName、そのためにクラスUserAuthenticationも変更する必要があるとします。

幸いにも、ISPは問題を回避する方法を提供します。通常、このようなパラメーターオブジェクトまたはデータトランスポートオブジェクトは小さく始まり、時間とともに成長します。これが扱いにくい場合は、次のアプローチを検討してください。消費するクラスのニーズに合わせたインターフェイスを導入します。例:インターフェースを導入し、Userクラスから派生させます:

class User : IUserAuthenticationInfo, IUserLocationInfo { ... }

各インターフェイスはUser、消費クラスがその操作をフルフィルするために必要なクラスの関連プロパティのサブセットを公開する必要があります。プロパティのクラスターを探します。インターフェイスを再利用してみてください。消費クラスの場合、の代わりにUserAuthentication使用します。次に、可能であれば、インターフェイスを「ステンシル」として使用して、クラスを複数の具象クラスに分割します。IUserAuthenticationInfoUserUser


1
ユーザーが複雑になると、たとえばサブユーザーの組み合わせが爆発的に増加します。たとえば、ユーザーのプロパティが3つしかない場合、7つの組み合わせがあります。あなたの提案はいいように聞こえますが、実行不可能です。
user949300

1.分析的にあなたは正しい。ただし、ドメインのモデル化方法によっては、関連情報の一部がクラスター化される傾向があります。したがって、実際には、インターフェイスとプロパティのすべての可能な組み合わせを扱う必要はありません。2.概説されたアプローチは、普遍的な解決策を意図したものではありませんでしたが、答えにさらに「可能」および「可能」を追加する必要があります。
テオレンドルフ

2

私のコードでこの問題に直面したとき、私は基本的なモデルのクラス/オブジェクトが答えだと結論付けました。

一般的な例は、リポジトリパターンです。リポジトリを介してデータベースをクエリする場合、多くの場合、リポジトリ内のメソッドの多くは同じパラメーターを多数使用します。

リポジトリの経験則は次のとおりです。

  • 複数のメソッドが同じ2つ以上のパラメーターを使用する場合は、パラメーターをモデルオブジェクトとしてグループ化する必要があります。

  • メソッドが2つ以上のパラメーターを受け取る場合、パラメーターはモデルオブジェクトとしてグループ化する必要があります。

  • モデルは共通のベースから継承できますが、それが本当に理にかなっている場合に限ります(通常、継承を念頭に置いて開始するよりも後でリファクタリングする方が適切です)。


プロジェクトが少し複雑になり始めるまで、他のレイヤー/エリアからのモデルの使用に関する問題は明らかになりません。それは、より少ないコードがより多くの仕事またはより多くの複雑さを生み出すことを発見したときだけです。

そして、はい、異なるレイヤー/目的に対応する同一のプロパティを持つ2つの異なるモデル(つまり、ViewModelsとPOCO)を使用することはまったく問題ありません。


2

SOLIDの個々の側面を確認しましょう。

  • 単一の責任:人々がクラスのセクションのみを通過する傾向がある場合、おそらく違反します。
  • Open / closed:クラスのセクションが渡される場所、オブジェクト全体が渡される場所のみに関係ありません。(ここで認知的​​不協和が始まると思います。遠くのコードを変更する必要がありますが、クラス自体は問題ないようです。)
  • リスコフ置換:問題なし、サブクラスは行っていません。
  • 依存関係の反転(具体的なデータではなく、抽象化に依存)。違反している。人々は抽象化されておらず、クラスの具体的な要素を取り出してそれをやり取りします。ここが主な問題だと思います。

設計の本能を混乱させる傾向がある1つのことは、クラスが本質的にグローバルオブジェクト用であり、本質的に読み取り専用であることです。このような状況では、抽象化に違反してもそれほど害はありません。変更されていないデータを読み取るだけで、かなり弱い結合が作成されます。それが巨大な山になったときにのみ、痛みが顕著になります。
設計の本能を回復するために、オブジェクトがあまりグローバルではないと仮定してください。Userオブジェクトをいつでも変更できる場合、関数にはどのようなコンテキストが必要ですか?オブジェクトのどのコンポーネントが一緒に変異する可能性が高いでしょうか?これらは、User、参照されたサブオブジェクトとして、または関連するフィールドの「スライス」だけを公開するインターフェースとして、重要ではありません。

別の原則:の一部を使用する関数を見てください Userどのフィールド(属性)が一緒になる傾向があるかを確認します。これはサブオブジェクトの良い予備リストです-実際にそれらが一緒に属しているかどうかを考える必要があります。

多くの作業が必要であり、やや困難です。特に、サブオブジェクトにオーバーラップがある場合、関数に渡す必要があるサブオブジェクト(サブインターフェイス)を特定するのが少し難しくなるため、コードの柔軟性がやや低下します。

Userサブオブジェクトがオーバーラップする場合、分割は実際には見苦しくなり、すべての必須フィールドがオーバーラップからのものである場合、人々はどちらを選択するかについて混乱します。階層的に分割する場合(たとえばUserMarketSegment、持っているもの、持っているものなどUserLocation)、人々は自分が書いている機能がどのレベルにあるのか確信が持てません。それは、そのLocation レベルでユーザーデータを処理していMarketSegmentますか?これが時間の経過とともに変化することはまったく役に立ちません。つまり、関数のシグネチャを変更することに戻り、時にはコールチェーン全体で変更することになります。

言い換えれば:ドメインを本当に知っていて、どのモジュールがのどの側面を扱っているかについてかなり明確な考えを持っていない限りUser、プログラムの構造を改善する価値はありません。


1

これは本当に興味深い質問です。それは依存します。

メソッドがUserオブジェクトの異なるパラメーターを要求するために内部で将来変更される可能性があると思われる場合は、必ずすべてを渡す必要があります。利点は、メソッドの外部のコードが、使用するパラメーターの観点からメソッド内の変更から保護されることです。これにより、外部からの変更のカスケードが発生します。したがって、ユーザー全体を渡すとカプセル化が増加します。

ユーザーのメール以外を使用する必要がないと確信している場合は、それを渡す必要があります。これの利点は、より広い範囲のコンテキストでメソッドを使用できることです。たとえば、それを使用できます会社のメールまたは誰かが入力したメールを使用します。これにより、柔軟性が向上します。

これは、依存関係を注入するかどうか、グローバルに利用可能なオブジェクトを持つかどうかなど、広い範囲または狭い範囲を持つためのクラスの構築に関する幅広い質問の一部です。現時点では、狭い範囲が常に良いと考える不幸な傾向があります。ただし、この場合のように、カプセル化と柔軟性の間には常にトレードオフがあります。


1

できる限り少ないパラメーターと必要な数のパラメーターを渡すのが最善だと思います。これにより、テストが簡単になり、オブジェクト全体を作成する必要がなくなります。

あなたの例では、user-idまたはuser-nameのみを使用する場合、これがあなたが渡すべきすべてです。このパターンが数回繰り返され、実際のユーザーオブジェクトがはるかに大きい場合は、そのための小さなインターフェイスを作成することをお勧めします。かもしれない

interface IIdentifieable
{
    Guid ID { get; }
}

または

interface INameable
{
    string Name { get; }
}

これにより、モックでのテストがはるかに簡単になり、どの値が実際に使用されているかがすぐにわかります。そうしないと、多くの場合、他の多くの依存関係を持つ複雑なオブジェクトを初期化する必要がありますが、最後には1つまたは2つのプロパティが必要になります。


1

時々私が遭遇したことがあります:

  • メソッドは、いくつかのプロパティのみを使用している場合でも、多くのプロパティを持つ型User(またはProductその他)の引数を取ります。
  • 何らかの理由で、完全に読み込まれたUserオブジェクトがない場合でも、コードの一部でそのメソッドを呼び出す必要があります。インスタンスを作成し、メソッドが実際に必要とするプロパティのみを初期化します。
  • これは何度も起こります。
  • しばらくして、User引数を持つメソッドに遭遇すると、そのメソッドの呼び出しを見つけて、どこからUser来たのかを見つけなければならないので、どのプロパティが設定されているかを知る必要があります。メールアドレスを持つ「本物の」ユーザーですか、それともユーザーIDといくつかの権限を渡すために作成されただけですか?

Userメソッドが必要とするプロパティを作成し、いくつかのプロパティのみを設定する場合、呼び出し側はメソッドの内部動作について必要以上によく知っています。

さらに悪いこと、のインスタンスある場合、Userどのプロパティが設定されているかを知るために、それがどこから来たのかを知る必要があります。あなたはそれを知る必要はありません。

時間が経つにつれて、開発者はUserメソッド引数のコンテナとして使用されると判断すると、使い捨てのシナリオでプロパティの追加を開始する場合があります。クラスはほとんど常にnullまたはデフォルトになるプロパティで混雑しているため、今では見苦しくなっています。

このような破損は避けられませんが、プロパティのいくつかにアクセスする必要があるという理由だけで、オブジェクトを渡すときに何度も発生します。危険ゾーンは、誰かがインスタンスを作成Userし、メソッドにそれを渡すことができるようにいくつかのプロパティを設定するのを初めて目にするときです。暗い道だから足を下ろしてください。

可能な場合は、渡す必要があるものだけを渡すことで、次の開発者に適切な例を設定します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.