Статьи

Разграничение полей состояния и зависимости в Java

Недавняя  статья в DZone  напомнила мне вопрос, с которым я лично сталкивался:

Каков наилучший способ визуально отличить поля состояния от полей зависимости в классе Java?

Это может привести к тому, что вы столкнетесь с собственным вопросом:  почему вас это волнует?

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

Два полезных информационных поля могут быть переданы:

  1. Statefulness  — Сколько состояния поддерживает этот класс? (объекты с внутренним управлением)
  2. Зависимости  — Какие вещи мы должны предоставить этому классу, чтобы он функционировал? (внешне управляемые объекты)

К сожалению, Java не предоставляет простой способ указать, какие поля предназначены для какой цели. Для иллюстрации приведем классически структурированный Java-класс, содержащий поля состояния и зависимости:

public class Radio {
    private int volumeLevel;
    private float station;
    private Set<Float> presets;
    private Antenna antenna;
    private PowerSupply powerSupply;

    // constructors, accessors, and other methods
}

Кто-то с общим пониманием радио может предположить, что первые три поля — это состояние радио, а последние два — его зависимости.

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

Комментарии

Одним из очевидных подходов будет использование комментариев.

public class Radio {
    // state
    private int volumeLevel;
    private float station;
    private Set<Float> presets;

    // dependencies
    private Antenna antenna;
    private PowerSupply powerSupply;
}

Плюсы : просто и не нарушает стандартные правила форматирования.

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

Структурное разделение внутри класса

Другой подход заключается в разделении групп полей внутри класса, таких как размещение полей зависимостей в верхней части полей класса и состояний под конструкторами, в нижней части файла или с вкраплениями между получателями / установщиками.

public class Radio {
    private Antenna antenna;
    private PowerSupply powerSupply;

    public Radio(Antenna antenna, PowerSupply powerSupply) { ... }

    private int volumeLevel;
    public int getVolumeLevel() { ... }
    public void setVolumeLeve(int volumeLeve) { ... }

    private float station;
    public float getStation() { ... }
    public void tuneStation(float amount) { .. }

    private Set<Float> presets = new HashSet<>();
    public void addPreset(float station) { ... }
    public void removePreset(float station) { ... }

    // other methods
}

Плюсы : работает с любой комбинацией модификаторов полей, аннотаций и JavaDocs на уровне полей.

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

Венгерская нотация

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

public class Radio {
    private int volumeLevel;
    private float station;
    private Set<Float> presets;

    private Antenna antennaDependency;
    private PowerSupply powerSupplyDependency;
}

Плюсы : работает с любой комбинацией модификаторов полей, аннотаций и JavaDocs на уровне полей.

Минусы : делает имена длиннее; если вы хотите иметь имена методов получения / установки без слова «зависимость», вам придется вручную их кодировать.

Маркировка зависимостей как финальная

Если вы поклонник внедрения на основе конструктора, поля зависимостей могут (и должны) быть объявлены как окончательные. Напротив, поля состояния обычно изменчивы и не будут помечены как окончательные.

public class Radio {
    private int volumeLevel;
    private float station;
    private Set<Float> presets = new HashSet<>();

    private final Antenna antenna;
    private final PowerSupply powerSupply;
}

Плюсы : хорошая практика.

Минусы : поля состояний, которые инициализируются непостоянными объектами (например, пустой коллекцией), также должны получать  final модификатор, что делает поле состояния неотличимым от поля зависимостей. При использовании внедрения на основе сеттера зависимости не всегда могут быть помечены как окончательные.

IoC Field Injection Аннотации

Если вы используете Spring, CDI или какой-либо другой IoC-каркас, вы можете применять аннотации с добавлением полей к полям зависимостей, но не к полям состояний. Наличие этих аннотаций может быть достаточным для визуального различения полей.

@Component
public class Radio {
    private int volumeLevel;
    private float station;
    private final Set<Float> presets = new HashSet<>();

    @Autowired
    private Antenna antenna;
    @Autowired
    private PowerSupply powerSupply;
}

Плюсы : удобно, если уже используется инфраструктура IoC.

Минусы : будет работать только на классах, управляемых IoC. Требует использования полевой инъекции, которая  уступает инъекции конструктора . Поля состояния могут быть аннотированы в определенных обстоятельствах, делая различие менее очевидным, например

