Как убедиться, что шаблон строителя завершен? - PullRequest
13 голосов
/ 07 июля 2011

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


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

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

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

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

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

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

Ответы [ 7 ]

11 голосов
/ 07 июля 2011

Решение

Хороший способ структурировать этот беглый шаблон 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.

11 голосов
/ 07 июля 2011

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


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

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

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

6 голосов
/ 27 июля 2013
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"));
    }
}
4 голосов
/ 09 августа 2018

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

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

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

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

2 голосов
/ 16 января 2013

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

0 голосов
/ 13 декабря 2011

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


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

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

В 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 - защищенный метод.missionsManager может гарантировать, что MyPermissions содержит только полностью определенные разрешения.

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

0 голосов
/ 07 июля 2011

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

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)
...