我会遭受封装使用过度的困扰吗?


11

我在各种项目的代码中注意到了一些东西,这些东西似乎让我感到代码难闻,有些事情要做,但我无法解决。

在尝试编写“干净的代码”时,我倾向于过度使用私有方法,以使我的代码更易于阅读。问题在于代码确实更干净,但是测试起来也更加困难(是的,我知道我可以测试私有方法...),总的来说,这对我来说是个坏习惯。

这是一个类示例,该类从.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();

我更喜欢这样做,因为我不想在客户端的端代码中放入无用的代码行,从而使客户端类难以实现。

那么,这真的是个坏习惯吗?如果可以,我该如何避免呢?请注意,以上只是一个简单的示例。想象一下,在有些复杂的情况下也会发生相同的情况。

Answers:


17

我想您实际上是在正确的轨道上,希望对客户端隐藏实现细节。您想要设计您的类,以便客户端看到的接口是您可以想到的最简单,最简洁的API。客户不仅不会“打扰”实施细节,而且还可以自由地重构底层代码,而不必担心必须修改该代码的调用者。减少不同模块之间的耦合会带来一些实际好处,您绝对应该为此而努力。

因此,如果遵循我刚刚提供的建议,您将得到在代码中已经注意到的东西,一堆逻辑隐藏在公共接口后面,并且不容易访问。

理想情况下,您应该只能根据其公共接口对类进行单元测试,并且如果类具有外部依赖项,则可能需要引入伪造/模拟/存根对象以隔离要测试的代码。

但是,如果您这样做了,但仍然感到无法轻松测试课程的每个部分,那么您的一堂课很有可能做得太多。本着SRP原则的精神,您可以遵循Michael Feathers所谓的“新芽类模式”,并将原始类的一部分提取到新类中。

在您的示例中,您有一个导入器类,该类也负责读取文本文件。您的选择之一是将一大堆文件读取代码提取到单独的InputFileParser类中。现在,所有这些私有功能都已公开,因此易于测试。同时,解析器类对于外部客户端不是可见的(将其标记为“内部”,不要发布头文件,或者只是不将其作为API的一部分进行宣传),因为它们将继续使用原始进口商,其界面将继续保持简短有趣。


1

作为将大量方法调用放入类构造函数中的一种替代方法(我同意通常会避免这种习惯),您可以创建一个工厂方法来处理所有您不想打扰客户端的额外初始化工作一起。这意味着您将公开一个看起来像您的constructGroupOfCustomers()方法一样的方法,并将其用作类的静态方法:

GroupOfCustomersImporter importer = 
    new GroupOfCustomersImporter.CreateInstance();

或作为单独工厂类的方法:

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

这些只是我想到的头几个选择。

还值得考虑的是,您的直觉试图告诉您一些信息。当您的直觉告诉您代码开始发臭时,它可能会发臭,因此最好在发臭之前对它进行一些处理!可以肯定的是,就其本身而言,代码本身可能没有本质上的错误,但是快速重新检查和重构将有助于解决您对问题的想法,因此您可以放心地进行其他工作,而不必担心是否要构建一个潜在的纸牌屋。在这种特殊情况下,将构造函数的某些职责委派给工厂(或由工厂调用)可确保您可以更好地控制类的实例化及其潜在的后代。


1

添加我自己的答案,因为最终发表评论的时间过长。

封装和测试完全相反,这是绝对正确的-如果您隐藏几乎所有内容,则很难进行测试。

但是,通过提供流而不是文件名,可以使示例更易于测试-这样,您就可以从内存或文件(如果需要)中提供已知的csv。当您需要添加http支持时,这还将使您的班级更加强大;)

另外,考虑使用lazy-init:

public class Csv
{
  private CustomerList list;

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

1

除了初始化一些变量或对象的状态外,构造函数不应做太多事情。在构造函数中执行过多操作会引发运行时异常。我会尽量避免客户做这样的事情:

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
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.