Чи страждаю я від надмірного використання капсуляції?


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, який ви можете придумати. Не тільки клієнт не буде "заважати" деталями реалізації, але ви також будете вільні до рефакторного базового коду, не переживаючи, що виклики цього коду повинні бути змінені. Скорочення з’єднання між різними модулями має певну користь, і ви повинні неодмінно прагнути до цього.

Тож якщо ви будете дотримуватися поради, яку я щойно надав, ви отримаєте щось, що ви вже помітили у своєму коді, цілу купу логіки, яка залишається прихованою за загальнодоступним інтерфейсом і не є легкодоступною.

В ідеалі ви повинні мати змогу перевірити свій клас лише на його загальнодоступному інтерфейсі, і якщо він має зовнішні залежності, вам може знадобитися ввести підроблені / макет / заглушки об'єктів, щоб ізолювати тестований код.

Однак якщо ви це зробите, і ви все ще відчуваєте, що не можете легко перевірити кожну частину класу, то дуже ймовірно, що ваш один клас робить занадто багато. У дусі принципу SRP ви можете прослідкувати за тим, що Майкл Пірс називає "Шаблон класу паростків" і витягти шматок вашого оригінального класу в новий клас.

У вашому прикладі у вас є клас імпортера, який також відповідає за читання текстового файлу. Один із ваших варіантів - витягнути фрагмент коду для читання файлів в окремий клас 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
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.