SOLIDに切り替えた後、大幅に増加したクラスの数を管理および整理しますか?


50

ここ数年、私たちは少しずつ一歩ずつ、より良い記述コードに徐々に切り替えてきました。私たちはついに、少なくともSOLIDに似たものへの切り替えを始めていますが、まだまだそこにはありません。切り替えを行って以来、開発者からの最大の不満の1つは、以前はすべてのタスクで開発者が5〜10個のファイルを操作するだけで十分だった数十個のファイルをピアレビューおよびトラバースできないことです。

切り替えを開始する前に、アーキテクチャは次のように編成されていました(1〜2桁以上のファイルが付与されています)。

Solution
- Business
-- AccountLogic
-- DocumentLogic
-- UsersLogic
- Entities (Database entities)
- Models (Domain Models)
- Repositories
-- AccountRepo
-- DocumentRepo
-- UserRepo
- ViewModels
-- AccountViewModel
-- DocumentViewModel
-- UserViewModel
- UI

ファイルに関しては、すべてが非常に線形でコンパクトでした。明らかにコードの重複、密結合、および頭痛の種がたくさんありましたが、誰もがそれを横断して理解することができました。完全な初心者、つまりVisual Studioを開いたことのない人は、わずか数週間でそれを理解できました。全体的なファイルの複雑さの欠如により、初心者の開発者や新規採用者も、あまり時間をかけずに貢献を始めることが比較的簡単になります。しかし、これはコードスタイルの利点が出てこないところです。

コードベースを改善するためのあらゆる試みを心から支持しますが、このような大規模なパラダイムシフトについて、チームの他のメンバーからプッシュバックを得るのは非常に一般的です。現在の最大のこだわりのポイントは次のとおりです。

  • 単体テスト
  • クラス数
  • ピアレビューの複雑さ

ユニットテストは時間の無駄だと信じており、個々のコードよりも全体としてコードをはるかに迅速に処理テストできると信じているため、チームにとっては信じられないほど難しい販売でした。SOLIDの承認として単体テストを使用することはほとんど無益であり、この時点ではほとんど冗談になりました。

クラス数はおそらく克服すべき最大のハードルです。5〜10個のファイルを使用していたタスクは、70〜100個かかるようになりました。これらの各ファイルにはそれぞれ異なる目的がありますが、膨大な量のファイルが圧倒される場合があります。チームからの反応は、主にうめき声と頭を掻くものでした。以前は、タスクには1つまたは2つのリポジトリ、1つまたは2つのモデル、ロジックレイヤー、およびコントローラーメソッドが必要でした。

単純なファイル保存アプリケーションを構築するには、ファイルが既に存在するかどうかを確認するクラス、メタデータを書き込むクラスDateTime.Now、ユニットテストの時間を注入できるように抽象化するクラス、ロジックを含むすべてのファイルのインターフェイス、ファイルがあります各クラスの単体テストと、DIコンテナにすべてを追加するための1つ以上のファイルを格納します。

中小規模のアプリケーションの場合、SOLIDは非常に簡単に販売できます。誰もが保守性の利点と容易さを理解しています。ただし、非常に大規模なアプリケーションでは、SOLIDの価値提案が見られないだけです。だから私は、組織と管理を改善して、成長する痛みを乗り越える方法を見つけようとしています。


最近完了したタスクに基づいて、ファイルボリュームの例を少し強くしたいと思いました。ファイル同期要求を受信するために、新しいマイクロサービスのいずれかに機能を実装するタスクが与えられました。要求が受信されると、サービスは一連の検索とチェックを実行し、最終的にドキュメントをネットワークドライブと2つの個別のデータベーステーブルに保存します。

ドキュメントをネットワークドライブに保存するには、いくつかの特定のクラスが必要でした。

- IBasePathProvider 
-- string GetBasePath() // returns the network path to store files
-- string GetPatientFolderName() // gets the name of the folder where patient files are stored
- BasePathProvider // provides an implementation of IBasePathProvider
- BasePathProviderTests // ensures we're getting what we expect

- IUniqueFilenameProvider
-- string GetFilename(string path, string fileType);
- UniqueFilenameProvider // performs some filesystem lookups to get a unique filename
- UniqueFilenameProviderTests

- INewGuidProvider // allows me to inject guids to simulate collisions during unit tests
-- Guid NewGuid()
- NewGuidProvider 
- NewGuidProviderTests

- IFileExtensionCombiner // requests may come in a variety of ways, need to ensure extensions are properly appended.
- FileExtensionCombiner
- FileExtensionCombinerTests

