クラスを細かくしすぎていませんか?単一責任原則はどのように適用されるべきですか?


9

私は3つの基本的な手順を含む多くのコードを記述しています。

  1. どこかからデータを取得します。
  2. そのデータを変換します。
  3. そのデータをどこかに置きます。

私は通常、それぞれの設計パターンに触発された3種類のクラスを使用します。

  1. ファクトリー-あるリソースからオブジェクトを構築します。
  2. メディエーター-ファクトリーを使用するには、変換を実行してから、司令官を使用します。
  3. 司令官-そのデータを別の場所に配置します。

私のクラスは非常に小さく、多くの場合単一の(パブリック)メソッドです。たとえば、データの取得、データの変換、作業の実行、データの保存などです。これはクラスの急増につながりますが、一般的にはうまく機能します。

私がテストに来るときに苦労しているのは、結局は密結合テストになります。例えば;

  • 工場-ディスクからファイルを読み取ります。
  • Commander-ファイルをディスクに書き込みます。

もう1つがないとテストできません。ディスクの読み取り/書き込みを行うための追加の「テスト」コードを作成することもできますが、それから繰り返します。

.Netを見ると、Fileクラスは別のアプローチをとっており、(私の)ファクトリーとコマンダーの責任を組み合わせています。Create、Delete、Exists、Readの機能がすべて1か所にあります。

.Netの例をたどって、特に外部リソースを扱う場合は、クラスを一緒に結合する必要がありますか?結合されたコードですが、意図的です。テストではなく、元の実装で発生します。

ここでの問題は、単一責任の原則をやや熱心に適用したことですか?読み取りと書き込みを担当する個別のクラスがあります。特定のリソース(システムディスクなど)の処理を担当する結合クラスがある場合。



6
Looking at .Net, the File class takes a different approach, it combines the responsibilities (of my) factory and commander together. It has functions for Create, Delete, Exists, and Read all in one place.-「責任」と「やるべきこと」を混同していることに注意してください。責任は「関心領域」のようなものです。Fileクラスの責任は、ファイル操作を実行することです。
Robert Harvey

1
調子がいいですね。必要なのは、テストメディエーター(または、すべての種類の変換に1つずつ)です。テストメディエーターは、.netのFileクラスを使用して、ファイルが正しいことを確認するためにファイルを読み取ることができます。SOLIDの観点からは問題ありません。
Martin Maat

1
@Robert Harveyが述べたように、SRPは責任に関するものではないため、くだらない名前になっています。それは、「変化する可能性のある、1つのトリッキー/難しい領域をカプセル化して抽象化する」ことについてです。STDACMCが長すぎると思います。:-)とはいえ、3つの部分に分割するのは妥当だと思います。
user949300

1
FileC#からのライブラリの重要な点は、Fileクラスがファサードであり、すべてのファイル操作を1か所にまとめることができるということですが、内部では、クラスと同様の読み取り/書き込みクラスを使用して、実際には、ファイル処理のためのより複雑なロジックが含まれています。Fileファイルシステムを実際に使用するプロセスは別のレイヤーの背後で抽象化されるため、このようなクラス()は依然としてSRPに準拠しています。それが事実であるとは言いませんが、そうである可能性があります。:)
アンディ

回答:


5

単一の責任の原則に従うことがここであなたを導いたものであるかもしれませんが、あなたがどこにいるかは別の名前です。

コマンドクエリの責任分担

勉強してみてください。おなじみのパターンに従ってそれを見つけることができると思います。これをどこまで実行するのか疑問に思うのはあなただけではありません。酸のテストは、これに従うことが本当のメリットをもたらすか、それともあなたが従う盲目的なマントラであるかであるので、考える必要はありません。

テストについて懸念を表明しました。CQRSをフォローしてもテスト可能なコードを書くことができないとは思いません。コードをテストできなくするような方法でCQRSをフォローしているだけかもしれません。

制御のフローを変更せずに、ポリモーフィズムを使用してソースコードの依存関係を反転させる方法を知るのに役立ちます。あなたのスキルセットがテストを書く上でどこにあるのか本当にわかりません。

ライブラリで見つけた習慣に従うことは、最適ではありません。ライブラリには独自のニーズがあり、率直に言って古いです。したがって、最良の例でさえ、当時の最良の例にすぎません。

これは、CQRSに従わない完全に有効な例がないと言っているのではありません。それに従うことは常に少し苦痛になります。常に支払う価値があるとは限りません。しかし、あなたがそれを必要とするなら、あなたはそれを使ってうれしいでしょう。

使用する場合は、次の警告に注意してください。

