О пользе и преимуществах методики контрактного программирования (Design by Contracts, DBC) написано много. Вот несколько хороших статей:
Компилятор IntelliJ / Android Studio как всегда на высоте. Последняя версия Eclipse 4.3 (Kepler) не отстает, в ней nullness-анализ сильно допилили. Правда, есть пара особенностей:
Update:В контексте null-анализа стоит так же упомянуть библиотеку Guava.Optional, с помощью которой можно явно помечать объекты, которые могут принимать null-значения. Полезнейшая штука.
- Теляков С.В. Проектирование по контракту. RSDN Magazine #1-2010
- Computer Programming/Design by Contract
- Todd Plessel. Design By Contract: A Missing Link In The Quest For Quality Software
- Building bug-free O-O software: An Introduction to Design by Contract
- пред-условия, которым должен соответствовать вызывающий код;
- пост-условия, которые гарантируются методом.
- Повышение надежности программы за счет систематической и гарантированной проверки входных данных.
- Исключение лишних проверок в коде. Корректность данных проверяется в методе, а не в вызывающем коде.
- Автоматическое документирование кода, создание спецификации API программного модуля.
- Косвенно: повышение эффективности статического анализа кода.
- Contracts for Java: Another Entrant in Java Programming by Contract
- Design by Contract
- Java Argument Validation
Assert, Exception и Guava
Язык Java не поддерживает концепцию Design by Contracts напрямую и, вероятно, не будет поддерживать в обозримом будущем. Все что доступно разработчикам - Programming With Assertions. Т.е. assert-ы и исключения. Пример:public BigInteger modInverse(BigInteger m) { //проверка пред-условий. В случае ошибки - exception if (m == null) { throw new ArithmeticException("Value is null"); } if (m.signum <= 0) { throw new ArithmeticException("Modulus not positive: " + m); } ... //проверка пост-условий assert this.multiply(result).mod(m).equals(ONE) : this; return result; }Пред-условия предлагается проверять всегда и в случае ошибки генерировать exception. Пост-условия - проверять через assert, т.е. только в отладочном коде. При необходимости, включать проверку assert в релизе. Несмотря на то, что такой подход вполне рабочий, он не лишен ряда недостатков:
- код метода загромождается;
- условия контракта "размазаны" в теле метода;
- поздняя проверка контракта (во время выполнения, а не в процессе программирования/компиляции);
- "неотключаемость" проверок и т.д.
adb shell setprop debug.assert 1
(assert будет работать до перезагрузки девайса). Вместо стандартных assert можно использовать JUnit Assert и исключать их из релиза с помощью ProGuard. Update: для проверки состояния стандартных контролов Android хорошо подойдет библиотека FEST Android.
Частично указанные выше проблемы можно решить с помощью набора библиотек Guava. В состав Guava входит библиотека Preconditions, предоставляющая набор функций для явной и компактной записи предусловий. Пример выше можно переписать с помощью Guava.Preconditions следующим образом:
public BigInteger modInverse(BigInteger m) { //проверка пред-условий. В случае ошибки - exception com.google.common.base.Preconditions.checkNotNull(m, "Value is null"); com.google.common.base.Preconditions.checkArgument(m.signum <= 0, "Modulus not positive: %d", m); ... //проверка пост-условий assert this.multiply(result).mod(m).equals(ONE) : this; return result; }
Null анализ. JSR 305, JSR 308 и статический анализ кода
В Java 5 был добавлен механизм аннотаций - появилась возможность снабжать код Java метаданными. Аннотации находят много полезных применений, в том числе, Android-специфичных. Одно из типичных применений аннотаций - помощь в null-анализе. С помощью аннотаций @Nullable и @NotNull разработчик явно указывает, какие параметры могут принимать значения null, какие не могут, какие методы могут возвращать null, какие нет. По сути, это частный случай "контрактов" концепции Design by Contracts. Подобные "контракты" позволяют отловить распространенные ошибки, связанные с null, в частности:- Null Pointer Access/Dereference: попытка обращения к объекту, в то время как он равен null; null pointer exception в результате.
- Redundant Check For Null: лишняя проверка на null там, где точно известно, что значение не может быть равно null.
public @NonNull String test(@Nullable String b) { return b; //Ошибка! функция не должна возвращать null, тогда как b может быть равна null } public void makeTest() { String result = test(null); if (result != null) { ... } //!Ошибка: test не может возвращать null, проверка на null не нужна }В приведенном коде две ошибки. Первая: согласно аннотации @NonNull, функция test не может возвращать null, но в данной она null может вернуть. Вторая ошибка: поскольку test не может возвращать null, то проверка результатов работы функции на null - лишняя. Статические анализаторы кода легко ловят подобные ошибки на стадии компиляции кода. Первоначально появилось великое множество вариантов Null-аннотаций. Естественно, это оказалось не удобно. Поэтому была предпринята попытка выработать стандартный набор "аннотаций, способных помочь статическим анализаторам в поиске багов" - см. JSR-305: Annotations for Software Defect Detection. Помимо null-аннотаций, набор включал и другие полезные аннотации - для работы с регулярными выражениями, многопоточностью, аннотации для борьбы с утечкой ресурсов и т.д. (полный список). В настоящий момент, этот JSR-документ не развивается и помечен как Dormant (бездействующий, пассивный). Тем не менее, предложенный в JSR-305 аннотаций утвердился в качестве стандарта де-факто и, по крайней мере в части null-аннотаций, его поддерживают многие инструменты статического анализа (проблемы с именованием все равно остались: например, в IntelliJ используется директива @NotNull вместо @NonNull из JSR-305). Замечу, что помимо JSR-305, есть JSR-303: Bean Validation, описывающий набор аннотаций
javax.validation.constraints.*
. Он уже реализован в Java EE 6. Однако, эти аннотации предназначены для проверки кода во время исполнения, а не для статического анализа.
В Java 8 планируют существенно расширить возможности аннотаций, см. JSR-308: Annotations on Java Types. Например, можно будет написать ArrayList<@Nonnull String>
. По идее, возможности nullness-анализа на стадии компиляции должны существенно возрасти.
Конечно, расстановка аннотаций в коде требует дополнительной работы со стороны программиста. Некоторые разработчики считают, что такая работа может не оправдать себя - аннотаций не достаточно, чтобы выловить многие null-ошибки, см.
JSR 305: a silver bullet or not a bullet at all? Тем не менее, несомненно, что аннотации позволяют более точно описать намерения программиста и помочь статическим анализаторам. А количество дополнительной работы можно и нужно минимизировать. Разработчики библиотеки Google Guice предлагают следующий подход:
- запретить null по умолчанию;
- все nullable-значения явно помечать @Nullable.
Инструменты для анализ кода
Теперь поговорим о средствах анализа кода. Статических анализаторов кода для Android достаточно много, но не все из них содержат средства nullness-анализа. Вот некоторые, которые точно содержат:- Компилятор Eclipse, (раздел документации Using null annotations);
- FindBugs, (поддерживаемые аннотации JSR-305);
- Checker Framework, (документация - заявлена поддержка JSR-305);
- Компилятор IntelliJ / Android Studio, (раздел документации Nullable How-To);
- Julia, коммерческий продукт;
- Klocwork, коммерческий продукт.
Тестовый пример
public static class TestClass { @NonNull String _NonNull /*= "test1"*/; //Error 1: _NonNull is not initialized in the code below @Nullable String _Nullable; @Nullable private String test_nullable(@NonNull String paramNonNull, @Nullable String paramNullable) { Log.i(LOG_TAG, _NonNull.toString()); //null pointer dereference because of Error 1 Log.i(LOG_TAG, _Nullable.toString()); //Error 2: null pointer dereference Log.i(LOG_TAG, paramNonNull.toString()); Log.i(LOG_TAG, paramNullable.toString()); //Error 3: null pointer dereference _NonNull = null; //Error 4: store of null value into nonnull-variable double d = java.lang.Math.random(); if (d > 0.99f) { return null; } if (d > 0.9f ) { _Nullable = "test2"; _NonNull = "test3"; } if (_Nullable != null) { //no error 1: _Nullable can be null here String m = _Nullable.toString(); //no error 2: because _Nullable is not null here Log.i(LOG_TAG, m); Log.i(LOG_TAG, _Nullable.toString()); //no error 3: because _Nullable is not null here } if (_NonNull != null) { //Error 5: redundant nullcheck Log.i(LOG_TAG, _NonNull.toString()); } if (_NonNull.length() > 2) { //Error 6: possible null pointer dereference return _NonNull; } else { return _Nullable; } } @NonNull private String test_nonnull(@NonNull String paramNonNull, @Nullable String paramNullable) { return paramNullable; //Error 7: paramNullable can be null, but function must returns not-null } public void makeTests() { String t1 = test_nullable(null, "test4"); //Error 8: first param must be not null if (t1 != null) { //no error 4: t1 can be null Log.i(LOG_TAG, t1.toString()); //no error 5: t1 is not null here } Log.i(LOG_TAG, t1.toString()); //Error 9: possible null pointer dereference String t2 = test_nonnull("test5", null); if (t2 != null) { //Error 10: redundant nullcheck Log.i(LOG_TAG, t2.toString()); } Log.i(LOG_TAG, t2.toString()); //no error 6: t2 is not null here } }Вполне допускаю, что данный пример охватывает не все возможные типы ошибок. Но типичные ошибки в нем точно присутствуют.
Результаты тестирования
Issue | Eclipse 4.1 (Indigo) | Eclipse 4.2 (Juno) | Eclipse 4.3 (Kepler) | FindBugs 2.0.3 | Checker Framework 1.7.1 | Android Studio 0.4 |
Error 1 | FAILED | ok | ok | ok | ok | FAILED |
Error 2 | FAILED | ok | ok | ok | ok | ok |
Error 3 | FAILED | ok | ok | FAILED | ok | ok |
Error 4 | FAILED | ok | ok | ok | ok | ok |
Error 5 | FAILED | FAILED | ok | FAILED | ok | ok |
Error 6 | FAILED | FAILED | FAILED | ok | FAILED | FAILED |
Error 7 | FAILED | ok | ok | ok | ok | ok |
Error 8 | ok | ok | ok | FAILED | ok | ok |
Error 9 | FAILED | ok | ok | FAILED | ok | ok |
Error 10 | FAILED | ok | ok | FAILED | FAILED | ok |
No Error 1 | ok | ok | ok | ok | ok | ok |
No Error 2 | ok | ok | ok | ok | ok | ok |
No Error 3 | ok | FAILED | FAILED | ok | FAILED | ok |
No Error 4 | ok | ok | ok | ok | ok | ok |
No Error 5 | ok | ok | ok | ok | ok | ok |
No Error 6 | FAILED | ok | ok | ok | ok | ok |
- По умолчанию null-analysis с учетом аннотаций отключен, его нужно включить в настройках проекта:
Java Compiler/Errors Warnings/Null analysis/Enable annotation-based null analysis
- Из-за ложных срабатываний (например, см. "no error 3" в тестовом примере) приходится выставлять уровень предупреждений для всех типов ошибок в Warning.
Тяжелая артиллерия: библиотеки, реализующие Design By Contracts в Java
Обратимся теперь к библиотекам Java. Варианты реализации подобных библиотек не плохо рассмотрены в диссертации создателя популярной библиотеки Modern Jass слайды (pdf), диссертация (pdf). В целом, реализации DBC для Java отличаются прежде всего способом описания контрактов (аннотации, скриптовый язык в комментариях, отдельные методы и даже классы) и способом проверки соблюдения контракта (пре-процессор, подмена байткода, собственный компилятор, интерпретатор, аспектно-ориентированное программирование и т.д.). Вот, далеко не полный, список библиотек, реализующих концепцию Design by Contracts в Java:- Modern Jass. Контракты задаются аннотациями. Плагин для Eclipse есть только под Mac. Последнее обновление: 2007 год.
public class Bar { @Pre("args.length % 2 == 0") public static void main(String[] args){ System.out.println("Hello, " + args.length); } }
- Contract4J. Контракты задаются аннотациями Java и записываются на языке AspectJ. Последнее обновление: 2010 год.
@Contract public class SearchEngine { ... @Pre @Post("$return != null && $return.isValid()") public PhoneNumber search (String first, String last, Address streetAddress) { PhoneNumber result = doSearch (first, last, streetAddress); return result; } ... }
- jContractor. Контракты записываются в дополнительных методах, имеющих определенные названия. Например, для проверки предусловий и постусловий метода "push" создаются дополнительные методы push_Precondition и push_Precondition, а для проверки инварианта класса метод _Invariant. Последнее обновление библиотеки: 2003 год.
class Stack { public void push (Object o) { ... } protected boolean push_Precondition (Object o) { return o != null; } protected boolean push_Postcondition (Object o, Void RESULT) { return implementation.contains(o) && (size() == OLD.size() + 1); } }
- C4J. Контракт для класса задается отдельным классом. В процессе работы C4J instrumentor автоматически изменяет код классов, связанных контрактом - если для метода класса задано предусловие, то в начало метода добавляется вызов предусловия; если задано постусловие, то в конце метода производится его вызов и т.д. Библиотека гибко настраивается - проверку контрактов можно отключить целиком или для отдельных методов, в случае нарушения контракта можно генерировать исключения или ограничиться записью в лог файл и т.д. Последнее обновление: 2013 год.
@ContractReference(TimeOfDayContract.class) public class TimeOfDay { ... public void setHour(int hour) { this.hour = hour; } } public class TimeOfDayContract extends TimeOfDay { ... @Override public void setHour(int hour) { if (preCondition()) { assert hour >= HOUR_MIN : "hour >= HOUR_MIN"; // precondition clause 1 assert hour <= HOUR_MAX : "hour <= HOUR_MAX"; // precondition clause 2 } if (postCondition()) { ... } }
- Cofoja (contracts for java). Библиотека, разрабатываемая сотрудниками Google в рамках ныне отмененного правила использования 20% свободного времени на разработку сторонних проектов. Контракты задаются через аннотации. Плагина для Eclipse нет, но прикрутить библиотеку к Eclipse можно: Contracts for Java (Cofoja) V1.0 in Eclipse (PDF) (у меня к 64-битному Eclipse 4.3 cofoja не прикрутилась, а к 32-битному - без проблем).
@Requires("n >= 0") @Ensures({ "!result || old (n) % 2 == 1", /* For testing purposes: check that old () has the right value. */ "old (n) == n" }) public static boolean odd(int n) { if (n == 0) { return false; } else { return even(n - 1); }
- Java Modeling Language (JML). Контракты описываются на специальном языке в комментариях к методам.
//@ requires (* x is positive *); /*@ ensures (* \result is an approximation to @ the square root of x *) @ && \result >= 0; @*/ public static double sqrt(double x) { return Math.sqrt(x); }
Выводы.
Библиотек, позволяющих использовать методологию Design By Contracts в Android приложениях, найти не получилось. Так что полноценной удобной поддержки DBC в Android, похоже, обеспечить пока не удастся. Тем не менее, кое-какие DBC-инструменты для Android-разработчиков все же доступны. Перечислю их еще раз:- Библиотека Preconditions из Guava позволяет явно и компактно задавать проверку предусловий.
- Для проверки постусловий методов можно использовать assert'ы. Поскольку стандартные assert'ы java на девайсах и эмуляторе по умолчанию отключены, их следует не забывать включать. Или же использовать JUnit Assert + FEST Android и исключать их из релиза с помощью ProGuard.
- Аннотации и @NonNull, @Nullable из JSR-305 позволяют явно указать код, в котором не могут или, наоборот, могут присутствовать null-значения. С этими аннотациями умеют работать многие бесплатные и платные статические анализаторы кода, в частности, FindBugs и Checker Framework.
- Если вы используете Eclipse, то имеет смысл последовать совету разработчиков библиотеки Google Guice и считать, что по умолчанию никакие параметры и переменные не могут принимать null-значение, а методы - не могут null возвращать. Достигается это за счет использования аннотации org.eclipse.jdt.annotation.NonNullByDefault, с помощью которой следует пометить все пакеты в вашем проекте. В результате, необходимость проставлять @NotNull отпадет, потребуется лишь помечать @Nullable те места в коде, где могут присутствовать null-значения.
- Следует использовать наиболее свежую версию Eclipse 4.3 (Keppler), т.к. в ней существенно доработаны средства null analyse. По умолчанию они выключены, их следует включить в настройках компилятора Java.
Update:В контексте null-анализа стоит так же упомянуть библиотеку Guava.Optional, с помощью которой можно явно помечать объекты, которые могут принимать null-значения. Полезнейшая штука.
Комментариев нет:
Отправить комментарий