Эволюция программного проекта и ООП

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

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

Стало интересно выполнить анализ применимости этих понятий для общепринятых парадигм программирования, например для ООП. Хорошо, если результат этой работы будет полезен и Вам.

image

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

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

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


Развитие программного проекта

Что такое проект без необходимости развития? Такие проекты изредка встречаются и в основном характеризуются быстрой сдельной оплатой без возникновения последующих обязательств со стороны программиста, например:


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

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

Во всех остальных ситуациях программист, минимизируя свой труд, должен развивать структурно-сложный проект, то есть:


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

Если поискать аналогии развитию программного проекта, то можно вспомнить эволюцию биологического вида.

Программный проект подобен сложному развивающемуся "организму".
Программист для этого организма - изменчивость и естественный отбор в одном лице.
Зарплата программиста совместно с его навыками - скорость эволюции.

Труд программиста не легок, но у программиста есть «помощник». Этот помощник спрятан где-то глубоко в устройстве нашего мира, в котором есть две особенности:


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

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

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

Здесь попробую выписать примеры использования абстрагирования из разных областей.


Абстракция Алгоритмы Область применения
Натуральные числа Алгоритмы количественных расчетов Задачи учёта хозяйственных ценностей
Масса-характеристика материального тела Алгоритмы-операций сравнения количества вещества Задачи сравнения ценности товара, не поддающегося счету
Интерфейс с операциями для коллекции элементов: полный обход, сравнение и обмен позиций Алгоритмы сортировки коллекции Программирование
Интерфейс одинаковых операций для «концевого узла» и «узла ветвления» в дереве Алгоритмы на основе шаблона проектирования «Компоновщик» Разработка сложного программного проекта
Ключевое понятие «Работник» Формулировки в разделе «Трудовой договор» Трудовой кодекс


Строительный блок программного проекта

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

Компонент — участок кода (процедура, класс, deployment component и т.д.):


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


Закономерности в развитии программного проекта

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


  1. Утверждения, описывающие свойства компонента.
    1.1. Корректно написанный компонент обязательно используется и чаще несколько раз.
    1.2. В каждом месте использования компонента от него ожидается неизменное поведение, приводящее к повторяемому результату.
    1.3. При использовании компонента в нескольких местах результат должен удовлетворять каждому месту использования.
    1.4. Поведение, заложенное в компоненте, формирует ограничения для мест использования этого компонента.
    1.5. В каждом месте использования компонента могут быть задействованы все его ограничения.
    1.6. Любое изменение компонента изменяет его ограничения и требует проверки всех мест его использования, что порождает затраты времени программиста.
    1.7. Компонент целесообразно записывать в виде кода в одном экземпляре, то есть необходимо устранять дублирование одинакового кода. Это уменьшит количество правок при внесения изменения в компонент.
  2. Утверждения, описывающие закономерности в реализации программистом новой задачи.
    2.1 Вариант реализации новой задачи целесообразно выбирать минимизируя затраты времени программиста.
    2.2. Для реализации новой задачи программист может добавить новые компоненты или изменить поведения старых компонентов.
    2.3. Добавление компонента в основном требует проверки только в месте нового использования, и порождает минимальные затраты времени программиста.
    2.4. Обусловленное новой задачей изменение поведения компонента, согласно утверждению [1.6], требует проверки в месте нового использования и во всех местах старого использования, что порождает дополнительные затраты времени программиста по сравнению с ситуацией в утверждении [2.3]. В случае опубликованного компонента это требует работы всех программистов, использовавших измененный компонент.
  3. Утверждения, описывающие закономерности во взаимодействии универсальных алгоритмов и их специализаций:
    3.1. Существует возможность написать базовый компонент (название вводится по аналогии с базовым классом и далее для краткости будем использовать слово »база»). База выполняет только самые главные черты некоторого универсального алгоритма.
    3.2. Существует возможность написать компонент-специализацию (далее для краткости будем использовать слово »специализация»). Специализация дополняет универсальный алгоритм базы, делая его применимым в конкретной области использования.
    3.3. База, как следует из утверждений [3.1], [3.2], имеет меньшую сложность и меньше ограничений в применении, чем специализация.
    3.4. Согласно утверждению [1.7] специализацию целесообразно разрабатывать без дублирования кода универсального алгоритма из базы.
    3.5. Места использования базы не требуют проверки после внесения изменений в корректно сформированную специализацию.


Понятия объектно-ориентированного программирования

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


Класс, Объект

Данные понятия ООП закрепляют целесообразность использования специального вида компонента, описываемого совокупностью некоторых внутренних данных и методов работы с этими данными. Все утверждения группы [1] и [2] транслируются в ООП, для которого термин компонент заменяется понятием класс.

При этом, на первый взгляд, отношения класса и объекта исчерпываются группой утверждений [3], в которой база заменяется понятием класс, а реализация — понятием объект. Причем реализация получается динамическая, то есть изменяемая в процессе исполнения программы.


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

Понятие »инкапсуляция» можно рассмотреть с двух «сторон».

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

Вторая сторона понятия »инкапсуляция» — это сокрытие внутренней реализации компонента. Это сокрытие возможно с использованием понятий база и реализация, описанных в группе утверждений [3]. Для этого публичные методы класса отождествляются с базой, а приватные и защищенные методы класса — с реализацией. В местах использования используются ограничения, формируемые базой, и потому появляется возможность производить изменения в реализации, не касающиеся базовых ограничений. И эти изменения реализации не нужно проверять в местах использования базы [3.5], что обеспечивает минимизацию трудозатрат программиста.

Примечательно, что понятие »инкапсуляция» имеет аналогию в биологии. Этот процесс первой своей стороной схож с биологическими функциями «Клеточной мембраны».


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

Понятия »наследование» продолжает закреплять важность использования связки база + реализация. Для этого в группе утверждений [3] необходимо методы родительского класса отождествить с базой, а методы класса-наследника отождествить с реализацией.

В своей реализации понятие »наследование» позволяет использовать утверждение [2.3], то есть использовать дополнение кода вместо его изменения и дублирования. При этом необходимо исключить дублирование базового алгоритма. Однако, у подхода, использующего наследование для специализации универсального алгоритма, есть существенный минус. Этот недостаток — наличие двух сильно-связных компонентов, которые тяжело изменять независимо. Эти связи-зависимости порождаются отношением родитель-наследник.

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


База Реализация Область применения
Публичные методы класса Приватные методы класса Инкапсуляция
Защищенные методы родительского класса Методы класса-наследника Наследование
Интерфейс динамической библиотеки Функционал динамической библиотеки Компонент=динамическая библиотека
Шаблонные (обобщенные) методы и классы (template, generic) Инстанцирование шаблона с указываемыми аргументами Обобщенное программирование
Универсальные методы, принимающие делегаты Специализация методов указанием конкретных процедур обработки Процедуры сортировки или формирования дерева, с указанием метода оценки порядка элементов
Классы, предусматривающие взаимодействие с шаблоном «Посетитель» Формирование «Посетителя» с требуемым функционалом Шаблон проектирования «Посетитель»
Панель управления АЭС Совокупность автоматики и оборудования АЭС Сокрытие сложности системы от оператора АЭС

Параллельно, подмечаю, что для понятия »наследование» из ООП, так же можно найти аналогию в процессах биологической эволюции. В биологии для этого используется термин »Наследственность».


Полиморфизм

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

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

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


Заключение

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

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

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

Спасибо Вам за внимание.


Отзывы

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


Ссылки

Под редакцией Борисовой М.В.

© Habrahabr.ru