【问题标题】:Encapsulating builders in Java在 Java 中封装构建器
【发布时间】:2016-02-22 20:12:46
【问题描述】:

我的代码库中有 2 个构建器,可以这样访问:

return new Developer.Builder("John", "Doe").age(30).build();
return new Manager.Builder("Eve", "Page").age(40).build();

我想通过封装在一个枚举中使 API 更简单:

return Google.Developer.Builder("John", "Doe").age(30).build();
return Google.Manager.Builder("Eve", "Page").age(40).build();

我的目标是简化流程

  1. 将公司名称从 Google 更改为 Microsoft
  2. 如果添加了新角色(除了开发人员和经理),我的代码的用户可以在一个地方了解它。

我想到的唯一选择是将公司名称作为枚举 - 但是我将无法实现构建器模式。

【问题讨论】:

  • 为什么不让Employee 类提供这些构建器,以便RoleCompany 可以成为它们自己的枚举(并且您可以将相应的构建器方法添加到Employee)?跨度>
  • 没有不变的公司名单; enum 可能不是代表他们的最佳方式。
  • 一家公司是否应该或可以建模为枚举完全取决于应用程序——我们没有足够的信息。
  • @AndyTurner :您能否解释一下枚举更改的潜在问题。几乎我一直在使用的所有枚举在未来都会发生变化。
  • @MickMnemonic :我正在评估您的选择。让我一会儿回来。

标签: java design-patterns enums


【解决方案1】:

您可以创建类似于您描述的 API:

enum Google {
    Developer, Manager;

    public Builder builder(String name) {
        return new Builder(this, name);
    }

    public static class Builder {
        public Builder(Google role, String name) { ... }
        public Builder age(int age) { ... }
        public Employee build() { ... }
    }
}

现在你可以写了

Employee e = Google.Developer.builder("John").age(30).build();

我不明白这一切有什么意义。建设者是否以某种不平凡的方式依赖公司和角色?

如果不是,您可以将 Builder 定义为一个单独的类,并使用一个接口来标记曾经代表公司角色的内容,类似于 Sleiman 的回答。 如果这在您的应用程序中有意义,您甚至可以使用 company 参数化 Employee 类...

interface CompanyRole { /* just a marker */ }

enum Google implements CompanyRole { 
    ...
    Employee.Builder<Google> builder(String name) {
        return new Employee.Builder<>(this, name);
    }
}

class Employee<T extends CompanyRole> { 
    ... 

    static class Builder<T extends CompanyRole> {
        EmployeeBuilder(T role, String name) { ... }
        Employee<T> build() { ... }
    }
}

你仍然可以写

Employee<Google> e = Google.Developer.builder("John").age(30).build();

【讨论】:

  • 目前还没有,但我只想照顾未来的依赖关系。我想这种方法在一些改变后对我有用。一旦测试,我会接受这个答案。
  • 我同意乔尼的观点。我不明白为什么构建器会被声明为Google 而不是Employee。我会期待像Employee guy = Employee.getBuilder().setName("John", "Smith").setRole(Role.MANAGER).setCompany(Company.GOOGLE).setAge(45).build(); 这样的东西。我认为这使得以后更容易将员工的想法子类化为多态行为。如有必要,它允许您稍后将角色和公司转变为战略。
  • 相反,将Builder 放在Employee 类之外需要Employee 是一个bean,或者它们需要相互访问包。无论哪种方式,您都有可能在构造函数调用后创建Employee 的数据状态不可信的情况。在我看来,构建器的全部意义在于确保构建对象的构造函数不会被构建器以外的任何人调用,并且其构造函数后的状态是值得信赖的。
【解决方案2】:

您可以添加代表公司的界面

interface Company{
}

并且拥有众多知名公司,

enum Companies implements Company{
 GOOGLE,
 MICROSOFT,
 ...
}

现在在您的构建器中,您可以添加一个采用Company 而不是enum 的方法

Builder company(Company company){
  addCompany(company);
  return this;
}

并像这样流畅地构造它

Developer.Builder("John", "Doe").company(Companies.GOOGLE).age(30).build();

现在,公司可以是常量,也可以是您从数据库加载的东西(任何实现 Company 的东西)。无论哪种方式,它都是类型安全的。

【讨论】:

  • 这种方法的问题在于,并非所有公司都有相同的角色。所以我需要在 company() 方法中抛出异常。像 Google.Developer.Builder() 这样的解决方案可以让客户更轻松。
【解决方案3】:

评论

return Google.Developer.DevBuilder("John", "Doe").age(30).build();

这毫无意义。仔细看看,上面的调用会导致一个类Google,它包含一个内部类Developer。该类定义了一个名为DevBuilder静态方法,它接受两个参数,名字和姓氏,并返回Builder/DeveloperBuilder 的实例。

这不是面向对象的可扩展方法。尽管您给我们的背景很少,但我认为公司是静态对象,不会发生变化。参考您在 cmets 中所做的示例 - 新公司比新 CompressFormat 更有可能。

此外,除了对age(int)build() 的动态调用之外,不可能通过多态来改变行为。

动态方法

下面是一种更动态的方法的概念(当然应该添加机制,以确保公司只有一个对象,例如Company.byName("Google") 等)

public static void main(String[] args) {
    Company google = new Google();
    Manager manager = google.newManager();
}

static abstract class Company {
    public Manager newManager() {
        return new ManagerBuilder("Eve", "Page").age(40).build();
    }
}

static class Google extends Company {
}

您可以轻松添加新公司并更改经理(或任何其他员工)的​​创建方式,您也可以使用默认值。

重构

再玩一些,您可以通过创建两个基类如下来删除员工及其相应构建器的类中的样板代码

static abstract class Person<P extends Person<P>> {
    protected final String firstName;
    protected final String lastName;
    protected final int age;

    public <T extends AbstractBuilder<P, T>> Person(AbstractBuilder<P, T> builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
    }
}

static abstract class AbstractBuilder<P extends Person, T extends AbstractBuilder<P, T>> {
    protected final String firstName;
    protected final String lastName;
    protected int age;

    public AbstractBuilder(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    abstract T self();

    abstract P build();

    T age(int age) {
        this.age = age;
        return self();
    }
}

利用上述方法,创建一个类Manager 及其Builder 产生以下代码

static class Manager extends Person<Manager> {
    public <T extends AbstractBuilder<Manager, T>> Manager(AbstractBuilder<Manager, T> builder) {
        super(builder);
    }
}

static class ManagerBuilder extends AbstractBuilder<Manager, ManagerBuilder> {
    public ManagerBuilder(String firstName, String lastName) {
        super(firstName, lastName);
    }

    @Override
    ManagerBuilder self() {
        return this;
    }

    @Override
    Manager build() {
        return new Manager(this);
    }
}

Manager及其Builder,或任何其他员工都可以扩展更多字段。

【讨论】:

  • 感谢详细的文章,我在这里学到了一些重要的实践。需要明确的是,我的代码中的 DevBuilder 是一个静态嵌套类,而不是静态方法。您开始回答时注意到静态嵌套类不提供可扩展的设计 - 但您在回答中遵循了相同的做法。我误会了什么?
  • 如果你写了new Google.Developer.DevBuilder("John", "Doe").age(30).build() 那么 DevBuilder 将是一个嵌套类。没有 new 它只是一个静态方法。我使用了静态嵌套类,因为我将所有类放在一个文件中,但这只是为了创建一个快速示例。如果类是嵌套的并且您不使用 static 关键字,则将这些类绑定到它们所嵌套的类的对象。
  • Google.Developer.DevBuilder 也可以解释为对实例Developer 的方法DevBuilder 的非静态方法调用,它是@987654343 类型的枚举 @。这并没有使方法更好,因为如果要添加任何公司,则必须重新编译。
猜你喜欢
  • 2016-11-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-20
  • 2018-12-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多