Всегда ли нужно создавать новые классы?

      Комментарии к записи Всегда ли нужно создавать новые классы? отключены

Начнем с вопроса, казалось бы, не имеющего никакого отношения к рассматриваемому вопросу, а именно — всегда ли нужно создавать новый класс для каждой новой задачи? Правильный ответ, конечно же, нет. Это было бы странно и неэффективно. Фишка состоит в том, что мы можем использовать уже существующие классы, адаптируя их функциональность для выполнения новых задач. Таким образом появляется возможность не создавать систему классов с нуля, а задействовать уже имеющиеся решения, которые были созданы ранее, при работе над предыдущими проектами. Впрочем, наше высказывание о странности и неэффективности создания новых классов не является истиной в последней инстанции. Могут быть ситуации, когда существующие классы по каким-либо причинам не устраивают архитектора, и тогда требуется создать новый класс. Следует, однако, избегать ситуаций, когда созданный класс (а точнее, его набор операций и атрибутов) практически повторяет существующий, лишь незначительно отличаясь от него. Все-таки лучше не изобретать велосипед и стараться создавать классы на основе уже существующих, и только если подходящих классов не нашлось — создавать свои, которые, в свою очередь, могут (и должны!) служить основой для других классов. Мы уже не говорим о том, что создание классов предполагает значительный объем усилий по кодированию и тестированию. В общем случае, сказанное выше можно проиллюстрировать такой диаграммой (рис. 4.7):

Рис. 4.7.

В дополнение можно назвать несколько причин, почему стоит использовать уже существующие классы:

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

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

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

А теперь внимание — мы много говорили о том, что нужно создавать классы на основе уже существующих, но так и не сказали ни слова о том, как это сделать. Пришло время внести ясность в этот вопрос.

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

Обобщение (или, как часто говорят, наследование) на диаграммах обозначается очень просто — незакрашенной треугольной стрелкой, направленной в направлении суперкласса (рис. 4.8).

Для того чтобы научиться эффективно моделировать наследование, обратимся к классикам, а именно к Г. Бучу. Он советует проводить эту процедуру в такой последовательности:

1. Найдите атрибуты, операции и обязанности, общие для двух или более классов из данной совокупности. Это позволит избежать ненужного дублирования структуры и функциональности объектов.

2. Вынесите эти элементы в некоторый общий суперкласс, а если такого не существует, то создайте новый класс.

3. Отметьте в модели, что подклассы наследуются от суперкласса, установив между ними отношение обобщения.

Рис. 4.8.

А вот и пример применения этого подхода (рис. 4.9):

Рис. 4.9.

На первый взгляд, кажется странным, что класс точка не имеет никаких атрибутов, а круг имеет только радиус. С прямоугольником, вроде бы, все понятно — ширина и высота, но вот только где он расположен в пространстве, этот прямоугольник? Давайте попробуем следовать советам Буча. Итак, положение всех трех фигур можно однозначно определить с помощью пары чисел. Для точки — это вообще единственные ее характеристики, для круга и прямоугольника — их центры (под центром прямоугольника мы понимаем точку пересечения его диагоналей). Вот они, общие атрибуты! Таким образом, мы создали суперкласс Фигура, имеющий два атрибута — координаты центра. Все остальные классы на этой диаграмме связаны с классом Фигура отношением обобщения, т. е. в них нужно доопределить только недостающие атрибуты — радиус, ширину и высоту. Атрибуты, описывающие координаты центра, эти классы имеют изначально как потомки класса Фигура — они их наследуют. Заметим, что операции классов мы тут не рассматриваем: понятно, что с ними была бы та же история.

Так, с наследованием вроде бы разобрались. Пришло время для маленькой провокации с нашей стороны. Классы-потомки ведь наследуют атрибуты и операции суперкласса? Таким образом, они могут наследовать и их интерфейсы — то есть объекты абсолютно разной природы могут иметь один и тот же интерфейс! Так как же тогда определить, какого же все-таки класса объект? Да и нужно ли это вообще?

