Статьи

Генерация сеттеров и геттеров с использованием Java :: Geci

В этой  статье мы создали очень простые генераторы hello-world, чтобы представить среду и способы генерации генераторов в целом. В этой статье мы рассмотрим генератор аксессоров, который определен в базовом модуле Java :: Geci и является коммерческим, а не демо-генератором. Несмотря на то, что генератор — это коммерческий класс, использующий сервисы фреймворка, он имеет простой код, чтобы его можно было представить в статье.

Что такое генератор доступа?

Аксессорами являются сеттеры и геттеры. Когда у класса много полей, и мы хотим помочь инкапсуляции, мы объявляем эти поля privateравными и создаем установщики и получатели, пару для каждого поля, которая может установить значение для поля (установщик) и может получить значение поля (добытчик). Обратите внимание, что вопреки тому, что думают многие юниоры, создание сеттеров и геттеров само по себе не является инкапсуляцией, но может быть инструментом для правильной инкапсуляции. И в то же время, обратите внимание, что он также не может быть инструментом для правильной инкапсуляции. Вы можете прочитать больше об этом в « Эффективной Java Джошуа Блоха : 3-е издание»,  пункт 16.

Прочитайте это с некоторой осторожностью, все же. В книге говорится, что она была обновлена ​​для Java 9. Эта версия Java содержит систему модулей. В пункте 16 главы это не упоминается, и даже в этом издании все еще говорится об использовании закрытых членов с установщиками и получателями для открытых классов, что в случае Java 9 также может означать классы в пакетах, которые модуль не экспортирует.

Многие разработчики утверждают, что сеттеры и геттеры изначально являются злом и признаком плохого дизайна. Не ошибись! Они не выступают за непосредственное использование необработанных полей. Это было бы еще хуже. Они утверждают, что вы должны программировать с более объектно-ориентированным мышлением. На мой взгляд, они правы, и все же в моей профессиональной практике мне приходится использовать множество классов, поддерживающих унаследованные приложения, использующие устаревшие фреймворки, которые содержат сеттеры и геттеры, которые необходимы инструментам программирования вокруг приложения. Теория это одно; реальная жизнь это другое. Различные интегрированные среды разработки и многие другие инструменты генерируют для нас сеттеры и геттеры, если только мы не забудем выполнить их при добавлении нового поля.

Сеттер — это метод, который имеет аргумент того же типа, что и поле, и возвращает void. (Aka не возвращает никакого значения.) Имя установщика, по соглашению, — это setи имя поля с заглавной первой буквой. Для поля businessOwnerсеттер обычно setBusinessOwner. Сеттер устанавливает значение поля равным аргументу сеттера.

Получатель также является методом, который не имеет никакого аргумента, но возвращает значение аргумента, и, следовательно, он имеет тот же тип возврата, что и тип поля. Имя получателя, по соглашению, является get, и снова, имя поля пишется с большой буквы. Таким образом, получатель будет getBusinessOwner.

В случае полей типа booleanили Boolean, получатель может иметь isпрефикс, поэтому isBusinessOwnerтакже может быть допустимым именем в случае, если поле имеет некоторый логический тип.

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

Как создать аксессоры

Генератор доступа должен генерировать код для некоторых полей класса. Этот генератор является идеальным кандидатом для генератора фильтрованного поля в Java :: Geci. Генератор отфильтрованного поля расширяет AbstractFilteredFieldsGeneratorкласс, и его process()метод вызывается один раз для каждого отфильтрованного поля. Этот метод также получает в Fieldкачестве третьего параметра в дополнение к обычному параметру Sourceи CompoundParamsпараметру, которые мы уже видели в статье несколько недель назад .

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

Основная часть кода генератора следующая:

public class Accessor extends AbstractFilteredFieldsGenerator {

    ...

