web-dev-qa-db-ja.com

カプセル化の過剰使用に悩まされますか?

さまざまなプロジェクトのコードに、コードの匂いがするようで、何か悪いことをしているように見えましたが、対処できません。

「クリーンなコード」を書こうとしている間、コードを読みやすくするためにプライベートメソッドを使いすぎる傾向があります。問題は、コードは確かにきれいですが、テストするのがより難しいことです(ええ、私はプライベートメソッドをテストできることを知っています...)。一般的に、それは私にとって悪い習慣のようです。

これは、.csvファイルから一部のデータを読み取り、顧客のグループ(さまざまなフィールドと属性を持つ別のオブジェクト)を返すクラスの例です。

public class GroupOfCustomersImporter {
    //... Call fields ....
    public GroupOfCustomersImporter(String filePath) {
        this.filePath = filePath;
        customers = new HashSet<Customer>();
        createCSVReader();
        read();
        constructTTRP_Instance();
    }

    private void createCSVReader() {
        //....
    }

    private void read() {
        //.... Reades the file and initializes the class attributes
    }

    private void readFirstLine(String[] inputLine) {
        //.... Method used by the read() method
    }

    private void readSecondLine(String[] inputLine) {
        //.... Method used by the read() method
    }

    private void readCustomerLine(String[] inputLine) { 
        //.... Method used by the read() method
    }

    private void constructGroupOfCustomers() {
        //this.groupOfCustomers = new GroupOfCustomers(**attributes of the class**);
    }

    public GroupOfCustomers getConstructedGroupOfCustomers() {
        return this.GroupOfCustomers;
    }
}

あなたがクラスに仕事を成し遂げるためにいくつかのプライベートメソッドを呼び出すコンストラクターしかないのを見ることができるので、私はそれが お勧めではない であることを知っていますが、代わりにクラスのすべての機能をカプセル化することを好みますメソッドを公開する場合、クライアントはこのように動作するはずです。

GroupOfCustomersImporter importer = new GroupOfCustomersImporter(filepath)
importer.createCSVReader();
read();
GroupOfCustomer group = constructGoupOfCustomerInstance();

実装の詳細でクライアントクラスを悩ませているクライアントのサイドコードに無用なコード行を入れたくないので、私はこれを好みます。

それで、これは実際に悪い習慣ですか?はいの場合、どうすれば回避できますか?上記は単なる例です。同じ状況がもう少し複雑な状況で起こっていると想像してください。

11
Florents Tselai

実装の詳細をクライアントから隠したいと思っている限り、実際には正しい方向に進んでいると思います。クライアントに表示されるインターフェースが、考えられる最もシンプルで最も簡潔なAPIになるようにクラスを設計したいとします。クライアントは実装の詳細に「悩まされる」ことはありませんが、そのコードの呼び出し元を変更する必要があることを心配することなく、基になるコードを自由にリファクタリングすることもできます。異なるモジュール間の結合を減らすことにはいくつかの真の利点があり、そのために確実に努力する必要があります。

したがって、ここで提供したアドバイスに従うと、コードですでに気づいたもの、つまりパブリックインターフェイスの背後に隠れたままで簡単にアクセスできない一連のロジックができてしまいます。

理想的には、パブリックインターフェースに対してのみクラスを単体テストできる必要があり、外部依存関係がある場合は、テスト対象のコードを分離するためにfake/mock/stubオブジェクトを導入する必要がある場合があります。

ただし、これを行っても、クラスのすべての部分を簡単にテストできないと感じた場合は、1つのクラスが多すぎる可能性があります。 SRPの原則 の精神では、 Michael Feathers が「スプラウトクラスパターン」と呼んでいるものに従い、元のクラスのチャンクを新しいクラスに抽出できます。

あなたの例では、テキストファイルの読み取りも担当するインポータークラスがあります。オプションの1つは、ファイル読み取りコードのチャンクを別のInputFileParserクラスに抽出することです。これらのプライベート関数はすべてパブリックになり、テストが容易になりました。同時に、パーサークラスは外部クライアントから見えるものではありません(「内部」とマークし、ヘッダーファイルを公開しないか、単にAPIの一部としてそれをアドバタイズしないでください)。そのインターフェースが短くて甘いままである輸入業者。

17
DXM

多くのメソッド呼び出しをクラスコンストラクターに配置する代わりに-私は同意するが、これは一般的に回避する習慣である-代わりに、クライアントに煩わされたくない余分な初期化のすべてを処理するファクトリーメソッドを作成することができます。と一緒に。これは、constructGroupOfCustomers()メソッドのように見えるメソッドをパブリックとして公開し、クラスの静的メソッドとして使用することを意味します。

GroupOfCustomersImporter importer = 
    new GroupOfCustomersImporter.CreateInstance();

または別のファクトリクラスのメソッドとして:

ImporterFactory factory = new ImporterFactory();
GroupOfCustomersImporter importer = factory.CreateGroupOfCustomersImporter();

これらは、頭の真上から考える最初の2つのオプションです。

あなたが本能があなたに何かを伝えようとしているということも考慮する価値があります。コードがにおいがし始めていることを腸が告げると、おそらくにおいがします。悪臭がする前に、何かについてそれを行うことをお勧めします。表面的には、コード自体に本質的に問題はないかもしれませんが、簡単な再チェックとリファクタリングは、問題についての考えを解決するのに役立ち、他の問題に自信を持って進むことができます。カードの潜在的な家。この特定のケースでは、コンストラクターの責任の一部をファクトリーに(またはファクトリーによって呼び出されるように)委任することで、クラスのインスタンス化と将来の子孫の可能性をより詳細に制御できるようになります。

1
S.Robins

コメントには長すぎたので、自分の答えを追加しました。

カプセル化とテストがかなり逆であることは完全に正しいです。ほとんどすべてを非表示にすると、テストが困難になります。

ただし、ファイル名ではなくストリームを指定することで、サンプルをよりテストしやすくすることができます。この方法では、メモリから既知のcsvを指定するか、必要に応じてファイルを指定します。また、httpサポートを追加する必要がある場合、クラスをより堅牢にします;)

また、lazy-initの使用を検討してください。

public class Csv
{
  private CustomerList list;

  public CustomerList get()
  {
    if(list == null)
        load();
     return list;
  }
}
1
Max

コンストラクターは、いくつかの変数またはオブジェクトの状態を初期化する以外は、あまり多くのことをすべきではありません。コンストラクターで実行しすぎると、ランタイム例外が発生する可能性があります。私はクライアントが次のようなことをするのを避けようとします:

MyClass a;
try {
   a = new MyClass();
} catch (MyException e) {
   //do something
}

代わりに:

MyClass a = new MyClass(); // Or a factory method
try {
   a.doSomething();
} catch (MyException e) {
   //do something
}
1
trotuar