简体   繁体   English

编写/实现API:可测试性与信息隐藏

[英]Writing/implementing an API: testability vs information hiding

Many times I am involved in the design/implementation of APIs I am facing this dilemma. 很多时候,我参与API的设计/实现,我正面临着这种困境。

I am a very strong supporter of information hiding and try to use various techniques for that, including but not limited to inner classes, private methods, package-private qualifiers, etc. 我是信息隐藏的强大支持者,并尝试使用各种技术,包括但不限于内部类,私有方法,包私有限定符等。

The problem with these techniques is that they tend to prevent good testability. 这些技术的问题在于它们往往会妨碍良好的可测试性。 And while some of these techniques can be resolved (eg package-privateness by putting a class into the same package), others are not so easy to tackle and either requires reflection magic or other tricks. 虽然可以解决其中一些技术(例如,通过将类放入同一个包中来实现包的私有性),但其他技术并不容易解决 ,要么需要反射魔术或其他技巧。

Let's look at concrete example: 让我们看一下具体的例子:

public class Foo {
   SomeType attr1;
   SomeType attr2;
   SomeType attr3;

   public void someMethod() {
      // calculate x, y and z
      SomethingThatExpectsMyInterface something = ...;
      something.submit(new InnerFoo(x, y, z));
   }

   private class InnerFoo implements MyInterface {
      private final SomeType arg1;
      private final SomeType arg2;
      private final SomeType arg3;

      InnerFoo(SomeType arg1, SomeType arg2, SomeType arg3) {
         this.arg1 = arg1;
         this.arg2 = arg2;
         this.arg3 = arg3;
      }

      @Override
      private void methodOfMyInterface() {
         //has access to attr1, attr2, attr3, arg1, arg2, arg3
      }
   }
}

There are strong reasons not to expose InnerFoo - no other class, library should have access to it as it does not define any public contract and the author deliberately didn't want it to be accessible. 有充分的理由不公开InnerFoo - 没有其他类,图书馆应该有权访问它,因为它没有定义任何公共合同,并且作者故意不希望它被访问。 However to make it 100% TDD-kosher and accessible without any reflection tricks, InnerFoo should be refactored like this: 然而,为了使其100%TDD-kosher并且无需任何反射技巧即可访问, InnerFoo应该像这样重构:

private class OuterFoo implements MyInterface {
   private final SomeType arg1;
   private final SomeType arg2;
   private final SomeType arg3;
   private final SomeType attr1;
   private final SomeType attr2;
   private final SomeType attr3;

   OuterFoo(SomeType arg1, SomeType arg2, SomeType arg3, SomeType attr1, SomeType attr2, SomeType attr3) {
      this.arg1 = arg1;
      this.arg2 = arg2;
      this.arg3 = arg3;
      this.attr1 = attr1;
      this.attr2 = attr2;
      this.attr3 = attr3;
   }

   @Override
   private void methodOfMyInterface() {
      //can be unit tested without reflection magic
   }
}

This examply only involves 3 attrs, but it is pretty reasonable to have 5-6 and the OuterFoo constructor would then have to accept 8-10 parameters! 这个例子只涉及3个attrs,但是5-6是非常合理的,然后OuterFoo构造函数必须接受8-10个参数! Add getters on top, and you already have 100 lines of completely useless code (getters would be also required to get these attrs for testing). 在顶部添加getter,你已经有100行完全无用的代码(getter也需要获得这些attrs进行测试)。 Yes, I could make the situation a bit better by providing a builder pattern but I think this is not only over-engineering but also defeats the purpose of TDD itself! 是的,我可以通过提供构建器模式来改善情况,但我认为这不仅仅是过度工程,而且还会破坏TDD本身的目的!

Another solution for this problem would be to expose a protected method for class Foo , extend it in FooTest and get the required data. 此问题的另一个解决方案是为Foo类公开受保护的方法,在FooTest扩展它并获取所需的数据。 Again, I think this is also a bad approach because protected method does define a contract and by exposing it I have now implicitly signed it. 同样,我认为这也是一个糟糕的方法,因为protected方法确实定义了一个契约,并且通过暴露它我现在已经隐式签署了它。

