Tuesday, August 11, 2015

Maybe<Maybe<Maybe<T>>>

Многие языки имеют концепцию "нулевого указателя", унаследованную от близких к железкам языков. Ничего удивительного, что в языках вроде С/C++ где "объект" == "область памяти", nullptr, он же NULL, он же "нулевой указатель", был синонимом "отсутствия объекта".

Многие языки более высокого уровня переняли это соответствие в том или ином виде: Java (null), Obj-C (nil), python (Nothing) и т.п.

Проблема этого подхода в том, что возможность того, что возвращенное значение может быть nullptr - это часть устного контракта между написавшим и использующим метод. Например, может ли следующий код выбросить NullPointerException?
public void doWork() {
    final String some = getSome();

    if (some.isEmpty()) {
        doIfEmpty();
    } else {
        doIfContainsSomething();
    } 
}
На практике это неизвестно, т.к. зависит от деталей реализации метода getSome. Самая большая проблема в том, что даже если getSome никогда не возвращает null сейчас, нет никакой гарантии, что он не сделает этого в будущем.

Важно понимать, что код постоянно меняется и эволюционирует. Это не так актуально для библиотечных проектов, которые имеют достаточно длительный жизненный цикл, однако для приложений которыми пользуется большое количество пользователей и которые активно развиваюся, ситуация "внезапно возникающего NPE", на мой взгляд, достаточно неприятна.

Еще более интересно и неприятно обстоят дела на Obj-C, где разыменование nil не приводит к падению.

Дело в том, что исправление ошибки тем проще (и дешевле!) чем раньше она обнаружена. В случае Obj-C ошибка разыменования nil может привести к странному поведению значительно позднее места своего возникновения. Например: семантика сравнения такова, что если !(A > B), и !(A < B), то A == B. Если у вас есть объект, определяющий, скажем, методы сравнения, то
[nil isGreaterThan:objectToTestWith] == nil // i.e. false
[nil isLessThan:objectToTestWith] == nil    // i.e. false
[nil isEqualTo:objectToTestWith] == nil    // i.e. false
То, есть, очевидно, что семантика сравнения нарушена. Само по себе это не проблематично, до тех пор, пока на основе таких выражений не строится дальнейшая логика. И что хуже, логика, побочные эффекты которой видны длительное время, например, код базы данных. А искать причину повреждения данных несколько дней - удовольствие из малоприятных, поэтому лучше избегать таких неочевидных вещей (и лучше пусть оно закрешится сразу и проблема будет быстро найдена и исправлена).

Итак, что же такое Maybe.

Maybe - это контейнерный тип, семантика которого такова, что он может содержать или не содержать значения. То есть мы выносим "возможно нет значения" на уровень системы типов вместо того, чтобы оставить его на уровне устного контракта между разработчиками. Один из возможных способов определить этот тип выглядит так:

abstract class Maybe<T> {
   public static <T> Maybe<T> just(final T value) {
      return new Maybe<T>() {
         @Override public boolean isDefined() {
            return true;
         }

         @Override public T get() {
            return value;
         }

         @Override public T or(final T defaultValue) {
            return value;
         }
      };
   }

   @SuppressWarnings("unchecked")
   public static <T> Maybe<T> nothing() {
      return new Maybe<T>() {
         @Override public boolean isDefined() {
            return false;
         }

         @Override public T get() {
            throw new RuntimeException("Value is not defined");
         }

         @Override public T or(final T defaultValue) {
            return defaultValue;
         }
      };
   }

   public abstract boolean isDefined();
   public abstract T get();
   public abstract T or(final T defaultValue);
}
И используется он таким образом:
final Maybe<String> noString = Maybe.<String>nothing();
final Maybe<String> string = Maybe.just("string");

if (noString.isDefined()) {
    System.out.println("error!");
} else {
    System.out.println("as expected noString is nothing.");
}

System.out.println("string is: " + string.get());
System.out.println("alternative of string is: " + string.or("should not be printed"));
System.out.println("alternative of noString is: " + noString.or("alternative"));
Существуют библиотеки, имеющие реализацию похожего типа, например тип Optional в guava. Тип конвертера также имеется в этой библиотеке.

Первый и самый важный плюс, это явно выраженное намерение.

Как известно многим (увы не всем), код пишется не для машины. Код пишется для людей. И чем точнее и недвусмысленнее основная идея, заложенная в код, будет видна читающему или пользующемуся кодом в дальнейшем, тем лучше.

Без использования Maybe, этот метод может возвращать либо объект, либо null:
public String getSome();
Однако, если использовать Maybe, то метод с такой сигнатурой всегда должен вернуть объект. Чтобы иметь возможность вернуть "отсутствие объекта" нужно явно поменять сигнатуру на:
public Maybe<String> getSome();
Из новой сигнатуры четко видно, что возможна ситуация когда объекта просто нет.