- IPatientFileWriter
-- Task SaveFileAsync(string path, byte[] file, string fileType)
-- Task SaveFileAsync(FilePushRequest request) 
- PatientFileWriter
- PatientFileWriterTests

したがって、かなり簡単な保存を実行するために、合計で15のクラス(POCOとscaffoldingを除く)になります。この数は、いくつかのシステムのエンティティを表すためにPOCOを作成し、他のORMと互換性のないサードパーティシステムと通信するためにいくつかのリポジトリを作成し、特定の操作の複雑さを処理するロジックメソッドを作成する必要があるときに大きく膨れ上がりました。


52
「以前は5〜10個のファイルを使用していたタスクが、70〜100個かかるようになりました!」これは決して正常なことではありません。そのような多くのファイルを変更する必要がある、どのような変更を行っていますか?
陶酔

43
タスクごとにさらに多くのファイルを変更する必要があるという事実(さらに多く!)は、間違った間違いをしていることを意味します。全体のポイントは、観察された変更パターンを反映する方法でコードを(時間とともに)整理し、変更をより簡単にすることです。SOLIDのすべての原則には、その背後に特定の理由があります(いつ、なぜ適用する必要があるか)。これらを盲目的に適用することで、このような状況に陥ったようです。単体テスト(TDD)でも同じです。設計/アーキテクチャの実行方法を十分に把握せずに実行している場合は、自分自身を穴に掘り下げることになります。
フィリップミロヴァーノヴィッチ

60
あなたは仕事を成し遂げるための実用的なツールではなく、宗教としてSOLIDを明確に採用しました。SOLIDで何かがより多くの作業を行っているか、物事をより困難にしている場合は、それをしないでください。
whatsisname

25
@Euphoric:問題は両方の方法で発生する可能性があります。70-100のクラスが過剰である可能性にあなたが反応していると思います。しかし、これがたまたま5〜10個のファイルに詰め込まれた大規模なプロジェクトであることは不可能ではありません(以前に20KLOCファイルで作業したことがあります...)。
フラット

18
私は「オブジェクト幸福病」と呼ぶ思考の混乱があります。これは、大規模なコードベースで作業するコストを削減する多くの可能な技術の1つではなく、OO技術がそれ自体で終わりであるという信念です。あなたは特に「SOLID HAPPY DISEASE」という高度な形態を持っています。ソリッドは目標ではありません。コードベースの維持コストを削減することが目標です。あなたの提案を、それが教義的であるかどうかではなく、その文脈で評価してください。(あなたの提案もおそらく実際には教義ではないということは、SOLIDも考慮すべき良い点です。)
Eric Lippert

回答:


104

単純なファイル保存アプリケーションを構築するには、ファイルが既に存在するかどうかを確認するクラス、メタデータを書き込むクラス、DateTime.Nowを抽象化するクラス、ユニットテストの時間を注入できるクラス、ロジック、各クラスの単体テストを含むファイル、およびDIコンテナにすべてを追加する1つ以上のファイル。

単一の責任という考えを誤解していると思います。クラスの単一の責任は「ファイルを保存する」かもしれません。そのためには、その責任を、ファイルが存在するかどうかをチェックするメソッド、メタデータを書き込むメソッドなどに分割します。これらの各メソッドには、クラス全体の責任の一部である単一の責任があります。

抽象化するクラスDateTime.Nowはいいですね。ただし、必要なのはいずれか1つだけで、環境機能を抽象化する責任を持つ他の環境機能で単一のクラスにまとめることができます。繰り返しますが、複数のサブ責任を持つ単一の責任です。

「ロジックを含むすべてのファイルのインターフェイス」は必要ありません。副作用のあるクラス、たとえばファイルまたはデータベースの読み取り/書き込みを行うクラスのインターフェイスが必要です。その場合でも、それらはその機能の公開部分にのみ必要です。そのため、たとえば、AccountRepoにはインターフェイスが必要ない場合があります。実際のデータベースアクセスには、そのリポジトリに挿入されるインターフェイスのみが必要な場合があります。

ユニットテストは時間の無駄だと信じており、個々のコードよりも全体としてコードをはるかに迅速に処理テストできると信じているため、チームにとっては信じられないほど難しい販売でした。SOLIDの承認として単体テストを使用することはほとんど無益であり、この時点ではほとんど冗談になりました。