特にCQRSは、システム全体ではなく、システムの特定の部分(DDD用語ではBoundedContext)でのみ使用する必要があります。この考え方では、各境界コンテキストは、モデル化する方法について独自の決定を必要とします。

マーティンフローラー:CQRS


以前は見られなかった興味深いCQRS。コードはテスト可能です。これは、より良い方法を見つけることを目的としています。私はモックを使用し、可能な場合は依存性注入を使用します(これはあなたが参照しているものだと思います)。
ジェームズウッド

これについて初めて読んだとき、私は自分のアプリケーションを通じて似たようなものを識別しました:柔軟な検索の処理、フィルター可能/並べ替え可能な複数のフィールド(Java / JPA)は頭痛の種であり、基本的な検索エンジンを作成しない限り、大量の定型コードにつながりますこれを処理します(私はrsql-jpaを使用します)。私は同じモデル(たとえば、両方のJPAエンティティ)を持っていますが、検索は専用の汎用サービスで抽出され、モデルレイヤーはそれを処理する必要がなくなりました。
Walfrat 2017年

3

コードが単一責任の原則に準拠しているかどうかを判断するには、より広い視点が必要です。コード自体を分析するだけでは答えることはできません。将来、要件を変更する原因となる可能性のある力またはアクターを考慮する必要があります。

アプリケーションデータをXMLファイルに保存するとします。読み取りまたは書き込みに関連するコードを変更する要因は何ですか?いくつかの可能性:

  • アプリケーションに新しい機能が追加されると、アプリケーションデータモデルが変更される可能性があります。
  • 新しい種類のデータ(画像など)をモデルに追加できます
  • ストレージフォーマットは、アプリケーションロジックとは関係なく変更される可能性があります。たとえば、相互運用性やパフォーマンスの問題により、XMLからJSONまたはバイナリフォーマットに変更します。

これらすべてのケースで、読み取りと書き込みの両方のロジックを変更する必要があります。言い換えれば、それらは別個の責任ではありません

しかし、別のシナリオを想像してみてください。アプリケーションはデータ処理パイプラインの一部です。別のシステムで生成されたCSVファイルを読み取り、分析と処理を実行してから、別のファイルを出力して、3番目のシステムで処理します。この場合、読み取りと書き込みは独立した責任であり、分離する必要があります。

結論:一般に、ファイルの読み取りと書き込みが別々の責任であるかどうかは、アプリケーションでの役割に依存するとは言えません。しかし、テストについてのあなたのヒントに基づいて、私はそれがあなたの場合の単一の責任であると思います。


2

一般的にあなたは正しい考えを持っています。

どこかからデータを取得します。そのデータを変換します。そのデータをどこかに置きます。

あなたには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形式の場合、Strings を変換できます。

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あざけることによって、上記readertransformerwriter


それは私が今いるところです。私は各機能を個別にテストできますが、それらをテストすることにより、それらは結合されます。たとえば、FileWriterをテストするには、書き込まれた内容を別の何かで読み取る必要があります。明らかな解決策はFileReaderを使用することです。Fwiw、メディエーターは、ビジネスロジックの適用など、他のことをすることがよくあります。あるいは、おそらく基本的なアプリケーションのMain関数によって表されます。
James Wood

1
@JamesWoodは、統合テストでよくあるケースです。ただし、テストでクラスを結合する必要はありません。FileWriterを使用する代わりに、ファイルシステムから直接読み取ることでテストできますFileReader。目標が何であるかは実際にあなた次第です。を使用するFileReaderと、FileReaderまたはのいずれかでテストがFileWriter失敗します。デバッグに時間がかかる場合があります。
サミュエル

また、stackoverflow.com / questions / 1087351 /も参照してください。これは、テストをより良いものにするのに役立つ可能性があります
Samuel

それは私が今いるところのほとんどです -それは100%真実ではありません。Mediatorパターンを使用しているとおっしゃいました。これはここでは役に立たないと思います。このパターンは、非常に混乱したフローの中で互いに相互作用する多くの異なるオブジェクトがある場合に使用されます。すべての関係を促進し、それらを1つの場所に実装するために、メディエーターをそこに配置します。これはあなたのケースではないようです。あなたは非常によく定義された小さな単位を持っています。また、@ Samuelによる上記のコメントのように、1つのユニットをテストし、他のユニットを呼び出さずにアサーションを実行する必要があります
Emerson Cardoso

@EmersonCardoso; 質問のシナリオを少し簡略化しました。私のメディエーターのいくつかは非常に単純ですが、他のメディエーターはより複雑で、しばしば複数のファクトリー/コマンダーを使用します。単一のシナリオの詳細を避けようとしているのですが、複数のシナリオに適用できるより高いレベルの設計アーキテクチャにもっと興味があります。
James Wood

