«Серебряной пули нет, — сказал в прошлом веке Фредерик Брукс, надолго остудив пыл благородных рыцарей от программирования в борьбе с Драконом сложности. — Я не очень верю в драконов, зато верю в то, что сложность — понятие субъективное. Одно из определений слова «сложный» — трудный, запутанный. То, что для одного человека является трудным и запутанным, для другого может быть легким и ясным». В природе нет ничего трудного и запутанного. Может быть, программные системы, которые мы разрабатываем, мы сами делаем сложными и запутанными?
Программа — это задача + модель + алгоритм + структура данных. Программа создается для того, чтобы решить определенную задачу, а модель описывает то, что должна сделать программа для решения поставленной задачи, но не как она это должна сделать. Ключевым понятием в определении программы является задача. Как неразумно обсуждать техническую систему в отрыве от задачи, которую она решает, так бессмысленно рассматривать программу вне целей, для которых она создается. Для решения разных задач даже в одной предметной области будут созданы разные модели и разработаны разные программы. Созданные в мышлении человека модели предметной области могут относиться как к явлениям реального мира (например, движение космического объекта в гравитационном поле и атмосфере Земли), так и к таким понятиям, как интернет-магазин. Модель — это ментальный образ будущего инструмента, который позволит нам решить поставленную задачу. Для расчета траекторий межпланетных перелетов достаточно моделировать гравитационное поле как суперпозицию точечных масс Солнца и планет. А решение отдельных задач глобальной спутниковой навигационной системы (ГЛОНАСС/GPS) невозможно без привлечения моделей времени и пространства из общей теории относительности. Итак, программа — это записанное на понятном некоторому вычислителю языке решение стоящей перед нами задачи.
Об объектно-ориентированном подходе
История развития языков программирования схожа с историей развития человеческой речи. Существует мнение, что первые высказывания человека состояли исключительно из требований помощи и обязательно содержали глаголы в повелительном наклонении («дай!», «неси!», «ломай!», «режь!», «тяни!» и т. п.). Причем эти команды сопровождались жестами, которые точно указывали, к чему конкретно это действие должно быть применено, что очень похоже на команды языков программирования — например, ассемблеров с точным указанием адресов памяти или регистров.
Человек вначале научился отличать одну практическую ситуацию, взятую в целом, от другой. Выделение отдельных элементов этих ситуаций (предметов, над которыми совершаются действия, действий, которые совершаются над предметами) осуществлялось позже — по мере того как в практической деятельности человек все больше знакомился с окружающими его вещами, познавал их свойства и их отношения друг к другу и к самому человеку. Постепенно человек начал выделять из конкретной ситуации объект действия (данные) и само действие (функции). Овладение способностью выделять объекты действия было настоящей революцией в умственном развитии первобытного человека. А это уже очень похоже на объектно-ориентированный подход (ООП), применяемый в программировании. При помощи естественного языка человек материализует свои ментальные модели мира, чтобы передать их другому человеку. При помощи ООП программист материализует свои ментальные модели программного продукта, чтобы передать их на исполнение вычислителю. Но действительно ли ООП — это главное русло нашей реки?
Объектно-ориентированный подход к построению ментальных моделей физического мира имеет более чем двухтысячелетнюю историю успешного применения и берет начало еще в трудах Аристотеля. В мире Аристотеля существуют только единичные и конкретно определенные вещи с заданным набором свойств и отнесенные к одной и только одной категории. Да, теперь мы уже научились говорить не просто «неси», а «неси дрова» или «неси камень». В своем развитии языки программирования остановились на том, что научились различать объекты (дрова и камень), но не научились выделять действия над ними. И с точки зрения языка программирования men.carry (firewood) и men.carry (stone) будут разными языковыми единицами, если только объекты firewood и stone не имеют общего предка. Просто реализация этими объектами интерфейса «то, что может носить человек» нас не выручит, поскольку это будут две реализации, а следовательно, и единиц исходного кода тоже будет две.
Здесь скрыто одно из основных ограничений ООП, которое делает наши программы сложнее, чем они могли бы быть, заставляя нас использовать костыли в виде паттернов проектирования, чтобы компенсировать врожденную хромоту ООП. Когда мы пытаемся определить, подходит нам конкретный дизайн программной системы или нет, мы не можем рассматривать данное решение изолированно. Мы должны рассматривать его с точки зрения разумных предположений о том, как будет использоваться данный дизайн впоследствии. Если перевести этот тезис на язык «дров» и «камней», то это будет звучать примерно так. Проектируя программные объекты «дрова» и «камень», мы должны быть в курсе планов Господа по совершенствованию программной системы. А именно — мы должны предполагать, что Он не ограничится созданием дров и камней, а на шестой день сотворения мира создаст человека, который их будет перетаскивать.
Другое ограничение ООП заключается в том, что каждый объект принадлежит только одной иерархии («is а») классов, пусть даже с возможностью множественного наследования, и имеет раз и навсегда заданный набор свойств. Например, красная роза — это цветок, а цветок — это растение. Это противоречит реальности, в которой объекты могут эволюционировать, приобретать новые свойства и утрачивать ранее существовавшие. Например, роза может стать товаром, а потом подарком. Наследником какого класса должна быть роза-подарок? Роза-товар или роза-цветок? Другой пример. Человек рождается с очень ограниченным набором свойств: возраст, вес, рост, пищать, питаться и портить памперсы. Время идет, и он приобретает новые наборы свойств: ученик школы, покупатель, пассажир, студент, наемный работник, предприниматель, родитель и т. д. А возможно, и не приобретает. Например, не каждый человек становится предпринимателем. Или утрачивает. Следовательно, один и тот же объект должен иметь возможность принадлежать разным классам, и этот набор классов должен быть динамическим, изменяясь в ходе эволюции объекта и самой программной системы. На набор классов, к которым относится объект, как правило, накладываются ограничивающие взаимосвязи. Например, чтобы стать солдатом, человек должен достичь 18 лет.
Еще одна странность ООП. «Поведение — это то, как объект действует и реагирует; поведение выражается в терминах состояния объекта и передачи сообщений» [1]. Но если я моделирую столкновение двух автомобилей, то какой из них и какие получает и передает сообщения?
«Состояние объекта характеризуется перечнем (обычно статическим) всех свойств данного объекта и текущими (обычно динамическими) значениями каждого из этих свойств» [1]. Но принадлежит ли свойство объекта самому объекту? Нет. Например, мы говорим: данное яблоко — зеленое. Но что это означает на самом деле? Это значит, что если мы направим источник света, близкого по спектру к солнечному свету, то данное яблоко поглотит все длины волн, кроме тех, которые соответствуют диапазону зеленого цвета, и наблюдатель, который способен воспринимать весь спектр солнечного света, увидит только отраженный зеленый свет. Если источник или наблюдатель имеет другой диапазон, например, инфракрасный, то цвет яблока будет черным. Таким образом, свойство не есть неотъемлемая характеристика объекта, свойство — это возможное проявление объекта при его взаимодействии с другими объектами. Например, свойство предмета «плавать по поверхности» может проявляться во взаимодействии с водой и не проявляться во взаимодействии со спиртом.
Мы говорим: швабра «состоит из» (агрегирует) щетки и палки. Но что это означает? Если мы рассматриваем швабру как предмет, способный перемещаться в пространстве, то в этом случае агрегация никак не проявляется — мы рассматриваем швабру как атомарный объект, и нам интересны лишь ее общая масса и размеры. Но если мы станем нагружать швабру и испытывать ее на прочность, то агрегация проявится как взаимодействие ее структурных составляющих (щетки и палки), и результат испытания будет зависеть от того, происходит это взаимодействие посредством вбитых гвоздей или посредством пазов, шипов и клея. Поэтому агрегация есть также взаимодействие объектов.
Если мы говорим, что объект a является мужем объекта b, то мы декларируем, что объекты a и b связаны ассоциацией. Как же может проявляться эта связь? Во-первых, сама эта связь есть результат взаимодействия трех объектов: a, b и объекта с (некоего регистратора, где эта связь рождается, и там же она, кстати, может и исчезнуть). Во-вторых, она может проявляться во взаимодействии объектов, например a и b: совместное расходование семейного бюджета или рождение и воспитание общего ребенка. Нет смысла спрашивать a.isMarried (b), он может и соврать. Но вполне осмысленна функция isMarried (a, b, c). Следовательно, связи, как и свойства, есть возможные взаимодействия между объектами.
«Идентичность — свойство объекта, которое отличает его от всех других объектов» [1]. Что должно соответствовать в действительности этому утверждению? Допустим, я перекрасил свой автомобиль. Для меня он, безусловно, остался тем же самым, но для ГИБДД это будет совсем другой объект. А когда автомобиль перестанет быть тем же самым? Идентичность возникает лишь при связывании конкретных объектов. Каждая швабра будет состоять из вполне конкретной щетки и палки. У каждого мужа будет вполне конкретная жена, а у каждого клиента — свой экземпляр счета в банке.
Итак, в нашем ментальном мире нет объектов и их свойств, а есть структуры и их взаимодействия.
Онтология Витгенштейна
Мир Людвига Витгенштейна [2], австрийского философа и логика, представителя аналитической философии, не является миром вещей, как у Аристотеля, для которого сущность языка — существительное. Язык, согласно Витгенштейну, представляет собой не набор имен или свойств, а состоит из предложений, в которых выражаются взаимодействия предметов. В мире Витгенштейна первичны факты — взаимодействия между предметами, а вещи определяются совокупностью их возможных взаимодействий. Такой взгляд на мир хорошо соответствует современной теории категорий, в которой морфизмы — задают взаимодействия объектов, а функторы — определяют эволюцию модели во времени.
В отличие от статичного мира Аристотеля, мир Витгенштейна динамичен — в ходе эволюции мира факты могут добавляться и представление о вещах будет меняться. Благодаря такой трактовке мира, вещь выступает не как нечто данное, застывшее, вполне определенное, а как некоторая сущность с нечеткими, изменчивыми границами. Программные системы динамически эволюционируют подобно миру Витгенштейна — в ходе развития программной системы разработчики добавляют объектам новые возможные взаимодействия, которые расширяют наборы их свойств и связей.
Витгенштейн ввел понятие «языковая игра» — единое целое: язык и действия, с которыми он переплетен. «Сколько же существует типов предложения? Скажем, утверждение, вопрос, повеление? Имеется бесчисленное множество таких типов, и бесконечно разнообразны виды употребления всего того, что мы называем «знаками», «словами», «предложениями». И эта множественность не представляет собой чего-то устойчивого, раз и навсегда данного…». Разве мы не играем в подобную языковую игру, когда описываем модель предметной области при помощи пользовательских историй?
О предметно-ориентированном языке
В будущем среды программирования станут в основном похожими на САПР со встроенным языком DSL (Domain Specific Language), а созданием прикладных программных систем будут заниматься специалисты в конкретной области — например, инженер будет собирать программную модель для исследования летных качеств нового самолета из готовой элементной базы: фюзеляжа, крыльев, двигателей, соединительных креплений и моделей их взаимодействия с набегающим потоком. Означает ли это, что для каждой предметной области мы должны создавать свой уникальный DSL? И да, и нет. Да, в том смысле, что практически во всех устоявшихся областях профессиональной деятельности людей, например математике, медицине, биологии, существует свой специфический язык, поэтому в каждой предметной области нам придется создавать свой DSL для описания конкретных моделей решения специфических задач. Нет, потому что профессиональные языки строятся на основе единого синтаксиса естественного языка и должен существовать универсальный синтаксис для описания ментальных моделей, основанный на общих законах человеческого мышления.
Попытки создания универсального синтаксиса DSL предпринимались не раз [3–5], однако мне неизвестны примеры серьезного практического применения предложенных подходов. Может быть, это происходит потому, что в них пытаются моделировать статический мир объектов Аристотеля? Вполне возможно, что основой для универсального синтаксиса DSL могут стать динамический мир взаимодействий Витгенштейна и категорный подход, отражающие фундаментальные особенности нашего мышления. DSL подобно ДНК должен описывать:
- сценарии сборки модели из готовых компонентов;
- инициализацию начального состояния;
- законы эволюции модели во времени.
Главным строительным блоком языка должно стать взаимодействие, в котором проявляются свойства объектов, изменяются их состояния и рождаются новые экземпляры. Каждый объект в ходе эволюции может быть дополнен новым набором потенциальных взаимодействий, и это дополнение не должно затрагивать ранее созданный код. Появление «человека» в мире «камней» и «дров» при таком подходе приведет к созданию нового взаимодействия carry (carrier, thing, space, gravitation). Элемент carrier должен реализовывать взаимодействия take (carrier, thing) и move (carrier, thing, space). А элемент thing должен реализовывать взаимодействие weight (thing, gravitation) и shape (thing, space). В этом случае наша модель действия «нести» получится достаточно универсальной. Метод не будет меняться от того, что человек реализует взаимодействие take руками, а подъемный кран — специальным захватом, поэтому можно легко пополнять программную систему любыми элементами, которые реализуют необходимые взаимодействия для carrier, thing, и не понадобится менять универсальную функцию.
***
Программирование — новый вид человеческой деятельности, которая по ошибке была отнесена к инженерии. Инженерия — это там, где применяются законы естественных наук для конструирования новых продуктов, а в разработке ПО еще не открыты свои законы Ньютона или хотя бы сопромат, которые помогли бы проектировать и обосновывать правильность архитектуры новой нетривиальной программной системы. Программирование, скорее, гуманитарная дисциплина, и серьезных продвижений в ее теоретическом основании можно добиться, лишь используя достижения гуманитарных наук: философии, психологии, лингвистики, семиотики и др.
Литература
- Буч Гради. Объектно-ориентированный анализ и проектирование с примерами приложений на С++. – М: Бином, 1998.
- Витгенштейн Л. Философские исследования. – Кембридж, 1945.
- Simonyi Charles, Intentional Programming . From Wikipedia, the free encyclopedia.
- Czarnecki Krzysztof, Eisenecker Ulrich. Generative Programming: Methods, Tools, and Applications. Addison Wessley, 2000.
- Dmitriev Sergey. Language Oriented Programming: The Next Programming Paradigm, 2004.
Сергей Архипенков (sergey@arkhipenkov.ru) — независимый эксперт (Москва). Статья подготовлена по материалам доклада автора на конференции «Разработка ПО – 2012» (Central and Eastern European Software Engineering Confrence in Russia, CEE-SECR).