Перейти к основному содержимому

Введение

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

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

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

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

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

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

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

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

Оценка качества декомпозиции проекта

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

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

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

Подытожим сказанное о проблемах разбиения программы на компоненты и организации их взаимодействия. Для оценки качества программного проекта нужно учитывать, кроме всех прочих, следующие два показателя:

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

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

Что принесло с собой ООП

Класс

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

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

Эффективным механизмом ослабления связанности между программными компонентами в случае объектно-ориентированной декомпозиции является так называемая инкапсуляция.

Инкапсуляция

Инкапсуляция — это ограничение доступа к данным и их объединение с методами, обрабатывающими эти данные. Доступ к отдельным частям класса регулируется с помощью специальных ключевых слов: public (открытая часть), private (закрытая часть) и protected (защищенная часть).

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

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

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

С ООП связаны еще два инструмента, грамотное использование которых повышает качество проектов: наследование классов и полиморфизм.

Наследование

Наследование — механизм получения нового класса из существующего. Производный класс создается путем дополнения или изменения существующего класса. Благодаря этому реализуется концепция повторного использования кода. С помощью наследования может быть создана иерархия родственных типов, которые совместно используют код и интерфейсы.

Полиморфизм

Полиморфизм дает возможность создавать множественные определения для операций и функций. Какое именно определение будет использоваться, зависит от контекста программы. Вы уже знакомы с одной из разновидностей полиморфизма в языке C++ — перегрузкой функций.

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

От структуры — к классу

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