@Value("${default.volume.level}")
private int volumeLevel;

Пользовательские Аннотации Документации

Мы можем создавать свои собственные аннотации, чтобы различать виды полей.

public class Radio {
    @State
    private int volumeLevel;
    @State
    private float station;
    @State
    private final Set<Float> presets = new HashSet<>();

    @Dependency
    private Antenna antenna;
    @Dependency
    private PowerSupply powerSupply;
}

Плюсы : визуально эффективны. Работает с любой комбинацией модификаторов полей и JavaDocs на уровне полей.

Минусы : если на полях несколько аннотаций, различие становится менее очевидным.

Ломбок Аннотации для государственных полей

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

public class Radio {
    @Getter @Setter
    private int volumeLevel;
    @Getter @Setter
    private float station;
    @Getter
    private final Set<Float> presets = new HashSet<>();

    private Antenna antenna;
    private PowerSupply powerSupply;
}

Плюсы  Удобно , если уже с помощью Ломбок.

Недостатки  Недействительно, когда  @Data используются аннотации или если для полей зависимостей нужны также методы получения и установки. Не все поля состояния могут нуждаться в методах получения и / или установки. Например, радио API может ограничить базовое  volume поле только для того, чтобы быть измененным только через эти методы:

public void increaseVolume() { ... }
public void decreaseVolume() { ... }

Зависимости в абстрактном родительском классе

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

// BaseRadio.java
public abstract class BaseRadio {
    protected Antenna antenna;
    protected PowerSupply powerSupply;
}

// Radio.java
public class Radio extends BaseRadio {
    private int volumeLevel;
    private float station;
    private final Set<Float> presets = new HashSet<>();
}

Плюсы : эффективен при отделении полей состояния от полей зависимостей, но …

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

Держатель зависимостей Inner Class

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

public class Radio {    
    private int volumeLevel;
    private float station;
    private final Set<Float> presets = new HashSet<>();

    private final Dependencies dependencies = new Dependencies();
    private class Dependencies {
        Antenna antenna;
        PowerSupply powerSupply;
    }

    public Radio(Antenna antenna, PowerSupply powerSupply) {
        dependencies.antenna = antenna;
        dependencies.powerSupply = powerSupply;
    }
}

Плюсы : Различие явное, учитывая, что поля зависимостей находятся в своей области видимости.

Минусы : слово «зависимости» появляется так много раз! Boilerplate-у. Вы теряете некоторую помощь IDE / компилятора, такую ​​как принудительное применение final и генерация конструктора с типами зависимостей в качестве аргументов.

Владелец Зависимости Private Top-Level Class

Вместо внутреннего класса вы можете перемещать зависимости в закрытый класс верхнего уровня.

// Radio.java
public class Radio {
    private int volumeLevel;
    private float station;
    private final Set<Float> presets = new HashSet<>();
    private final Dependencies dependencies;

    public Radio(Antenna antenna, PowerSupply powerSupply) {
        dependencies = new Dependencies(antenna, powerSupply);
    }
}

class Dependencies {
    final Antenna antenna;
    final PowerSupply powerSupply;

    public Dependencies(Antenna antenna, PowerSupply powerSupply) {
        this.antenna = antenna;
        this.powerSupply = powerSupply;
    }
}

Плюсы : такие же, как у внутреннего владельца класса.

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

Государство на единой карте

Зачем вообще беспокоиться об отдельных полях состояния? Мы могли бы поместить все государства в одну карту!

public class Radio {
    private enum StateProperty {
        VOLUME_LEVEL, STATION, PRESETS
    }
    private final Map<StateProperty, Object> state = new EnumMap<>(StateProperty.class);

    private final Antenna antenna;
    private final PowerSupply powerSupply;
}

Плюсы : Должен в достаточной степени отговорить авторов классов от того, чтобы их классы были наполнены.

Минусы : геттеры и сеттеры становятся более сложными для написания. Влияние на производительность из-за внедрения экземпляра Map и т. Д.

* * * * *

Мне нравятся элементы венгерской нотации, пользовательские аннотации и (как ни странно) класс частного владельца верхнего уровня. Но в целом, я не думаю, что есть какой-то один подход, который я бы хотел принять в качестве конвенции.

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