Статьи

Лучший из двух миров

Типобезопасные виды с использованием абстрактного шаблона документа

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

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

siQNe3MEh6TA9QAKcIUf1lA

Есть ограничения для этих статических моделей. По мере развития вашей системы вам нужно будет добавлять больше полей, изменять отношения между компонентами и, возможно, создавать дополнительные реализации для различных целей. Вы знаете историю. Внезапно, статические компоненты для каждого существительного уже не так забавны. Итак, вы начинаете смотреть на других разработчиков. Как они решают это? В нетипизированных языках, таких как JavaScript, вы можете обойти это, используя Карты. Информация о компоненте может храниться в виде пар ключ-значение. Если одной подсистеме необходимо сохранить дополнительное поле, она может сделать это, не определяя поле заранее.

1
2
var myCar = {model: "Tesla", color: "Black"};
myCar.price = 80000; // A new field is defined on-the-fly

Это ускоряет разработку, но в то же время сопряжено с большими затратами. Вы теряете безопасность типов! Кошмар каждого настоящего разработчика Java. Также сложнее тестировать и поддерживать, так как у вас нет структуры для использования компонента. В недавнем рефакторинге, который мы провели в Speedment , мы столкнулись с этими проблемами статического и динамического проектирования и предложили решение под названием Шаблон абстрактного документа .

Абстрактный шаблон документа

SWD-IduLEylpsrxRH_ZLVxQ

Документ в этой модели похож на карту в JavaScript. Он содержит несколько пар ключ-значение, в которых тип значения не указан. На вершине этого нетипизированного абстрактного документа находится ряд черт , микро-классов, которые выражают определенное свойство класса. Черты имеют типизированные методы для получения конкретного значения, которое они представляют. Классы существительных представляют собой просто объединение различных признаков поверх абстрактной базовой реализации интерфейса исходного документа. Это можно сделать, поскольку класс может наследоваться от нескольких интерфейсов.

Реализация

Давайте посмотрим на источник для некоторых из этих компонентов.

Document.java

01
02
03
04
05
06
07
08
09
10
public interface Document {
    Object put(String key, Object value);
 
    Object get(String key);
 
    <T> Stream<T> children(
            String key,
            Function<Map<String, Object>, T> constructor
    );
}

BaseDocument.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public abstract class BaseDocument implements Document {
 
    private final Map<String, Object> entries;
 
    protected BaseDocument(Map<String, Object> entries) {
        this.entries = requireNonNull(entries);
    }
 
    @Override
    public final Object put(String key, Object value) {
        return entries.put(key, value);
    }
 
    @Override
    public final Object get(String key) {
        return entries.get(key);
    }
 
    @Override
    public final <T> Stream<T> children(
            String key,
            Function<Map<String, Object>, T> constructor) {
 
        final List<Map<String, Object>> children =
            (List<Map<String, Object>>) get(key);
 
        return children == null
                    ? Stream.empty()
                    : children.stream().map(constructor);
    }
}

HasPrice.java

01
02
03
04
05
06
07
08
09
10
11
12
public interface HasPrice extends Document {
 
    final String PRICE = "price";
 
    default OptionalInt getPrice() {
        // Use method get() inherited from Document
        final Number num = (Number) get(PRICE);
        return num == null
            ? OptionalInt.empty()
            : OptionalInt.of(num.intValue());
    }
}

Здесь мы только выставляем геттер по цене, но, конечно, вы можете реализовать сеттер таким же образом. Значения всегда можно изменить с помощью метода put (), но тогда вы рискуете установить значение другого типа, чем ожидает получатель.

Car.java

1
2
3
4
5
6
7
8
public final class Car extends BaseDocument
        implements HasColor, HasModel, HasPrice {
 
    public Car(Map<String, Object> entries) {
        super(entries);
    }
 
}

Как видите, окончательный класс существительных минимален, но вы все равно можете получить доступ к полям цвета, модели и цены, используя типизированные геттеры. Добавить новое значение в компонент так же просто, как поместить его на карту, но оно не отображается, если оно не является частью интерфейса. Эта модель также работает с иерархическими компонентами. Давайте посмотрим, как будет выглядеть черта HasWheels.

HasWheels.java

1
2
3
4
5
6
7
public interface HasWheels extends Document {
    final String WHEELS = "wheels";
 
    Stream<Wheel> getWheels() {
        return children(WHEELS, Wheel::new);
    }
}

Это так просто! Мы используем тот факт, что в Java 8 вы можете ссылаться на конструктор объекта как ссылку на метод. В этом случае конструктор класса Wheel принимает только один параметр — Map <String, Object>. Это означает, что мы можем ссылаться на него как на функцию <Map <String, Object>, Wheel>.

Вывод

У этой модели есть как преимущества, так и недостатки. Структура документа легко расширяется и расширяется по мере роста вашей системы. Разные подсистемы могут предоставлять разные данные через trait-интерфейсы. Одну и ту же карту можно просматривать как разные типы в зависимости от того, какой конструктор использовался для создания представления. Другое преимущество состоит в том, что вся иерархия объектов существует в одной карте, что означает, что ее легко сериализовать и десериализовать, используя существующие библиотеки, например , инструмент Google gson . Если вы хотите, чтобы данные были неизменяемыми, вы можете просто обернуть внутреннюю карту в unmodifiableMap () в конструкторе, и вся иерархия будет защищена.

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

  • Если вы хотите увидеть реальный пример шаблона абстрактного документа в действии, взгляните на исходный код проекта Speedment, где он управляет всеми метаданными о пользовательских базах данных.