2

結局、密結合テストになります。例えば;

  • 工場-ディスクからファイルを読み取ります。
  • Commander-ファイルをディスクに書き込みます。

したがって、ここでの焦点は、それらを結合するものにあります。2つの間にオブジェクト(File?など)を渡しますか?それは、お互いではなく、それらが結合されているファイルです。

あなたが言ったことから、あなたはクラスを分けました。落とし穴は、より簡単または「理にかなっている」ため、それらを一緒にテストしていることです。

なぜCommanderディスクからの入力が必要なのですか?重要なのは、特定の入力を使用して書き込むことだけです。その後、テストの内容を使用して、ファイルが正しく書き込まれたことを確認できます。

あなたがテストしている実際の部分Factoryは「このファイルを正しく読み取って正しいものを出力するでしょうか」ですか?したがって、テストで読み取る前にファイルをモックします。

あるいは、一緒に結合されたときにFactoryとCommanderが機能することをテストするのは問題ありません-統合テストに非常に満足しています。ここでの質問は、それらを個別に単体テストできるかどうかの問題です。


その特定の例では、それらを結びつけるものがリソースです(システムディスクなど)。それ以外の場合、2つのクラス間の相互作用はありません。
James Wood

1

どこかからデータを取得します。そのデータを変換します。そのデータをどこかに置きます。

これは典型的な手続き型のアプローチで、David Parnasが1972年に書いたものです。あなたは物事の進行に集中します。問題の具体的な解決策は、常に間違っている上位レベルのパターンとして捉えます。

オブジェクト指向のアプローチを追求するなら、私はむしろあなたのドメインに集中したいと思います。それはすべて何ですか?システムの主な責任は何ですか?ドメインエキスパートの言語で提示される主な概念は何ですか?したがって、ドメインを理解し、それを分解し、上位レベルの責任領域をモジュールとして扱い、名詞として表される下位レベルの概念をオブジェクトとして扱います。ここに私が最近の質問に提供したがあります、それは非常に関連があります。

そして、凝集性に明らかな問題があり、あなたはそれを自分で述べました。入力ロジックを変更してテストを作成する場合、そのデータを次のレイヤーに渡すのを忘れる可能性があるため、機能が機能していることを証明することはできません。これらの層は本質的に結合されています。そして、人工的な分離は事態をさらに悪化させます。私自身も知っています。7年間のプロジェクトで、100人年を肩から背負ってこのスタイルで完全に書かれています。できれば逃げましょう。

SRP全体についてです。問題の領域、つまりドメインに適用される結束力がすべてです。これが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アプローチでは値オブジェクトに行われることに注意してください。


1

私がテストに来るときに苦労しているのは、結局は密結合テストになります。例えば;

  • 工場-ディスクからファイルを読み取ります。
  • Commander-ファイルをディスクに書き込みます。

ファイルシステムで作業するときは、漏れやすい抽象化に気をつけてください。私はそれがあまりにも頻繁に無視されているのを見ました。

クラスがこれらのファイルに出入りするデータを操作する場合、ファイルシステムは実装の詳細(I / O)となり、ファイルシステムから分離する必要があります。これらのクラス(ファクトリー/コマンダー/メディエーター)は、提供されたデータの格納/読み取りのみがジョブである場合を除き、ファイルシステムを認識しません。ファイルシステムを扱うクラスは、パスなどのコンテキスト固有のパラメーターをカプセル化する必要があります(コンストラクターを介して渡される場合があります)。そのため、インターフェイスはその性質を明らかにしませんでした(ほとんどの場合、インターフェイス名の「ファイル」という単語は匂いです)。


「これらのクラス(ファクトリー/コマンダー/メディエーター)は、提供されたデータを格納/読み取ることだけがジョブである場合を除いて、ファイルシステムを認識しないでください。」この特定の例では、それだけです。
James Wood

0

私の意見では、あなたは正しい道を歩み始めたように聞こえますが、十分にそれをしていません。機能を1つのことを行う別のクラスに分割し、それを適切に行うことは正しいと思います。

それをさらに一歩進めるために、Factory、Mediator、およびCommanderクラスのインターフェースを作成する必要があります。その後、他の具体的な実装の単体テストを作成するときに、これらのクラスのモックアウトバージョンを使用できます。モックを使用すると、メソッドが正しい順序で正しいパラメーターで呼び出され、テスト中のコードがさまざまな戻り値で正しく動作することを検証できます。

また、データの読み取り/書き込みを抽象化することもできます。ここでファイルシステムに移動しますが、将来的にはデータベースまたはソケットに移動することもできます。データのソース/宛先が変更されても、メディエータークラスを変更する必要はありません。


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