これは、ユニットテストについても誤解していることを示しています。単体テストの「ユニット」はコードの単位ではありません。コード単位とは何ですか?クラス?方法?変数?単一の機械命令ですか?いいえ、「ユニット」とは、分離の単位、つまり、コードの他の部分から分離して実行できるコードを指します。自動テストが単体テストであるかどうかの簡単なテストは、結果に影響を与えずに他のすべての単体テストと並行して実行できるかどうかです。単体テストについてはさらにいくつかの経験則がありますが、それが重要な尺度です。

したがって、コードの一部を実際に他の部分に影響を与えずに全体としてテストできる場合は、それを行います。

常に実用的であり、すべてが妥協であることを忘れないでください。DRYに忠実であればあるほど、コードはより緊密に結合する必要があります。抽象化を導入するほど、コードのテストは簡単になりますが、理解は難しくなります。イデオロギーを避け、理想とそれをシンプルに保つことの間のバランスを見つけてください。開発と保守の両方で最大効率のスイートスポットがあります。


27
私は、人々が「メソッドは1つのことだけを行うべきである」というあまりにも頻繁に繰り返されるマントラに固執しようとすると同様の頭痛が発生し、技術的にメソッドにできるという理由だけで1行のメソッドのトンに終わることを付け加えたい。
Logarr

8
Re 「常に実用的であり、すべてが妥協であることを忘れないでください」:ボブおじさんの弟子たちは、これについては知られていない(元の意図に関係なく)。
ピーターモーテンセン

13
最初の部分をまとめると、通常、プラグインパーコレーター、フリップスイッチ、砂糖の必要性を補充するチェック、オープン冷蔵庫、取り出しミルク、ゲットの完全なスイートではなく、コーヒーインターンがあります-アウトスプーン、下ろしカップ、コーヒーを注ぐ、砂糖を追加する、ミルクを追加する、カップを攪拌する、およびカップを配達するインターン。; P
ジャスティンタイム2モニカの復活

12
OPの問題の根本的な原因は、単一のタスクを実行する必要がある関数と、単一の責任
アレフゼロ

6
「ルールは、賢者の導きと愚か者の服従のためのものです。」-ダグラス・バーダー
カラヌス

29

5〜10個のファイルを使用していたタスクは、70〜100個かかるようになりました。

これは、単一責任原則(SRP)の反対です。その点に到達するには、機能を非常にきめ細かな方法で分割する必要がありますが、それはSRPの目的ではありません- 結合性の重要な概念を無視します。

SRPによれば、ソフトウェアは、単一の設計変更を適用することができるように、変化に対する可能な理由によって定義される線に沿ってモジュールに分割されなければならないただ一つの他の場所の変更を必要とせずにモジュール。この意味での単一の「モジュール」は複数のクラスに対応する可能性がありますが、1つの変更で数十のファイルに触れる必要がある場合、実際には複数の変更であるか、SRPが間違っています。

元々SRPを策定したBob Martinは、数年前に状況を明確にするためにブログ投稿を書きまし。SRPの目的のために、「変更する理由」が何であるかについてある程度議論します。全体を読む価値がありますが、特別な注意に値するものの中に、SRPのこの代替の表現があります。

同じ理由で変化するものを集めてください。さまざまな理由で変化するものを分離します。

(エンファシス鉱山)。SRPは、物事を可能な限り小さなピースに分割することではありません。それは良い設計ではなく、あなたのチームは抵抗する権利があります。コードベースの更新と保守が難しくなります。単体テストの考慮事項に基づいてチームを販売しようとしているように聞こえますが、それは馬の前にカートを置くことです。

同様に、インターフェイスの分離の原則は絶対的なものと見なされるべきではありません。コードをSRPほど細かく分割する必要はなく、通常はSRPと非常によく一致します。一部のクライアントが使用しないメソッドがインターフェイスに含まれていることは、それを分割する理由ではありません。あなたは再び結束を探しています。

また、深い継承階層を優先する理由として、オープンクローズド原則またはリスコフ代替原則を採用しないことをお勧めします。スーパークラスを持つサブクラスよりも密結合はなく、密結合は設計上の問題です。代わりに、そうすることが理にかなっている場合はどこでも、継承よりも合成を優先します。これにより、カップリングが減少し、特定の変更を変更する必要があるファイルの数が減り、依存関係の逆転にうまく適合します。


1
行がどこにあるかを把握しようとしているだけだと思います。最近のタスクでは、かなり簡単な操作を実行する必要がありましたが、既存の足場や機能がほとんどないコードベースにありました。そのため、私がする必要があるのは非常にシンプルでしたが、すべてが非常にユニークであり、共有クラスに収まらないようでした。私の場合、ドキュメントをネットワークドライブに保存し、2つの別々のデータベーステーブルに記録する必要がありました。各ステップを取り巻くルールはかなり特殊でした。ファイル名の生成(単純なGUID)でさえ、テストをより便利にするためにいくつかのクラスがありました。
JDデイビス

