Метод ланцюга проти інкапсуляції


17

Існує класична проблема OOP методу ланцюга методів проти методів "єдиної точки доступу":

main.getA().getB().getC().transmogrify(x, y)

проти

main.getA().transmogrifyMyC(x, y)

Перший, мабуть, має перевагу в тому, що кожен клас відповідає лише за менший набір операцій, і робить все набагато більш модульним - додавання методу до C не вимагає жодних зусиль в A, B або C для його викриття.

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

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

Відповіді:


25

Я вважаю, що Закон Деметера надає важливе керівництво в цьому (з його перевагами та недоліками, які, як завжди, слід оцінювати на основі конкретних випадків).

Перевага дотримання Закону Деметра полягає в тому, що отримане програмне забезпечення, як правило, є більш ремонтопридатним та пристосованим. Оскільки об'єкти менш залежні від внутрішньої структури інших об'єктів, контейнери об'єктів можуть бути змінені, не переробляючи їх абонентів.

Недоліком Закону Деметра є те, що він іноді вимагає написання великої кількості маленьких методів "обгортки" для розповсюдження викликів методів до компонентів. Крім того, інтерфейс класу може стати об'ємним, оскільки він розміщує методи для вміщених класів, в результаті чого клас без згуртованого інтерфейсу. Але це також може бути ознакою поганого дизайну ОО.


Я забув про той закон, дякую, що нагадали мені. Але про що я тут прошу, головним чином, які переваги та недоліки є, або точніше, як я повинен вирішити використовувати один стиль над іншим.
Дуб

@Oak, я додав цитати, що описують переваги та недоліки.
Péter Török

10

Я, як правило, намагаюся зберегти ланцюжок методів максимально обмеженим (заснованим на Законі про деметер )

Єдиний виняток, який я роблю, - це вільні інтерфейси / внутрішнє програмування стилю DSL.

Мартін Фаулер робить таке ж відмінність у доменних мовах, але з причин порушення розділення запитів команд, де зазначено:

що кожен метод повинен бути або командою, яка виконує дію, або запитом, який повертає дані абоненту, але не обидва.

Фаулер у своїй книзі на сторінці 70 говорить:

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


3

Я думаю, питання полягає в тому, чи використовуєте ви відповідну абстракцію.

У першому випадку ми маємо

interface IHasGetA {
    IHasGetB getA();
}

interface IHasGetB {
    IHasGetC getB();
}

interface IHasGetC {
    ITransmogrifyable getC();
}

interface ITransmogrifyable {
    void transmogrify(x,y);
}

Там, де головний тип IHasGetA. Питання: чи підходить ця абстракція. Відповідь не тривіальна. І в цьому випадку це виглядає дещо, але це як би то не було теоретичний приклад. Але побудувати інший приклад:

main.getA(v).getB(w).getC(x).transmogrify(y, z);

Часто краще, ніж

main.superTransmogrify(v, w, x, y, z);

Тому що в останньому прикладі , як thisі в mainзалежності від типів v, w, x, yі z. Крім того, код насправді не виглядає набагато краще, якщо в кожному декларації методу є півдесятка аргументів.

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

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

Наприклад, ви можете мати:

class Main implements IHasGetA, IHasGetA, IHasGetA, ITransmogrifyable {
    IHasGetB getA() { return this; }
    IHasGetC getB() { return this; }
    ITransmogrifyable getC() { return this; }
    void transmogrify(x,y) {
        return x + y;//yeah!
    }
}

Де mainє примірник Main. Якщо знання класу mainзменшує залежність IHasGetAзамість Main, ви виявите, що зв'язок насправді є досить низькою. Код виклику навіть не знає, що він насправді викликає останній метод на оригінальному об'єкті, який насправді ілюструє ступінь роз'єднання.
Ви досягаєте шляху стислих і ортогональних абстракцій, а не заглиблюючись у внутрішні місця реалізації.


Дуже цікавий момент про велике збільшення кількості параметрів.
Дуб

2

Закон Деметри , а @ Петера Török вказує, пропонує «компактну» форму.

Крім того, чим більше методів, які ви явно згадуєте у своєму коді, тим більше класів, від яких залежить ваш клас, збільшуються проблеми обслуговування. У вашому прикладі компактна форма залежить від двох класів, тоді як довша - від чотирьох класів. Більш довга форма не лише суперечить Закону Деметра; це також змусить вас змінювати код щоразу, коли ви змінюєте будь-який із чотирьох посилальних методів (на відміну від двох у компактній формі).


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

1
@Oak: Робити що-небудь наосліп , ніколи не є корисним. Потрібно переглянути плюси і мінуси і приймати рішення на основі доказів. Сюди входить і Закон Деметера.
CesarGon

2

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

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

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


1

Я часто виявляю, що логікою програми легше пограбувати ланцюговими методами. Мені customer.getLastInvoice().itemCount()краще вписується в мій мозок customer.countLastInvoiceItems().

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


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