不要为不可变对象声明接口


27

不要为不可变对象声明接口

[编辑]所涉及的对象代表数据传输对象(DTO)或纯旧数据(POD)

这是一个合理的指导方针吗?

到目前为止,我经常为不可变(无法更改数据)的密封类创建接口。我一直在小心谨慎,不要在我关心不变性的任何地方使用该接口。

不幸的是,界面开始渗透代码(这不仅仅是我担心的代码)。您最终要通过一个接口,然后又想将其传递给某些代码,而这些代码确实想假设要传递给它的东西是不可变的。

由于这个问题,我正在考虑永远不要为不可变对象声明接口。

这可能会对单元测试产生影响,但是除此之外,这似乎是一个合理的指南吗?

还是我应该使用另一种模式来避免出现“扩展接口”问题?

(我之所以使用这些不可变的对象,有几个原因:主要是出于线程安全,因为我编写了许多多线程代码;还因为这意味着我可以避免将防御性的对象副本传递给方法。代码变得更加简单在很多情况下,当您知道某事物是不可变的时,如果您已经获得了接口,那么您就不会知道这一点。事实上,如果您不提供通过接口引用的对象的防御性副本,通常情况下您甚至无法克隆操作或序列化的任何方式...)

[编辑]

由于我想使对象不可变的原因,为了提供更多上下文,请参阅Eric Lippert的这篇博客文章:

http://blogs.msdn.com/b/ericlippert/archive/tags/immutability/

我还应该指出,我在这里使用一些较低级别的概念,例如在多线程作业队列中正在操纵/传递的项目。这些本质上是DTO。

约书亚·布洛赫(Joshua Bloch)也在他的《有效的Java》一书中建议使用不可变对象


跟进

谢谢您的反馈。我已经决定继续使用该指导原则,以适用于DTO及其同类。到目前为止,它运行良好,但是只有一个星期了……看起来还不错。

我还想问一些与此有关的其他问题;尤其是我所说的“深层或浅层不变性”(我从深层和浅层克隆中窃取的术语)-但这是另一个问题。


3
+1个好问题。请解释为什么它对您如此重要,以致该对象属于特定的不可变类,而不是仅实现相同接口的对象。您的问题有可能根源于其他地方。依赖注入对于单元测试非常重要,但是它具有许多其他重要的好处,如果您强迫您的代码要求特定的不可变类,这些好处就会消失。
史蒂文·多格特

1
您使用不可变一词使我有些困惑。明确地说,您是指不能继承和覆盖的密封类,还是您要公开的数据是只读的,一旦创建对象便无法更改。换句话说,您是否要保证对象是特定类型的,或者它的值永远不会改变。在我的第一条评论中,我假设是指前者,但是现在进行您的编辑,听起来更像是后者。还是您同时关注两者?
史蒂芬·多格特

3
我很困惑,为什么不只让设置器离开界面呢?但是另一方面,我确实不愿意看到那些对象的接口,这些对象实际上是域对象和/或需要这些对象的工厂的DTO,这给我造成了认知上的不和谐。
迈克尔·布朗

2
都是不可变的类的接口呢?例如,Java的类允许定义一个List<Number>能容纳IntegerFloatLongBigDecimal,等...所有这一切都是不可变的自己。

1
@MikeBrown我猜只是因为接口暗示了不变性并不意味着实现会强制执行它。您可以仅将其保留为约定(即记录接口要求不可改变性),但是如果有人违反约定,则可能会遇到一些非常讨厌的问题。
vaughandroid13年

Answers:


17

在我看来,您的规则是一个好规则(或者至少不是一个坏规则),但这仅仅是因为您所描述的情况。我不会说我在所有情况下都同意这一点,因此,从我内心的学徒的角度来看,我不得不说,您的规则在技术上过于宽泛。

通常,除非将不可变对象本质上用作数据传输对象(DTO),否则您不会定义它们,这意味着它们包含数据属性,但逻辑很少,没有依赖性。如果真是这样,就像您在这里看到的那样,我想您可以直接使用具体类型而不是使用接口。

