编码是通过接口进行编程时所学的最早和最基本的原理之一。
定义
基于接口的编程将应用程序定义为组件的集合,其中组件之间的应用程序编程接口(API)调用只能通过抽象接口进行,而不能通过具体的类进行。 通常,通过使用Factory模式之类的技术通过其他接口来获取类的实例。
https://zh.wikipedia.org/wiki/基于界面的编程
例如,以下签名不符合标准,因为它使用了具体的类:
<T>ArrayList<T>foo();
合规签名应为:
<T>List<T>foo();
这种设计的基本原理是,调用方不必关心具体的问题,而只关心合同。 因此,提供程序的实现可以更改而不会影响调用者代码。 通过接口进行编程有助于避免连锁反应 ,即在特定位置更改代码库将需要在许多其他位置进行更改。
冰山一角
我在课程中教过它,并且是在代码审查中率先指出它的人之一。 但是,最近,我开始考虑它是否应该像现在这样普遍存在。
回到前面的示例:如果签名引用List
,则实现代码可以返回ArrayList
, LinkedList
或与此相关的符合List
任何实现。 它肯定会在所有这些情况下编译 。 但是,切换返回的实现可能会有其他副作用。
您可能知道ArrayList
由一个数组支持,而LinkedList
由彼此引用的Node
对象支持。 根据执行的操作,两者均提供不同的性能结果。
Data structure | Time complexity | |||||||
---|---|---|---|---|---|---|---|---|
Average | Worst | |||||||
Access | Search | Insertion | Deletion | Access | Search | Insertion | Deletion | |
Array | θ(1) | θ(n) | θ(n) | θ(n) | O(1) | O(n) | O(n) | O(n) |
Doubly-Linked List | θ(n) | θ(n) | θ(1) | θ(1) | O(n) | O(n) | O(1) | O(1) |
因此,返回的确切内容可能是实现细节,也可能不是。
不只是剪纸
抛弃先前的问题很容易:查看代码并检查实现非常简单。
但是,此检查并不意味着依存关系不会改变。 更糟糕的是,以上示例仅在冰山顶部刷过。 另一个案例涉及OOP的核心原则:
这两个准则自然会导致Decorator模式 :
![装饰图案](https://i-blog.csdnimg.cn/blog_migrate/29576d284b03fbfd4178e031c9846481.png)
例如,让我们使用Cactoos库中的Scalar
层次结构:
![标量](https://i-blog.csdnimg.cn/blog_migrate/cf113c457ad146a92464f17ac15109e5.png)
该设计遵循上述原理。 组合不同的Scalar
对象非常简单, 例如 :
Scalarscalar=newStickyScalar<String>(
newRetryScalar<String>(
newScalar<String>(){
@Override
publicStringgetValue(){
// Get the value from a network call
}
}
,3)
);
所有这些嵌套的装饰器层使得很难理解有关值的属性。 例如,在上面的代码片段中,一旦成功读取一次, StickyScalar
保留该值。 在下面,有3次尝试从网络读取数据。
仅仅知道一个变量包含Scalar
在大多数情况下可能还不够:随着时间的流逝,有些返回相同的值,而有些则没有,等等。
结论
处理抽象而不是混凝土被认为是一种好习惯。 在system-GUI中,最常用的示例是返回一个Window
引用,知道它将在Unix系统上是UnixWindow
,而在Windows系统上是WindowsWindow
。 在这种情况下-以及其他一些事情,这是正确的做法。
但是,在其他情况下则不是。 正确管理的类型不是接口,而是更具体的类型。