3
繰り返しになりますが、@ JDDavisは、純粋にテスト容易性の目的で単一のクラスよりも複数のクラスを選択することにより、カートを馬の前に置きます。詳細についてアドバイスすることはできませんが、個々の機能を変更するには多数のファイルを変更する必要があるという問題は、正当化しようとするべき問題ではなく、対処する必要がある(および回避しようとする)問題です。
ジョンボリンジャー

同意して、これを追加します。ウィキペディアを引用すると、「マーティンは責任を変更の理由として定義し、クラスまたはモジュールには変更する(つまり書き換える)理由は1つだけでなければならないと結論付けています。」「彼は最近、「この原則は人に関するものです」と述べました。」実際、これは、SRPの「責任」が機能ではなく利害関係者を指すことを意味すると考えています。クラスは、1人の関係者(プログラムの変更を必要とする人)だけが必要とする変更を担当する必要があります。そうすることで、変更を要求するさまざまな関係者に応じて可能な限り変更します。
コロディアス

12

5〜10個のファイルを使用していたタスクは、70〜100個かかるようになりました。

これは嘘です。タスクは5〜10個のファイルのみを取りました。

ファイルが10個未満のタスクは解決していません。どうして?C#を使用しているからです。C#は高水準言語です。Hello Worldを作成するためだけに10個以上のファイルを使用しています。

あなたはそれらを書いていないので、あなたはそれらに気づかないことを確認してください だからあなたはそれらを見ない。あなたはそれらを信頼します。

問題はファイルの数ではありません。あなたが今あまりにも多くのことをしているので、あなたは信頼していません。

そのため、これらのテストが機能すると、.NETのファイルを信頼する方法でこれらのファイルを信頼できるようになります。それがユニットテストのポイントです。ファイルの数は誰も気にしません。彼らは信頼できないものの数を気にします。

中小規模のアプリケーションの場合、SOLIDは非常に簡単に販売できます。誰もが保守性の利点と容易さを理解しています。ただし、非常に大規模なアプリケーションでは、SOLIDの価値提案が見られないだけです。

非常に大規模なアプリケーションでは、変更は困難です。ここで適用する最良の知恵は、ボブおじさんから来たものではありません。これは、Michael Feathersの著書 『Legacy Effective with Legacy Code』に記載されています。

書き換えフェストを開始しないでください。古いコードは苦労して得た知識を表しています。問題があり、新しい改良されたパラダイムで表現されていないため、それを投げ捨てるのは、新しい問題のセットを求めているだけで、知識を獲得することはできません。

代わりに、テスト不能な古いコードをテスト可能にする方法を見つけてください(Feathersのレガシーコードが話す)。この比phorのコードはシャツのようなものです。大きな部分は、継ぎ目を削除する方法でコードを分離するために元に戻すことができる自然な継ぎ目で結合されます。これを行うと、テストの「スリーブ」を添付して、残りのコードを分離できます。テストスリーブを作成するとき、作業シャツでこれを行ったため、スリーブに自信があります。(今、この比phorは傷つき始めています)。

このアイデアは、ほとんどのショップのように、最新の要件のみが作業コードにあるという仮定に基づいています。これにより、テストでそれをロックダウンすることができ、実証済みの稼働状態のすべてを失うことなく、実証済みの稼働中のコードを変更できます。この最初のテストウェーブが準備できたら、「レガシー」(テスト不能)コードをテスト可能にする変更を開始できます。これは常に行われていることであり、新しいテストは、コードが実際にあなたが思っていることをしていることを示すことによって、継ぎ目テストがあなたをバックアップしているので、あなたは大胆になることができます。

これは何と関係があります:

SOLIDに切り替えた後、大幅に増加したクラスの数を管理および整理しますか?

抽象化。

悪い抽象化を持つコードベースを嫌いにすることができます。悪い抽象化は、私を内側に見せるものです。中を覗いても驚かないでください。私が期待したものとほぼ同じである。

良い名前と、インターフェイスの使用方法を示す読みやすいテスト(例)を提供し、物を見つけることができるように整理して、10、100、または1000ファイルを使用してもかまいません。

わかりやすい名前の物を見つけてください。良い名前の物を良い名前の物に入れる。

このすべてを正しく行うと、タスクを終了するのに必要なファイルを3〜5個の他のファイルのみに依存するように抽象化できます。70-100のファイルはまだそこにあります。しかし、彼らは3〜5の背後に隠れています。これは、3〜5が正しいことをすることを信頼する場合にのみ機能します。

ですから、本当に必要なのは、人々が信頼するこれらすべてのものとテストのために良い名前を思いつくための語彙です。それがなければ、あなたも私を狂わせるでしょう。

@Delioth は、成長する痛みについて良い点を示しています。食器洗い機の上の食器棚にある食器に慣れている場合、朝食バーの上にある食器に慣れるのに時間がかかります。いくつかのことを難しくします。いくつかのことを簡単にします。しかし、人々が料理の行き先に同意しないと、あらゆる種類の悪夢を引き起こします。大規模なコードベースでは、問題は一度に移動できるのは一部の皿だけです。これで、2つの場所に料理ができました。ややこしい。料理が本来あるべき場所にあると信じることを難しくします。あなたがこれを乗り越えたいのなら、やるべきことは皿を動かし続けることだけです。

それに関する問題は、朝食バーで料理を食べることがこのすべてのナンセンスを経る前に価値があるかどうかを本当に知りたいです。そのために私がお勧めできるのはキャンプに行くことだけです。

新しいパラダイムを初めて試すとき、それを適用すべき最後の場所は大規模なコードベースにあります。これは、チームのすべてのメンバーに当てはまります。SOLIDが機能している、OOPが機能している、または関数型プログラミングが機能していると信じて誰も信じてはいけません。すべてのチームメンバーは、おもちゃプロジェクトで、新しいアイデアを使って遊ぶ機会を得る必要があります。少なくともそれがどのように機能するかを見ることができます。彼らはそれがうまくいかないことを見ることができます。それは彼らが大きな混乱を起こす直前にそれを行うことを学ぶことを可能にします。

人々に安全な遊び場を提供することで、新しいアイデアを取り入れ、料理が新しい家で本当に機能するという自信を持たせることができます。


3
質問の痛みの一部も痛みを増している可能性が高いことに言及する価値があるかもしれません-一方、そう、彼らはこの1つのために15個のファイルを作成する必要があるかもしれません...今、彼らはGUIDProviderまたはBasePathProviderを再び書く必要はありません、またはExtensionProviderなど。これは、新しいグリーンフィールドプロジェクトを開始するときに受けるのと同じ種類のハードルです。サポート機能の束は、ほとんど些細で、書くのは愚かですが、まだ書く必要があります。それらを構築するために吸い込みますが、それらがそこにあれば、それらについて考える必要はないはずです....今まで。
デリオス

@Deliothこれは事実だと信じられないほど信じています。以前は、機能の一部のサブセットが必要な場合(AppSettingsに格納されたURLだけが必要だとしましょう)、単純に1つの大規模なクラスが渡されて使用されていました。新しいアプローチでは、AppSettings単にURLやファイルパスを取得するために全体を回る必要はありません。
JDデイビス

1
書き換えフェストを開始しないでください。古いコードは苦労して得た知識を表しています。問題があり、新しい改良されたパラダイムで表現されていないため、それを投げ捨てるのは、新しい問題のセットを求めているだけで、知識を獲得することはできません。この。絶対に。
Flot2011

10

コードが十分に分離されていないか、タスクサイズが大きすぎるかのように聞こえます。

codemodまたは大規模なリファクタリングを行っていない限り、コードの変更 5〜10ファイルにする必要があります。単一の変更が多くの​​ファイルに影響を与える場合、おそらく変更がカスケードされていることを意味します。いくつかの改善された抽象化(より単一の責任、インターフェースの分離、依存関係の逆転)が役立つはずです。また、単一の責任になりすぎて、もう少し実用的なものを使用できる可能性もあります。つまり、短くて薄いタイプの階層です。これにより、コードが何をしているのかを知るために多数のファイルを理解する必要がないため、コードも理解しやすくなります。

また、あなたの仕事が大きすぎることの兆候かもしれません。「ちょっと、この機能を追加する」のではなく(UIの変更とAPIの変更とデータアクセスの変更とセキュリティの変更とテストの変更が必要)。これにより、ビット間に適切な契約を設定する必要があるため、レビューと理解が容易になります。

そしてもちろん、単体テストはこれらすべてを助けます。彼らはあなたにまともなインターフェースを作ることを強制します。テストに必要なビットを挿入するのに十分な柔軟性のあるコードを作成するように強制します(テストが困難な場合は、再利用が困難になります)。そして、エンジニアが多ければ多いほど、テストする必要があるため、過剰なエンジニアリングから人々を遠ざけます。


