何时在接口设计中使用泛型


11

我有一些接口,希望将来由第三方实现,并且自己提供一个基本实现。我将仅使用几个示例。

目前,它们被定义为

项目:

public interface Item {

    String getId();

    String getName();
}

ItemStack:

public interface ItemStackFactory {

    ItemStack createItemStack(Item item, int quantity);
}

ItemStackContainer:

public interface ItemStackContainer {

    default void add(ItemStack stack) {
        add(stack, 1);
    }

    void add(ItemStack stack, int quantity);
}

现在,ItemItemStackFactory我完全可以预见到一些第三方的需要,以在将来扩展。ItemStackContainer将来也可以扩展,但不能以我无法预见的方式扩展到提供的默认实现之外。

现在,我正在尝试使此库尽可能强大。这仍处于早期阶段(pre-pre-alpha),因此这可能是过度设计(YAGNI)的行为。这是使用泛型的合适地方吗?

public interface ItemStack<T extends Item> {

    T getItem();

    int getQuantity();
}

public interface ItemStackFactory<T extends ItemStack<I extends Item>> {

    T createItemStack(I item, int quantity);
}

我担心这可能最终导致难以阅读和理解实现和用法。我认为,建议尽可能避免嵌套泛型。


1
Kinda感觉您正在以任何一种方式公开实现细节。为什么呼叫者需要与堆叠而不是简单数量一起工作并了解/关心堆叠?
cHao 2015年

那是我没想到的。确实为这个特定问题提供了一些很好的见解。我将确保对我的代码进行更改。
Zymus

Answers:


9

当您的实现也可能是通用的时,您可以在界面中使用通用。

例如,任何可以接受任意对象的数据结构都是通用接口的理想选择。示例:List<T>Dictionary<K,V>

您想要以通用的方式提高类型安全性的任何情况都适合使用泛型。A List<String>是仅对字符串进行操作的列表。

您希望以通用的方式应用SRP并避免使用特定于类型的方法的任何情况都适合使用泛型。例如,可以将PersonDocument,PlaceDocument和ThingDocument替换为Document<T>

抽象工厂模式是通用的一个很好的用例,而普通工厂方法将简单地创建从公共,具体接口继承的对象。


我真的不同意通用接口应该与通用实现配对的想法。在接口中声明通用类型参数,然后在各种实现中对其进行优化或实例化是一项强大的技术。这在Scala中尤其常见。接口抽象事物;类专门针对他们。
本杰明·霍奇森

@BenjaminHodgson:这只是意味着您正在针对特定类型的通用接口上生成实现。总体方法仍然是通用的,尽管有些少。
罗伯特·哈维

我同意。也许我误解了您的答案-您似乎建议不要使用这种设计。
本杰明·霍奇森

@BenjaminHodgson:不会针对具体接口而不是通用接口编写工厂方法吗?(尽管抽象工厂是通用的)
Robert Harvey

我认为我们同意:)我可能会将OP的示例写为class StackFactory { Stack<T> Create<T>(T item, int count); }。如果您想进一步澄清,我可以在答案中写出我的意思是“在子类中优化类型参数”的示例,尽管答案只会与原始问题相切。
本杰明·霍奇森

8

您对如何在界面中引入通用性的计划对我来说似乎是正确的。但是,您关于这是否一个好主意的问题需要更复杂的答案。

多年来,我代表自己工作过的公司面试了许多软件工程职位的候选人,而且我逐渐意识到,大多数求职者都对使用通用类感到满意(例如,标准集合类),但不能编写通用类。大多数人在其职业生涯中从未写过一个通用类,并且看起来如果被要求写一个通用类将会完全迷失。因此,如果您在希望实现接口或扩展基本实现的任何人身上强制使用泛型,则可能会让您失望。

但是,您可以尝试提供接口和基本实现的通用和非通用版本,以便不使用通用名称的人可以在狭窄,类型繁重的世界中快乐地生活,泛型确实可以从更广泛的语言理解中受益。


3

现在,ItemItemStackFactory我完全可以预见到一些第三方的需要,以在将来扩展。

这是您的决定。如果您不提供泛型,则Item每次使用泛型时,它们都需要更新其实现:

class MyItem implements Item {
}
MyItem item = new MyItem();
ItemStack is = createItemStack(item, 10);
// They would have to cast here.
MyItem theItem = (MyItem)is.getItem();

您至少应该按照ItemStack建议的方式制作通用名称。泛型的最大好处是您几乎不需要强制转换任何东西,而提供强迫用户强制转换的代码是恕我直言,是犯罪行为。


2

哇...对此我有完全不同的看法。

我绝对可以预见一些第三方需求

这是个错误。您不应该编写“面向未来”的代码。

同样,接口是自上而下编写的。您不必看一个类就说“嘿,这个类可以完全实现一个接口,然后我也可以在其他地方使用该接口”。这是错误的方法,因为只有使用接口的类才真正知道接口应该是什么。相反,您应该考虑“在这一点上,我的类需要一个依赖项。让我为该依赖项定义一个接口。当完成编写此类后,我将编写该依赖项的实现。” 有人是否会重用您的界面并将其替换为自己的默认实现完全无关紧要。他们可能永远不会做。这并不意味着该接口是无用的。它使您的代码遵循控制反转原则,这使您的代码在许多地方都具有很好的可扩展性”


0

我认为在您的情况下使用泛型过于复杂。

如果选择接口的泛型版本,则设计将指示

  1. ItemStackContainer <A>包含一种堆栈类型A。(即ItemStackContainer不能包含多种堆栈类型。)
  2. ItemStack专用于特定类型的项目。因此,没有所有类型的堆栈的抽象类型。
  3. 您需要为每种项目类型定义一个特定的ItemStackFactory。它被视为工厂模式太好了。
  4. 编写更难的代码,例如ItemStack <?扩展Item>,降低可维护性。

这些隐含的假设和限制是要谨慎决定的非常重要的事情。因此,尤其是在早期阶段,不应引入这些过早的设计。需要简单,易于理解的通用设计。


0

我会说这主要取决于这样一个问题:如果有人需要扩展的功能ItemItemStackFactory

  • 它们会否只是覆盖方法,因此子小节的实例将像基类一样使用,只是行为有所不同?
  • 还是会添加成员并以不同方式使用子类?

在第一种情况下,不需要泛型,在第二种情况下,可能需要泛型。


0

也许您可以有一个接口层次结构,其中Foo是一个接口,Bar是另一个接口。每当一个类型必须同时为两者时,它就是FooAndBar。

为避免过度设计,请仅在必要时设置FooAndBar类型,并保留永远不会扩展任何内容的接口列表。

对于大多数程序员来说,这要舒适得多(最喜欢他们的类型检查或从不处理任何类型的静态类型)。很少有人编写自己的泛型类。

另请注意:界面是关于可以执行哪些可能的操作的信息。它们实际上应该是动词而不是名词。


这可能是使用泛型的替代方法,但是最初的问题是在建议的情况下询问何时使用泛型。您是否可以改善您的答案,以表明不再需要泛型,但是使用多个接口才是所有人的答案?
杰伊·埃尔斯顿
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.