Чи слід завжди повністю інкапсулювати внутрішню структуру даних?


11

Врахуйте цей клас:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThings(){
        return things;
    }

}

Цей клас виставляє масив, який він використовує для зберігання даних, будь-якому зацікавленому коду клієнта.

Я це робив у додатку, над яким працюю. У мене був ChordProgressionклас, який зберігає послідовність Chords (і робить деякі інші речі). Він мав Chord[] getChords()метод, який повертав масив акордів. Коли структуру даних довелося змінити (з масиву в ArrayList), весь код клієнта зламався.

Це змусило мене задуматися - можливо, наступний підхід краще:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThing(int index){
        return things[index];
    }

    public int getDataSize(){
        return things.length;
    }

    public void setThing(int index, Thing thing){
        things[index] = thing;
    }

}

Замість того, щоб розкривати саму структуру даних, усі операції, запропоновані структурою даних, тепер пропонуються безпосередньо класом, що її додає, використовуючи загальнодоступні методи, які делегують структурі даних.

Коли структура даних змінюється, лише ці методи доводиться змінювати, але після їх виконання весь код клієнта все ще працює.

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


Чи підходить такий підхід? Що ти думаєш про це? Які недоліки у нього є інші? Чи доцільно змушує клас впроваджувати принаймні три загальнодоступні методи, щоб просто делегувати внутрішню структуру даних?

Відповіді:


14

Код типу:

   public Thing[] getThings(){
        return things;
    }

Це не має особливого сенсу, оскільки ваш метод доступу не робить нічого, крім прямого повернення внутрішньої структури даних. Ви можете також просто заявити Thing[] thingsпро своє public. Ідея методу доступу полягає в створенні інтерфейсу, який захищає клієнтів від внутрішніх змін і не дозволяє їм маніпулювати фактичною структурою даних, за винятком дискретних способів, дозволених інтерфейсом. Як ви дізналися, коли зламався весь код вашого клієнта, ваш метод доступу цього не зробив - це просто марний код. Я думаю, що багато програмістів прагнуть писати подібний код, тому що вони десь дізналися, що все потрібно інкапсулювати методами доступу - але це з тих причин, які я пояснив. Робити це просто для "слідування за формою", коли метод доступу не виконує жодної мети - це лише шум.

Я б точно рекомендував запропоноване вам рішення, яке реалізує деякі найважливіші цілі інкапсуляції: надання клієнтам надійного, стриманого інтерфейсу, який ізолює їх від внутрішніх деталей реалізації вашого класу та не дозволяє їм торкатися внутрішньої структури даних. очікуйте способів, які ви вирішите прийнятними - "закон найменш необхідної пільги". Якщо ви подивитеся на великі популярні рамки OOP, такі як CLR, STL, VCL, запропонований вами шаблон є широко поширеним саме з цієї причини.

Ви завжди повинні це робити? Не обов'язково. Наприклад, якщо у вас є класи помічників або друзів, які по суті є складовою вашого основного класу робітників і не є "передньою", це не обов'язково - це зайвий збиток, який додасть багато непотрібного коду. І в такому випадку я б взагалі не використовував метод доступу - це безглуздо, як пояснено. Просто оголосіть структуру даних у такий спосіб, який охоплюється лише до основного класу, який її використовує - більшість мов підтримують способи цього - friendабо оголошуючи її у тому ж файлі, що і основний клас робітників тощо.

Єдиний недолік, який я бачу у вашій пропозиції, полягає в тому, що потрібно більше кодувати (і тепер вам доведеться перекодувати свої споживчі класи - але все одно ви повинні / повинні були це зробити.) Але це насправді не є недоліком - потрібно робити це правильно, а іноді це вимагає більше роботи.

Одна з речей, яка робить хорошого програміста хорошим - це те, що вони знають, коли додаткова робота варта, а коли ні. Зрештою, введення додаткових коштів тепер окупиться великими дивідендами - якщо не за цим проектом, то за іншими. Навчіться правильно кодувати і користуватися головою про це, а не лише робототехнічно виконувати встановлені форми.

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

Якщо ви відкриваєте всю структуру даних через клас, що містить ІМО, вам потрібно подумати про те, чому цей клас взагалі інкапсульований, якщо це не просто забезпечити більш безпечний інтерфейс - "клас обгортки". Ви говорите, що містить клас для цієї мети не існує - тому, можливо, у вашому дизайні щось не так. Подумайте про розбиття своїх класів на більш стримані модулі та їх розшарування.

Клас повинен мати одне чітке і стримане призначення і надати інтерфейс для підтримки цієї функціональності - не більше. Можливо, ви намагаєтесь зв’язати речі, які не належать один одному. Коли ви це зробите, все буде порушуватися щоразу, коли вам доведеться ввести зміни. Чим менші та більш дискретні ваші заняття, тим легше змінювати речі: Подумайте LEGO.


