Вільна муфта в об'єктно-орієнтованому дизайні


16

Я намагаюся навчитися GRASP, і я виявив, що це пояснено ( тут, на сторінці 3 ) про низьке з'єднання, і я був дуже здивований, коли виявив це:

Розглянемо метод addTrackдля Albumкласу, два можливі методи:

addTrack( Track t )

і

addTrack( int no, String title, double duration )

Який метод зменшує з'єднання? Другий - так, оскільки клас, що використовує клас "Альбом", не повинен знати клас "Трек". Загалом, параметри методів повинні використовувати базові типи (int, char ...) та класи з пакетів java. *

Я схильний погодитися з цим; Я вважаю, що addTrack(Track t)це краще, ніж addTrack(int no, String title, double duration)через різні причини:

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

  2. Якщо addTrackце метод інтерфейсу, і вимоги потребують, щоб у a було Trackмати більше інформації (скажімо, рік чи жанр), тоді інтерфейс потрібно змінити, щоб метод підтримував інший параметр.

  3. Інкапсуляція порушена; якщо addTrackв інтерфейсі, то він не повинен знати внутрішніх даних Track.

  4. Це насправді більше поєднано по-другому, з багатьма параметрами. Припустимо, noпараметр потрібно змінити з intна, longтому що їх більше, ніж MAX_INTтреків (або з будь-якої причини); то і те, Trackі метод потрібно змінити, тоді як якщо методом було б addTrack(Track track)лише те, Trackбуло б змінено.

Всі 4 аргументи насправді пов’язані між собою, а деякі з них є наслідками з боку інших.

Який підхід кращий?


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

Чесно кажучи, коли я читаю про "Кращі практики", я беру їх із зерном солі!
AraK

@Derek Я знайшов документ, шукаючи в Google "приклад шаблонів зрозуміти"; Я не хто написав це, але оскільки він був з університету, я вважаю, що це надійно. Я шукаю приклад на основі наданої інформації та ігнорування джерела.
m3th0dman

4
@ m3th0dman "але оскільки я був з університету, я вважаю, що це надійно". Для мене, оскільки він з університету, я вважаю це ненадійним. Я не довіряю тому, хто не працював над багаторічними проектами, розповідаючи про найкращі практики в розробці програмного забезпечення.
AraK

1
@AraK Надійний не означає безперечного; і ось що я тут роблю, ставлячи під сумнів це.
m3th0dman

Відповіді:


15

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

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

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

Наприклад, розгляньте кнопку "Додати до списку відтворення" та Playlistоб'єкт. Введення Trackоб'єкта може розглядатися як збільшення з'єднання, якщо врахувати лише ці два об'єкти. Тепер у вас є три взаємозалежні класи замість двох. Однак це не є повнотою вашої системи. Вам також потрібно імпортувати доріжку, відтворити композицію, показати доріжку тощо. Додавання ще одного класу до цієї суміші незначно.

Тепер розглянемо необхідність додавати підтримку для відтворення треків через мережу, а не лише локально. Вам просто потрібно створити NetworkTrackоб’єкт, який відповідає одному інтерфейсу. Без Trackоб'єкта вам доведеться створювати функції скрізь:

addNetworkTrack(int no, string title, double duration, URL location)

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

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


1
+ З’єднання з примітивами все ще з’єднується незалежно від того, як воно нарізане.
JustinC

+1 для згадування параметра "Додати URL / пульсацію".
user949300

4
+1 Цікавим для цього є також обговорення принципу інверсії залежності в DIP в дикій природі, де використання примітивних типів насправді сприймається як "запах" примітивної одержимості із значенням "Об'єкт ". Мені це здається, що було б краще передати об’єкт Track, що цілий куточок примітивних типів ... І якщо ви хочете уникнути залежності / з'єднання з певними класами, використовуйте інтерфейси.
Мар'ян Венема

Відповідь прийнята через приємне пояснення різниці між загальною системою зв'язку та з’єднанням модулів.
m3th0dman

10

Моя рекомендація:

Використовуйте

addTrack( ITrack t )

але будьте впевнені, що ITrackце інтерфейс, а не конкретний клас.

Альбом не знає внутрішньої частини ITrackвиконавців. Це лише в поєднанні з контрактом, визначеним ITrack.

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


1
Я вважаю, що Track - це просто простий об'єкт передачі даних про боби / дані, де над ним є лише поля та геттери / сетери; чи потрібен інтерфейс у цьому випадку?
m3th0dman

6
Вимагається? Напевно, ні. Сугестивний, так. Конкретне значення треку може і буде розвиватися, але те, що вимагає від нього клас споживачів, мабуть, не буде.
JustinC

2
@ m3th0dman Завжди залежить від абстракцій, а не від конкрементів. Це стосується незалежно від Trackтого, щоб бути німим чи розумним. Trackє конкрементом. ITrackінтерфейс - це абстракція. Таким чином, ви зможете мати різні типи треків у майбутньому, якщо вони відповідають ITrack.
Тулан Кордова

