Слабая связь в объектно-ориентированном дизайне

16

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

Рассмотрим метод addTrackдля Albumкласса, два возможных метода:

addTrack( Track t )

и

addTrack( int no, String title, double duration )

Какой метод уменьшает сцепление? Второй делает, так как класс, использующий класс Album, не должен знать класс Track. В общем случае параметры методов должны использовать базовые типы (int, char ...) и классы из пакетов java. *.

Я склонен не соглашаться с этим; Я считаю, addTrack(Track t)что лучше, чем addTrack(int no, String title, double duration)по разным причинам:

  1. Для метода всегда лучше использовать как можно меньше параметров (согласно «Чистому коду» дяди Боба ни один или один предпочтительно, 2 в некоторых случаях и 3 в особых случаях; более 3 требуют рефакторинга - это, конечно, рекомендации, а не правила Холли) ,

  2. Если addTrackэто метод интерфейса, а требования Trackдолжны иметь больше информации (скажем, год или жанр), то интерфейс необходимо изменить, чтобы метод поддерживал другой параметр.

  3. Инкапсуляция нарушена; если addTrackв интерфейсе, то он не должен знать внутренности Track.

  4. Это на самом деле более связано вторым способом, со многими параметрами. Предположим, что noпараметр необходимо изменить с intна, longпотому что есть больше, чем MAX_INTдорожки (или по любой причине); тогда Trackнужно изменить и метод, и метод, в то время как если бы метод был изменен addTrack(Track track)только на Track.

Все 4 аргумента на самом деле связаны друг с другом, и некоторые из них являются следствием других.

Какой подход лучше?

m3th0dman
источник
2
Это документ, который был составлен профессором или тренером? Судя по URL предоставленной вами ссылки, похоже, что это было для класса, хотя я не вижу в документе никаких свидетельств того, кто его создал. Если бы это было частью класса, я бы посоветовал вам задать эти вопросы человеку, предоставившему документ. Между прочим, я согласен с вашей аргументацией - мне казалось очевидным, что класс Album хотел бы знать о классе Track по своей сути.
Дерек
Честно говоря, всякий раз, когда я читаю о «Best Practices», я беру их с крошкой соли!
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 - это просто простой объект передачи bean / data, где над ним есть только поля и методы получения / установки; интерфейс требуется в этом случае?
m3th0dman
6
Необходимые? Возможно нет. Наводит на мысль, да. Конкретное значение трека может и будет развиваться, но то, что требует от него потребительский класс, вероятно, не будет.
JustinC
2
@ m3th0dman Всегда зависит от абстракций, а не от конкреций. Это относится независимо от Trackтого, глупы ли вы или умны. Trackэто конкреция. ITrackИнтерфейс это абстракция. Таким образом, вы сможете иметь различные типы треков в будущем, если они соответствуют ITrack.
Тулаинс Кордова
4
Я согласен с идеей, но теряю префикс «я». «Чистый код» Роберта Мартина, стр. 24: «Предыдущее« я », столь распространенное в сегодняшних устаревших пачках, в лучшем случае отвлекает, а в худшем - слишком много информации. Я не хочу, чтобы мои пользователи знали, что я передаю им интерфейс."
Бенджамин Брумфилд
1
@BenjaminBrumfield Вы правы. Мне также не нравится префикс, хотя я оставлю в ответе для ясности.
Тулаинс Кордова
4

Я бы сказал, что второй пример метода, скорее всего, увеличивает сцепление, поскольку он, скорее всего, создает экземпляр объекта Track и сохраняет его в текущем объекте Album. (Как было предложено в моем комментарии выше, я бы предположил, что классу Album будет присуще понятие класса Track где-то внутри него.)

В первом примере метода предполагается, что экземпляр Track создается вне класса Album, поэтому, по крайней мере, мы можем предположить, что создание экземпляра класса Track не связано с классом Album.

Если бы лучшие практики предполагали, что у нас никогда не будет ссылки на один класс во втором классе, вся объектно-ориентированное программирование будет выброшено в окно.

Дерек
источник
Я не вижу, как наличие неявной ссылки на другой класс делает его более связанным, чем наличие явной ссылки. В любом случае, два класса связаны. Я действительно думаю, что лучше, чтобы связь была явной, но я не думаю, что она «более» связана в любом случае.
TMN
1
@TMN, дополнительная связь заключается в том, что я подразумеваю, что второй пример, вероятно, закончится внутренним созданием нового объекта Track. Создание экземпляра объекта связано с методом, который в противном случае должен просто добавлять объект Track в какой-то список в объекте Album (нарушая принцип единой ответственности). Если необходимо когда-либо изменить способ создания Трека, метод addTrack () также необходимо изменить. Это не так в случае с первым примером.
Дерек
3

