Анализ объектно-ориентированной парадигмы и поиск лучшей альтернативы

-

Вычислительные машины и программное обеспечение

УДК 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).

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