Следующий плюс, это возможность раннего обнаружения ошибок.

Если метод getSome, как уже было сказано выше, не возвращает null, но после некоторых внутренних модификаций начинает, то весь код, который работал ранее в предположении что getSome всегда возвращает объект (предположение было верно в тот момент когда этот код писался), оказывается сломан. И что самое плохое, сломан втихую, то есть это не будет обнаружено до тех пор пока getSome не вернет null для каждого места где это критично.

С другой стороны, как было показано выше, в случае если метод начинает возвращать Maybe для того, чтобы выразить факт того, что объекта может не быть, весь код, который ранее вызывал этот метод, будет сломан явно, то есть компилятор просто не даст такому коду собраться.

Действительно, в изначальном примере:
public void doWork() {
    final String some = getSome();

    if (some.isEmpty()) {
        doIfEmpty();
    } else {
        doIfContainsSomething();
    } 
} 
Строка
final String some = getSome();
Перестанет компилироваться как только сигнатура метода getSome изменится. Это даст возможность тому, кто внес изменения в getSome, корректно исправить поведение всех мест, где getSome используется.

Следующий плюс это то, что, поскольку Maybe всегда не null, у него всегда можно вызывать методы.

С одной стороны
public void doWork() {
    final Maybe<String> some = getSome();

    if (some.value().isEmpty()) {
        doIfEmpty();
    } else {
        doIfContainsSomething();
    } 
}
Вызов value() без проверки isValue() все равно приведет к падению. И это, увы, неизбежно. Но, это не единственный способ использовать значение. Например, если добавить в систему интерфейс сравнения, то то, что ранее требовало кучи кода, сравнение двух потенциально нулевых объектов, теперь можно выразить одной строчкой. Привожу больше кода, но сравнение тут только последняя строка:
public interface Comparator<T> {
    boolean isEquals(T l, T r);
} 

public final class Comparsions {
    final Comparator<String> COMPARE_STRINGS_CASE_SENSITIVE = new Comparator<String>() {
            // comparator implementation
        } ;
} 
Также это позволяет использовать значение по умолчанию таким образом:
textView.setText(something.or(""));
Этот подход хорош тем, что он позволяет обойтись без проверок тогда, когда логически эти проверки не нужны. Например, имея интерфейс конвертирования, можно преобразовать один Maybe в другой без разворачивания значения:
interface Converter<T, F> {
    T convert(F f);
}

final Maybe<String> maybeString = /* ... */;

final Maybe<Integer> strlen = maybeString.convert(GET_STRING_LENGHT);
Конечно, использование типа Maybe (или Optional, или аналога), делет код несколько более многословным, однако преимущества, которые это дает, окупают эти сложности полностью.

Friday, August 7, 2015

Android View visibility DSL

Часто встречаю такой код при разработке для Android:
view.setVisibility(condition ? View.VISIBLE : View.GONE);
Использование такой конструкции приводит к тому, что это тернарное выражение дублируется в setVisibility практически каждого view, который нужно показать или спрятать. В качестве альтернативы можно использовать простенький DSL, который позволит писать что то вроде:
gone(view,
     otherView,
     moreView)
    .orVisibleIf(condition);
Плюсов сразу несколько -это позволяет управлять видимостью нескольких view одновременно, убирает многократное повторение условного выражения и в целом короче и лучше читается. А вот "определение" этого DSL:
final class ViewVisibilityTools {
    public interface OrVisible {
        void orVisibleIf(boolean condition);
    }

    public OrVisible gone(final View...views) {
        return new OrVisible() {
            @Override public void orVisibleIf(final boolean condition) {
                setVisible(views, condition, View.GONE);
            }
        };
    }

    public OrVisible invisible(final View...views) {
        return new OrVisible() {
            @Override public void orVisibleIf(final boolean condition) {
                setVisible(views, condition, View.INVISIBLE);
            }
        };
    }

    private void setVisible(final View views[],
                            final boolean condition,
                            final int alternative) {
        final int visibility;

        if (condition) visibility = View.VISIBLE;
        else visibility = alternative;

        for (final View v: views) {
            v.setVisibility(visibility);
        }
    }
}
Нужно только статически импортировать методы gone и invisible там где они нужны.

Monday, March 9, 2015

API тесты, тестирование парсеров и toString

Мы живем в несовершенном мире. В совершенном мы имеем возможность получить полную спецификацию API, написать по ней тесты и потом написать парсеры для ответов сервера, опираясь на тесты.

Но мир несовершенен и поэтому можно столкнуться с ситуацией когда есть 100500 кода, парсящего ответы от сервера, написанного не очень хорошо, большая часть давно и многое людьми которые уже ушли.

Есть два варианта - вздохнуть и оставить все как есть или пытаться привести эту механику в порядок.