Don't get me wrong. 别误会我的意思。 I like to write testable code . 我喜欢编写可测试的代码 I love concise, clean APIs, short code blocks, readability, etc. But what I don't like is making any sacrifices when it comes to information hiding just because it is easier to unit test . 我喜欢简洁,干净的API,短代码块,可读性等等。但是我不喜欢的是在信息隐藏方面做出任何牺牲只是因为它更容易进行单元测试

Can anybody provide any thoughts on this (in general, and in particular)? 任何人都可以对此提出任何想法(一般而言,特别是)? Are there any other, better solutions for given example? 对于给定的例子,还有其他更好的解决方案吗?

My go-to answer for this type of thing is a "test proxy". 我对这类事情的回答是“测试代理”。 In your test package, derive a subclass from your system under test, which contains "pass-through" accessors to protected data. 在您的测试包中,从您正在测试的系统派生一个子类,该子类包含受保护数据的“传递”访问器。

Advantages: 好处:

  • You can directly test, or mock, methods you don't want made public. 您可以直接测试或模拟您不希望公开的方法。
  • Since the test proxy lives in the test package, you can ensure it is never used in production code. 由于测试代理位于测试包中,因此可以确保它永远不会在生产代码中使用。
  • A test proxy requires far fewer changes to code in order to make it testable than if you were testing the class directly. 测试代理对代码的更改要少得多,以使其比您直接测试类时更易于测试。