    @Override
    public void process(Source source, Class<?> klass, 
                        CompoundParams params, 
                        Field field) throws Exception {
        final var id = params.get("id");
        source.init(id);
        var isFinal = Modifier.isFinal(field.getModifiers());
        var name = field.getName();
        var fieldType = GeciReflectionTools.typeAsString(field);
        var access = check(params.get("access", "public"));
        var ucName = cap(name);
        var setter = params.get("setter", "set" + ucName);
        var getter = params.get("getter", "get" + ucName);
        var only = params.get("only");
        try (var segment = source.safeOpen(id)) {
            if (!isFinal && !"getter".equals(only)) {
                writeSetter(name, setter, fieldType, access, segment);
            }
            if (!"setter".equals(only)) {
                writeGetter(name, getter, fieldType, access, segment);
            }
        }
    }
}

Код на месте многоточия содержит еще несколько методов, которые мы рассмотрим позже. Первый вызов — получить параметр id. Это специальный параметр, и в случае, если он не определен, params.get("id")возврат по умолчанию является мнемоникой генератора. Это единственный параметр, который имеет такое глобальное значение по умолчанию.

Вызов source.init(id)гарантирует, что сегмент будет рассматриваться как «затронутый», даже если генератор ничего не записывает в этот сегмент. В некоторых случаях это может произойти, и при написании генератора никогда не повредит вызову source.init(id)какого-либо сегмента, в который генератор намеревается записать.

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

Следующее, что нужно генератору сеттера / геттера, — это имя поля, а также строковое представление типа поля. Статический служебный метод GeciReflectionTools.typeAsString()— это удобный инструмент в среде, который предоставляет именно это.

Необязательный параметр конфигурации accessпопадет в переменную с тем же именем и будет использоваться в случае, если модификатор доступа установщика и получателя должен отличаться от public. По умолчанию это publicи определяется как второй аргумент метода params.get(). Метод check()является частью генератора. Он проверяет правильность модификатора и в большинстве случаев предотвращает генерацию синтаксически ошибочного кода (например, создание сеттеров и геттеров с модификатором доступа pritected). Мы рассмотрим этот метод в ближайшее время.

Следующее — это имя получателя и установщика. По умолчанию это set/get+ заглавное имя поля, но оно также может быть определено параметром конфигурации setterи getter. Таким образом, вы можете иметь, isBusinessOwnerесли это абсолютно необходимо.

Последний параметр конфигурации является ключевым only. Если код указывает only='setter'или only='getter'тогда будет создан только установщик или только получатель.

Сегмент, в который хочет записать генератор, открывается в начале блока try-with-resources и затем вызывает local writeSetterи writeGetterметоды. Есть два разных способа открыть сегмент из исходного объекта. Один звонит open(id), другой если safeOpen(id). Первый метод попытается открыть сегмент, и если сегмент с именем не определен в исходном файле класса, метод вернется null. Генератор может проверить недействительность и имеет возможность использовать другое имя сегмента, если оно запрограммировано так. С другой стороны, safeOpen()выбрасывает, GeciExceptionесли сегмент не может быть открыт. Это более безопасная версия, чтобы избежать последующих исключений нулевого указателя в генераторе. Не хорошо.

Обратите внимание, что установщик записывается только в том случае, если поле не является окончательным и если onlyключ конфигурации НЕ был сконфигурирован для getter(только).

Давайте посмотрим на эти два метода. В конце концов, это реальные основные методы генераторов, которые действительно генерируют код.

private static void writeGetter(String name, String getterName,
                                String type, String access, Segment segment) {
    segment.write_r(access + " " + type + " " + getterName + "(){")
            .write("return " + name + ";")
            .write_l("}")
            .newline();
}

private static void writeSetter(String name, String setterName,
                                String type, String access, Segment segment) {
    segment.write_r(access + " void " + setterName + "(" +
            type + " " + name + "){")
            .write("this." + name + " = " + name + ";")
            .write_l("}")
            .newline();
}

Методы получают имя поля, имя метода доступа, тип поля в виде строки, строку модификатора доступа и Segmentкод, в который необходимо записать код. Генераторы кода не записывают напрямую в исходные файлы. Объект сегмента, предоставленный платформой, используется для отправки сгенерированного кода, и каркас вставляет записанные строки в исходный код, если это необходимо.