1
Дякую за відповідь. Питання: А як бути, якщо внутрішня структура даних має, можливо, 5 загальнодоступних методів? Наприклад, Java ArrayList має наступні методи: get(index), add(), size(), remove(index), і remove(Object). Використовуючи запропоновану техніку, клас, який містить цей ArrayList, повинен мати п'ять відкритих методів, щоб делегувати їх до внутрішньої колекції. І мета цього класу в програмі, швидше за все, не інкапсулює цей ArrayList, а скоріше робить щось інше. ArrayList - лише деталь. [...]
Авів Кон

Внутрішня структура даних - це просто звичайний член, тому що, використовуючи вищезазначену техніку, потрібно, щоб вона містила клас для подання додаткових п'яти публічних методів. На вашу думку - чи це розумно? А також - це поширене?
Авів Кон

@Prog - А як бути, якщо внутрішня структура даних має, можливо, 5 загальнодоступних методів ... ІМО, якщо вам здається, вам потрібно загорнути весь допоміжний клас всередині вашого основного класу і викрити його таким чином, вам потрібно переосмислити це дизайн - ваш громадський клас робить занадто багато / або не представляє відповідний інтерфейс. Клас повинен мати дуже стриману і чітко визначену роль, а його інтерфейс повинен підтримувати цю роль і лише цю роль. Подумайте про розбиття та розшарування занять. Клас не повинен бути «кухонною раковиною», яка містить усілякі предмети на ім’я інкапсуляції.
Вектор

Якщо ви відкриваєте всю структуру даних через клас обгортки, IMO, вам потрібно подумати про те, чому цей клас взагалі інкапсульований, якщо не просто забезпечити більш безпечний інтерфейс. Ви говорите, що клас, що містить, не існує для цієї мети - тому щось не так у цьому дизайні.
Вектор

1
@Phoshi - ключове слово лише для читання - я можу погодитися з цим. Але в ОП не йдеться лише про читання. наприклад remove, не читається тільки. Я розумію, що ОП хоче оприлюднити все - як в оригіналі коду до запропонованої зміниpublic Thing[] getThings(){return things;} Це мені не подобається.
Вектор

2

Ви запитували: Чи слід завжди цілком інкапсулювати внутрішню структуру даних?

Коротка відповідь: Так, більшість часу, але не завжди .

Довга відповідь: Я думаю, що класи поділяються на такі категорії:

  1. Класи, які інкапсулюють прості дані. Приклад: 2D точка. Створювати загальнодоступні функції, які надають можливість отримувати / встановлювати координати X і Y, легко, але ви можете приховати внутрішні дані легко без особливих проблем. Для таких класів розкриття деталей внутрішньої структури даних не вимагається.

  2. Класи контейнерів, які інкапсулюють колекції. STL має класичні контейнерні класи. Я вважаю std::stringі std::wstringсеред них теж. Вони забезпечують багатий інтерфейс для боротьби з абстракціями , але std::vector, std::stringі std::wstringтакож забезпечити можливість отримати доступ до необроблених даних. Я б не поспішав називати їх погано розробленими класами. Я не знаю виправдання для цих класів викриття їх необроблених даних. Однак у своїй роботі я вважав за необхідне викрити необроблені дані під час роботи з мільйонами сітчастих вузлів та даних про ці сітчасті вузли з міркувань продуктивності.

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

  3. Заняття, що мають в основному функціональний характер. Приклади: std::istream, std::ostream, ітератори таких контейнерів STL. Дуже нерозумно розкривати внутрішні деталі цих класів.

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

На закінчення: Єдиний раз, коли я вважав за потрібне розкрити внутрішню структуру даних класу, це коли це стало вузьким місцем продуктивності.


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

0

Замість повернення необроблених даних безпосередньо, спробуйте щось подібне

class ClassA {
  private Things[] things;
  ...
  public Things[] asArray() { return things; }
  public List<Thing> asList() { ... }
  ...
}

Отже, ви фактично надаєте користувацьку колекцію, яка представляє будь-яке обличчя світу. Тоді, у вашій новій реалізації,

class ClassA {
  private List<Thing> things;
  ...
  public Things[] asArray() { return things.asArray(); }
  public List<Thing> asList() { return things; }
  ...
}

Тепер у вас є відповідна інкапсуляція, приховати деталі реалізації та забезпечити зворотну сумісність (ціною).


Розумна ідея задньої сумісності. Але: Тепер у вас є відповідна інкапсуляція, приховуйте деталі реалізації - не дуже. Клієнти все ще мають справу з нюансами List. Методи доступу, які просто повертають учасників даних, навіть із наголосом, щоб зробити речі більш надійними, насправді не є хорошим IMO для інкапсуляції. Клас працівника повинен обробляти все це, а не клієнта. Чим "тупішим" повинен бути клієнт, тим він буде більш надійним. Вбік, я не впевнений, що ти відповів на питання ...
Вектор