Disadvantages: 缺点:

  • The class must be inheritable (no final ) 该类必须是可继承的(没有final
  • Any hidden members you need to access cannot be private; 您需要访问的任何隐藏成员都不能是私有的; protected is the best you can do. 受保护是你能做的最好的事情。
  • This isn't strictly TDD; 这不是严格的TDD; TDD would lend itself to patterns that didn't require a test proxy in the first place. TDD将适用于首先不需要测试代理的模式。
  • This isn't strictly even unit testing, because at some level you are dependent on the "integration" between the proxy and the actual SUT. 这不是严格的单元测试,因为在某种程度上,您依赖于代理和实际SUT之间的“集成”。

In short, this should normally be a rarity. 简而言之,这应该是罕见的。 I tend to use it only for UI elements, where best practice (and default behavior of many IDEs) is to declare nested UI controls as inaccessible from outside the class. 我倾向于仅将它用于UI元素,其中最佳实践(以及许多IDE的默认行为)是将嵌套的UI控件声明为从类外部无法访问。 Definitely a good idea, so you can control how callers get data from the UI, but that also makes it difficult to give the control some known values to test that logic. 绝对是一个好主意,因此您可以控制调用者如何从UI获取数据,但这也使得难以为控件提供一些已知值来测试该逻辑。

I think you should reconsider using reflection. 我认为你应该重新考虑使用反思。

It has its own downsides but if it allows you to maintain the security model you want without dummy code, that may be a good thing. 它有自己的缺点,但如果它允许你在没有虚拟代码的情况下维护你想要的安全模型,这可能是一件好事。 Reflection is often not required, but sometimes there is no good substitute. 通常不需要反思,但有时候没有好的替代品。

Another approach to information hiding is to treat the class/object as a black box and not access any non-public methods (Though this can allow tests to pass for the "wrong" reasons ie the answer is right but for the wrong reasons.) 信息隐藏的另一种方法是将类/对象视为黑盒而不访问任何非公共方法(虽然这可以允许测试通过“错误”原因,即答案是正确的但是出于错误的原因。)

I don't see how information hiding, in the abstract, is reducing your testability. 我不知道信息隐藏在抽象中是如何降低可测试性的。

If you were injecting the SomethingThatExpectsMyInterface used in this method rather than constructing it directly: 如果您正在注入此方法中使用的SomethingThatExpectsMyInterface而不是直接构造它:

public void someMethod() {
   // calculate x, y and z
   SomethingThatExpectsMyInterface something = ...;
   something.submit(new InnerFoo(x, y, z));
}

Then in a unit test you could inject this class with a mock version of SomethingThatExpectsMyInterface and easily assert what happens when you call someMethod() with different inputs - that the mockSomething receives arguments of certain values. 然后在单元测试中,您可以使用模拟版本的SomethingThatExpectsMyInterface注入此类,并轻松断言当您使用不同输入调用someMethod()时会发生什么 - mockSomething接收某些值的参数。

I think you may have over-simplified this example anyway as InnerFoo cannot be a private class if SomethingThatExpectsMyInterface receives arguments of its type. 我认为你可能过度简化了这个例子,因为如果SomethingThatExpectsMyInterface接收到其类型的参数,则InnerFoo不能是私有类。

"Information Hiding" doesn't necessarily mean that the objects you pass between your classes need to be a secret - just that you aren't requiring external code using this class to be aware of the details of InnerFoo or the other details of how this class communicates with others. “信息隐藏”并不一定意味着您在类之间传递的对象需要是秘密 - 只是您不需要使用此类的外部代码来了解InnerFoo的详细信息或其他详细信息班级与他人沟通。

SomethingThatExpectsMyInterface can be tested outside Foo , right? SomethingThatExpectsMyInterface可以在Foo外面测试,对吧? You can call its submit() method with your own test class that implements MyInterface . 您可以使用自己的实现MyInterface的测试类调用其submit()方法。 So that unit is taken care of. 所以这个单位得到了照顾。 Now you are testing Foo.someMethod() with that well-tested unit and your untested inner class. 现在,您正在使用经过充分测试的单元和未经测试的内部类测试Foo.someMethod() That's not ideal - but it's not too bad. 这并不理想 - 但也不算太糟糕。 As you test-drive someMethod() , you are implicitly test-driving the inner class. 在测试驱动someMethod() ,您隐式地测试驱动内部类。 I know that's not pure TDD, by some strict standards, but I would consider it sufficient. 根据一些严格的标准,我知道这不是纯粹的 TDD,但我认为这已经足够了。 You're not writing a line of the inner class except to satisfy a failing test; 除了满足失败的测试之外,你不是在编写内部类的一行; that there's a single level of indirection between your test and the tested code doesn't constitute a big problem. 在测试和测试代码之间存在单一级别的间接不会构成大问题。

In your example it looks like the Foo class really needs a collaborator InnerFoo. 在您的示例中,看起来Foo类确实需要协作者 InnerFoo。

In my opinion the tension between information hiding and testability is solved by the " composite simpler than the sum of its parts " motto. 在我看来,信息隐藏和可测试性之间的紧张关系通过“ 复合比其部分之和更简单 ”的座右铭来解决。

Foo is a facade over a composite (just InnerFoo in your case, but does not matter.) The facade object should be tested on its intended behaviour. Foo是复合材料的外观(在您的情况下只是InnerFoo,但无关紧要。)应该根据其预期行为测试外观对象。 If you feel that the InnerFoo object code is not driven enough by the tests on the behaviour of Foo, you should consider what InnerFoo represents in your design. 如果你觉得通过对Foo行为的测试不能充分驱动InnerFoo对象代码,你应该考虑InnerFoo在你的设计中代表什么。 It may be that you miss a design concept. 可能是你错过了一个设计概念。 When you find it, name it, and define its responsibilities, you may test its behaviour separately. 当您找到它,命名并定义其职责时,您可以单独测试其行为。

what I don't like is making any sacrifices when it comes to information hiding 我不喜欢的是在信息隐藏方面做出任何牺牲

First, work with Python for a few years. 首先,使用Python几年。

private is not particularly helpful. private不是特别有帮助。 It makes extension of the class hard and it makes testing hard. 它使类的扩展变得困难,并且使测试变得困难。

Consider rethinking your position on "hiding". 考虑重新思考你对“隐藏”的立场。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM