Как убедиться, что шаблон построителя завершен?
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
в список и последний вызов метода возвращают фактический объект, который вам нужен.
Если это единственный способ получить экземпляр этого объекта, последний вызов всегда нужно вызывать.
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"));
}
}
Ответ 4
Существует шаблон построителя шагов, который делает именно то, что вам нужно: http://rdafbn.blogspot.co.uk/2012/07/step-builder-pattern_28.html
Ответ 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 содержит только полные разрешения.
Для одного разрешения это хуже, чем первое решение, но для кучи разрешения оно более чистое.