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

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


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

Код выглядит примерно так:

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

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

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

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

Ответ 1

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


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

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

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

Ответ 2

Решение

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

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

Q6613429.java

package com.stackoverflow;

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.

Ответ 3

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"));
    }
}

Ответ 5

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

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)

Ответ 6

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

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

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

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

    public class MyPermissions extends Permission{

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

    }

    permissionManager.add(new MyPermissions() );

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

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