Статьи

Повышенная безопасность во время компиляции с фантомными типами

Вступление

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

1
2
3
4
5
6
public class MyPhantomType<T> {
  public String sayHello() {
     return 'hello';
  }
  // other methods/fields that never refer to T
}

Этот пример класса имеет параметр типа T, но он никогда не используется в коде. На первый взгляд это не очень полезно, но это не так! Все экземпляры объектов фантомных типов содержат информацию о типе, поэтому этот метод можно использовать для «пометки» значений некоторой дополнительной информацией, которую можно проверить во время компиляции. Конечно, мы можем в любой момент избежать печати, написав код без обобщений, но этого следует избегать любой ценой. Некоторые языки, такие как Scala, полностью запрещают сброс параметров типа, поэтому в Scala вам всегда придется полностью хранить информацию о типе.

Пример использования и реализация

Один из самых простых и полезных вариантов использования для фантомных типов — это идентификаторы базы данных. Если у нас есть типичное трехслойное веб-приложение Java (данные, сервис, веб), мы можем получить большую безопасность во время компиляции, заменив использование необработанных идентификаторов фантомными типами везде, кроме «конечных точек» архитектуры. Таким образом, слой данных будет помещать необработанные идентификаторы в запросы к базе данных, а веб-слой может получать необработанные идентификаторы из внешних источников (например, параметров HTTP), но в противном случае мы всегда имеем дело с фантомными типами. В этом примере я предполагаю, что тип идентификатора базы данных всегда 64-битный длинный номер. Сначала нам понадобится маркерный интерфейс, который будет реализован всеми «классами сущностей», которые должны поддерживаться механизмом идентификатора фантомного типа:

1
2
3
public interface Entity {
  Long getId();
}

Единственная цель этого интерфейса маркера состоит в том, чтобы ограничить наш фантомно идентифицированный идентификатор определенным набором помеченных классов и предоставить метод getId, который будет использоваться в реализации. Фактический фантомный тип является неизменным контейнером для одного значения идентификатора. Параметр type представляет «целевой тип» идентификатора, который позволяет различать значения идентификаторов различных объектов безопасным способом во время компиляции. Мне нравится называть этот класс Ref (сокращение от Reference), но это всего лишь личный выбор.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
@Value
@RequiredArgsConstructor(AccessLevel.PRIVATE)
public final class Ref<T extends Entity> implements Serializable {
  public final long id; 
 
  public static <T extends Entity> Ref<T> of(T value) {
    return new Ref<T>(value.getId());
  }
  public static <T extends Entity> Ref<T> of(long id, Class<T> clazz) {
    return new Ref<T>(id);
  }
 
  @Override
  public String toString() {
    return String.valueOf(id);
  }
 
}

В этом примере класса используются аннотации @Value и @RequiredArgsConstructor из проекта Lombok. Если вы не используете Lombok, добавьте реализации конструктора, getter, equals и hashCode вручную (или посмотрите полную реализацию ниже). Обратите внимание, что параметр типа T никогда нигде не используется. Это также означает, что вы не можете знать во время выполнения тип Ref, но это обычно не требуется.

Используя пример реализации

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

1
2
3
4
5
6
7
8
9
void addUserToGroup(long userId, long groupId);
// without parameter names
void addUserToGroup(long, long);
 
// VS
 
void addUserToGroup(Ref<User> userRef, Ref<Group> groupRef);
// without parameter names
void addUserToGroup(Ref<User>, Ref<Group>);

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

  1. Если у вас есть экземпляр фактического объекта, вызовите Ref.of (объект). Это самый распространенный метод в слоях, отличных от веб
  2. Если у вас есть необработанный идентификатор, и вы знаете тип цели, вызовите Ref.of (id, TargetType.class). Обычно это требуется в веб-слое, если необработанный идентификатор получен из внешнего источника.

Чтобы извлечь необработанное значение идентификатора из ссылки, вы можете прочитать поле или использовать метод получения. Обычно это требуется только перед построением запроса к базе данных.

Заключительные мысли

Чтобы понять преимущества ссылок, постарайтесь подумать о следующих случаях:

  • Что произойдет, если вы измените порядок параметров в вызове метода, который принимает идентификаторы разных типов? (например, наша addUserToGroup)
  • Что произойдет, если вы измените тип идентификатора базы данных (например, Integer -> Long или Long -> UUID)?
  • Насколько вероятно, что вы получите ошибки времени выполнения, если у вас часто есть параметры методов того же типа, что и идентификатор, но они не являются идентификаторами? Например, если у вас есть целочисленные идентификаторы и вы смешиваете идентификаторы и какие-то индексы списков в одном и том же методе

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

Идентификаторы базы данных являются простым примером фантомных типов. Другие типичные варианты использования включают в себя какие-то конечные автоматы (например, Order <InProcess>, Order <Completed> против просто объектов Order) и некоторую информацию о единицах для значений (например, LongNumber <Weight>, LongNumber <Temperature> и просто longs) ,

Реализация <T> (без Lombok)

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
32
33
34
35
36
37
38
public final class Ref<T extends Entity> implements Serializable {
  public final long id;
 
  public static <T extends Entity> Ref<T> of(T value) {
    return new Ref<T>(value.getId());
  }
  public static <T extends Entity> Ref<T> of(long id, Class<T> clazz) {
    return new Ref<T>(id);
  }
 
  @Override
  public String toString() {
    return String.valueOf(id);
  }
 
  private Ref(long id) {
    this.id = id;
  }
 
  public long getId() {
    return this.id;
  }
 
  @Override
  public int hashCode() {
    return (int) (id ^ (id >>> 32));
  }
 
  @Override
  public boolean equals(Object o) {
    if (this == o)
      return true;
    if (o == null || o.getClass() != this.getClass())
      return false;
    Ref<?> other = (Ref<?>) o;
    return other.id == this.id;
  }
}

Ссылка: Повышенная безопасность во время компиляции благодаря фантомным типам от нашего партнера по JCG Джунаса Джаванайнена из технического блога Gekkio .