たとえば、User
という広く共有されているクラスを持つアプリケーションがあるとします。このクラスは、ユーザー、ユーザーのID、名前、各モジュールへのアクセスレベル、タイムゾーンなどに関するすべての情報を公開します。
ユーザーデータは明らかにシステム全体で広く参照されていますが、システムは、何らかの理由で、このユーザーオブジェクトをそれに依存するクラスに渡す代わりに、そこから個々のプロパティを渡すように設定されています。
ユーザーIDを必要とするクラスは、GUID userId
をパラメーターとして必要とするだけですが、ユーザー名も必要になる場合があるため、別のパラメーターとして渡されます場合によっては、これが個々のメソッドに渡されるため、値がクラスレベルでまったく保持されません。
Userクラスのさまざまな情報にアクセスする必要があるたびに、パラメーターを追加して変更を加える必要があります。また、新しいオーバーロードの追加が適切でない場合は、メソッドまたはクラスコンストラクターへのすべての参照も変更する必要があります。
ユーザーはほんの一例です。これは私たちのコードで広く実践されています。
これはオープン/クローズドの原則に違反していると思いますか?既存のクラスを変更するだけでなく、最初にそれらを設定して、広範囲にわたる変更が将来必要になる可能性が非常に高くなるようにしますか?
User
オブジェクトを渡しただけの場合、使用しているクラスに小さな変更を加えることができます。パラメータを追加する必要がある場合、クラスへの参照に何十もの変更を加える必要があるかもしれません。
この実践によって他の原則が破られていますか?おそらく依存関係の逆転?抽象化については言及していませんが、ユーザーは1種類しかないため、ユーザーインターフェイスを用意する必要はありません。
基本的な防御的プログラミングの原則など、違反されている他の非SOLID原則はありますか?
私のコンストラクタは次のようになります:
MyConstructor(GUID userid, String username)
またはこれ:
MyConstructor(User theUser)
投稿の編集:
「パスIDまたはオブジェクト?」で質問に回答することが提案されています。これは、どちらに行くかという決定が、この質問の中心にあるSOLID原則)に従う試みにどのように影響するかという質問には答えません。
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
オブジェクトが必要です。次に、前と同じ使用例に戻りましょう。ただし、「user」全体を渡すように修正します。
var user = blogPostRepository.Find(32);
var foo = new Foo();
foo.Bar(user);
コンパイラエラーが発生しました。 blogPostRepository.Find
メソッドはBlogPost
オブジェクトを返します。これを巧みに「ユーザー」と呼びます。次に、この「ユーザー」をBar
メソッドに渡し、BlogPost
を受け入れるメソッドにUser
を渡すことができないため、すぐにコンパイラエラーが発生します。
言語のタイプシステムは、正しいコードをより迅速に記述し、実行時ではなくコンパイル時に欠陥を特定するために活用されています。
実際、ユーザー情報の変更は他の問題の兆候にすぎないため、多くのコードをリファクタリングする必要があります。 User
オブジェクト全体を渡すことにより、User
クラスに関する何かが変更されたときにユーザー情報を受け入れるすべてのメソッドシグネチャをリファクタリングする必要がないという利点に加えて、上記の利点が得られます。
これはオープン/クローズドの原則に違反していると思いますか?
いいえ、それはその原則の違反ではありません。その原則は、それを使用するコードの他の部分に影響を与えるような方法でUser
を変更しないことに関係しています。 User
への変更はそのような違反である可能性がありますが、無関係です。
この実践によって他の原則が破られていますか?依存関係の逆転か?
いいえ。ユーザーオブジェクトの必要な部分のみを各メソッドに注入するという説明は逆です。これは純粋な依存関係の逆転です。
基本的な防御的プログラミングの原則など、違反されている他の非SOLID原則はありますか?
いいえ。このアプローチは完全に有効なコーディング方法です。それはそのような原則に違反していない。
しかし、依存関係の逆転は原則にすぎません。それは破られない法律ではありません。また、純粋なDIはシステムを複雑にする可能性があります。ユーザーオブジェクト全体をメソッドまたはコンストラクターのいずれかに渡すのではなく、必要なユーザー値をメソッドに注入するだけで問題が発生する場合は、そのようにしないでください。それはすべて、原則と実用主義のバランスをとることに関するものです。
コメントに対処するには:
チェーンの5つのレベルで新しい値を不必要に解析し、すべての参照を既存の5つのメソッドすべてに変更する必要があるという問題があります...
ここでの問題の一部は、「不必要に[pass] ...」コメントに従って、このアプローチが明らかに嫌いであることです。そして、それで十分です。ここに正しい答えはありません。負担が大きいと感じたら、そのようにしないでください。
ただし、オープン/クローズの原則に関しては、厳密にそれに従っている場合、「...すべての参照を既存の5つのメソッドすべてに変更...」は、それらのメソッドが変更されているはずのときに変更されたことを示しています。変更はできません。しかし実際には、オープン/クローズの原則はパブリックAPIには適切ですが、アプリの内部にはあまり意味がありません。
...しかし、実行可能な限り、この原則を順守する計画には、将来の変更の必要性を減らすための戦略が含まれるでしょうか。
しかし、あなたは YAGNI territory をさまよい、それでもそれは原則に直交します。ユーザー名を受け取るメソッドFoo
があり、Foo
にも生年月日を取得する場合は、原則に従って、新しいメソッドを追加します。 Foo
は変更されません。繰り返しますが、これはパブリックAPIにとっては良い習慣ですが、内部コードにとってはナンセンスです。
前述したように、それは、特定の状況におけるバランスと常識についてです。これらのパラメーターが頻繁に変更される場合は、はい、User
を直接使用します。あなたが説明する大規模な変更からあなたを救うでしょう。ただし、頻繁に変更されない場合は、必要なものだけを渡すのも良い方法です。
はい、既存の関数を変更すると、オープン/クローズド原則に違反します。要件の変更のために変更をクローズする必要がある何かを変更しています。より良い設計(要件が変更されても変更されないようにする)は、ユーザーにする必要があるものをユーザーに渡すことです。
ただし、way関数がその機能を実行するために必要な情報よりも多くの情報を渡す可能性があるため、インターフェイス分離の原則に反する可能性があります。
だから、ほとんどのものと同じように-依存します。
ユーザー名だけを使用すると、機能がより柔軟になり、ユーザー名の出所に関係なく、完全に機能するユーザーオブジェクトを作成する必要なく、ユーザー名を操作できます。データのソースが変わると思われる場合は、変更に対する回復力を提供します。
ユーザー全体を使用すると、使用方法が明確になり、呼び出し元との契約がよりしっかりします。より多くのユーザーが必要になると思われる場合は、変更に対する回復力を提供します。
この設計は パラメータオブジェクトパターン に従います。メソッドシグネチャに多くのパラメータを設定することで発生する問題を解決します。
これはオープン/クローズドの原則に違反していると思いますか?
いいえ。このパターンを適用すると、開閉の原則(OCP)が有効になります。たとえば、User
の派生クラスは、消費クラスで異なる動作を引き起こすパラメータとして提供できます。
この実践によって他の原則が破られていますか?
それはできます。 SOLID原則に基づいて説明します。
単一責任の原則(SRP)は、あなたが説明したように設計されている場合、違反する可能性があります:
このクラスは、ユーザー、ユーザーのID、名前、各モジュールへのアクセスレベル、タイムゾーンなどに関するすべての情報を公開します。
問題は、すべての情報にあります。 User
クラスに多くのプロパティがある場合、それは巨大な Data Transfer Object になり、消費するクラスの観点から無関係な情報を転送します。例:消費クラスの観点からUserAuthentication
プロパティUser.Id
とUser.Name
は関連しますが、User.Timezone
は関連しません。
インターフェイス分離の原則(ISP)も同様の推論に違反していますが、別の見方が追加されています。例:消費クラスUserManagement
で、プロパティUser.Name
をUser.LastName
に分割する必要があるとします。User.FirstName
クラスUserAuthentication
もこれに合わせて変更する必要があります。
幸いにも、ISPは問題を回避する方法を提供します。通常、そのようなパラメーターオブジェクトまたはデータ転送オブジェクトは、最初は小さく、時間の経過とともに大きくなります。これが手に負えなくなった場合は、次のアプローチを検討してください。使用するクラスのニーズに合わせて調整されたインターフェイスを導入します。例:インターフェースを導入し、User
クラスをそこから派生させます。
class User : IUserAuthenticationInfo, IUserLocationInfo { ... }
各インターフェイスは、消費クラスがその操作を完全に実行するために必要なUser
クラスの関連プロパティのサブセットを公開する必要があります。プロパティのクラスターを探します。インターフェイスを再利用してみてください。消費クラスUserAuthentication
の場合、IUserAuthenticationInfo
の代わりにUser
を使用します。次に、可能であれば、インターフェイスを「ステンシル」として使用して、User
クラスを複数の具象クラスに分割します。
SOLIDの個々の側面を確認してみましょう。
設計本能を混乱させる傾向がある1つのことは、クラスが本質的にグローバルオブジェクト用であり、本質的に読み取り専用であることです。このような状況では、抽象化に違反してもそれほど害はありません。変更されていないデータを読み取るだけでは、かなり弱い結合になります。それが巨大な山になったときのみ、痛みが顕著になります。
設計本能を復活させるには、オブジェクトがあまりグローバルではないと仮定します。 User
オブジェクトをいつでも変更できる場合、関数にはどのようなコンテキストが必要ですか?オブジェクトのどのコンポーネントが一緒に変異する可能性がありますか?これらはUser
から分割できます。参照されるサブオブジェクトとして、または関連フィールドの「スライス」だけを公開するインターフェースとしては、それほど重要ではありません。
別の原則:User
の一部を使用する関数を調べ、どのフィールド(属性)が一緒になりがちかを確認します。これは、サブオブジェクトの良い予備リストです。それらが実際に一緒に属しているかどうかを確実に考える必要があります。
特にサブオブジェクトがオーバーラップしている場合は、関数に渡す必要のあるサブオブジェクト(サブインターフェース)を識別するのが少し難しくなるため、コードの柔軟性が低下します。
サブオブジェクトがオーバーラップしている場合、User
を分割すると実際には醜くなり、必要なフィールドがすべてオーバーラップしている場合、どちらを選択するかについて混乱します。階層的に分割すると(たとえば、UserMarketSegment
にUserLocation
が含まれているなど)、人々は自分が書いている関数がどのレベルにあるのかわからなくなります。 Location
レベルまたはMarketSegment
レベルのデータ?これが時間の経過とともに変化する可能性があること、つまり、コールチェーン全体で関数のシグネチャの変更に戻ることはありません。
つまり、ドメインを本当に理解していて、どのモジュールがUser
のどの側面を処理しているかについてかなり明確な考えがない限り、プログラムの構造を改善する価値はありません。
私自身のコードでこの問題に直面したとき、基本的なモデルクラス/オブジェクトが答えであると結論しました。
一般的な例は、リポジトリパターンです。多くの場合、リポジトリを介してデータベースにクエリを実行するとき、リポジトリ内の多くのメソッドは同じパラメータを多数使用します。
リポジトリの経験則は次のとおりです。
複数のメソッドが同じ2つ以上のパラメーターを取る場合、パラメーターはモデルオブジェクトとしてグループ化する必要があります。
メソッドが3つ以上のパラメーターを取る場合、パラメーターはモデルオブジェクトとしてグループ化する必要があります。
モデルは共通のベースから継承できますが、それが本当に理にかなっている場合に限ります(通常、継承を念頭に置いて開始するよりも後でリファクタリングする方が適切です)。
他のレイヤー/エリアのモデルの使用に関する問題は、プロジェクトが少し複雑になるまで明らかになりません。コードが少なくなると、多くの作業や複雑な作業が発生するようになります。
そしてはい、異なるレイヤー/目的に役立つ同じプロパティを持つ2つの異なるモデル(つまり、ViewModelsとPOCOs)を持つことはまったく問題ありません。
できる限り少ないパラメーターを渡し、必要なパラメーターを渡すことが最善であると思います。これにより、テストが容易になり、オブジェクト全体をクレートする必要がなくなります。
あなたの例では、user-idまたはuser-nameのみを使用する場合は、これで渡すだけです。このパターンが数回繰り返され、実際のユーザーオブジェクトがはるかに大きい場合は、そのための小さなインターフェイスを作成することをお勧めします。かもしれない
interface IIdentifieable
{
Guid ID { get; }
}
または
interface INameable
{
string Name { get; }
}
これにより、モックを使用したテストがはるかに簡単になり、実際に使用されている値がすぐにわかります。それ以外の場合は、多くの場合、他の多くの依存関係を持つ複雑なオブジェクトを初期化する必要がありますが、最終的に必要なのは1つまたは2つのプロパティだけです。
これは私が時々遭遇したものです:
User
(またはProduct
など)の引数を取ります。User
オブジェクトが設定されていなくても、コードの一部でそのメソッドを呼び出す必要があります。インスタンスを作成し、メソッドが実際に必要とするプロパティのみを初期化します。User
引数を持つメソッドに遭遇すると、そのメソッドへの呼び出しを見つけて、User
がどこから来ているのかを見つけて、どのプロパティが設定されているかを知る必要があります。 。それはメールアドレスを持つ「本物の」ユーザーですか、それともユーザーIDといくつかの権限を渡すために作成されただけですか?User
を作成し、それらがメソッドに必要なプロパティであるためにいくつかのプロパティのみを設定する場合、呼び出し元は、メソッドの内部動作について、必要以上に詳細を知っています。
さらに悪いことに、haveUser
のインスタンスである場合、どのプロパティが入力されているかを知るために、それがどこから来たかを知る必要があります。あなたはそれを知る必要はありません。
時間の経過とともに、開発者はUser
がメソッド引数のコンテナーとして使用されるのを見ると、使い捨てのシナリオでそれにプロパティを追加し始める可能性があります。現在は、ほとんど常にnullまたはデフォルトになるプロパティでクラスが乱雑になっているため、醜くなっています。
このような破損は避けられないものではありませんが、いくつかのプロパティにアクセスする必要があるという理由だけでオブジェクトを渡すときに何度も発生します。危険ゾーンは、誰かがUser
のインスタンスを作成し、いくつかのプロパティを設定して、それをメソッドに渡すことができるのを初めて目にするときです。暗い道なので足を下に置いてください。
可能であれば、渡す必要のあるものだけを渡すことで、次の開発者に適切な例を設定します。
これは本当に興味深い質問です。それは依存します。
メソッドが将来的に内部で変更され、Userオブジェクトの異なるパラメーターが必要になると思われる場合は、必ず全体を渡す必要があります。利点は、メソッドの外部のコードが、使用しているパラメーターの観点からメソッド内の変更から保護されることです。これにより、言うまでもなく、外部で一連の変更が発生します。したがって、ユーザー全体を渡すと、カプセル化が向上します。
ユーザーのメール以外に何も使用する必要がないと確信している場合は、それを渡す必要があります。これの利点は、メソッドをより幅広いコンテキストで使用できることです。たとえば、次のように使用できます。会社のメールまたは誰かが入力したばかりのメールで。これにより柔軟性が向上します。
これは、依存関係を挿入するかどうか、グローバルに使用できるオブジェクトを含めるかどうかなど、スコープを広くまたは狭くするクラスの構築に関する幅広い質問の一部です。現時点では、狭い範囲が常に良いと考える傾向があります。ただし、この場合のように、カプセル化と柔軟性の間には常にトレードオフがあります。