如何使参数保持低计数并仍保持第三方依存关系独立?


13

我使用第三方图书馆。他们向我传递了一个POJO,出于我们的意图和目的,可能是这样实现的:

public class OurData {
  private String foo;
  private String bar;
  private String baz;
  private String quux;
  // A lot more than this

  // IMPORTANT: NOTE THAT THIS IS A PACKAGE PRIVATE CONSTRUCTOR
  OurData(/* I don't know what they do */) {
    // some stuff
  }

  public String getFoo() {
    return foo;
  }

  // etc.
}

由于很多原因,包括但不限于封装他们的API和促进单元测试,我想包装他们的数据。但是我不希望核心类依赖于它们的数据(再次出于测试原因)!所以现在我有这样的事情:

public class DataTypeOne implements DataInterface {
  private String foo;
  private int bar;
  private double baz;

  public DataTypeOne(String foo, int bar, double baz) {
    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
  }
}

public class DataTypeTwo implements DataInterface {
  private String foo;
  private int bar;
  private double baz;

  public DataTypeOne(String foo, int bar, double baz, String quux) {
    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
    this.quux = quux;
  }
}

然后这个:

public class ThirdPartyAdapter {
  public static makeMyData(OurData data) {
    if(data.getQuux() == null) {
      return new DataTypeOne(
        data.getFoo(),
        Integer.parseInt(data.getBar()),
        Double.parseDouble(data.getBaz()),
      );
    } else {
      return new DataTypeTwo(
        data.getFoo(),
        Integer.parseInt(data.getBar()),
        Double.parseDouble(data.getBaz()),
        data.getQuux();
      );
  }
}

该适配器类与其他一些必须了解的第三方API结合在一起,从而限制了它在我系统的其余部分中的普遍性。但是...这个解决方案很重要!在第40页的清理代码中:

超过三个论点(多元论)需要非常特殊的理由-因此无论如何都不应使用。

我考虑过的事情:

  • 创建工厂对象而不是静态辅助方法
    • 不能解决拥有数十亿论点的问题
  • 创建具有依赖构造函数的DataTypeOne和DataTypeTwo的子类
    • 仍然具有受多态保护的构造函数
  • 创建完全相同的接口的完全独立的实现
  • 多个上述想法同时

这种情况应该如何处理?


请注意,这不是反腐败层的情况。他们的API没有错。问题是:

  • 我不希望我的数据结构具有 import com.third.party.library.SomeDataStructure;
  • 我无法在测试用例中构造它们的数据结构
  • 我当前的解决方案导致非常高的参数计数。我想保持参数计数低,而无需传递其数据结构。
  • 这个问题是“ 什么是反腐败层?”。我的问题是“ 如何使用一种模式(任何模式)来解决这种情况?”

我也不要求代码(否则这个问题将在SO上),只是要求足够的答案以使我能够有效地编写代码(该问题未提供)。


如果有多个这样的第三方POJO,则可能值得花精力编写使用带有某些约定的Map(例如,将键int_bar命名为Map)作为测试输入的自定义测试代码。或将JSON或XML与一些自定义中介代码一起使用。实际上,这是一种用于测试com.thirdparty的DSL。
user949300

清洁法规的完整报价:The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification — and then shouldn’t be used anyway.
Lilienthal'1

11
盲目遵守模式或编程准则是它自己的反模式
Lilienthal 2015年

2
“封装他们的API并促进单元测试”听起来像是过度测试和/或测试引起的设计损害我的情况(或表明您可以从头开始进行不同的设计)。问问自己:这是否真的使您的代码更易于理解,更改和重用?我把钱花在“不”上。您换掉这个库的可能性有多现实?大概不是。如果您将其换出,这是否真的使将完全不同的替换到位变得更容易?同样,我打赌“不”。
jpmc26 2015年

1
@JamesAnderson我只是复制了完整的引号,因为我发现它很有趣,但是从代码片段中我不清楚它是指通用函数还是特定的构造函数。我并不是要认可该声明,正如jpmc26所说,我的下一条评论应该向您表明我没有这样做。我不确定为什么您会觉得需要攻击学者,但是使用多音节并不能使某人成为学术精英,而他们却栖息在他的象牙塔上。
Lilienthal 2015年

Answers:


10

当有多个初始化参数时,我使用的策略是创建一个仅包含初始化参数的类型

public class DataTypeTwoParameters {
    public String foo;  // use getters/setters instead if it's appropriate
    public int bar;
    public double baz;
    public String quuz;
}

然后,DataTypeTwo的构造函数接受一个DataTypeTwoParameters对象,并通过以下方式构造DataTypeTwo:

DataTypeTwoParameters p = new DataTypeTwoParameters();
p.foo = "Hello";
p.bar = 4;
p.baz = 3;
p.quuz = "World";

DataTypeTwo dtt = new DataTypeTwo(p);

这给了很多机会来弄清楚进入DataTypeTwo的所有参数是什么以及它们的含义。您还可以在DataTypeTwoParameters构造函数中提供合理的默认值,以便仅以API使用者喜欢的任何顺序完成需要设置的值。


有趣的方法。您将把相关内容放在哪里Integer.parseInt?在设置器中,还是在参数类之外?
durron597

5
在参数类之外。参数类应该是一个“哑”对象,除了表达所需的输入及其类型外,不要尝试做任何其他事情。解析应该在其他地方完成,例如:p.bar = Integer.parseInt("4")
Erik

7
这听起来像参数对象模式
gnat 2015年

9
...或反模式。
Telastyn

1
...或者您可以将其重命名DataTypeTwoParametersDataTypeTwo
user253751

14

在这里,您确实有两个单独的问题:包装API和使参数计数保持较低。

包装API时,其想法是像从头开始一样设计接口,除了要求外,一无所知。您说他们的API没什么问题,然后在同一个清单中列出了他们的API的许多问题:可测试性,可构造性,一个对象中的参数太多,等等。编写您希望拥有的API 。如果那需要多个对象而不是一个,请执行此操作。如果需要将上一层包装到创建 POJO 的对象上,请执行此操作。

然后,有了所需的API后,参数计数可能不再是问题。如果是这样,则可以考虑以下几种常见模式:

  • 参数对象,如Erik的answer所示
  • 生成器模式,让您创建一个单独的生成器对象,然后调用一些制定者的单独设定参数,然后创建你的最终目标。
  • 原型模式,在那里你克隆你想要的对象的子类与领域已经内部设置。
  • 您已经熟悉的工厂。
  • 以上几种组合。

请注意,这些创建模式通常最终会调用一个polyadic构造函数,将其封装后,您应该认为还可以。多元构造函数的问题不是一次调用它们,而是在您每次需要构造一个对象时都被迫调用它们的时候。

请注意,通过存储对OurData对象的引用并转发方法调用,而不是尝试重新实现其内部结构,通常可以更轻松,更易于维护地传递给底层API 。例如:

public class DataTypeTwo implements DataInterface {
  private OurData data;

  public DataTypeOne(OurData data) {
    this.data = data;
  }

   public String getFoo() {
    return data.getFoo();
  }

  public int getBar() {
    return Integer.parseInt(data.getBar());
  }
  ...
}

答案的前半部分:非常好,非常有帮助,+ 1。答案的下半部分:“通过存储对OurData对象的引用来传递到底层API ”-这是我试图避免的问题,至少在基类中要这样做,以确保不存在依赖关系。
durron597

1
因此,您只能在的一种实现中执行此操作DataInterface。您为模拟对象创建另一个实现。
Karl Bielefeldt

@ durron597:是的,但是如果它确实困扰您,您已经知道如何解决该问题。
布朗

1

我认为您可能对鲍伯叔叔的建议过于严格地解释了。对于具有逻辑,方法和构造函数等的普通类,多元构造函数确实确实感觉很像代码气味。但是对于严格来说是公开字段的数据容器并且已经由本质上已经由Factory对象生成的东西而言,我认为它并不算太糟糕。

可以使用Parameter Object模式,如注释中所建议的那样,可以为您包装这些构造函数参数,您的本地数据类型包装器实际上已经是Parameter对象。您所有Parameter对象要做的就是打包参数(如何创建参数?如何使用polyadic构造函数?),然后稍后再将其打包到几乎相同的对象中。

如果您不想公开您的字段的setter并调用它们,我认为坚持在定义明确且封装好的工厂中使用polyadic构造函数是可以的。


问题是,我的数据结构中的字段数已更改多次,并且可能会再次更改。这意味着我需要在所有测试用例中重构构造函数。带有合理默认值的参数模式听起来是一种更好的方法。将可变版本保存为不可变形式可以使我的生活在许多方面变得更加轻松。
durron597
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.