Действительно, объекты разной природы (или говоря проще, разных классов) могут поддерживать один и тот же интерфейс именно так, как того ожидает пользователь. Примером тому может служить рассмотренная выше диаграмма с геометрическими фигурами. Все рассмотренные фигуры имеют, например, операцию рисования на экране. С точки зрения пользователя в каждом случае это одно и то же действие. Однако реализованы эти операции по-разному — ведь процедура изображения прямоугольника сильно отличается от подобной процедуры для круга. Но для пользователя это неважно: ведь сигнатура-то одна и та же! А возможно это благодаря еще одному из основных принципов ООП — полиморфизму. Как мы только что упомянули, работа механизма полиморфизма основана на совпадении сигнатуры метода, объявленного в интерфейсе, и сигнатуры самого метода. Методы внутри классов-потомков могут быть (и наверняка будут!) переопределены, их реализации будут различными, а сигнатуры останутся неизменными. Таким образом (и в этом легко ощутить мощь ООП), выполняя одни и те же операции, разные объекты могут вести себя по-разному.

Полиморфизм является основой для реализации механизма интерфейсов в языках программирования. Вот, кстати, и ответ на вопрос, какого класса объект: как только пользователь обращается к некоторой операции через интерфейс, определяется фактический класс объекта и вызывается соответствующая операция класса. Примеры полиморфизма можно увидеть в самых обыденных вещах, которыми мы пользуемся в повседневной жизни. Оглянитесь вокруг — мир построен по ООП, Матрица работает! Например, всем привычная кредитная карточка, являясь интерфейсом для доступа к банковскому счету через банкомат (и не только), одинаково работает в любой стране, вот только ведет себя чуть-чуть по-разному, т. к. банкомат выдает деньги в местной валюте. Согласны, пример не очень корректный, но зато очень наглядный! Думаем, понаблюдав за окружающим миром, читатель сам сможет привести массу примеров полиморфизма.

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

Отношения между классами

Ни один из объектов окружающего нас мира не существует сам по себе. Птицы летают потому, что есть воздух, на который опираются их крылья. Каждый из нас связан с массой других людей разнообразными родственными, профессиональными и другими связями, предполагающими различные типы отношений. Точно так же и классы связаны между собой. И чтобы в полной мере овладеть ООП, нам необходимо понять суть этих отношений и научиться их идентифицировать.

Мы сказали, что объекты находятся в определенных отношениях друг с другом. Один из типов таких отношений — это зависимость. Думаем, суть такого отношения понятна уже из его названия — зависимость возникает тогда, когда реализация класса одного объекта зависит от спецификации операций класса другого объекта. И если изменится спецификация операций этого класса, нам неминуемо придется вносить изменения и в зависимый класс. Приведем простой пример, опять-таки взятый из нашей повседневности. Иногда к нам в руки попадают видеофайлы, воспроизвести которые с лету не удается. Почему? Правильно, потому что на компьютере не установлены соответствующие кодеки. То есть операция Воспроизведение, реализуемая программой-медиаплеером, зависит от операции Декомпрессия, реализуемой кодеком. Если спецификация операции Декомпрессия изменится, придется менять код медиаплеера, иначе он просто не сможет работать с каким-то кодеком и, в лучшем случае, завершит свою работу с ошибкой. А вот так зависимость между классами изображается в UML (рис. 4.10):

Рис. 4.10.

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

Другой вид отношений между объектами — это ассоциация. Это просто связь между объектами, по которой можно между ними перемещаться. Ассоциация может иметь имя, показывающее природу отношений между объектами, при этом в имени может указываться направление чтения связи при помощи треугольного маркера. Однонаправленная ассоциация может изображаться стрелкой. Проиллюстрируем сказанное примерами (рис. 4.11):

Рис. 4.11.

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

Рис. 4.12.

