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

发布于 2024-10-18 01:45:34 字数 2459 浏览 6 评论 0原文

很多时候,我在参与 API 的设计/实现时都面临着这样的困境。

我是信息隐藏的坚定支持者,并尝试使用各种技术来实现这一点,包括但不限于内部类、私有方法、包私有限定符等。

这些技术的问题在于它们往往会妨碍良好的可测试性。虽然其中一些技术可以解决(例如,通过将类放入同一个包中来实现包私有性),但其他技术则不太容易解决,要么需要反射魔法或其他技巧。

让我们看一下具体的例子:

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
      }
   }
}

有充分的理由不公开 InnerFoo - 没有其他类、库应该访问它,因为它没有定义任何公共合约,而且作者故意不希望它这样做易于访问。然而,为了使其 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
   }
}

这个例子只涉及 3 个属性,但有 5-6 个属性和 < code>OuterFoo 构造函数必须接受 8-10 个参数!在顶部添加 getters,你已经有 100 行完全无用的代码(还需要 getters 来获取这些属性进行测试)。是的,我可以通过提供构建器模式让情况好一点,但我认为这不仅是过度设计,而且违背了 TDD 本身的目的!

此问题的另一个解决方案是公开类 Foo 的受保护方法,在 FooTest 中扩展它并获取所需的数据。再说一次,我认为这也是一个糟糕的方法,因为受保护的方法确实定义了一个合约,并且通过公开它,我现在已经隐式签署了它。

别误会我的意思。 我喜欢编写可测试的代码我喜欢简洁、干净的 API、简短的代码块、可读性等。但我不喜欢的是在信息隐藏方面做出任何牺牲仅仅因为它更容易单元测试

有人可以对此提供任何想法(一般而言,特别是)吗?对于给定的示例,还有其他更好的解决方案吗?

Many times I am involved in the design/implementation of APIs I am facing this dilemma.

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 (e.g. 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. However to make it 100% TDD-kosher and accessible without any reflection tricks, InnerFoo should be refactored like this:

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! 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). 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!

Another solution for this problem would be to expose a protected method for class Foo, extend it in FooTest and get the required data. 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.

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.

Can anybody provide any thoughts on this (in general, and in particular)? Are there any other, better solutions for given example?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

嘿哥们儿 2024-10-25 01:45:34

我对此类事情的首选答案是“测试代理”。在您的测试包中,从被测系统派生一个子类,其中包含受保护数据的“直通”访问器。

优点:

  • 您可以直接测试或模拟您不想公开的方法。
  • 由于测试代理位于测试包中,因此您可以确保它永远不会在生产代码中使用。
  • 与直接测试类相比,测试代理需要对代码进行更少的更改才能使其可测试。

缺点:

  • 类必须是可继承的(没有final
  • 您需要访问的任何隐藏成员都不能是私有的;受保护是你能做的最好的事情。
  • 这并不是严格意义上的 TDD;而是 TDD。 TDD 适合一开始就不需要测试代理的模式。
  • 严格来说,这甚至不是单元测试,因为在某种程度上,您依赖于代理和实际 SUT 之间的“集成”。

简而言之,这通常应该是罕见的。我倾向于仅将它用于 UI 元素,其中最佳实践(以及许多 IDE 的默认行为)是将嵌套 UI 控件声明为从类外部不可访问。这绝对是个好主意,这样您就可以控制调用者如何从 UI 获取数据,但这也使得很难为控件提供一些已知值来测试该逻辑。

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)
  • Any hidden members you need to access cannot be private; protected is the best you can do.
  • This isn't strictly TDD; TDD would lend itself to patterns that didn't require a test proxy in the first place.
  • This isn't strictly even unit testing, because at some level you are dependent on the "integration" between the proxy and the actual 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. 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.

执妄 2024-10-25 01:45:34

我认为你应该重新考虑使用反射。

它有其自身的缺点,但如果它允许您在没有虚拟代码的情况下维护所需的安全模型,那可能是一件好事。反思往往是不需要的,但有时没有好的替代品。

信息隐藏的另一种方法是将类/对象视为黑匣子,并且不访问任何非公共方法(尽管这可能允许测试因“错误”原因而通过,即答案是正确的,但原因错误。)

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 i.e. the answer is right but for the wrong reasons.)

我乃一代侩神 2024-10-25 01:45:34

我不明白信息隐藏在抽象中是如何降低你的可测试性的。

如果您注入此方法中使用的 SomethingThatExpectsMyInterface 而不是直接构造它:

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

那么在单元测试中,您可以使用 SomethingThatExpectsMyInterface 的模拟版本注入此类,并轻松断言什么当您使用不同的输入调用 someMethod() 时会发生 - mockSomething 接收某些值的参数。

我认为您可能已经过度简化了这个示例,因为如果 SomethingThatExpectsMyInterface 接收其类型的参数,则 InnerFoo 不能是私有类。

“信息隐藏”并不一定意味着您在类之间传递的对象需要保密 - 只是您不需要使用此类的外部代码来了解 InnerFoo 的详细信息或该类如何与其他类通信的其他细节。

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:

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.

I think you may have over-simplified this example anyway as InnerFoo cannot be a private class if SomethingThatExpectsMyInterface receives arguments of its type.

"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.

×眷恋的温暖 2024-10-25 01:45:34

SomethingThatExpectsMyInterface 可以在 Foo 之外进行测试,对吗?您可以使用您自己的实现 MyInterface 的测试类调用其 submit() 方法。这样该单位就得到了照顾。现在,您正在使用经过充分测试的单元和未经测试的内部类来测试 Foo.someMethod()。这并不理想——但也不算太糟糕。当您测试驱动 someMethod() 时,您正在隐式测试驱动内部类。我知道,按照某些严格的标准,这不是纯粹的 TDD,但我认为这已经足够了。除了满足失败的测试之外,您不会编写一行内部类;您的测试和被测试代码之间存在单级间接并不构成大问题。

SomethingThatExpectsMyInterface can be tested outside Foo, right? You can call its submit() method with your own test class that implements MyInterface. So that unit is taken care of. Now you are testing Foo.someMethod() with that well-tested unit and your untested inner class. That's not ideal - but it's not too bad. As you test-drive someMethod(), you are implicitly test-driving the inner class. I know that's not pure TDD, by some strict standards, but I would consider it sufficient. 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.

话少情深 2024-10-25 01:45:34

在您的示例中,看起来 Foo 类确实需要一个协作者 InnerFoo。

在我看来,信息隐藏和可测试性之间的紧张关系可以通过“复合比各部分之和更简单”座右铭。

Foo 是复合体上的外观(在您的情况下只是 InnerFoo,但并不重要。)应该测试外观对象的预期行为。如果您觉得 InnerFoo 对象代码没有受到 Foo 行为测试的足够驱动,您应该考虑 InnerFoo 在您的设计中代表什么。可能你错过了一个设计理念。当你找到它、命名它并定义它的职责时,你可以单独测试它的行为。

In your example it looks like the Foo class really needs a collaborator 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. 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. 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.

我只土不豪 2024-10-25 01:45:34

我不喜欢在信息隐藏方面做出任何牺牲

首先,使用 Python 几年。

private 并不是特别有用。它使课程的扩展变得困难,并且使测试变得困难。

考虑重新考虑你对“隐藏”的立场。

what I don't like is making any sacrifices when it comes to information hiding

First, work with Python for a few years.

private is not particularly helpful. It makes extension of the class hard and it makes testing hard.

Consider rethinking your position on "hiding".

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文