私は3つの基本的な手順を含む多くのコードを記述しています。
私は通常、それぞれの設計パターンに触発された3種類のクラスを使用することになります。
私のクラスは非常に小さい傾向があり、多くの場合、単一の(パブリック)メソッドです。データの取得、データの変換、作業の実行、データの保存。これはクラスの増加につながりますが、一般的にはうまく機能します。
私がテストに来るときに苦労しているのは、結局は密結合テストになります。例えば;
もう1つがないとテストできません。ディスクの読み取り/書き込みを行うための追加の「テスト」コードを作成することもできますが、それから繰り返します。
.Netを見ると、 File クラスは異なるアプローチをとり、(私の)ファクトリーとコマンダーの責任を組み合わせます。 Create、Delete、Exists、Readの機能がすべて1か所にあります。
.Netの例をたどって、特に外部リソースを扱う場合は、クラスを一緒に結合する必要がありますか?結合されたコードですが、意図的です。テストではなく、元の実装で発生します。
私の問題は、単一責任原則をやや熱心に適用したことですか?読み取りと書き込みを担当する別のクラスがあります。特定のリソースの処理を担当する結合クラスを使用できる場合。システムディスク。
単一の責任の原則に従うことは、ここであなたを導いたものかもしれませんが、あなたがどこにいるかは別の名前です。
勉強してみてください。おなじみのパターンに従ってそれを見つけることができると思います。これをどこまで実行するのか疑問に思うのはあなただけではありません。酸のテストは、これを実行することで本当のメリットが得られるか、それとも単なる盲目的なマントラであるかを考える必要がないためです。
テストについて懸念を表明しました。 CQRSをフォローしてもテスト可能なコードを作成できなくなるとは思いません。コードをテストできなくする方法でCQRSをフォローしているだけかもしれません。
制御のフローを変更せずに、ポリモーフィズムを使用してソースコードの依存関係を反転させる方法を知るのに役立ちます。あなたのスキルセットがテストを書く上でどこにあるのか本当にわかりません。
ライブラリで見つけた習慣に従うことは、最適ではありません。ライブラリには独自のニーズがあり、率直に言って古いです。したがって、最良の例でさえ、当時の最良の例にすぎません。
これは、CQRSに従わない完全に有効な例がないと言っているのではありません。それに従うことは常に少し苦痛になります。常に支払う価値があるとは限りません。しかし、あなたがそれを必要とするなら、あなたがそれを使ってうれしいでしょう。
使用する場合は、この警告の言葉に注意してください。
特に、CQRSはシステムの特定の部分(DDD用語ではBoundedContext)でのみ使用し、システム全体では使用しないでください。この考え方では、各境界付きコンテキストは、それがどのようにモデル化されるべきかについて独自の決定を必要とします。
コードが単一責任の原則に準拠しているかどうかを判断するには、より広い視点が必要です。コード自体を分析するだけでは答えられないので、将来的に要件を変更する原因となる可能性のある力またはアクターを考慮する必要があります。
アプリケーションデータをXMLファイルに保存するとします。読み取りまたは書き込みに関連するコードを変更する要因は何ですか?いくつかの可能性:
これらすべてのケースで、両方読み取りと書き込みのロジックを変更する必要があります。言い換えれば、それらはnot別の責任です。
しかし、別のシナリオを想像してみてください。アプリケーションはデータ処理パイプラインの一部です。別のシステムで生成されたCSVファイルを読み取り、分析と処理を実行してから、別のファイルを出力して、3番目のシステムで処理します。この場合、読み取りと書き込みは独立した責任であり、分離する必要があります。
結論:一般に、ファイルの読み取りと書き込みが別々の責任であるかどうかは、アプリケーションでの役割によって異なります。しかし、テストについてのあなたのヒントに基づいて、私はそれがあなたの場合の単一の責任であると思います。
一般的にあなたは正しい考えを持っています。
どこかからデータを取得します。そのデータを変換します。そのデータをどこかに置きます。
あなたには3つの責任があるようです。 「メディエーター」のIMOは、多くのことを行っている可能性があります。私はあなたがあなたの3つの責任をモデル化することから始めるべきだと思います:
interface Reader[T] {
def read(): T
}
interface Transformer[T, U] {
def transform(t: T): U
}
interface Writer[T] {
def write(t: T): void
}
次に、プログラムは次のように表すことができます。
def program[T, U](reader: Reader[T],
transformer: Transformer[T, U],
writer: Writer[U]): void =
writer.write(transformer.transform(reader.read()))
これはクラスの急増につながります
これは問題ではないと思います。 IMOの多くの小さなまとまりのあるテスト可能なクラスは、大きなまとまりの少ないクラスよりも優れています。
私がテストに来るときに苦労しているのは、結局は密結合テストになります。もう1つがないとテストできません。
各部分は独立してテスト可能でなければなりません。上記のようにモデル化すると、ファイルの読み取り/書き込みを次のように表すことができます。
class FileReader(fileName: String) implements Reader[String] {
override read(): String = // read file into string
}
class FileWriter(fileName: String) implements Writer[String] {
override write(str: String) = // write str to file
}
統合テストを作成して、これらのクラスをテストし、ファイルシステムに対する読み取りと書き込みを確認できます。残りのロジックは変換として記述できます。たとえば、ファイルがJSON形式の場合、String
sを変換できます。
class JsonParser implements Transformer[String, Json] {
override transform(str: String): Json = // parse as json
}
次に、適切なオブジェクトに変換できます。
class FooParser implements Transformer[Json, Foo] {
override transform(json: Json): Foo = // ...
}
これらはそれぞれ独立してテスト可能です。 program
、reader
、およびtransformer
をモックすることにより、上記のwriter
を単体テストすることもできます。
結局、密結合テストになります。例えば;
- 工場-ディスクからファイルを読み取ります。
- Commander-ファイルをディスクに書き込みます。
したがって、ここでの焦点はそれらを結合するものです。 2つの間にオブジェクト(File
など)を渡しますか?それから、それらは互いにではなく、それらが結合されているファイルです。
あなたが言ったことから、クラスを分けました。罠はあなたがそれらを一緒にテストしているということですそれがより簡単であるか、または「理にかなっている」から。
ディスクから取得するためにCommander
への入力が必要なのはなぜですか?重要なのは、特定の入力を使用して書き込むことです。次に、テスト内を使用して、ファイルが正しく書き込まれたことを確認できます。
Factory
をテストしている実際の部分は、「このファイルを正しく読み取って正しいものを出力しますか?」したがって、ファイルを読み取る前にモックしますテスト中。
あるいは、一緒に結合されたときにFactoryとCommanderが機能することをテストすることは問題ありません-それは、統合テストに非常に満足しています。ここでの質問は、それらを個別に単体テストできるかどうかの問題です。
どこかからデータを取得します。そのデータを変換します。そのデータをどこかに置きます。
これは典型的な手続き型のアプローチで、 David Parnas が1972年に書いたものです。あなたはhowに集中しています。あなたは問題の具体的な解決策をより高いレベルのパターンとして捉えますが、これは常に間違っています。
オブジェクト指向のアプローチを追求する場合、私はむしろあなたの domain に集中したいと思います。それは何についてですか?システムの主な責任は何ですか?ドメインエキスパートの言語で提示される主な概念は何ですか?したがって、ドメインを理解し、それを分解し、上位レベルの責任範囲を modules として扱い、名詞として表される下位レベルの概念をオブジェクトとして扱います。こちらが 例 最近の質問に提供したもので、非常に関連性があります。
そして、まとまりに明らかな問題があり、あなたはそれを自分で述べました。入力ロジックである変更を行い、それに対してテストを作成する場合、そのデータを次のレイヤーに渡すことを忘れる可能性があるため、機能が機能していることを証明することはできません。これらの層は本質的に結合されています。そして、人工的な分離は事態をさらに悪化させます。私自身も知っています:7年完全にこのスタイルで書かれた私の肩の後ろに100人年のプロジェクト。できれば逃げましょう。
SRP全体についてです。問題の領域、つまりドメインに適用される cohesion がすべてです。これがSRPの基本原理です。これにより、オブジェクトはスマートになり、自分たちの責任を実装します。誰もそれらを制御せず、誰もデータを提供しません。データと動作を組み合わせて、後者のみを公開します。したがって、オブジェクトは、生データの検証、データ変換(つまり、動作)、永続性の両方を組み合わせます。次のようになります。
class FinanceTransaction
{
private $id;
private $storage;
public function __construct(UUID $id, DataStorage $storage)
{
$this->id = $id;
$this->storage = $storage;
}
public function perform(
Order $order,
Customer $customer,
Merchant $merchant
)
{
if ($order->isExpired()) {
throw new Exception('Order expired');
}
if ($customer->canNotPurchase($order)) {
throw new Exception('It is not legal to purchase this kind of stuff by this customer');
}
$this->storage->save($this->id, $order, $customer, $merchant);
}
}
(new FinanceTransaction())
->perform(
new Order(
new Product(
$_POST['product_id']
),
new Card(
new CardNumber(
$_POST['card_number'],
$_POST['cvv'],
$_POST['expires_at']
)
)
),
new Customer(
new Name(
$_POST['customer_name']
),
new Age(
$_POST['age']
)
),
new Merchant(
new MerchantId($_POST['merchant_id'])
)
)
;
その結果、いくつかの機能を表すまとまったクラスがかなりあります。検証は通常、少なくとも [〜#〜] ddd [〜#〜] アプローチでは値オブジェクトに行われることに注意してください。
私がテストに来るときに苦労しているのは、結局は密結合テストになります。例えば;
- 工場-ディスクからファイルを読み取ります。
- Commander-ファイルをディスクに書き込みます。
ファイルシステムを操作するときは、抽象化の漏れに注意してください。見過ごされがちですが、説明したような症状があります。
クラスがこれらのファイルに出入りするデータを操作する場合、ファイルシステムは実装の詳細(I/O)となり、ファイルシステムから分離する必要があります。これらのクラス(ファクトリー/コマンダー/メディエーター)は、提供されたデータの格納/読み取りのみがジョブである場合を除き、ファイルシステムを認識しないでください。ファイルシステムを扱うクラスは、パスなどのコンテキスト固有のパラメーターをカプセル化する必要があります(コンストラクターを介して渡される可能性があります)。そのため、インターフェイスはその性質を明らかにしませんでした(ほとんどの場合、インターフェイス名の「ファイル」という単語は匂いです)。
私の意見では、あなたは正しい道を進み始めたように思えますが、あなたは十分にそれをしていません。機能を1つのことを行う別のクラスに分割し、それを適切に行うことは正しいと思います。
それをさらに一歩進めるために、Factory、Mediator、およびCommanderクラスのインターフェースを作成する必要があります。その後、他のクラスの具体的な実装の単体テストを作成するときに、これらのクラスのモックアウトバージョンを使用できます。モックを使用すると、メソッドが正しい順序で正しいパラメーターで呼び出され、テスト中のコードがさまざまな戻り値で正しく動作することを検証できます。
また、データの読み取り/書き込みを抽象化することもできます。ここではファイルシステムに移動しますが、将来的にはデータベースまたはソケットに移動することもできます。データのソース/宛先が変更されても、メディエータークラスを変更する必要はありません。