Однако, для того, чтобы привести ее в порядок нужно быть уверенным что ничего не сломается. Ну или хотя бы минимизировать этот шанс насколько это возможно разумными усилиями. Для этой цели хорошо подходят тесты. Для каждого парсера можно заготовить один или несколько тестов такого вида:

public void testParse() throws Exception {
    final String input = "some, for example, json";

    final ExpectedResult expected = new ExpectedResult(/* ... */);

    assertEquals(expected,
                 ParserUnderTest.parse(input));
}


Но вот беда - для тестов нужен образец, с которым сравнивать результат.

Хорошо если объекты небольшие и их немного. В противном случае, если объекты содержат списки объектов, которые, потенциально, содержат списки объектов и т.п. - извлечение данных и ручное конструирование объектов-образцов может занять достаточно продолжительное время, к тому же может оказаться довольно занудной процедурой. Но зачем делать самому то, что может сделать машина?

В java у объектов есть метод toString. Согласно документации, этот метод

Returns a string representation of the object. In general, the toString method returns a string that "textually represents" this object. The result should be a concise but informative representation that is easy for a person to read. It is recommended that all subclasses override this method.


Любопытно то, что результат должен быть "easy for person to read". Но мы можем дополнительно наложить ограничение чтобы он был "easy for machine to parse". Когда я столкнулся с такой задачей, то я принял следующий формат: объект в круглых скобках, список в квадратных, отображение (map) в фигурных. Получается что то вида:

(ClassName field=(OtherClass field="string" intField=1))

[(ClassName field=null), (ClassName field=null)]


Очевидно, что такой формат подпадает под оба критерия - он легко может быть разобран машиной и в то же время не теряет человекочитаемости.

Для формирования такого вывода удобно определить класс с интерфейсом вроде такого:
public final class ToStringBuilder {
    public ToStringBuilder(final String className);
    public ToStringBuilder(final Object o);

    public <T> ToStringBuilder field(final String name,
                                     final List<T> o);

    public <K, V> ToStringBuilder field(final String name,
                                        final Map<K, V> o);

    public ToStringBuilder field(final String name,
                                 final Object o);

    public String toString();
}


В таком случае реализация метода toString для каждого класса API представляется довольно тривиальной:

public final class SomeApiClass {
    /* ... */ 

    private final int mIntField;
    private final String mStringField;
    
    @Override public String toString() {
        return new ToStringBuilder(this)
            .field("mIntField", mIntField)
            .field("mStringField", mStringField)
            .toString();
    }
}


более того, в языках где существует reflection, можно использовать его для автоматического вывода объектов.

Таким образом, у нас есть возможность получить строковое представление, которое затем можно распарсить и из распаршеного представления сгенерировать объект-образец.

Тут также возможно несколько путей - строить объекты с помощью reflection'а же или просто сгенерировать код, создающий требуемые объекты. Я пошел по последнему пути, так как это делает код тестов легче поддерживаемым.

Аналогично можно по разному осуществлять сравнение объектов, однако, как выяснилось, самым простым способом было сравнивать строковые представления объектов. То есть:

assertEquals(expected.toString(), parsed.toString());


Собирая все воедино:

- Для каждого участвующего класса реализуется метод toString. - Код, обрабатывающий ответ сервера, дополняется кодом записывающим в файл ответ сервера и строковое представление распаршенного объекта ответа.
- Создается утилита-парсер (это долгосрочное вложение - такая утилита может легко использоваться повторно, к тому же такая утилита достаточно проста - реализация на языке Haskell заняла у меня около 80 строк), которая парсит из файла из п1 пару "тело ответа"-"объект образец" и генерирует код теста такого вида, как был представлен в начале статьи - все данные для этого уже есть на этом этапе.

В результате из дампа вида:

---------- request:
some magic json!
---------- response:
[(ApiObject id=4242, name="magicName"), (ApiObject id=2223, name="othername")]


Можно автоматизированно получить код для теста вида:

final String input = "some magic json!";

assertEquals(Arrays
             .asList
             (new ApiObject(4242,
                            "magicName"),
              new ApiObject(2223,
                            "othername")).toString(), 
             PARSER.parse(input).toString());


Таким образом легко подготовить тесты для парсеров объектов ответа практически любой сложности и при этом время, необходимое на то, чтобы сгенерировать такие тесты очень слабо зависит от сложности объектов.

Upd: Кстати, да, есть вариант реализовать toString таким образом, чтобы он сразу генерировал код создания, без промежуточного шага с парсером. Однако это ломает человекочитаемость вывода toString.

И, как показала практика, сравнивать строками годится только в случае простых объектов - этот способ может не работать при использовании HashMap и HashSet. Так что для более сложных объектов придется использовать reflection-based решение для сравнения в тестах или писать код equals вручную.