Как гарантировать, что шаблон строителя завершен?

РЕДАКТИРОВАТЬ: я не беспокоюсь о том, что вызов в неправильном порядке, так как это осуществляется с помощью нескольких интерфейсов, я просто беспокоюсь о том, что метод терминала вызывается вообще.


Я использую шаблон для создания разрешений в нашей системе. Я выбрал шаблон компоновщика, потому что безопасность очень важна в нашем продукте (в нем участвуют несовершеннолетние, например, COPPA и др.), Я чувствовал, что крайне важно, чтобы разрешения были читаемыми, и чувствовал, что удобочитаемость имеет первостепенное значение (т.е. используйте свободный стиль шаблон построителя, а не одна функция с 6 значениями).

Код выглядит так:

 permissionManager.grantUser( userId ).permissionTo( Right.READ ).item( docId ).asOf( new Date() );

Методы заполняют частный компонент поддержки, который при наличии терминального метода (то есть asOf) фиксирует разрешение для базы данных; если этот метод не вызывается, ничего не происходит. Иногда разработчики забывают вызывать метод терминала, который не вызывает ошибку компилятора и его легко пропустить при быстром чтении / просмотре кода.

Что я мог сделать, чтобы предотвратить эту проблему? Я не хотел бы возвращать объект Permission, который должен быть сохранен, поскольку он вносит больше шума и делает код разрешения трудным для чтения, отслеживания, отслеживания и понимания.

Я думал о том, чтобы поставить флаг на основу, которая помечается командой терминала. Затем проверьте флаг в finalize метод и записать в журнал, если объект был создан без сохранения. (Я знаю это finalize не гарантированно бежать, но это лучшее, что я могу придумать.)

7 ответов

Решение

Теперь есть плагин для компиляции на основе обработки аннотаций, который проверит это и выдаст ошибку компиляции, если метод отсутствует: проверка конца предложения Fluent API.

Вы можете либо аннотировать ваш последний метод с помощью @Endаннотации или, если вы не управляете классом, вы все равно можете предоставить текстовый файл с полностью определенными именами методов и заставить проверку работать.

Затем Maven может проверить во время компиляции.

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

Решение

Хороший способ структурировать этот свободный API-интерфейс - это не просто возвращать this из каждого метода вернуть экземпляр Method Object Pattern который реализует Interface который поддерживает только метод, который должен быть next в списке, и последний вызов метода вернет нужный вам объект.

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

Q6613429.java

package com.stackru;

import javax.annotation.Nonnull;
import java.util.Date;

public class Q6613429
{
    public static void main(final String[] args)
    {
        final Rights r = PermissionManager.grantUser("me").permissionTo("ALL").item("EVERYTHING").asOf(new Date());
        PermissionManager.apply(r);
    }

    public static class Rights
    {
        private String user;
        private String permission;
        private String item;
        private Date ofDate;

        private Rights() { /* intentionally blank */ }
    }

    public static class PermissionManager
    {
        public static PermissionManager.AssignPermission grantUser(@Nonnull final String user)
        {
            final Rights r = new Rights(); return new AssignPermission() {

                @Override
                public AssignItem permissionTo(@Nonnull String p) {
                    r.permission = p;
                    return new AssignItem() {
                    @Override
                    public SetDate item(String i) {
                        r.item = i;
                        return new SetDate()
                    {
                        @Override
                        public Rights asOf(Date d) {
                            r.ofDate = d;
                            return r;
                        }
                    };}
                };}
            };
        }

        public static void apply(@Nonnull final Rights r) { /* do the persistence here */ }

        public interface AssignPermission
        {
            public AssignItem permissionTo(@Nonnull final String p);
        }

        public interface AssignItem
        {
            public SetDate item(String i);
        }

        public interface SetDate
        {
            public Rights asOf(Date d);
        }
    }
}

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

Вот более полный пример с дополнительными вещами в середине:

UrlBuilder.java

Это обеспечивает надежный проверенный исключительный способ построения URL объекты.

Смешивание постоянства со строительством смешивает проблемы:

Создание объекта и его хранение - это разные задачи, и их не следует смешивать. Учитывая, что .build() не подразумевает .store() и наоборот и buildAndStore() указывает на то, что смешивание проблем немедленно делает разные вещи в разных местах, и вы получаете гарантии, которые вы хотите.

Поместите ваш вызов в код персистентности в другой метод, который принимает только полностью созданный экземпляр Rights,

Вы можете написать правило для PMD или Findbugs, если вы действительно хотите применить его в коде. Это будет иметь то преимущество, что оно уже доступно во время компиляции.


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

grantUser () вернет ISetPermission, у которого есть метод licenseTo(), который вернет IResourceSetter, у которого есть метод item()...

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

public class MyClass {
    private final String first;
    private final String second;
    private final String third;

    public static class False {}
    public static class True {}

    public static class Builder<Has1,Has2,Has3> {
        private String first;
        private String second;
        private String third;

        private Builder() {}

        public static Builder<False,False,False> create() {
            return new Builder<>();
        }

        public Builder<True,Has2,Has3> setFirst(String first) {
            this.first = first;
            return (Builder<True,Has2,Has3>)this;
        }

        public Builder<Has1,True,Has3> setSecond(String second) {
            this.second = second;
            return (Builder<Has1,True,Has3>)this;
        }

        public Builder<Has1,Has2,True> setThird(String third) {
            this.third = third;
            return (Builder<Has1,Has2,True>)this;
        }
    }

    public MyClass(Builder<True,True,True> builder) {
        first = builder.first;
        second = builder.second;
        third = builder.third;
    }

    public static void test() {
        // Compile Error!
        MyClass c1 = new MyClass(MyClass.Builder.create().setFirst("1").setSecond("2"));

        // Compile Error!
        MyClass c2 = new MyClass(MyClass.Builder.create().setFirst("1").setThird("3"));

        // Works!, all params supplied.
        MyClass c3 = new MyClass(MyClass.Builder.create().setFirst("1").setSecond("2").setThird("3"));
    }
}

Существует шаблон построения шагов, который делает именно то, что вам нужно: http://rdafbn.blogspot.co.uk/2012/07/step-builder-pattern_28.html

Примените новое разрешение в отдельном шаге, который сначала проверяет, что Строитель был построен правильно:

PermissionBuilder builder = permissionManager.grantUser( userId ).permissionTo( Right.READ ).item( docId ).asOf( new Date() );
permissionManager.applyPermission(builder); // validates the PermissionBuilder (ie, was asOf actually called...whatever other business rules)

Помимо использования Diezel для генерации всего набора интерфейсов, нужно заставить их получить объект "токен":

    Grant.permissionTo (missionsManager.User( userId).permissionTo( Right.READ).item( docId).asOf( new Date())); 

пользователи не смогут завершить оператор, пока метод last / exit не вернет правильный тип. Grant.permissionTo может быть статическим методом, статически импортированным, простым конструктором. Он получит все, что ему нужно для фактической регистрации разрешения в accessManager, поэтому его не нужно настраивать или получать через конфигурацию.

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

    открытый класс MyPermissions extends Permission {

    public void configure () {
    grantUser (userId).permissionTo (Right.READ).item (docId).asOf (new Date ());
    }

    }

    allowManager.add (new MyPermissions ());

grantUser - это защищенный метод. missionsManager может гарантировать, что MyPermissions содержит только полностью определенные разрешения.

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

Другие вопросы по тегам