И насчет ролей, и насчет кратности на этой диаграмме все понятно — человек может вообще не работать, работать в одной или более компаниях, а вот компании в любом случае нужен хотя бы один сотрудник. Кстати, о кратности. Ассоциация может объединять три и более класса. В этом случае она называется n-арной и изображается ромбом на пересечении линий, как показано на этой диаграмме, позаимствованной нами из Zicom Mentor (рис. 4.13):

Рис. 4.13.

Ранее мы говорили, что ассоциация — это просто связь между объектами. На самом деле, в реальности связи бывают просто связями крайне редко. Обычно при ближайшем рассмотрении под ассоциацией понимается более сложное отношение между классами, например, связь типа часть-целое. Такой вид ассоциации называется ассоциацией с агрегированием. В этом случае один класс имеет более высокий статус (целое) и состоит из низших по статусу классов (частей). При этом выделяют простое и композитное агрегирование и говорят о собственно агрегациии композиции. Простая агрегация предполагает, что части, отделенные от целого, могут продолжать свое существование независимо от него. Под композитным же агрегированием понимается ситуация, когда целое владеет своими частями и их время жизни соответствует времени жизни целого, т. е. независимо от целого части существовать не могут. Примеры этих видов ассоциаций и их обозначений в UML можно увидеть на следующей диаграмме (рис. 4.14).

Рис. 4.14.

Примеры, как нам кажется, очень простые и понятные. Винчестер можно вынуть из компьютера и установить в новый компьютер или в USB-карман, т. е. существование жесткого диска с разборкой системного блока не заканчивается. А вот кнопки без окон обычно существовать не могут — с закрытием окна кнопки также исчезают.

И, наконец, еще одна важная вещь, касающаяся ассоциации. В отношении между двумя классами сама ассоциация тоже может иметь свойства и, следовательно, тоже может быть представлена в виде класса. Пример прост (рис. 4.15):

Рис. 4.15.

Действительно, перед началом трудовых отношений работник и работодатель подписывают между собой контракт, который имеет такие атрибуты, как, например, описание работ, сроки их выполнения, порядок оплаты и т. д.

А вот более сложный, но, опять-таки, взятый из реальной жизни пример моделирования отношений между классами, позаимствованный нами из Zicom Mentor (рис. 4.16):

Рис. 4.16.

И наконец, доказательство того, что UML можно использовать для чего угодно, в том числе и для записи сказок: диаграмма, описывающая предметную область сказки о Курочке Рябе и взятая с сайта конкурса шуток на UML (http://www.umljokes.com/) (рис. 4.17):

Рис. 4.17.

Узнаете рассказ, знакомый с детства?

Выводы

  • Инкапсуляция защищает внутреннее устройство объекта и реализуется путем ограничения доступа к атрибутам и операциям класса из других частей программы.
  • Обобщение позволяет повторно использовать уже существующие решения, создавая новые классы путем наследования от имеющихся классов.
  • Полиморфизм позволяет работать с группой разнородных объектов одинаковым образом, не задумываясь о различиях в реализации.
  • Инкапсуляция, наследование и полиморфизм — три кита, на которых держится ООП.
  • В любой системе между объектами существуют отношения разных типов.
  • Отношение зависимости означает, что реализация одного класса зависит от спецификации операций другого класса.
  • Ассоциация выражает отношение между несколькими равноправными объектами и может иметь направление, роли и кратность, а также изображаться в виде класса ассоциации.
  • Композиция и агрегация используются, если между объектами существуют отношения типа часть-целое, причем композиция предполагает, что части не могут существовать отдельно от целого.

Контрольные вопросы

  • Какие три принципа лежат в основе ООП?
  • Что такое интерфейс? На каком из базовых принципов ООП основан механизм интерфейсов?
  • Что такое n-арная ассоциация?
  • В чем разница между агрегацией и композицией?
  • Что такое класс ассоциации?

Статьи к прочтению:

Создание и Развитие Персонажа в Divinity: Original Sin 2


Похожие статьи: