Как гарантировать, что шаблон строителя завершен?
РЕДАКТИРОВАТЬ: я не беспокоюсь о том, что вызов в неправильном порядке, так как это осуществляется с помощью нескольких интерфейсов, я просто беспокоюсь о том, что метод терминала вызывается вообще.
Я использую шаблон для создания разрешений в нашей системе. Я выбрал шаблон компоновщика, потому что безопасность очень важна в нашем продукте (в нем участвуют несовершеннолетние, например, 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);
}
}
}
Это обеспечивает цепочку вызовов конструирования и очень дружелюбно относится к завершению кода, поскольку показывает, каким является следующий интерфейс, и это единственный доступный метод.
Вот более полный пример с дополнительными вещами в середине:
Это обеспечивает надежный проверенный исключительный способ построения 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 содержит только полностью определенные разрешения.
Для одного разрешения это хуже, чем первое решение, но для множества разрешений оно чище.