1
@Vector - ти прав. Повернута структура даних все ще змінюється, і побічні ефекти знищують приховування інформації.
BobDalgleish

Повернута структура даних все ще змінюється, і побічні ефекти вбиватимуть приховування інформації - так, це теж - небезпечно. Я просто думав з точки зору того, що потрібно від клієнта, що було в центрі уваги питання.
Вектор

@BobDalgleish: чому б не повернути копію оригінальної колекції?
Джорджіо

1
@BobDalgleish: Якщо немає вагомих причин ефективності, я б розглядав можливість повернення посилання на внутрішні структури даних, щоб дозволити його користувачам змінити їх як дуже погане дизайнерське рішення. Внутрішній стан об'єкта повинен змінюватися лише за допомогою відповідних публічних методів.
Джорджіо

0

Ви повинні використовувати інтерфейси для цих речей. Не допоможе у вашому випадку, оскільки масив Java не реалізує ці інтерфейси, але вам слід це робити з цього моменту:

class ClassA{

    public ClassA(){
        things = new ArrayList<Thing>();
    }

    private List<Thing> things; // stores data

    // stuff omitted

    public List<Thing> getThings(){
        return things;
    }

}

Таким чином ви можете змінитись ArrayListна LinkedListбудь-що інше, і ви не зламаєте жодного коду, оскільки всі колекції Java (крім масивів), які мають (псевдо?) Випадковий доступ, можливо, будуть реалізовані List.

Ви також можете використовувати Collection, які пропонують менше методів, ніж Listале можуть підтримувати колекції без випадкового доступу, або Iterableякі навіть можуть підтримувати потоки, але не пропонують багато в плані методів доступу ...


-1 - поганий компроміс та не особливо безпечний ІМО: Ви піддаєте внутрішню структуру даних клієнтові, просто маскуєте її та сподіваєтесь на найкраще, тому що "Колекції Java ... ймовірно реалізують Список". Якщо ваше рішення було по-справжньому засноване на поліморфності / успадкуванні - що всі колекції незмінно реалізовуються Listяк похідні класи, це матиме більше сенсу, але просто "сподіватися на краще" - це не дуже гарна ідея. "Хороший програміст виглядає в одну сторону".
Вектор

@Vector Так, я припускаю, що майбутні колекції Java будуть реалізовувати List(або Collection, принаймні Iterable). У цьому і полягає вся суть цих інтерфейсів, і прикро, що Java Arrays не реалізує їх, але вони є офіційним інтерфейсом для колекцій на Java, тому не так далеко припускати, що будь-яка колекція Java буде реалізовувати їх - якщо тільки ця колекція не є старшою ніж List, і в такому випадку дуже просто його обернути абстрактним списком .
Ідан Ар'є

Ви говорите, що ваше припущення практично гарантовано відповідає дійсності, так що добре - я зніму голосування (коли мені це дозволять), тому що ви були досить пристойні, щоб пояснити, і я не хлопець на Java крім осмосу. Проте я не підтримую цю ідею розкривати внутрішню структуру даних, незалежно від того, як це робиться, і ви прямо не відповіли на питання ОП, що стосується насправді інкапсуляції. тобто обмеження доступу до внутрішньої структури даних.
Вектор

1
@Vector Так, користувачі можуть кинути ListINTO ArrayList, але це не так, як реалізація може бути захищена на 100% - ви завжди можете використовувати відображення для доступу до закритим полях. Це нахмуриться, але кастинг також нахмурився (не так сильно). Суть інкапсуляції не у запобіганні зловмисному злому, а скоріше, щоб унеможливити використання користувачами залежно від деталей впровадження, які ви можете змінити. Використання Listінтерфейсу робить саме це - користувачі класу можуть залежати від Listінтерфейсу замість конкретного ArrayListкласу, який може змінитися.
Ідан Ар'є

Ви завжди можете використовувати рефлексію для доступу до приватних полів, безумовно - якщо хтось хоче написати неправильний код і перервати дизайн, він може це зробити. скоріше, це запобігти користувачам ... - це одна з причин інкапсуляції. Інше - забезпечити цілісність та послідовність внутрішнього стану вашого класу. Проблема не в "зловмисному злому", а в поганій організації, що призводить до неприємних помилок. "Закон про найменш необхідну пільгу" - даруйте споживачеві вашого класу лише те, що є обов'язковим - не більше. Якщо ви обов'язково оприлюднюєте всю внутрішню структуру даних, у вас є проблема дизайну.
Вектор

-2

Це досить часто, щоб приховати внутрішню структуру даних від зовнішнього світу. Десь це надмірно вбивається в DTO. Я рекомендую це для доменної моделі. Якщо його взагалі потрібно викрити, поверніть незмінну копію. Поряд із цим я пропоную створити інтерфейс із такими методами, як отримати, встановити, видалити тощо.


1
Здається, це не пропонує нічого суттєвого за три попередні відповіді
gnat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.