Эту статью я подготовил во время дискуссий по поводу паттерна Мост, но тогда не опубликовал. Думал разобрались, так как было упомянута Domain Driven Design, и казалось, что необходимость проектирования и программирования именно в стиле ООП никем не оспариваются. Но все же со временем я столкнулся с непониманием. Это будет чисто историческая теоретическая статья. Конечно, даже без попытки обхвата всей широты темы. Но это так сказать посыл молодому разработчику, который читает по верхам и не может выбрать каких принципов и правил ему придерживаться, что первично, а что вторично.
Заглавие этой темы для многих сейчас может показать очень спорным (и скорее намерено провокационным, но для дела :) ). Но все же мы постараемся это здесь обосновать и понять какими свойствами должна обладать парадигма программирования, чтобы иметь право называться парадигмой.
Единственно прошу, если прочитали по диагонали — комментируйте сдержано.
Что нам говорит Флойд о парадигмах?
Термин «парадигма программирования» ввел Роберт Флойд (''R. W. Floyd.'' [http://www.ias.ac.in/resonance/May2005/pdf/May2005Classics.pdf The Paradigms of Programming] ''Communications of the ACM'', 22(8):455—460, 1979. Русский перевод см. в кн.: Лекции лауреатов премии Тьюринга за первые двадцать лет (1966—1985), М.: МИР, 1993.). Он в своей лекции в 1979 году говорит о следующем:
Знакомый пример парадигмы программирования — это структурное программирование, которая, кажется, доминирующей парадигмой в методологии программирования. Она разделяется на две фазы. В первой фазе, нисходящего проектирования, проблема разделяется на небольшое количество более простых подпроблем. Это постепенное иерархическое разложение продолжается пока возникнет выделенные подпроблемы, которые достаточно просты, чтобы с ними справиться непосредственно. Вторая фаза парадигмы структурного программирования влечет за собой работу вверх от конкретных объектов и функций к более абстрактным объектам и функциям, используемые всюду в модулях, произведенных нисходящим проектированием. Но парадигма структурного программирования не универсальна. Даже её самые ярые защитники признали бы, что её отдельно недостаточно, чтобы сделать все сложные проблемы легкими. Другие парадигмы высокого уровня более специализированного типа продолжают быть важными. (Это не точный перевод, а авторская компиляция на основе лекции Р. Флойда, но максимально придерживаясь его слов. Формулировки изменены и скомпонованы лишь для выделения основной мысли Р.Флойда и понятного его изложения.)
Далее он упоминает о динамическом программировании и логическом программировании, называя их также парадигмами. Но их особенностью является то, что они были развиты из специализированной предметной области, были найдены некоторые успешные алгоритмы и построены соответствующие программные системы. Далее он говорит о том, что языки программирования должны поддерживать парадигмы программирования. И при этом указывает, что парадигма структурного программирования является парадигмой более высокого уровня:
Парадигма '''даже''' более высокого уровня абстракции, чем '''парадигма структурного программирования''' — это конструирование иерархии языков, где программы на языке самого высокого уровня воздействует с абстрактными объектами, и переводят их на программы языка следующего более низкого уровня.
Особенности парадигм более высокого уровня
В данный момент, существует тенденция считать все возможные парадигмы стоящими на одном уровне, как возможные альтернативы при создании программного обеспечения. Но это не так. Парадигмы не являются взаимозаменяемыми.
Как мы видим Р. Флойд тоже различал парадигмы на более высокоуровневые, а более специализированные. Какие же особенности парадигм позволяют говорить, что они более высокоуровневые? Конечно, это возможность их применения к различным предметным задачам. Но что делает парадигмы, применимым к различным предметным задачам? Конечно, вопрос тут не в особенностях предметной задачи, которую можно решить тем или иным подходом. Все парадигмы, которые предлагают создавать алгоритмы тем или иным специализированным способом — вовсе не являются парадигмами, это лишь особый подход в рамках парадигмы более высокого уровня.
А парадигм высокого уровня существуют только две: структурное программирование и еще более высокого уровня объектно-ориентированное программирование. Причем эти две парадигмы на высоком уровне противоречат друг другу, а на низком уровне, уровне построения алгоритмов совпадают между собой. А уже подходы (парадигмы низкого уровня), такие как логический, динамический, функциональный вполне могут использоваться в рамках парадигмы структурного программирования, а некоторые появившиеся специализации — аспектное, агентно-ориентированное, событийно-ориентированное, используется в рамках парадигмы объектно-ориентированного программирования. Таким образом, это не означает, что программистам нужно знать только одну, или две высокоуровневые парадигмы, но и знание других подходов будет полезно, когда будет решаться более специализированная, низкоуровневая задача. Но в тоже время, когда приходится проектировать программное обеспечение, нужно начинать с парадигм более высокого уровня, и при необходимости переходить к более низкоуровневым. Но если возникает проблема выбора, каким принципам отдать предпочтение, никогда принципы парадигм более низкого уровня не должны главенствовать над принципами парадигм более высокого уровня. Так, например, принципы структурного программирования не должны соблюдаться в ущерб принципам объектно-ориентированного программирования, а принципы функционального или логического программирования, не должны нарушать принципы структурного программирования. Единственное, исключение — это быстродействие алгоритмов, которое есть проблема оптимизации кода компиляторами. Но так как построить совершенные компиляторы не всегда удается, а интерпретация парадигм более высокого уровня, конечно же более сложна, чем низкого уровня, иногда приходится идти на несоблюдение принципов парадигм высокого уровня.
Но возвратимся к нашему вопросу: что делает парадигмы, применимым к различным предметным задачам? Но чтобы на него ответить нам нужно сделать исторический экскурс.
Основы парадигмы структурного программирования
Мы знаем, что идеи о структурном программировании возникли после доклада Э. Дейкстры еще в 1965 году, где он обосновал отказ от оператора GOTO. Именно этот оператор превращал программы в неструктурированные (Спагетти-код), а Дейкстра доказал, что возможно написать программы без использования этого оператора в результате чего программы станут структурными.
Но одно дело теория, а другое практика. В этом смысле, представляет интерес рассмотреть, какая ситуация была к 1975 году. Это хорошо видно по книге Э. Йодана ([http://www.az-design.ru/index.shtml?Projects&AzBook&src/005/02YE000 Йодан Э. Структурное проектирование и конструирование программ, 1975]). Рассмотреть это важно потому, что сейчас спустя более 30 лет, принципы уже хорошо известные тогда, сейчас переоткрываются, и возводятся в новый ранг. Но при этом теряется исторический контекст, и иерархия важности этих принципов, что первично, а что вторично. Эта ситуация аморфности очень хорошо характеризует сегодняшнее состояние программирования.
Но что было тогда? Как описывает Йодан, все начинается с ответа на вопрос: «Что значит написать хорошую программу?». Вот первый критерий, на какие вопросы должна отвечать парадигма программирования высокого уровня. Если она не отвечает прямо на этот вопрос, а рассказывает вам как можно получить некоторые интересные характеристики вашей программы, то вы имеете дело с парадигмой низкого уровня — подходом при программировании.
На заре зарождения программирования, существовал такой подход к оценке программистов по скорости написания программ. А значит ли это, что он пишет хорошие программы? Пользуется ли он особым расположением и уважением руководства? Если ответ на последний вопрос утвердительный, то все вопросы совершенствования программирования представляют скорее академический интерес. Но руководство может также заметить, что некоторые суперпрограммисты могут делать программы очень быстро или писать очень эффективные программы, но эти программы иногда остаются неоформленными, их невозможно понять, сопровождать или модифицировать. А на последние тоже не мало тратится времени.
Примечателен, довольно характерный спор программистов:
* Программист А: “Моя программа в десять раз быстрее вашей, и она занимает в три раза меньше памяти!”
* Программист Б: “Да, но ваша программа не работает, а моя — работает!”
Но программы постоянно усложняются и поэтому нам недостаточно того, что программа просто работает. Нужны определенные методы верификации правильности работы программы и самого программиста. Причем это не тестирование программы, а проведение некоторой систематической процедуры проверки именно правильности программы в смысле её внутренней организации. То есть уже тогда, говоря современным языком, говорили о ревизии кода (Code review).
Кроме того, уже тогда говорили о гибкости программы — о простоте ее изменения, расширения и модификации. Для этого необходимо постоянно отвечать на вопросы определенного вида. “Что будет, если мы захотим расширить эту таблицу?”, “Что произойдет, если однажды мы захотим определить новую программу изменений?”, “А что, если нам придется изменить формат таких-то выходных данных?”, “Что будет, если кто-то решит вводить данные в программу другим способом?”.
Также говорили о важности спецификаций интерфейсов, т.е. формализованный подход к спецификации входов, функций и выходов, которые должны быть реализованы каждым модулем.
Кроме того, центральное внимание уделяли размеру и неизменности модуля. Причем что касается неизменности модуля, то она рассматривалась не целиком, а с выделением отдельных факторов:
1. Логическая структура программы, т.е. алгоритм. Если вся программа зависит от некоторого специального подхода, то в скольких модулях потребуется внести изменения при изменении алгоритма?
2. Аргументы, или параметры, модуля. Т.е. изменение спецификации интерфейсов.
3. Внутренние переменные таблиц и константы. Многие модули зависят от общих таблиц, если изменяется структура таких таблиц, то мы можем ожидать, что модули также изменятся.
4. Структура и формат базы данных. В большей степени эта зависимость аналогична зависимости от общих переменных и таблиц, упомянутой выше, с той разницей, что с практической точки зрения базу данных удобнее считать независимой от программы.
5. Модульная структура управления программой. Некоторые пишут модуль не особенно задумываясь над тем, каким образом он будет использоваться. Но если изменились требования. Какую часть логической структуры модуля нам придется изменить?
Эти и множество других аспектов (которые мы тут не рассмотрели) в целом и формулируют представление о структурном программировании. Забота об этих аспектах и делает структурное программирование парадигмой высокого уровня.
Основы парадигмы объектно-ориентированного программирования
Как мы могли видеть все принципы организации хороших программ рассматриваются в структурном программировании. Появление еще одного или группы неизвестных до этого принципов написания хороших программ могло бы изменить парадигму? Нет. Это всего лишь расширило бы способы и идеологию написания структурированных программ, т.е. парадигму структурного программирования.
Но если парадигмы высокого уровня призваны отвечать на вопрос как написать хорошую программу, а появление нового технического приема, или рассмотрение новых факторов не дает выйти за границы структурного программирования (т.к. оно и останется структурным, независимо от числа приемов и факторов), то что же тогда позволит выйти за границы этой парадигмы. Действительно, как известно из науки вообще парадигмы так быстро не меняются. Научные революции случаются редко, когда предшествующая парадигма уже на практике из имеющихся теоретических воззрений просто не может объяснить происходящих явлений. Аналогичную ситуацию мы имеем при смене парадигмы со структурной до объектно-ориентированной.
Уже признано, что причиной появления объектно-ориентированной парадигмы стала необходимость писать все более и более сложные программы, в то время как парадигма структурного программирования имеет некий придел, после которого развивать программу становится невыносимо сложно. Вот, например, что пишет Г. Шилдт:
На каждом этапе развития программирования появлялись методы и инструментальные средства для “обуздания” растущей сложности программ. И на каждом таком этапе новый подход вбирал в себя все самое лучшее из предыдущих, знаменуя собой прогресс в программировании. Это же можно сказать и об ООП. До ООП многие проекты достигали (а иногда и превышали) предел, за которым структурный подход к программированию оказывался уже неработоспособным. Поэтому для преодоления трудностей, связанных с усложнением программ, и возникла потребность в ООП. ([http://www.williamspublishing.com/Books/978-5-8459-1684-6.html Герберт Шилдт, C# 4.0 полное руководство, 2011])
Чтобы понять причину, почему именно объектно-ориентированное программирование, позволило писать более сложные программы и практически убрать проблему возникновения предела сложности, обратимся к одному из основоположников ООП — Гради Бучу ([http://www.helloworld.ru/texts/comp/other/oop/index.htm Гради Буч, Объектно-ориентированный анализ и проектирование]). Свое объяснение ООП он начинает с того, что значит сложность и какие системы можно считать сложными. То есть целенаправленно подходит к вопросу написания сложных программ. Далее переходит к вопросу связи сложности и человеческих возможностей понять эту сложность:
Существует еще одна главная проблема: физическая ограниченность возможностей человека при работе со сложными системами. Когда мы начинаем анализировать сложную программную систему, в ней обнаруживается много составных частей, которые взаимодействуют друг с другом различными способами, причем ни сами части системы, ни способы их взаимодействия не обнаруживают никакого сходства. Это пример неорганизованной сложности. Когда мы начинаем организовывать систему в процессе ее проектирования, необходимо думать сразу о многом. К сожалению, один человек не может следить за всем этим одновременно. Эксперименты психологов, например Миллера, показывают, что максимальное количество структурных единиц информации, за которыми человеческий мозг может одновременно следить, приблизительно равно семи плюс-минус два. Таким образом, мы оказались перед серьезной дилеммой. '''Сложность программных систем возрастает, но способность нашего мозга справиться с этой сложностью ограничена. Как же нам выйти из создававшегося затруднительного положения?'''
Затем он говорит о декомпозиции:
Декомпозиция: алгоритмическая или объектно-ориентированная? Какая декомпозиция сложной системы правильнее — по алгоритмам или по объектам? В этом вопросе есть подвох, и правильный ответ на него: важны оба аспекта. Разделение по алгоритмам концентрирует внимание на порядке происходящих событий, а разделение по объектам придает особое значение агентам, которые являются либо объектами, либо субъектами действия. Однако мы не можем сконструировать сложную систему одновременно двумя способами. Мы должны начать разделение системы либо по алгоритмам, либо по объектам, а затем, используя полученную структуру, попытаться рассмотреть проблему с другой точки зрения. Опыт показывает, что полезнее начинать с объектной декомпозиции. Такое начало поможет нам лучше справиться с приданием организованности сложности программных систем.
Таким образом, он также отдает предпочтение объектно-ориентированным принципам над структурными принципами, но подчеркивает важность обоих. Другими словами, структурные принципы должны подчиняться объектно-ориентированным принципам для того, чтобы человеческий мозг мог справится со сложностью возникающих задач. Далее он подчеркивает важность модели:
Важность построения модели. Моделирование широко распространено во всех инженерных дисциплинах, в значительной степени из-за того, что оно реализует принципы декомпозиции, абстракции и иерархии. Каждая модель описывает определенную часть рассматриваемой системы, а мы в свою очередь строим новые модели на базе старых, в которых более или менее уверены. Модели позволяют нам контролировать наши неудачи. Мы оцениваем поведение каждой модели в обычных и необычных ситуациях, а затем проводим соответствующие доработки, если нас что-то не удовлетворяет. Полезнее всего создавать такие модели, которые фокусируют внимание на объектах, найденных в самой предметной области, и образуют то, что мы назвали объектно-ориентированной декомпозицией.
Теперь, если посмотреть внимательнее, оказывается, что объектно-ориентированная парадигма есть не что иное как моделирование вообще, наиглавнейший аспект которого наиболее четко выразил С. Лем:
Моделирование — это подражание Природе, учитывающее немногие ее свойства. Почему только немногие? Из-за нашего неумения? Нет. Прежде всего потому, что мы должны защититься от избытка информации. Такой избыток, правда, может означать и ее недоступность. Художник пишет картины, но, хотя мы могли бы с ним поговорить, мы не узнаем, как он создает свои произведения. О том, что происходит в его мозгу, когда он пишет картину, ему самому неизвестно. Информация об этом находится в его голове, но нам она недоступна. Моделируя, следует упрощать: машина, которая может написать весьма скромную картину, рассказала бы нам о материальных, то есть мозговых, основах живописи больше, чем такая совершенная «модель» художника, какой является его брат-близнец. Практика моделирования предполагает учет некоторых переменных и отказ от других. Модель и оригинал были бы тождественны, если бы процессы, происходящие в них, совпадали. Этого не происходит. Результаты развития модели отличаются от действительного развития. На это различие могут влиять три фактора: упрощенность модели по сравнению с оригиналом, свойства модели, чуждые оригиналу, и, наконец, неопределенность самого оригинала. (фрагмент произведения «Сумма технологий», Станислав Лем, 1967)
Таким образом, С. Лем говорит о абстрагировании как основе моделирования. В тоже время абстрагирование и есть главный признак объектно-ориентированной парадигмы. Г. Буч по этому поводу пишет:
Разумная классификация, несомненно, — часть любой науки. Михальски и Степп утверждают: «неотъемлемой задачей науки является построение содержательной классификации наблюдаемых объектов или ситуаций. Такая классификация существенно облегчает понимание основной проблемы и дальнейшее развитие научной теории». Почему же классификация так сложна? Мы объясняем это отсутствием «совершенной» классификации, хотя, естественно, одни классификации лучше других. Кумбс, Раффья и Трал утверждают, что «существует столько способов деления мира на объектные системы, сколько ученых принимается за эту задачу». Любая классификация зависит от точки зрения субъекта. Флуд и Кэрсон приводят пример: «Соединенное Королевство… экономисты могут рассматривать как экономический институт, социологи — как общество, защитники окружающей среды — как гибнущий уголок природы, американские туристы — как достопримечательность, советские руководители — как военную угрозу, наконец, наиболее романтичные из нас, британцев — как зеленые луга родины».
И дальше он рассказывает, о выборе ключевых абстракций, тех которые нам необходимы:
'''Поиск и выбор ключевых абстракций.''' Ключевая абстракция — это класс или объект, который входит в словарь проблемной области. '''Самая главная ценность ключевых абстракций заключена в том, что они определяют границы нашей проблемы''': выделяют то, что входит в нашу систему и поэтому важно для нас, и устраняют лишнее. Задача выделения таких абстракций специфична для проблемной области. Как утверждает Голдберг, «правильный выбор объектов зависит от назначения приложения и степени детальности обрабатываемой информации».
Как мы уже отмечали, определение ключевых абстракций включает в себя два процесса: открытие и изобретение. Мы открываем абстракции, слушая специалистов по предметной области: если эксперт про нее говорит, то эта абстракция обычно действительно важна. Изобретая, мы создаем новые классы и объекты, не обязательно являющиеся частью предметной области, но полезные при проектировании или реализации системы. Например, пользователь банкомата говорит «счет, снять, положить»; эти термины — часть словаря предметной области. Разработчик системы использует их, но добавляет свои, такие, как база данных, диспетчер экрана, список, очередь и так далее. Эти ключевые абстракции созданы уже не предметной областью, а проектированием.
Наиболее мощный способ выделения ключевых абстракций — сводить задачу к уже известным классам и объектам.
Итак, объектно-ориентированная парадигма становится парадигмой высокого уровня, и главенствует над принципами парадигмы структурного программирования, так как занимается моделированием реальности, строит модели предметных областей на языке специалистов этих областей. Если Вы этим пренебрежете ради написания хорошей программы, которую станет легко модифицировать, расширять, в которой будут четкие интерфейсы и независимые модули, вы возвратитесь на уровень парадигмы структурного программирования. Ваша программа будет всем хороша, но её нельзя будет понять, так как она не будет соответствовать реальности, она будет объяснена в терминах только известных вам, а специалист знающий предметную область не сможет без вашей помощи разобраться в программе. В конце концов, сложность будет понижаться в очень узком диапазоне, хотя вы и организовали хорошую программу. Но именно программу, а не модель. Отсутствие модели, или лишь её поверхностное представление, «взорвет» вашу хорошую программу изнутри, и не даст дальше развивать и сопровождать её в дальнейшем. Когда вы вводите классы, абстракций которых не существует, когда эти классы чисто системные и не имеют ничего общего с предметной областью, когда они введены лишь для упрощения потоков взаимодействия других классов — ваше программное обеспечение становится «с бородой», и если путем рефакторинга не следить за такими участками в один прекрасный момент развитие вашего ПО остановится, и станет не возможным — вы достигните предела структурного программирования (а вам казалось, используя классы и объекты вам это не грозит?).