Соединение - это лишь один из многих аспектов, которые нужно получить в своем коде. Уменьшая сцепление, вы не обязательно улучшаете свою программу. В общем, это лучшая практика, но в данном конкретном случае почему не Trackследует знать?

Используя Trackкласс для передачи Album, вы облегчаете чтение кода, но, что более важно, как вы уже упоминали, вы превращаете статический список параметров в динамический объект. Это в конечном итоге делает ваш интерфейс гораздо более динамичным.

Вы упоминаете, что инкапсуляция нарушена, но это не так. Albumдолжен знать внутренности Track, и, если вы не использовали объект, Albumдолжен был бы знать каждую часть информации, передаваемую ему, прежде чем он мог бы все же использовать его. Вызывающая сторона также должна знать внутренние компоненты Track, поскольку она должна создавать Trackобъект, но вызывающая сторона должна знать эту информацию все равно, если она была передана непосредственно методу. Другими словами, если преимущество инкапсуляции заключается в том, что она не знает содержимого объекта, его нельзя было бы использовать в этом случае, так как он Albumдолжен использовать Trackинформацию s точно так же.

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

Чтобы решить эту проблему, вам нужно разделить Trackкомпоненты интерфейса и логические компоненты, создав два отдельных класса. Для вызывающего абонента Trackстановится легким классом, который предназначен для хранения информации и предлагает незначительные оптимизации (вычисленные данные и / или значения по умолчанию). Внутри Albumвы будете использовать класс с именем TrackDAOдля выполнения тяжелой работы, связанной с сохранением информации Trackв базе данных.

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

Нил
источник
3

Оба верны

addTrack( Track t ) 

это лучше (как вы уже аргументировано) , а

addTrack( int no, String title, double duration ) 

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

В то время как вы говорите о более читабельном / поддерживаемом коде, в статье говорится о соединении . Менее связанный код не обязательно легче реализовать и понять.

k3b
источник
См. Аргумент 4; Я не вижу, как второй менее связан.
m3th0dman
3

Низкая связь не означает отсутствие связи. Что-то где-то должно знать об объектах в другом месте кодовой базы, и чем больше вы уменьшаете зависимость от «пользовательских» объектов, тем больше причин вы приводите к изменению кода. То, что автор, которого вы цитируете, продвигает со второй функцией, является менее связанным, но также менее объектно-ориентированным, что противоречит всей идее GRASP как методологии объектно-ориентированного проектирования. Все дело в том, как спроектировать систему как совокупность объектов и их взаимодействия; избегать их - все равно что учить вас водить машину, говоря, что вместо этого вы должны ездить на велосипеде.

Вместо этого, правильный путь - уменьшить зависимость от конкретных объектов, что является теорией «слабой связи». Чем меньше конкретных конкретных типов, о которых должен знать метод, тем лучше. Именно этим утверждением первый вариант на самом деле менее связан, поскольку второй метод, использующий более простые типы, должен знать обо всех этих более простых типах. Конечно, они встроены, и код внутри метода, возможно, должен заботиться, но сигнатура метода и вызывающие его методы определенно этого не делают . Изменение одного из этих параметров, относящихся к концептуальной звуковой дорожке, потребует большего количества изменений, когда они разделены по сравнению с тем, когда они содержатся в объекте дорожки (который является точкой объектов; инкапсуляция).

Пройдя еще один шаг вперед, если предполагалось, что Track будет заменен чем-то, что делает ту же работу лучше, возможно, будет необходим интерфейс, определяющий необходимые функции, ITrack. Это может позволить различные реализации, такие как «AnalogTrack», «CdTrack» и «Mp3Track», которые предоставляют дополнительную информацию, более специфичную для этих форматов, и в то же время обеспечивают базовое представление данных ITrack, которое концептуально представляет «дорожку»; конечный фрагмент аудио. Track также может быть абстрактным базовым классом, но для этого необходимо, чтобы вы всегда хотели использовать реализацию, присущую Track; переопределите его как BetterTrack, и теперь вы должны изменить ожидаемые параметры.

Таким образом, золотое правило; Программы и их компоненты кода всегда будут иметь причины для изменения. Вы не можете написать программу, которая никогда не потребует редактирования кода, который вы уже написали, чтобы добавить что-то новое или изменить его поведение. Ваша цель в любой методологии (GRASP, SOLID, любой другой акроним или модное слово, которое вы можете придумать) состоит в том, чтобы просто определить вещи, которые должны будут измениться со временем, и спроектировать систему так, чтобы эти изменения были как можно проще сделать (переведено; затрагивает как можно меньше строк кода и затрагивает как можно меньше других областей системы, выходящих за рамки предполагаемого изменения). В данном случае, скорее всего, изменится то, что дорожка получит больше элементов данных, которые addTrack () может интересовать, а может и не заботиться, а не этот трек будет заменен на BetterTrack.

Keiths
источник