4
Я погоджуюся з ідеєю, але втрачаю префікс «Я». З чистого кодексу Роберта Мартіна, сторінка 24: "Попередній я, такий поширений у сьогоднішніх застарілих шатах, - це відволікання в кращому випадку і занадто багато інформації в гіршому випадку. Я не хочу, щоб мої користувачі знали, що я їм передаю інтерфейс. "
Бенджамін Брамфілд

1
@BenjaminBrumfield Ви праві. Мені також не подобається префікс, хоча у відповіді я залишу для наочності.
Tulains Córdova

4

Я можу стверджувати, що другий приклад методу, швидше за все, збільшує зв'язок, оскільки він, швидше за все, створює об'єкт Track та зберігає його в поточному об'єкті Album. (Як було запропоновано в моєму коментарі вище, я вважаю, що йому властиво, щоб клас альбому мав поняття класу Track десь усередині нього.)

Перший приклад методу передбачає, що Доріжка отримується миттєво поза класом Альбом, тому, принаймні, можна припустити, що інстанція класу Трек не пов'язана з класом Альбом.

Якщо найкраща практика підказує, що ми ніколи не маємо посилання одного класу на другий клас, все об'єктно-орієнтоване програмування буде викинуто у вікно.


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

1
@TMN, додаткове з'єднання полягає в тому, як я маю на увазі, що другий приклад, ймовірно, закінчиться внутрішньо, створивши новий об'єкт Track. Ідентифікація об'єкта приєднується до методу, який інакше повинен просто додавати об'єкт Track до якогось списку в об'єкті альбому (порушуючи єдиний принцип відповідальності). Якщо спосіб створення треку колись потрібно буде змінити, метод addTrack () також повинен бути змінений. Це не так у випадку першого прикладу.
Derek

3

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

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

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

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

Щоб вирішити проблему, вам потрібно буде розділити Trackїї компоненти інтерфейсу та її логічні компоненти, створивши два окремих класи. Абоненту Trackстає легкий клас, який призначений для зберігання інформації та пропонування незначних оптимізацій (обчислені дані та / або значення за замовчуванням). Всередині Albumви б використовували клас з іменем TrackDAOдля виконання важкого підйому, пов'язаного із збереженням інформації Trackв базі даних.

Звичайно, це лише приклад. Я впевнений, що це зовсім не ваш випадок, тому не соромтесь користуватися без Trackпровини. Просто пам’ятайте, щоб мати на увазі абонента під час створення класів та створювати інтерфейси, коли це потрібно.


3

Обидва вірні

addTrack( Track t ) 

це краще (як ви вже аргументовано) , а

addTrack( int no, String title, double duration ) 

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

Поки ви говорите про більш читабельний / підтримуваний код, у статті йдеться про з'єднання . Менш пов'язаний код не обов’язково простіше реалізувати та зрозуміти.


Див. Аргумент 4; Я не бачу, як той другий менш сполучений.
m3th0dman

3

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

Натомість правильний проспект полягає у зменшенні залежності від конкретних об'єктів, що є теорією «вільної з’єднання». Чим менше певних типів конкретних методів має знати, тим краще. Саме за цим твердженням перший варіант насправді менш поєднаний, оскільки другий метод, який бере простіші типи, повинен знати про всі ці простіші типи. Зрозуміло, що вони вбудовані, і код всередині методу, можливо, повинен бути обережним, але підпис методу та виклики методу, безумовно, не роблять . Зміна одного з цих параметрів, що стосуються концептуальної звукової доріжки, потребуватиме більше змін, коли вони відокремлені проти того, коли вони містяться в об'єкті треку (що є точкою об'єктів; інкапсуляція).

Якщо піти на крок далі, якби очікували, що трек буде замінений чимось, що зробив ту саму роботу краще, можливо, буде потрібний інтерфейс, що визначає необхідну функціональність, ITrack. Це може призвести до різних реалізацій, таких як "AnalogTrack", "CdTrack" та "Mp3Track", що надало додаткову інформацію, більш конкретну для цих форматів, при цьому надаючи базову експозицію даних ITrack, що концептуально являє собою "трек"; кінцева пісня з аудіо. Track може подібним чином бути абстрактним базовим класом, але для цього потрібно завжди бажати використовувати реалізацію, властиву Track; повторно виконати його як BetterTrack, і тепер вам доведеться змінити очікувані параметри.

Таким чином, золоте правило; програми та їх кодові компоненти завжди матимуть підстави змінюватися. Ви не можете написати програму, яка ніколи не потребуватиме редагування коду, який ви вже написали, щоб додати щось нове або змінити його поведінку. Ваша мета в будь-якої методології (Грасп, SOLID, будь-який інший абревіатурою або ключовим словом ви можете думати) просто ідентифікувати речі , які будуть змінюватися з плином часу, а також розробити систему таким чином , що ці зміни , як легко зробити , як можна (перекладено; торкання якомога менше рядків коду та зачіпання якнайменше інших областей системи поза межами можливих змін, які ви плануєте). Насправді, найімовірніше, що зміниться, це те, що трек набере більше членів даних, про які addTrack () може або не піклується про це, не що трек буде замінено на BetterTrack.

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