Я страдаю от чрезмерного использования инкапсуляции?

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();

Я предпочитаю это, потому что я не хочу помещать бесполезные строки кода в коде на стороне клиента, беспокоя клиентский класс деталями реализации.

Итак, это на самом деле вредная привычка? Если да, как я могу избежать этого? Обратите внимание, что приведенное выше является просто примером. Представьте, что та же самая ситуация происходит в чем-то более сложном.

Флоренс Целай
источник

Ответы:

17

Я думаю, что вы на самом деле на правильном пути, если хотите скрыть детали реализации от клиента. Вы хотите спроектировать свой класс так, чтобы интерфейс, который видит клиент, был самым простым и кратким API, который вы можете придумать. Мало того, что клиент не будет «возиться» с деталями реализации, вы также сможете свободно рефакторировать базовый код, не беспокоясь о том, что вызывающие его лица должны быть изменены. Уменьшение связи между различными модулями имеет некоторое реальное преимущество, и вам обязательно следует к этому стремиться.

Поэтому, если вы будете следовать только что приведенному мною совету, у вас получится нечто, что вы уже заметили в своем коде, целая куча логики, которая остается скрытой за общедоступным интерфейсом и недоступной.

В идеале, вы должны иметь возможность проводить модульное тестирование вашего класса только с его открытым интерфейсом, и, если у него есть внешние зависимости, вам может потребоваться ввести поддельные / mock / stub объекты для изоляции тестируемого кода.

Однако, если вы делаете это, и вы все еще чувствуете, что не можете легко протестировать каждую часть класса, то очень вероятно, что ваш один класс делает слишком много. В духе принципа SRP вы можете следовать тому, что Майкл Фезерз называет «Образец класса ростков», и извлекать часть своего исходного класса в новый класс.

В вашем примере у вас есть класс import, который также отвечает за чтение текстового файла. Один из вариантов - извлечь фрагмент кода для чтения файла в отдельный класс InputFileParser. Теперь все эти частные функции становятся общедоступными и поэтому легко тестируемыми. В то же время класс синтаксического анализатора не является чем-то видимым для внешних клиентов (пометьте его как «внутренний», не публикуйте заголовочные файлы или просто не рекламируйте его как часть вашего API), поскольку они будут продолжать использовать исходный импортер, чей интерфейс будет оставаться коротким и приятным.

DXM
источник
1

В качестве альтернативы введению большого количества вызовов методов в конструктор вашего класса - что, я согласен, это привычка, которую обычно следует избегать - вы можете вместо этого создать метод фабрики для обработки всех дополнительных вещей инициализации, которые вы не хотите беспокоить клиента сторона с. Это будет означать, что вы выставите метод на вид чего-то похожего на ваш constructGroupOfCustomers()метод как public, и будете использовать его как статический метод вашего класса:

GroupOfCustomersImporter importer = 
    new GroupOfCustomersImporter.CreateInstance();

или как метод отдельного фабричного класса, возможно:

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

Это всего лишь первая пара вариантов, о которых я подумал прямо в голове.

Также стоит учитывать, что ваши инстинкты пытаются вам что-то сказать. Когда ваша интуиция говорит вам, что код начинает пахнуть, он, вероятно, пахнет, и лучше с этим что-то сделать, прежде чем он пахнет! Конечно, на первый взгляд в вашем коде нет ничего плохого, однако быстрая перепроверка и рефакторинг помогут вам решить ваши проблемы, чтобы вы могли уверенно переходить к другим вещам, не беспокоясь о том, что вы создаете потенциальный карточный домик. В этом конкретном случае делегирование некоторых из обязанностей конструктора фабрике или ее вызову гарантирует, что вы сможете в большей степени контролировать создание экземпляров вашего класса и его потенциальных будущих потомков.

S.Robins
источник
1

Добавляю свой собственный ответ, так как это закончилось слишком долго для комментария.

Вы абсолютно правы в том, что инкапсуляция и тестирование довольно противоречивы - если вы скрываете почти все, это сложно проверить.

Однако вы можете сделать пример более тестируемым, указав поток, а не имя файла - таким образом, вы просто предоставите известный CSV из памяти или файл, если хотите. Это также сделает ваш класс более надежным, когда вам потребуется добавить поддержку http;)

Также рассмотрим использование lazy-init:

public class Csv
{
  private CustomerList list;

  public CustomerList get()
  {
    if(list == null)
        load();
     return list;
  }
}
Максимум
источник
1

Конструктор не должен делать слишком много, за исключением инициализации некоторых переменных или состояния объекта. Слишком много работы в конструкторе может вызвать исключения во время выполнения. Я бы постарался избежать того, чтобы клиент делал somenthing так:

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
}
trotuar
источник