2
5〜10個のファイルから70〜100個のファイルは、仮想的なものよりも少し多くなっています。私の最後のタスクは、新しいマイクロサービスの1つで機能を作成することでした。新しいサービスはリクエストを受け取り、ドキュメントを保存することになっていた。その際、2つの個別のデータベースとそれぞれのリポジトリでユーザーエンティティを表すクラスが必要でした。書き込みが必要な他のテーブルを表すリポジトリ。ファイルデータのチェックと名前生成を処理する専用クラス。そしてリストは続きます。言うまでもなく、ロジックを含むすべてのクラスは、単体テスト用にモックアップできるようにインターフェースで表されていました。
JDデイビス

1
古いコードベースに関しては、それらはすべて密結合されており、非常にモノリシックです。SOLIDアプローチでは、クラス間の唯一のカップリングはPOCOの場合であり、他のすべてはDIおよびインターフェースを介して渡されます。
JDデイビス

3
@JDDavis-待って、なぜ1つのマイクロサービスが複数のデータベースで直接動作するのですか?
テラスティン

1
開発マネージャーとの妥協でした。彼は非常にモノリシックで手続き型のソフトウェアを好みます。そのため、マイクロサービスは本来あるべきものよりもはるかにマクロです。インフラストラクチャが改善されると、ゆっくりと物事が独自のマイクロサービスに移行します。今のところ、特定の機能をマイクロサービスに移行するために、ストラングラーアプローチに従っています。複数のサービスが特定のリソースにアクセスする必要があるため、それらを独自のマイクロサービスに移行しています。
JDデイビス

4

ここで既に言及したことのいくつかについて説明したいと思いますが、オブジェクトの境界が描画される場所の観点からより詳しく説明します。ドメイン駆動設計に似た何かをフォローしている場合、オブジェクトはおそらくビジネスの側面を表しているでしょう。 CustomerおよびOrder、たとえば、オブジェクトになります。さて、あなたが出発点として持っていたクラス名に基づいて推測することになった場合、AccountLogicクラスにはどのアカウントで実行されるコードがありました。ただし、オブジェクト指向では、各クラスはコンテキストとアイデンティティを持つように意図されています。Accountオブジェクトを取得してからAccountLogicクラスに渡して、そのクラスにAccountオブジェクトを変更させないでください。それは貧血モデルと呼ばれるものであり、オブジェクト指向をあまりよく表していません。代わりに、あなたのAccountクラスには、Account.Close()またはなどのAccount.UpdateEmail()動作が必要です。これらの動作は、アカウントのそのインスタンスのみに影響します。

現在、これらの振る舞いの処理方法は、抽象化(つまり、インターフェース)で表される依存関係にオフロードできます(多くの場合、そうすべきです)。 Account.UpdateEmailたとえば、データベースやファイルを更新したり、サービスバスにメッセージを送信したりする場合があります。これは将来変更される可能性があります。そのため、Accountクラスは、たとえばに依存している可能性がIEmailUpdateありAccountRepositoryます。これは、オブジェクトによって実装される多くのインターフェイスの1つです。オブジェクトにアクセスを与えたくないかもしれないが、両方を実装するかもしれないが、他の(任意の)アカウントの検索や検索など、多すぎることになるためIAccountRepository、インターフェイス全体をAccountオブジェクトに渡したくないでしょう。およびインターフェース、AccountAccountRepositoryIAccountRepositoryIEmailUpdateAccountオブジェクトは、必要な小さな部分にのみアクセスできます。これにより、インターフェイス分離の原則を維持できます。

現実的には、他の人が言ったように、クラスの爆発に対処している場合、SOLID原則(および拡張機能によってオブジェクト指向)を間違った方法で使用している可能性があります。SOLIDは、コードを複雑にするのではなく、コードを単純化するのに役立つはずです。しかし、SRPのようなものが何を意味するかを本当に理解するには時間がかかります。ただし、より重要なことは、SOLIDがどのように機能するかは、ドメインおよび制限されたコンテキスト(別のDDD用語)に大きく依存することです。特効薬や万能薬はありません。

私が一緒に仕事をしている人々に強調したいもう1つのこと:繰り返しますが、OOPオブジェクトは振る舞いを持つべきであり、実際、データではなく振る舞いによって定義されます。オブジェクトにプロパティとフィールドのみがある場合、おそらく意図した動作ではないかもしれませんが、オブジェクトにはまだ動作があります。他のセットロジックを持たないパブリックに書き込み可能/設定可能なプロパティは、その包含クラスの動作が、何らかの理由でいつでもどこでも必要なビジネスロジックや検証を行わずにそのプロパティの値を変更できることを意味します。それは通常、人々が意図する動作ではありませんが、貧弱なモデルを持っている場合、それは一般的にあなたのクラスがそれらを使用している人にアナウンスする動作です。


2

したがって、かなり簡単な保存を実行するために、合計で15のクラス(POCOとscaffoldingを除く)になります。

それはおかしいです…。しかし、これらのクラスは私が自分で書く何かのように聞こえます。それではそれらを見てみましょう。インターフェースとテストは今のところ無視しましょう。

  • BasePathProvider-私見では、ファイルを扱う重要なプロジェクトが必要です。だから、私は、すでにそのようなものがあり、あなたはそれをそのまま使用できると思います。
  • UniqueFilenameProvider -確かに、あなたはすでに持っていますよね?
  • NewGuidProvider -GUIDを使用したい場合を除き、同じケース。
  • FileExtensionCombiner -同じケース。
  • PatientFileWriter -これが現在のタスクのメインクラスだと思います。

私には良さそうです。4つのヘルパークラスを必要とする新しいクラスを1つ記述する必要があります。4つのヘルパークラスはすべて非常に再利用可能であるように聞こえるので、コードベースのどこかに既にあると思います。それ以外の場合は、運が悪い(実際にファイルを作成してGUIDを使用するのはチームの人ですか?)か、その他の問題です。


テストクラスについては、新しいクラスを作成または更新するときにテストする必要があります。したがって、5つのクラスを記述することは、5つのテストクラスを記述することも意味します。しかし、これにより設計が複雑になることはありません。

  • 自動的に実行されるため、他の場所でテストクラスを使用することはありません。それだけです。
  • テスト対象のクラスを更新したり、ドキュメントとして使用したりしない限り、再度確認する必要があります(理想的には、テストはクラスの使用方法を明確に示します)。

インターフェイスに関しては、DIフレームワークまたはテストフレームワークのいずれかがクラスを処理できない場合にのみ必要です。あなたはそれらを不完全なツールの犠牲者として見るかもしれません。または、より複雑なものがあることを忘れることができる便利な抽象化としてそれらを見るかもしれません-インターフェースのソースを読むことは、その実装のソースを読むよりもはるかに短い時間です。


私はこの観点に感謝しています。この特定のケースでは、かなり新しいマイクロサービスに機能を記述していました。残念ながら、メインのコードベースでも、上記のいくつかを使用していますが、実際にはリモートで再利用可能な方法はありません。再利用可能にする必要があるものはすべて、何らかの静的クラスになるか、コードの周りにコピーして貼り付けるだけです。私はまだ少し進んでいると思いますが、すべてを完全に分析して切り離す必要があるわけではないことに同意します。
JDデイビス

@JDDavis私は他の答えとは違うものを書き込もうとしていました(私はほとんど同意します)。何かをコピー&ペーストするときはいつでも、何かを一般化するのではなく、再利用不可能な別のコードを作成するので、再使用を防ぎます。私見は、盲目的にルールに従った直後の、2番目に大きな罪です。スイートスポットを見つける必要があります。ルールに従うと生産性が高まり(特に将来の変更に関して)、ときどきそれらを壊すことは、努力が不適切でない場合に役立ちます。それはすべて相対的です。
maaartinus

@JDDavisそして、すべてはツールの品質に依存します。例:DIは企業的で複雑であると主張する人々がいますが、私はそれがほとんど無料である主張してます。+++ルール違反について:4つのクラスがあり、コードをよりfactorくする(少なくとも私の目には)大規模なリファクタリング後にのみ注入できる場所が必要なので、シングルトン(より優れたプログラマー)にすることにしましたより良い方法を見つけるかもしれませんが、私はそれで満足しています;これらのシングルトンの数は古くから変わっていません)。
maaartinus

この答えは、OPが質問に例を追加したときに私が考えていたことをほとんど表しています。@JDDavis簡単な場合に機能ツールを使用することで、定型的なコード/クラスを保存できることを追加します。たとえば、GUIプロバイダー-新しいインターフェイスを導入する代わりに、このための新しいクラスを導入する代わりに、これに利用Func<Guid>()=>Guid.NewGuid()て、コンストラクターのように匿名メソッドを挿入しませんか?そして、この.Netフレームワーク機能をテストする必要はありません。これはMicrosoftがあなたのために行ったことです。これにより、合計で4つのクラスが節約されます。
Doc Brown

...そして、提示した他のケースを同じ方法で単純化できるかどうかを確認する必要があります(おそらくすべてではありません)。
Doc Brown