В write(), write_l()и write_r()методы сегмента могут быть использованы для написания кода. Они работают очень похоже, String.formatесли есть более одного параметра, но они также заботятся о правильном табулировании. Когда код активизируется, write_r()сегмент будет помнить, что следующие за ним строки должны быть сведены в таблицу на четыре пробела вправо. Когда код вызывает write_l(), сегмент знает, что табуляция должна быть уменьшена на четыре символа (даже для фактической письменной строки). Они также обрабатывают многострочные строки, так что все они будут правильно табулированы.

Сгенерированный код также должен быть читабельным.

Последний нетривиальный метод — проверка модификатора доступа.

private static final Set<String> accessModifiers =
            Set.of("public", "private", "protected", "package");
...

    private String check(final String access) {
        if (!access.endsWith("!") && !accessModifiers.contains(access)) {
            throw new GeciException("'"+access+"' is not a valid access modifier");
        }
        final String modifiedAccess;
        if( access.endsWith("!")){
            modifiedAccess = access.substring(0,access.length()-1);
        }else {
            modifiedAccess = access;
        }
        if( modifiedAccess.equals("package")){
            return "";
        }
        return modifiedAccess;
    }

Цель этой проверки — защитить программиста от неправильного ввода модификатора доступа. Он проверяет , что доступ Модификатор либо private(я не вижу реальный случай использования для этого , хотя), protected, publicили package. Последний преобразуется в пустую строку, так как защищенный доступ к пакету используется по умолчанию для методов класса. В то же время использование пустой строки в конфигурации для обозначения частного доступа к пакету на самом деле не читается.

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

Осталось только методы mnemonicи cap:

private static String cap(String s) {
    return s.substring(0, 1).toUpperCase() + s.substring(1);
}

@Override
public String mnemonic() {
    return "accessor";
}

Метод mnemonic()используется платформой для определения источников, которые нуждаются в обслуживании этого генератора, а также для использования его в качестве значения по умолчанию для параметра конфигурации id. Все генераторы должны предоставить это. Другое — capзаглавная строка. Я не буду объяснять, как это работает.

Образец использования

@Geci("accessor filter='private | protected'")
public class Contained1 {

    public void callMe() {

    }

    private final String apple = "";
    @Geci("accessors only='setter'")
    private int birnen;

    int packge;

    @Geci("accessor access='package' getter='isTrue'")
    protected boolean truth;
    @Geci("accessor filter='false'")
    protected int not_this;

    public Map<String,Set<Map<Integer,Boolean>>> doNothingReally(int a, Map b, Set<Set> set){
        return null;
    }

    //<editor-fold id="accessor" desc="setters">

    //</editor-fold>

}

Класс аннотируется Geciаннотацией. Параметры accessor filter='private | protected'определяют имя генератора, который будет использоваться в этом исходном файле, и настраивают фильтр. Это говорит о том, что нам нужны сеттеры и геттеры для полей, которые являются частными и защищенными. Логическое выражение должно читаться следующим образом: «фильтруйте поле, является ли оно частным или защищенным».

Некоторые поля также аннотированы. birnenполучит только установщик, truthустановщик и получатель будут защищены пакетом и получатель получит имя isTrue(). Поле not_thisне получит установщик или получатель, потому что выражение фильтра переопределяется в аннотации поля и говорит: falseэто никогда не будет true, что необходимо обработать генератором.

Поле appleне аннотировано и будет обработано в соответствии с конфигурацией уровня класса. Он является частным, поэтому он получит доступ, а потому что finalон получит только получатель.

Код между

//<editor-fold id="accessor" desc="setters">

//</editor-fold>

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

Резюме

В этой статье мы рассмотрели генератор, который является реальным генератором коммерческого уровня в среде Java :: Geci. Обойдя код, мы обсудили, как работает код, а также некоторые другие, более общие аспекты написания генераторов кода. Следующим шагом является запуск проекта с использованием Java :: Geci в качестве тестовой зависимости, использование генератора доступа вместо генератора кода IDE (что позволяет забыть повторно выполнить генерацию метода получения сеттера), а позже, возможно, вы можете создайте свои собственные генераторы для еще более сложных задач, чем просто сеттеры и геттеры.