-►
Вычислительные машины и программное обеспечение
УДК 004.432
Г.С. Петросян
анализ объектно-ориентированной парадигмы
И ПОИСК ЛУЧШЕЙ АЛЬТЕРНАТИВЫ
Объектно-ориентированное программирование (ООП) в настоящее время является доминирующей парадигмой программирования. В данной статье произведен анализ причин этого успеха, выявлены практические задачи, решаемые ООП, и определены его недостатки. Для рассмотренных задач предложены альтернативные механизмы решения - более простые и общие и, следовательно, более подходящие в качестве основы для проектирования языков программирования.
С самого начала заметим, что термины «простой» и «сложный» будут использоваться как объективные. Простота (англ. simple, от лат. simplex) подразумевает ортогональность, отсутствие переплетения различных понятий. Противовесом является сложность (англ. complex) - сильная связанность. Легкость (англ. easy, от лат. ease ^ aise ^ adjacens), часто используемая как синоним простоты (что неправильно: легкость - субъективное понятие), относится к доступности (близости) объекта.
Цель статьи - показать недостатки ООП как парадигмы программирования и предложить более простую и гибкую альтернативу.
Характерные черты ООП
Отметим, что в дальнейшем изложении используется упрощенный взгляд на эволюцию промышленных языков программирования: структурные (С, Fortran) ^ гибридные (С++) ^ объектно-ориентированные (Java, C#).
Объектно-ориентированное программирование - общий термин, за которым могут скрываться сильно отличающиеся концепции [1]. Господствующее в настоящее время статическое ООП (C++, Java, C# и т. д.) имеет много существенных отличий от первоначального динамического (Smalltalk). В данной статье, если не говорится
иное, рассматривается именно статическое ООП. Тем не менее для ООП в целом можно выделить следующие общие характерные черты [2]:
1) объектно-ориентированную инкапсуляцию операций и данных;
2) наследование;
3) подтиповый полиморфизм.
Объектно-ориентированная инкапсуляция,
как следует из названия, делает объект основным элементом управления видимостью операций (методов) и состояния (данных); объекты «владеют» методами и данными.
Наследование представляет собой механизм повторного использования кода, тесно связанный с подтиповым полиморфизмом, который, в свою очередь, основан на динамическом распределении привязанных к объекту методов (позднем связывании).
Видно, что центральное понятие объекта -сложное (объединяет состояние, операции, контроль видимости, реализацию полиморфизма). То же можно сказать о наследовании (объединяет механизм и гранулярность повторного использования, объявление отношения подтиповости) и подтиповом полиморфизме (объединяет иерархию классов и возможность полиморфного использования). ООП - сложная парадигма, основные концепции которой тесно переплетены и с трудом могут рассматриваться независимо друг от друга.
Потребности, удовлетворяемые ООП
Успех ООП объясняется большим числом факторов, среди которых весомы внешние (близость к господствовавшему структурному программированию, популяризация в «правильный» момент времени, поддержка крупными корпорациями и т.д.). В то же время, во многом он осно-
ван на том, что именно ООП было первой парадигмой, давшей относительно легкие ответы на ряд практических вопросов написания и организации программ:
1) инкапсуляция «на нужном уровне»;
2) повторное использование кода;
3) полиморфизм.
Разберем подробнее каждый из этих пунктов.
Инкапсуляция. Дадим инкапсуляции довольно общее определение - это механизм сокрытия деталей реализации; определение «деталей» при этом зависит от требуемого уровня абстракции. Например, функции (процедуры) в C - это механизм инкапсуляции, скрывающий детали реализации алгоритма (поток управления) за его названием. Инкапсуляция является самым важным инструментом при построении больших систем, не только программных. Подобные системы, как правило, организованы в виде «стопки» уровней абстракции, реализация каждого из которых основана только на интерфейсе уровня непосредственно «ниже». Хорошим примером может служить современный стек сетевых протоколов.
До распространения ООП в распоряжении программистов было только два основных уровня инкапсуляции: функция и процесс (операционной системы); наиболее популярные языки программирования того времени (С, Fortran) не поддерживали, среди прочего, концепцию модулей. Потребность группировать, например, связанные процедуры и данные очевидна.
Многие авторитетные инженеры, включая создателей Objective-C и других языков программирования (см. [3]), отмечают, что именно инкапсуляция «на нужном уровне» - основной вклад ООП в промышленное программирование.
Повторное использование. Крайне желательной является возможность повторного использования написанного кода с возможностью адаптации к новому контексту использования. Причем как и в случае с инкапсуляцией, важна правильная гранулярность единиц повторного использования: единицей повторного использования в структурном программировании является функция (с фиксированными типами аргументов), что, очевидно, недостаточно для повторного использования «поведений» (наборов связанных функций) с возможностью их настройки.
Классическое ООП в качестве механизма повторного использования предлагает наследова-
ние: функциональность, заложенная в «базовый» класс, может использоваться классами-«наслед-никами», которые также могут переопределить отдельные методы.
Такой подход имеет множество общепризнанных изъянов. Современные объектно-ориентированные языки и научные работы, посвященные повторному использованияю (например, Scala [4, 5]), нередко предлагают полный отказ от наследования. Приведем наиболее существенные из недостатков.
• Единицей повторного использования является класс, от которого необходимо наследовать клиентов (нередко при отсутствии возможности множественного наследования); это слишком крупная единица для многих задач повторного использования поведения. Также повторное использование ограничено, т. к. не может быть расширено за пределы иерархии, образованной базовым классом.
• Внутренний интерфейс («точки расширения», методы для переопределения подклассами) и внешний интерфейс (методы для клиентов класса) не разделены, что приводит к проблеме «хрупкого базового класса» - подкласс может переопределить произвольный метод, тем самым потенциально нарушить инвариант базового класса - и анти-паттернам типа «вызов суперкласса» - требованию к подклассам об обязательном вызове переопределяемого метода.
• Проектирование хорошего базового класса крайне сложно, т. к. из-за его тесной связанности с подклассами требует априорного детального знания всех возможных будущих областей применения подклассов.
Полиморфизм. Определим полиморфизм как возможность работать с различными типами данных, используя унифицированный интерфейс. Из определения видно, что основное применение полиморфизма - написание алгоритмов, работающих с различными типами данных. Полиморфизм способствует повторному использованию кода, позволяет расширить область применения программы, не переписывая фундаментальных алгоритмов.
Отметим, что приведенное выше определение никак не связано с понятиями класса и наследования. Вместе с тем в ООП полиморфизм (который правильнее называть подтиповым полиморфизмом) реализуется именно путем наследования от базового класса, в терминах операций которого
4
Вычислительные машины и программное обеспечение^
написан алгоритм. Очевидно, это довольно узкий вид полиморфизма, в частности:
требуемое отношение наследования должно быть указано автором класса при его написании, что делает трудным, например, совместное использование двух независимых библиотек (т. к. у пользователя отсутствует возможность добавить в них требуемые отношения наследования);
недостаточная гибкость подхода приводит, например, к проблеме бинарного метода - невозможности в терминах ООП описать полиморфный бинарный метод [7].
Оценка ООП
Приведем еще раз список возможностей, которые были значительно усовершенствованы ООП по сравнению со структурным программированием:
1) инкапсуляция «на нужном уровне»;
2) повторное использование кода;
3) полиморфизм.
Отметим, как похож этот список на характерные черты ООП, приведенные ранее:
1) объектно-ориентированная инкапсуляция операций и данных;
2) наследование;
3) подтиповый полиморфизм.
Подобное сходство свидетельствует о том, что ООП дало конкретные и практически значимые ответы на важные вопросы организации программ, в чем его несомненное достоинство (и одна из основных причин успеха).
Но хороши ли эти ответы? Краткий анализ в предыдущих разделах показывает некоторые из их многочисленных недостатков. Если попытаться их обобщить, то можно отметить следующее: ООП - сложная парадигма. Центральные понятия объекта, класса и наследования перегружены назначением и обязанностями. Говоря простым языком, «когда у тебя есть только молоток, все начинает выглядеть, как гвоздь».
Кроме того, одной из основных проблем современного программирования является эффективное использование гетерогенного многопоточного аппаратного обеспечения со сложной иерархией уровней доступа к памяти. Практика показывает, что для этого необходим строгий контроль за расположением данных в памяти и их изменением. Объектно-ориентированная инкапсуляция, наоборот, поощряет зависимость от изменяемого состояния и его бесконтрольное из-
менение, при этом не давая необходимого уровня контроля за расположением данных.
Альтернативная ООП парадигма программирования
На основе проведенного анализа предложим более простую и гибкую парадигму программирования. Опять рассмотрим базовые потребности промышленного программирования:
1) инкапсуляция «на нужном уровне»;
2) повторное использование кода;
3) полиморфизм.
Для каждой из потребностей предложим альтернативный ООП механизм.
Инкапсуляция. В качестве «нужного уровня» инкапсуляции предложим модули. Модули были специально разработаны для решения этой задачи, но начали широко применяться только с распространением объектно-ориентированного программирования (где их функциональность дублируется классами).
Повторное использование. Для решения задачи повторного использования поведений (наборов связанных алгоритмов) будем использовать трейты [5] - специально разработанный с учетом опыта классического ООП механизм. Отметим, что трейты не требуют объектно-ориентированного языка программирования, хотя обычно описываются в таком контексте.
Полиморфизм. Обобщенное программирование («дженерики», «шаблоны» [6]) разработано для написания полиморфных алгоритмов, т. к. объектно-ориентированный подход не предоставляет необходимой гибкости. Позже объектно-ориентированные языки (Java, C# и др.) стали (частично) поддерживать этот подход, таким образом, как и в случае с модулями, предоставляя дублирующие друг друга средства.
Предлагается использовать обобщенное программирование в качестве единственного механизма полиморфизма.
В статье проведен анализ потребностей промышленного программирования и методов, предлагаемых ООП, для их удовлетворения. Выявлена объективная сложность и недостаточная гибкость объектно-ориентированного подхода. Предложена комбинация известных методов, специально разработанных для решения перечисленных потребностей, как основа более простой и гибкой парадигмы программирования.
В дальнейшем предполагается исследова- женных методов и проведение его детального ние построения нового языка программиро- сравнения с существующими промышленными вания на основе тесной интеграции предло- языками.
СПИСОК ЛИТЕРАТУРЫ
1. Pierce, B. Types and Programming Languages [Text] / B. Pierce. -MIT Press, 2002.
2. Mitchell, J.C. Concepts in programming languages [Text] / J.C. Mitchell. -Cambridge University Press, 2003. -278 p.
3. Biancuzzi, F. Masterminds of Programming [Text] / F. Biancuzzi, S. Warden. -O'Reilly Media, 2009.
4. Odersky, M. An overview of the Scala programming language [Text] / M. Odersky // ACM SIGPLAN. -2006.
5. Scharli, N. Traits: Composable Units of Behavior [Text] / N. Scharli, S. Ducasse, O. Nierstrasz [et al.]. -Springer - Science, 2003. -P. 327-339.
6. Musser, D. Generic Programming [Text] / D. Musser, A. Stepanov // Symbolic and Algebraic Computation: International symp. -1988. -P. 13-25.
7. Cardelli, L. On Binary Methods [Text] / L. Cardelli // Theory and Practice of Object Systems. -1995. -P. 221-242.
УДК 004.413:004.414.3:004.415.53
Е.В. Пышкин
ПРОБЛЕМЫ АВТОМАТИЗАцИИ ПРИЕМОЧНОГО ТЕСТИРОВАНИЯ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ПРИ ИСПОЛЬЗОВАНИИ ПОДХОДА «РАЗРАБОТКА, УПРАВЛЯЕМАЯ ОПИСАНИЕМ ПОВЕДЕНИЯ»
Подход «разработка, управляемая описанием поведения» (behavior driven development, BDD [1]) развивается в рамках гибких (agile) подходов к разработке программного обеспечения давно, тем не менее в отечественных источниках эта тема освещена недостаточно. Метод BDD возник и развивается и как технология, нацеленная на автоматизацию тестирования готового программного продукта заказчиком, и как проектная практика сродни практике «тесты вначале» и основанной на ней методологии разработки через тестирование (test-driven design). Действительно, использование модульных тестов как центрального элемента подхода «тесты вначале» не эквивалентно тестированию в строгом смысле этого понятия: речь идет здесь не столько о тестировании как динамическом методе обеспечения качества, сколько о применении определенных практик тестирования в процессе реализации функциональных требований [2]. Тесты в данном случае, фактически, выступают в качестве альтернативной формы записи этих функциональных требований.
Приемочные тесты относятся к категории тестов, труднее других поддающихся формализации. Одна из задач, решаемых в рамках подхода
«разработка, управляемая описанием поведения», собственно, и состоит в создании определенной инфраструктуры, которая позволила бы осуществить переход от описания тестовых сценариев, сформулированных заказчиком, к тестовым классам и методам для модульного тестирования. При этом предполагается использование существующих библиотек и фреймворков модульного тестирования (unit-тестирования), например библиотеки JUnit для языка Java.
Разработка через описание поведения: конструктивная идея
Согласно известной V-модели организации разработки, приемочные тесты - это своего рода элемент верхнего слоя абстрактной модели процесса разработки (рис. 1).
Приемочные тесты в наибольшей степени сфокусированы на пользовательских требованиях (нередко неясных и плохо сформулированных) и нацелены на процесс верификации, вообще говоря, уже протестированной системы, с участием представителей заказчика. Ошибки, выявляемые в ходе приемочных испытаний, как правило, означают, что либо существует неадек-