Java:如何实现设置程序的顺序无关紧要的步骤生成器?


10

编辑:我想指出,这个问题描述了一个理论上的问题,并且我知道我可以使用构造函数参数作为强制参数,或者如果API使用不正确则抛出运行时异常。然而,我在寻找,它的解决方案要求构造函数参数或运行时检查。

假设您有一个Car这样的界面:

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional
}

正如评论所暗示的,a Car必须具有Engineand,Transmission而a Stereo是可选的。这意味着可以build()Car实例创建的Builder 仅应在已将和都指定给该Builder实例的情况下才具有build()方法。这样,类型检查器将拒绝编译任何尝试在不使用or 的情况下创建实例的代码。EngineTransmissionCarEngineTransmission

这需要一个步骤构建器。通常,您将实现以下内容:

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine);
        }
    }

    public class BuilderWithEngine {
        private Engine engine;
        private BuilderWithEngine(Engine engine) {
            this.engine = engine;
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission);
        }
    }

    public class CompleteBuilder {
        private Engine engine;
        private Transmission transmission;
        private Stereo stereo = null;
        private CompleteBuilder(Engine engine, Transmission transmission) {
            this.engine = engine;
            this.transmission = transmission;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        public CompleteBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

有不同的建设者类的链(BuilderBuilderWithEngineCompleteBuilder),即加了一个又一个需要setter方法,包含所有可选setter方法,以及最后一堂课。
这意味着此步骤构建器的用户仅限于作者提供强制设置器的顺序。这是一个可能的用法示例(请注意,它们都是严格排序的:engine(e)首先是,然后是transmission(t),最后是可选的stereo(s))。

new Builder().engine(e).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).engine(e).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).engine(e).build();
new Builder().engine(e).transmission(t).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).stereo(s).build();

但是,在很多情况下,这对于构建器的用户而言并不理想,特别是如果构建器不仅具有设置器,还具有加法器,或者如果用户无法控制构建器的某些属性可用的顺序,则尤其如此。

我能想到的唯一解决方案非常复杂:对于已设置或尚未设置的强制属性的每种组合,我创建了一个专用的构建器类,该类知道在到达一个强制设置器之前需要调用哪些潜在的其他强制设置器。声明该build()方法应该可用的位置,并且这些设置器中的每个设置器都会返回一种更完整的构建器类型,该类型离包含build()方法更近了一步。
我在下面添加了代码,但是您可能会说我正在使用类型系统创建一个FSM,该FSM使您可以创建一个Builder,可以将其转换为BuilderWithEngineBuilderWithTransmission,然后都可以将其转换为CompleteBuilder,从而实现了build()方法。可以在任何这些构建器实例上调用可选的setter。 在此处输入图片说明

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder extends OptionalBuilder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            return new BuilderWithTransmission(transmission, stereo);
        }
        @Override
        public Builder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class OptionalBuilder {
        protected Stereo stereo = null;
        private OptionalBuilder() {}
        public OptionalBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
    }

    public class BuilderWithEngine extends OptionalBuilder {
        private Engine engine;
        private BuilderWithEngine(Engine engine, Stereo stereo) {
            this.engine = engine;
            this.stereo = stereo;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        @Override
        public BuilderWithEngine stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class BuilderWithTransmission extends OptionalBuilder {
        private Transmission transmission;
        private BuilderWithTransmission(Transmission transmission, Stereo stereo) {
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public BuilderWithTransmission stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class CompleteBuilder extends OptionalBuilder {
        private Engine engine;
        private Transmission transmission;
        private CompleteBuilder(Engine engine, Transmission transmission, Stereo stereo) {
            this.engine = engine;
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public CompleteBuilder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

如您所知,这不能很好地扩展,因为所需的不同构建器类的数量将是O(2 ^ n),其中n是强制设置器的数量。

因此,我的问题是:这可以做得更优雅吗?

(尽管Scala也可以接受,但我正在寻找一个适用于Java的答案)


1
是什么阻止您仅使用IoC容器来承受所有这些依赖关系?另外,我不清楚为什么,如果顺序与您所断言的无关紧要,那么您不能只使用返回的普通setter方法this
罗伯特·哈维

.engine(e)对一个构建器调用两次意味着什么?
Erik Eidt

3
如果您想静态验证它而无需为每个组合手动编写类,则可能必须使用颈胡须级的东西,例如宏或模板元编程。就我所知,Java的表达能力不足,与其他语言经过动态验证的解决方案相比,付出的努力可能不值得。
Karl Bielefeldt

1
罗伯特:目标是让类型检查器强制执行这样一个事实,即引擎和变速箱都是强制性的。这样一来,即使build()您还没有打电话engine(e),也无法打电话transmission(t)
derabbink '16

Erik:您可能想从默认Engine实现开始,然后再使用更具体的实现覆盖它。但是,如果engine(e)不是设置方法,而是加法器,则很有可能更有意义addEngine(e)。这对于Car可以生产具有多个引擎/电动机的混合动力汽车的建筑商将很有用。由于这是一个人为的示例,为简洁起见,我没有详细介绍为什么您可能要这样做。
derabbink '16

Answers:


3

根据您提供的方法调用,您似乎有两个不同的要求。

  1. 仅一个(必需)引擎,仅一个(必需)传输和仅一个(可选)立体声。
  2. 一个或多个(必需)引擎,一个或多个(必需)传输以及一个或多个(可选)立体声音响。

我认为这里的第一个问题是您不知道您想让班级做什么。部分原因是您不知道您希望构建的对象是什么样。

一辆汽车只能有一个引擎和一个变速器。甚至混合动力汽车也只有一个引擎(也许是一个GasAndElectricEngine

我将介绍两种实现:

public class CarBuilder {

    public CarBuilder(Engine engine, Transmission transmission) {
        // ...
    }

    public CarBuilder setStereo(Stereo stereo) {
        // ...
        return this;
    }
}

public class CarBuilder {

    public CarBuilder(List<Engine> engines, List<Transmission> transmission) {
        // ...
    }

    public CarBuilder addStereo(Stereo stereo) {
        // ...
        return this;
    }
}

如果需要引擎和变速器,则它们应该在构造函数中。

如果您不知道需要什么引擎或变速箱,则不要设置。这表明您正在创建的构建器距离堆栈太远。


2

为什么不使用空对象模式?摆脱这个生成器,您可以编写的最优雅的代码实际上是您不必编写的代码。

public final class CarImpl implements Car {
    private final Engine engine;
    private final Transmission transmission;
    private final Stereo stereo;

    public CarImpl(Engine engine, Transmission transmission) {
        this(engine, transmission, new DefaultStereo());
    }

    public CarImpl(Engine engine, Transmission transmission, Stereo stereo) {
        this.engine = engine;
        this.transmission = transmission;
        this.stereo = stereo;
    }

    //...

}

我就是这么想的。尽管我没有三个参数的构造函数,但是只有两个参数的构造函数具有必需的元素,然后是立体声的setter,因为它是可选的。
Encaitar '16

1
在一个简单的(人为)示例中Car,这很有意义,因为c'tor参数的数量非常小。但是,一旦您处理任何中等复杂的事物(> = 4个强制性参数),整个事情就会变得更加难以处理/可读性较差(“引擎或变速器先行吗?”)。这就是为什么要使用构建器的原因:API会强制您更加明确地说明正在构建的内容。
derabbink '16

1
@derabbink为什么在这种情况下不将您的班级分成小班?使用生成器只会掩盖该类做得太多并且变得无法维护的事实。
发现

1
结束疯狂模式的荣誉。
罗伯特·哈维

@Spotted一些类仅包含大量数据。例如,如果要生成一个访问日志类,其中包含有关HTTP请求的所有相关信息,然后将数据输出为CSV或JSON格式。将会有很多数据,如果您想在编译期间强制执行某些字段,则需要带有非常长的arg列表构造函数的构造器模式,这看起来不太好。
ssgao

1

首先,除非您有比我工作过的商店更多的时间,否则可能不值得允许任何操作顺序,或者仅仅因为您可以指定多个无线电设备而已。请注意,您在谈论的是代码,而不是用户输入,因此您可以拥有断言,这些断言将在单元测试期间失败,而不是在编译时先失败。

但是,如果您的约束(如注释中所述)必须具有引擎和变速箱,则可以通过将所有强制性属性置于生成器的构造函数中来强制执行此操作。

new Builder(e, t).build();                      // ok
new Builder(e, t).stereo(s).build();            // ok
new Builder(e, t).stereo(s).stereo(s).build();  // exception on second call to stereo as stereo is already set 

如果只有立体声是可选的,则可以使用构建器的子类来完成最后一步,但是除此之外,在编译时而不是在测试中获得错误的收益可能也不值得。


0

所需的不同构建器类的数量为O(2 ^ n),其中n是强制设置器的数量。

您已经猜出了此问题的正确方向。

如果要进行编译时检查,则需要(2^n)类型。如果要进行运行时检查,则需要一个可以存储(2^n)状态的变量。一个n位整数将起作用。


由于C ++支持非类型模板参数(例如,整数值),因此可以O(2^n)使用类似于this的方案将C ++类模板实例化为不同的类型。

但是,在不支持非类型模板参数的语言中,您不能依靠类型系统来实例化O(2^n)不同的类型。


下一个机会是使用Java注释(和C#属性)。当使用注释处理器时,这些附加的元数据可用于在编译时触发用户定义的行为。但是,实现这些对于您来说将是太多的工作。如果您正在使用为您提供此功能的框架,请使用它。否则,请检查下一个机会。


最后,请注意,O(2^n)在运行时将不同状态存储为变量(从字面上看,至少为n位宽的整数)非常容易。这就是为什么最受支持的答案都建议您在运行时执行此检查的原因,因为与潜在收益相比,实现编译时检查所需的工作量太大。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.