14

编辑:我不担心以错误的顺序调用,因为这是通过使用多个接口强制执行的,我只是担心终端方法被调用。


我正在使用构建器模式在我们的系统中创建权限。我选择了构建器模式,因为安全性在我们的产品中非常重要(它涉及未成年人,所以COPPA等),我觉得权限可读性非常重要,并且认为可读性是最重要的(即使用流利的风格builder 模式,而不是具有 6 个值的单个函数)。

代码如下所示:

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

这些方法填充了一个私有后备bean,当终端方法(即 asOf )提交对数据库的权限时;如果没有调用该方法,则不会发生任何事情。开发人员偶尔会忘记调用终端方法,这不会导致编译器错误,并且在快速阅读/浏览代码时很容易错过。

我能做些什么来防止这个问题?我不想返回需要保存的 Permission 对象,因为这会引入更多噪音并使权限代码更难阅读、遵循、跟踪和理解。

我曾考虑在终端命令标记的支持上放置一个标志。然后,检查finalize方法中的标志,如果对象是在没有持久化的情况下创建的,则写入日志。(我知道finalize不能保证运行,但这是我能想到的最好的。)

4

7 回答 7

12

解决方案

构建这种流畅的 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

于 2011-07-07T16:10:49.987 回答
11

如果您真的想在代码中强制执行,您可以为 PMD 或 Findbugs 编写规则。这样做的好处是它在编译时就已经可用。


运行时:如果您只想确保用户以正确的顺序调用您的构建器,则为每个步骤使用单独的接口。

grantUser() 将返回具有方法permissionTo() 的ISetPermission,它将返回具有方法item() 的IResourceSetter ...

您可以将所有这些接口添加到一个构建器,只需确保这些方法为下一步返回正确的接口。

于 2011-07-07T16:01:24.133 回答
7
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"));
    }
}
于 2013-07-27T09:19:36.473 回答
7

现在有一个基于注释处理的编译器插件,它将为您检查,如果方法不存在,则会抛出编译错误:Fluent API sentence end check

您可以使用注释对最终方法进行@End注释,或者如果您不控制类,您仍然可以提供具有完全限定方法名称的文本文件,并进行检查。

然后Maven 可以在编译期间进行检查

它仅适用于 Java 8 及更高版本,因为它使用了那里引入的新编译器插件机制。

于 2018-08-09T19:57:37.450 回答
2

有一个步骤构建器模式可以完全满足您的需要:http ://rdafbn.blogspot.co.uk/2012/07/step-builder-pattern_28.html

于 2013-01-16T09:54:01.830 回答
0

在一个单独的步骤中应用新权限,该步骤首先验证 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)
于 2011-07-07T16:46:41.310 回答
-1

除了使用Diezel生成整套接口之外,就是强制他们获取“token”对象:

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

在 last/exit 方法返回正确的类型之前,用户将无法完成语句。Grant.permissionTo 可以是静态方法、静态导入、简单的构造函数。它将获得实际将权限注册到permissionManager中所需的一切,因此无需配置或通过配置获取。

Guice 的人们使用另一种模式。他们定义了一个“可调用”,用于配置权限(在 Guice 中,这完全是关于绑定)。

    公共类 MyPermissions 扩展权限{

    公共无效配置(){
    grantUser(userId).permissionTo(Right.READ).item(docId).asOf(new Date());
    }

    }

    permissionManager.add(new MyPermissions() );

grantUser 是一个受保护的方法。permissionManager 可以确保 MyPermissions 只包含完全限定的权限。

对于单个权限,这比第一个解决方案更糟糕,但对于一堆权限,它更干净。

于 2011-12-13T00:00:44.070 回答