Данная статья является переводом документа «2010 internship technical report» библиотеки Cofoja. Документ раскрывает причины возникновения библиотеки и отвечает на вопросы критиков, которые можно встретить на Хабре в статьях посвященных данной библиотеки. Статья служит для распространения и понимании реализаций парадигмы Design By Contracts или Контрактного Программирования.
Статья разделена на две части в связи с объемом работы по переводу. Вторая часть в ближайшее время появится.
Содержание
Краткий обзор
1 Введение
2 Контрактные аннотации
3 Компоненты и Архитектура
4 Компиляция контрактного аспекта
4.1 Компиляция Совета
4.2 Точки соединения и связывание
4.3 Наследование Совета
4.4 Ограничения Техники Компиляции
5 Жизненный цикл Контракта
5.1 Крупный план на компиляцию Контракта
5.2 Планировка Контрактного метода
5.2.1 Классы и интерфейсы
5.3 Поведения Контракта во времени выполнения
5.3.1 Контрактная рекурсия
5.4 Специальный случай Old-value выражения
6 Обсуждение
7 Заключение
8 Благодарности
Список цитированной литературы
Nhat Minh Le
12 января 2011
Краткий обзор
Эта статья знакомит c Contracts for Java (Cofoja), новой библиотекой для контрактного программирования на языке Java и преемника Modern Jass (Johannes Rieken). Библиотека улучшает стратегию предшественника, основанную на отдельной компиляции заглушек), хотя и использует те же Java технологии, такие как обработка аннотаций (annotation processing) и байт-код инструментарий (bytecode instrumentation).
Contracts for Java продвигает минимализм с выразительным набором конструкций, напоминающий оригинальную идею Design By Contract пионером которой является язык Eiffel, что позволяет сосредотачиваться на надежности, выполнении и принужденной ненавязчивости в процессе компиляции.
Введение
Контрактное программирование это парадигма построения объектно-ориентированного программного обеспечения, которая впервые описана Бертраном Майером и распространенная благодаря его языку программирования Eiffel со встроенными Design By Contract конструкциями. По существу, это расширяет отношения иерархии класса с программно-принужденными ограничениями называемые контрактами. В оригинальном предложении Майера, они выражались как метод предусловий и постусловий, похожие на хоаровскую логику и инварианты класса, которые действуют как оба предусловия и постусловия ко всем интерфейсам класса. Контракты могут наследоваться в соответствии с принципом подстановки Барбары Лисковы.
В то время как контрактное программирование завоевало большой успех в его оригинальной реализации языка Eiffel, где она уважаема как фундаментальная идиома, оно было экспортировано и адаптировано с различными уровнями успеха в другие языки, включая Java.
Контракты в Java полагаются на сторонние библиотеки, т.к. сам язык только включает недоразвитую поддержку утверждения (assertion) как Java 6. Многочисленные усилия принесли дюжину альтернатив за последние годы, такие как JML, C4J, Contract4J, JASS и упомянутый ранее Modern Jass, на котором Cofoja основан.
Cofoja собрал все известное раннее и улучшил интересные подходы, которые были введены раздельно в упомянутых библиотеках, с целью создания устойчивой самостоятельной библиотеки специализированной на контрактном программирование на Java:
- Краткая спецификация контракта, используя Java аннотации (Contract4J, Modern Jass)
- Полная поддержка Java выражений в контракте, используя сопутствующую компиляцию через стандартный API компилятора (Modern Jass)
- Проверка во времени компиляции синтаксиса и типов (нет интерпретации во время выполнения выражении контрактов, как Contract4J)
- Онлайн и офлайн связывание контрактов в целевые объекты, используя переписывание байткода. (С4J, Contract4J, Modern Jass)
- И легковесный код основан на малом количестве зависимостей (в частности, нет зависимости с AspectJ как в случае Contract4J)
Главный взнос данной реализации является усовершенствованная версия компиляции моделей представленных в Modern Jass, вдохновленная техниками АОП и основа на использовании стандартов компилятора Java, производя заглушки и мнимые объекты (mocks)
Разделы 2 и 3 дает обзор библиотеки Cofoja и его использование, а разделы 4 и 5 охватывает детали представления контрактов, осуществление выполнения самих контрактов изнутри.
2 Контрактные аннотации
Все конструкции Cofoja выражаются через малый набор Java аннотации: Reuires, Ensures, Invariant, напрямую заимствован из Eiffel, и меньше употребимые ThrowEnsures и Contracted. Первые четыре аннотации печатаются для определения контрактов как списков высказываний (см. фрагмент 1)
<code> @Invariant("size() >= 0") interface Stack<T> { public int size(); @Requires("size() >= 1") public T peek(); @Requires("size() >= 1") @Ensures({ "size() == old(size()) - 1", "result == old(peek())" }) public T pop(); @Ensures({ "size() == old(size()) + 1", "peek() == old(obj)" }) public void push(T obj); } </code>
Фрагмент 1: Пример стека с контрактами.
Как и в Eiffel, контракты могут наследоваться, составляя фрагменты цепочек наследования, используя И и ИЛИ булевские операторы.
Эти аннотации обеспечивают полный набор основных контрактных спецификаций на верху которых легко построить более сложные поведения. В действительности, высказывания могут содержать произвольные Java выражения, т.е. любая проверка, написанная на Java, может быть написана в Cofoja контракте.
Cofoja, как и определено в его целях, не стремиться обеспечить новаторские и специализированные функциональные возможности, которые были введены в такие пакеты как JML и JASS. Вместо этого, он фокусируется на удобстве в использовании и доступности его основных возможностей. Даже добавляя маленькую функциональную возможность к целому массиву конструкций в существующий язык такой как Java является опробовано сложной задачей и предыдущие попытки библиотек контрактного программирования для Java часто заканчивались частичной поддержкой контрактных возможностей: для примера, JML только познакомился с пределами Java 5 несмотря на усилия привнести все ко времени; Modern Jass проваливается при компиляции кода с обобщенными типами и библиотеки, которые воздерживаются от манипуляции кода или байткода могут только принимать обязательства интерфейсов.
Одна из целей Cofoja привнести контракты как можно большому множеству вариация кода языка Java, с выразительностью на легкость и восстанавливаемость. Следующее статьи описывают фундаментальные техники, которые лежат в реализации контрактов в Cofoja. Вспомогательные возможности, такие как дебагинг, только упоминаются в соответствующих местах и заслуживают самостоятельного изучения, которое вне данных статей.
3 Компоненты и Архитектура
Фрагмент 2 описывает процесс компиляции и выполнения при включенных Cofoja контрактах. Контракты интегрируются в различных точках входов, используя Java цепь-инструменты (toolchain), и во времени компиляции и во времени выполнения:
- До компиляции Java, как только-чтение исходника препроцессором. Важно заметить, что этот шаг не изменяет исходные файлы; его исключительное назначение вытаскивать информацию, которая иначе не может быть доступна в аннотациях обработчика (см. ниже). Препроцессор не нужен, если Java компилятор имеет нестандартный API с пакетом com.sun.source
- При компиляции Java файлов, как annotation processor. Процессор аннотаций отвечает за текущую компиляцию контрактного кода в отдельные байт-код файлы.
- Во время загрузки класса, как Java агент, плагин для модуля запуска Java программ. Java агент переписывает байткод контрактных классов, соединяя контракты с целевыми объектами.
- Опциональный шаг. Шаг инструментария, который может быть выполнен вне момента запуска Java программ, как самостоятельный процесс, который объединяет нормальный свободный от контрактов байткод (в форме стандартных class файлов) с соответствующим контрактным байткодом (в форме контрактных файлов), чтобы синтезировать классы с действующими контрактами.
Также Cofoja поставляется с двумя утилитами: cofojac и cofojab, которые выполняют первые два шага и последний шаг, соответственно, для легкого выполнения из командной строки.
Ключевая идея, взятая от Modern Jass, является компиляция контрактов раздельно. Оригинальные исходные файлы в любом случае никогда не меняются. Она имеет недостатки, главным образом синтаксический (например, контрактный код пишется внутри двойных кавычек), но гарантирует следующие два свойства, которые вносит в данный подход надежность:
- Компиляция контрактов не пересекается с нормальным процессом компиляции; это ограничивает возможности возникновения багов при контрактной компиляции, используя намеченное компилятором поведение оригинального кода.
- Контрактный компилятор не нуждается в коде с контрактами для компиляции; компоновка кода с аннотациями достаточна. Если по какой-либо причине (например, баг в библиотеки или неподдерживаемый функционал Java) Cofoja не может использоваться на специфичном участке кода, тогда все еще можно продолжать разработку, оставляя контракты выборочно или на какое-то время.
Контракты сразу присоединяются к классам после того, как они обработаны, и поэтому их исходное представление как инертные данные (например, строковые литералы), проходят через множество форм в течение жизни. Следующие секции рассказывают, как контракты переводятся из их спецификаций в действующий байт-код. Раздел 4 описывает общую технику компиляции заимствованную из АОП, тогда как раздел 5 описывает внутреннюю реализацию в Cofoja.
4 Компиляция контрактного аспекта
Проверка контрактов создается путем перекрестного разреза в коде (crosscutting), если описывать аспектно-ориентированными терминами. Хотя контракты сами определяет свойства бизнес логики программы, проверка их представима как Аспекты приложения.
Подтверждение или несостоятельность вызванного кода может быть естественно выражено как Совет относительно контрактных методов. Modern Jass библиотека ввела новую технику компиляции, основанную на заглушках и использование стандартного Java компилятора для генерирования Совета в байт-код, который может быть позже связан через инструментарий Java агента. Такой подход имеет преимущество обойтись от дополнительного компилятора с его понимание Java языка. Он, однако, туго связывает код с контрактной логикой.
Cofoja улучшила этот подход, распространяя компиляционный процесс на систематические конструкции, которые отображают более прозрачно аспектные концепций.
4.1 Компиляция Совета
Главная проблема при компиляции кода Совета лежит в таком коде, который требует доступ к той же окружающей среде, что и метод к которому применяется Совет. Другими словами, часть Совета ожидает быть скомпилированным как, если бы он находился внутри тех же границ, как и изменяемый код. Предусловие, для примера, вероятно, проверяет аргументы вызова или любой контракт может захотеть вызвать приватный метод класса. Этого легко достичь, если компилятор знает об этом; стандартный Java компилятор, однако, не обращает внимание на них.
Для того это используется Java компилятор в таком порядке: Совет внедряется обратно в копию оригинального класса, превращаясь в байт-код и в результате образовавшиеся скомпилированные методы извлекаются. Эти методы могут без труда быть связанными внутрь окончательных классов (как дополнительные методы) до того пока они загрузятся. Таким образом, байт-код принадлежит дополнительному аспекту, к тому же скомпилированный и сохраненный отдельно и полностью пригодный для повторного использования.
Техника усовершенствовалось, так что методы оригинального класса могут быть заменены на заглушки. Что важно, так это компилятор должным образом генерирует вызовы к этим методам внутри Совет-кода; и их актуальное содержимое неважно. В этой связи, стандартный API обработки аннотации обеспечивает достаточную информацию к легко образуемым заглушкам отраженными элементами иерархии класса. Дополнительная выгода это расширения применимости классов доступная только как байт-код: сигнатуры методов могут быть прочитаны из класс-файлов, игнорируя связанные тела методов.
4.2 Точки соединения и связывание
Связывание заканчивается через переписывание байт-кода: дополнительные методы добавляются в класс и методы, к которым применены Советы, дополняются вызовами к ним в начале и на выходе метода.
Временные данные, такие как аргументы и возвращаемое значение, если они есть, необходимо явно скопировать, как если бы они неявно перемещались из метода в метод.
В добавок к этому, части Совета, которые окружают метод и имеют пост-условия с old-value выражениями являются банальными, но особыми случаями, для которых необходимо сохранять контекст. В Cofoja, решение для сохранения контекста сделано установкой дополнительных локальных переменных, которые добавляются к сигнатуре post-выполнения метода-Совета.
Контракты годятся особенно хорошо для этого подхода, т.к контекст информации естественно разбивается на набор не пересекающихся переменных. Две альтернативы: встраивание внутрь методов байт-кода Совета и дробление метода-Совета внутри вспомогательной сущности.
Встраивание внутрь методов явялется привлекательным решением, если код Совета не наследуется или гарантирует, что не будет доступа к любым приватным членам классов предков. Иначе встраивание ломает преимущества границ доступа.
Более общая стратегия состоит в расположении метода-Совета внутрь помощника (helper), который состоит из фактического кода, и обертки (wrapper), что заимствует оригинальное имя и сигнатуру оригинала, но реальная работа его подчиняется методам-Советам, которые ответственны за вызов вспомогательной процедуры.
4.3 Наследование Совета
Наследование является центральной частью Контрактной парадигмы. Но это в значительной степени ортогонально АОП. С данным подходом, должно быть ясно, что методы-Советы, естественно, наследуются через нормальную иерархию классов и они доступны во время связывания. С некоторым дополнительным усилием они могут быть также доступны с исходным кодом.
В зависимости от приложения, унаследованный Совет может сильно изменить саму свою сущность и назначение, возможны различные стратегии, но это выходит за рамки данного документа.
Тем не менее, возникает вопрос при использовании интерфейса. Следует заметить, что написание Совета для интерфейса является важной составляющей, если существует путь соединить эти куски внутрь конкретных классов реализующих этот интерфейс. Очевидно, пути инжектирования кода Аспекта внутрь оригинального интерфейса невозможно; получившийся Java-файл не будет даже компилироваться и это наносит удар по замыслу этой техники.
Вместо этого, дополнительные методы могут также жить в конкретном классе реализующий единственный интерфейс или в классе-утилите внутри тех же границ, что и интерфейс, к которому они присоединены (одинаковый пакет или outer-класс).
Первое решение использовать существующую связанную инфраструктуру: классы реализующие интерфейс могут легко присоединять методы-Советы и продолжать свое существование как обычно. Тем не менее, должна быть осторожность при превращении всех ссылок на реализованный класс на ссылки фактического класса, методы которого инжектированы. Также существуют задачи с дублированием кода.
Второе решение требует, чтобы куски Совета для интерфейса переделывать для работы с дополнительным параметром, действующий для управления фактического объекта. Тем не менее, это исключают любое дублирование. Следует заметить, что надежда на внешние процедуры не ограничивают выразительность этих конструкций, т.к. интерфейсы имеют только публичные конструкции.
Раздел 5.2.1 обращается к этим проблемам с практичной перспективы Контрактов: она сравнивает реализацию Modern Jass, который использует вариант первого решения и Cofoja, который применяет второе решение.
4.4 Ограничения Техники Компиляции
Данный подход компиляции Аспекта держится на гарантии, что Java-компилятор не оптимизирует метод внутри его границ. Фактически, это требует, чтобы методы исходного кода и байт-код методы сочетались один-к-одному, например, нет дробления метода при компиляции. При наличии заглушек, встраивание напрямую куска метода является другой большой нежелательной оптимизацией. На практике, это не представляет большую проблему, принимая во внимание текущие тенденции к оптимизации уровня виртуальной машины, чем компилятора. Кроме того, Контракты, в особенности, предназначены в основном для разработки и целей дебага, а поэтому, вероятно, не будут компилироваться с включенной оптимизацией.
Другая возможная более серьезная проблема является ошибки компиляции. Отчет ошибок требует большей осторожности, т.к. он склонен выдавать неуловимые противоречия. Простая идея отдавать результат из основного процесса компилятора, чтобы пользователю показывались образованные сообщения, не относящиеся к коду пользователя, а соответствующие смешанному коду с генерированными элементами. Cofoja дополняет такой вариант расширенными возможностями, чтобы помочь разъяснить часто встречающиеся сообщения об ошибках; более общее решение могло бы использовать улучшенную интеграцию с компилятором программных интерфейсов.
Список цитированной литературы
- J. Bloch. JSR 41: A Simple Assertion Facility. November 1999
- C4J
- D. R. Cok. Adapting JML to generic types and Java 1.6. Seventh International Workshop on Specification and Verification of Component-Based Systems, pp. 27–34, November 2008.
- Compiler Tree API
- Contrac4J
- Eiffel Software. The Power of Design by Contract
- A. Eliasson. Implement Design by Contract for Java using dynamic proxies. February 2002
- C. A. R. Hoare. An axiomatic basis for computer programming. Communications of the ACM, Volume 12, Issue 10, pp. 576–580. October 1969.
- Java with Assertions
- Java Modeling Language
- B. Liskov, J. Wing. A behavioral notion of subtyping. ACM Transactions on Programming Languages and Systems (TOPLAS), Volume 16, Issue 6, pp. 1811–1841. November 1994
- B. Meyer. Applying “Design by Contract.” Computer (IEEE), Volume 25, Issue 10, pp. 40–51. October 1992
- Oracle Corporation. The Java HotSpot Virtual Machine, v1.4.1
- J. Rieken. Design By Contract for Java — Revised. April 2007
- D. Wampler. Contract4J for Design by Contract in Java: Design Pattern-Like Protocols and Aspect Interfaces. 2006
Автор: kadkaz