2

抽象化に応じて、単一責任クラスを作成し、単体テストを作成することは正確な​​科学ではありません。学習時に一方向に大きく振りすぎて極端になり、意味のある規範を見つけることは完全に正常です。振り子が揺れすぎているように聞こえます。

私はこれがレールから外れていると思うところです:

ユニットテストは時間の無駄だと信じており、個々のコードよりも全体としてコードをはるかに迅速に処理テストできると信じているため、チームにとっては信じられないほど難しい販売でした。SOLIDの承認として単体テストを使用することはほとんど無益であり、この時点ではほとんど冗談になりました。

ほとんどのSOLID原則から得られる利点の1つ(確かに唯一の利点ではありません)は、コードの単体テストの作成が容易になることです。クラスが抽象化に依存している場合、抽象化をモックできます。分離された抽象化のモックは簡単です。クラスが1つのことを行うと、複雑さが低下する可能性が高くなります。つまり、考えられるすべてのパスを簡単に知り、テストすることができます。

チームが単体テストを作成していない場合、2つの関連することが起こっています。

第一に、彼らはこれらのインターフェースとクラスのすべてを作成するために、十分なメリットを実感せずに多くの特別な仕事をしています。ユニットテストを書くことで私たちの生活がどのように楽になるかを確認するには、少し時間がかかり、練習が必要です。ユニットテストを書くことを学ぶ人々がそれに固執する理由がありますが、あなた自身のためにそれらを発見するのに十分長く持続しなければなりません。あなたのチームがそれを試みていない場合、彼らは彼らがしている余分な仕事の残りは役に立たないように感じるでしょう。

たとえば、リファクタリングする必要がある場合はどうなりますか?100個の小さなクラスがあり、変更が機能するかどうかをテストするテストがない場合、これらの追加のクラスとインターフェイスは、改善ではなく、負担のように見えます。

第二に、単体テストを書くことは、コードが実際にどれだけの抽象化を必要としているかを理解するのに役立ちます。私が言ったように、それは科学ではありません。私たちはひどく始めて、あちこちで向きを変え、良くなります。単体テストには、SOLIDを補完する独特の方法があります。抽象化を追加したり、何かを分解したりする必要がある場合は、どのようにしてわかりますか?言い換えれば、あなたが「十分に堅い」とき、あなたはどのように知っていますか?多くの場合、答えは何かをテストできない場合です。

たぶん、あなたのコードは、多くの小さな抽象化とクラスを作成することなくテスト可能だろう。しかし、テストを書いていない場合、どうすればわかりますか?どこまで行きますか?私たちは物事をますます小さくすることに夢中になることができます。うさぎの穴です。コードのテストを作成する機能は、目的を達成したかどうかを確認するのに役立ちます。これにより、私たちは取りつかれを止め、先に進み、より多くのコードを書くことができます。

単体テストは、すべてを解決する特効薬ではありませんが、開発者の生活を向上させる本当に素晴らしい弾丸です。私たちは完璧ではなく、テストでもありません。しかし、テストは自信を与えてくれます。私たちはコードが正しいことを期待しており、間違っている場合は驚いていますが、逆ではありません。私たちは完璧ではなく、テストでもありません。しかし、コードをテストするとき、自信があります。コードが展開されたときに爪を噛む可能性は低く、今回は何が壊れるのか、それは私たちのせいだろうかと考えます。

それに加えて、ひとたび慣れてしまえば、単体テストを書くことでコードの開発が遅くなるのではなく、速くなります。古いコードの再検討やデバッグの時間を短縮して、干し草の山の針のような問題を見つけます。

バグが減り、より多くのことができ、不安を自信に置き換えます。それは流行やヘビ油ではありません。本物だ。多くの開発者がこれを証明します。あなたのチームがこれを経験していない場合、彼らはその学習曲線を押し通して、こぶを乗り越える必要があります。すぐに結果が得られないことを理解して、チャンスを与えてください。しかし、それが起こったとき、彼らは彼らがしたことをうれしく思い、決して振り返ることはありません。(または、彼らは孤立した勢力になり、単体テストやその他のほとんどの蓄積されたプログラミング知識が時間の無駄である方法について怒ったブログ記事を書きます。)

切り替えを行って以来、開発者からの最大の不満の1つは、以前はすべてのタスクで開発者が5〜10個のファイルを操作するだけで十分だった数十個のファイルをピアレビューおよびトラバースできないことです。

すべての単体テストに合格すると、ピアレビューは非常に簡単になり、そのレビューの大部分はテストに意味があることを確認するだけです。

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