我敢肯定会有一些单元测试的纯粹主义者会不同意,但是我认为DTO类可以安全地排除在单元测试和依赖注入之外。由于没有依赖关系,因此无需使用工厂来创建DTO。如果一切都可以根据需要直接创建DTO,那么无论如何实际上都无法注入其他类型,因此就不需要接口。而且由于它们不包含逻辑,因此无需进行任何单元测试。即使它们确实包含某些逻辑,只要它们没有依赖性,那么在必要时对逻辑进行单元测试也应该是很简单的。

因此,我认为制定一条规则,即所有DTO类均不应实现接口(尽管可能不必要),但这不会损害软件设计。由于您有一个要求,即数据必须是不可变的,并且您不能通过接口强制执行此操作,因此我想说,将该规则建立为编码标准是完全合法的。

但是,更大的问题是需要严格执行干净的DTO层。只要无接口的不可变类仅存在于DTO层中,并且DTO层没有逻辑和依赖性,那么您将很安全。但是,如果您开始混合各层,并且您的无接口类是业务层类的两倍,那么我认为您会遇到很多麻烦。


专门处理DTO或不可变值对象的代码应使用该类型的功能,而不是接口的功能,但这并不意味着不会有此类用例实现的接口使用案例,并且其他类使用的方法承诺返回的值将在最短时间内有效(例如,如果将接口传递给函数,则这些方法应返回有效值,直到该函数返回为止)。如果有很多类型可以封装相似的数据并且有一个愿望,那么这种接口将很有帮助...
supercat 2014年

...具有将数据从一个复制到另一个的方法。如果所有包含某些数据的类型都实现了相同的接口来读取数据,则可以在所有类型之间轻松复制此类数据,而无需使每个类型都知道如何从其他类型中导入。
2014年

到那时,您还可以消除您的吸气剂(和设置器,如果您的DTO由于某些愚蠢的原因而易变),并将这些字段公开。您永远不会在这里放任何逻辑,对不对?
凯文

我想是@Kevin,但是借助自动属性的现代便利性,使它们成为属性非常容易,为什么不呢?我想如果性能和效率是最重要的,那也许很重要。无论如何,问题是,您在DTO中允许什么级别的“逻辑”?即使您在其属性中添加了一些验证逻辑,只要它没有需要模拟的依赖关系,就可以对它进行单元测试。只要DTO不使用任何依赖项业务对象,那么在其中内置一些逻辑就是安全的,因为它们仍然可以测试。
史蒂文·多格加特

就我个人而言,我更喜欢使它们尽可能地清洁,但是总是有必要弯曲规则,因此最好还是敞开大门以防万一。
史蒂文·多格加特2016年

7

我曾经大肆宣传使我的代码不可滥用。我创建了只读接口来隐藏变异成员,为我的通用签名添加了很多约束,等等,等等。事实证明,大多数时候我是在做设计决定的,因为我不相信我的假想同事。“也许有一天他们会雇用一个新的入门级人员,他不会知道XYZ类不能更新DTO ABC。哦,不!” 其他时候,我专注于错误的问题-忽略了明显的解决方案-看不到穿过树林的森林。

我再也没有为DTO创建接口。我的工作假设是,接触我的代码的人员(主要是我自己)知道允许的内容和有意义的内容。如果我仍然犯同样的愚蠢错误,通常我不会尝试强化我的界面。现在,我大部分时间都在尝试理解为什么我仍然犯同样的错误。通常是因为我过度分析某些内容或缺少关键概念。自从我放弃了偏执狂以来,我的代码使用起来更加容易。最后,我还需要更少的“框架”,这些“框架”需要内部人的知识才能在系统上工作。

我的结论是找到最简单的方法。制作安全接口所增加的复杂性只会浪费开发时间,并使原本简单的代码变得复杂。当您有10,000个开发人员使用您的库时,请担心这样的事情。相信我,它将使您摆脱许多不必要的紧张局势。


我通常在需要依赖注入进行单元测试时保存接口。
特拉维斯公园公园

5

这似乎是一个不错的准则,但是出于奇怪的原因。我曾在许多地方使用接口(或抽象基类)来统一访问一系列不可变对象。策略往往落在这里。国家物体往往落在这里。我认为将接口设计成看起来是不可变的并在您的API中将其记录下来并不是没有道理的。

也就是说,人们确实倾向于过度接口纯旧数据(以下简称POD)对象,甚至是简单的(通常是不可变的)结构。如果您的代码没有某些基本结构的替代方案,则不需要接口。不,单元测试不是更改设计的充分理由(嘲笑数据库访问不是您提供接口的原因,这是将来更改的灵活性)-如果您进行测试,这不是世界末日按原样使用该基本结构。


2

当您知道某些东西是不可变的时,代码在许多情况下会变得简单得多-如果您已经获得接口,则代码不会变得简单。

我认为访问器实现者不必担心这一点。如果interface X是不可变的,那么接口实现者是否有责任确保他们以不变的方式实现接口?

但是,在我看来,没有不变的接口之类的东西-接口指定的代码约定仅适用于对象的公开方法,而与对象的内部无关。

将不变性实现为装饰器而不是接口是更为常见的做法,但是该解决方案的可行性实际上取决于对象的结构和实现的复杂性。


1
您能否提供一个将不变性实现为装饰器的示例?
vaughandroid13 2013年

我不认为这不是一件容易的事,但这是一个相对简单的思想练习-如果MutableObject具有n更改状态的m方法和返回状态的方法,ImmutableDecorator可以继续公开返回状态的方法(m),并且根据环境的不同,可以断言或调用可变方法之一时引发异常。
乔纳森·里奇

我真的不喜欢将编译时确定性转换为运行时异常的可能性……
Matthew Watson

可以,但是ImmutableDecorator如何知道给定方法是否更改了状态?
vaughandroid13年

在任何情况下,都不能确定实现接口的类对于接口定义的方法是否不变。@Baqueta装饰器必须具有有关基类实现的知识。
乔纳森·里奇

0

您最终要通过一个接口,然后又想将其传递给某些代码,而这些代码确实想假设要传递给它的东西是不可变的。

在C#中,期望“不可变”类型的对象的方法不应具有接口类型的参数,因为C#接口无法指定不可变性协定。因此,您提出的指南本身在C#中是没有意义的,因为您不能一开始就这样做(并且将来的语言版本不太可能使您做到这一点)。

您的问题源于对埃里克·利珀特(Eric Lippert)关于不变性的文章的细微误解。Eric没有定义接口IStack<T>,也没有IQueue<T>为不可变的堆栈和队列指定协定。他们没有。为了方便起见,他定义了它们。这些接口使他能够为空堆栈和队列定义不同的类型。我们可以使用单一类型为不可变堆栈提出不同的设计和实现,而无需使用接口或单独的类型来表示空堆栈,但是生成的代码看起来不那么干净,效率会低一些。

现在,让我们坚持Eric的设计。需要不可变堆栈的方法必须具有类型的参数,Stack<T>而不是一般接口IStack<T>,该接口代表一般意义上的堆栈的抽象数据类型。使用Eric的不可变堆栈时,如何执行此操作并不明显,他没有在文章中讨论此操作,但这是可能的。问题在于空堆栈的类型。您可以通过确保永远不会获得空堆栈来解决此问题。可以通过将虚拟值作为栈中的第一个值压入并且从不弹出来确保这一点。通过这种方式,你可以放心地投出的结果PushPopStack<T>

拥有Stack<T>工具IStack<T>会很有用。您可以定义需要堆栈,任何堆栈(不一定是不可变堆栈)的方法。这些方法可以具有type参数IStack<T>。这使您可以将不可变的堆栈传递给它。理想情况下,IStack<T>它将成为标准库本身的一部分。在.NET中,没有IStack<T>,但是不可变堆栈可以实现其他标准接口,从而使该类型更有用。

我前面提到的不可变堆栈的替代设计和实现确实使用了称为的接口IImmutableStack<T>。当然,在接口名称中插入“ Immutable”不会使实现它的每种类型都不可变。但是,在这个接口中,不变性的契约只是言语上的。一个好的开发者应该尊重它。

如果您正在开发一个小的内部库,则可以与团队中的每个人达成一致以遵守此合同,并且可以将其IImmutableStack<T>用作参数类型。否则,您不应使用接口类型。

我想补充一下,因为您已标记问题C#,所以在C#规范中没有DTO和POD之类的东西。因此,丢弃它们或精确定义它们可以改善问题。

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.