EXPEDIA GROUP ТЕХНОЛОДЖИ — ИНЖИНИРИНГ

Избегайте необязательных или обнуляемых атрибутов с помощью этого простого трюка

Просто выньте их таким образом

TLDR, сводка

Необязательные атрибуты, допускающие значение NULL, без необходимости вводят состояние ваших объектов и без необходимости делают ваш код более подверженным ошибкам и более сложным. По умолчанию необязательных атрибутов, допускающих значение NULL, можно и нужно избегать. Очень простой и идиоматический способ избежать их — просто убрать их.

Более длинная версия

Сколько раз вы сталкивались с такими уроками?

Обратите внимание, чрезмерное использование String и Int для всего — это отдельная проблема, «примитивная одержимость», но это совсем другая тема — тема этого поста строго о том, как составлять классы)

class User {
  String userId;
  String username;
  String bestFriend;
  String favoriteBand;
}

где userId и имя пользователя — это основные атрибуты, которые составляют основную часть, которую представляет класс, а bestFriend и favoriteBand — необязательные атрибуты, допускающие значение NULL, и на самом деле не имеют ничего общего с тем, что представляет собой пользователь.

Вы, вероятно, сталкивались с этим больше раз, чем можете сосчитать, даже в свежем коде, верно? Вероятно, большинство классов в наших кодовых базах содержат смесь ненулевых и нулевых атрибутов. (или, по крайней мере, очень значительная доля)

Почему это плохо

Поскольку любой данный объект теперь может находиться в состоянии

  1. имеющий все атрибуты
  2. все кроме bestFriend
  3. все кроме favoriteBand
  4. просто userId и username.

Это 4 (!) штата! По сравнению только с одним возможным состоянием, если бы оно всегда имело все атрибуты.

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

Помимо простой концептуальной умственной сложности, теперь вполне реальные ошибки будут возникать в результате таких действий, как

listOfFriends.add(someUser.getBestFriend()); //java.lang.UnsupportedOperationException

Итак, теперь каждая часть кода, имеющая дело с этими объектами, должна будет учитывать состояние и иметь дело с возможными отсутствующими значениями, например:

if (someUser.getBestFriend() != null) {
  listOfFriends.add(someUser.getBestFriend());
} else { //do nothing }

Хуже того, компилятор не предупредит о проблеме, если ее не устранить, это просто приведет к ошибкам во время выполнения.

Кроме того, концептуально, когда вам, например, нужен лучший друг кого-то, вам нужен объект лучший друг, а не исходный пользователь (не говоря уже о всех подробностях об этом пользователе), который считает этого человека своим лучшим другом!

Решение

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

class User {
  String userId;
  String username;
}

и превратились в свою вещь:

class BestFriend { 
  String originUserId;
  String bestFriendUserId;
}
 
class BestFriendService { 
  Optional<User> findBestFriendOf(String userId);
}
class FavoriteBand { 
  String userId; 
  String bandId; //or String bandName or whatever 
}
 
class FavoriteBandService { 
  Optional<User> findBestFriendOf(String userId);
}

Обратите внимание, что это не какое-то «умное» решение, и оно не полагается на аннотации, необязательные атрибуты, обнуляемые атрибуты Kotlin и т. д. — мы буквально просто перемещаем концепцию лучшего друга из пользовательского класса в его собственную вещь — что это такое! Он не принадлежит пользовательскому объекту!

Общие аргументы

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

Оставить атрибуты у пользователя и использовать типы Kotlin? / Необязательно / @NotNull и @Nullable решают эту проблему

Нет, это не так. Все, что они делают, это заставляют компилятор предупреждать и предотвращать такие вещи, как:

listOfFriends.add(someUser.getBestFriend()); //java.lang.UnsupportedOperationException

что здорово! Но фундаментальная проблема состояния и сложности (как концептуальной, так и в конкретном коде) все еще существует (то есть при работе с объектом мы все еще должны делать if/map или что-то еще), и класс остается плохо спроектированным, с атрибутами, которые на самом деле не имеет ничего общего с тем, что представляет собой пользователь.

Обзор кода выявит любые возникающие ошибки

Может быть! Во что бы то ни стало, вы можете тщательно просмотреть код, чтобы убедиться в отсутствии ошибок. Но… почему бы не написать код таким образом, чтобы полностью избежать проблемы, значительно упростив проверку кода, чтобы вам не пришлось (надеюсь) выявлять их при проверке кода?

Как насчет того, чтобы сделать атрибуты необязательными со значениями по умолчанию?

Иногда это вариант (каламбур). Но кого вы собираетесь установить как bestFriend, и какую группу вы собираетесь установить как favoriteBand? Не всегда возможно установить значение по умолчанию — часто значения по умолчанию просто устанавливаются на дрянные незначащие значения, такие как «NA» или «unknown» или что-то еще, просто чтобы угодить компилятору, когда на самом деле это сильный запах, что что-то не так. правильно, и есть основная проблема, которую следует исправить вместо этого — классы должны быть переработаны.

Иногда атрибуты действительно являются необязательными, обнуляемыми/иногда необязательными, обнуляемые атрибуты полезны, практичны и т. д.

Конечно! Несмотря на то, что может показаться, я на самом деле не привожу какой-то фундаменталистский аргумент, что абсолютно ни один класс не должен содержать необязательные атрибуты, допускающие значение NULL. Я просто говорю, что вместо того, чтобы по умолчанию небрежно и свободно добавлять необязательные атрибуты, допускающие значение NULL, к классам, вместо этого по умолчанию старайтесь, чтобы классы были свободны от необязательных атрибутов, допускающих значение NULL, насколько это возможно, и вводите их только тогда, когда это действительно уместно.

Что, если мне всегда нужно загружать пользователей, включая их лучшего друга и любимую группу?

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

Обратите внимание, что атрибуты могут быть нулевыми.

public class User {
    private int firstAttribute;
    private String secondAttribute;
    private Byte thirdAttribute;
    private Byte fourthAttribute = 0;
    private String fifthAttribute = 0;
    private int sixthAttribute;
    // etcetera
}

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

Другие соображения

Преимущества базы данных тоже

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

mysql> select * from user;
| user_id | username | best_friend_user_id | favorite_band |
| xxx     | foo      | null                | null          |
| yyy     | bar      | zzz                 | null          |
| zzz     | smurf    | yyy                 | The Smurfs    |

Они будут выглядеть так:

mysql> select * from user;
| user_id | username |
| xxx     | foo      |
| yyy     | bar      |
| zzz     | smurf    |

и

mysql> select * from best_friends;
| first_user | second_user |
| xxx| xxx         |
...
mysql> select * from favorite_bands;
| user_id | favorite_band |
| xxx     | The Smurfs    |
...

Преимущества инфраструктуры и производительности

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

  • природа —они часто составляют отношения между вещами, а не сами вещи, что верно как для bestFriend, так и для favoriteBand.
  • «загрузить профиль» —в то время как, например, пользовательский объект userId и username нужно загружать очень редко и изменять еще реже, bestFriend и favoriteBand могут меняться чаще и иметь разное кэширование и время жизни. . Мы не хотим без необходимости добавлять трафик в таблицу пользователей, перезагружать и хранить всего пользователя в кешах и так далее, когда на самом деле мы просто хотим обновить bestFriend и favoriteBand!

Так что разделение помогает и в этом, особенно при масштабной работе, как мы в Expedia Group™️.

Узнайте больше о технологиях в Expedia Group