【问题标题】:How and when to use Kotlin sealed classes in Java?如何以及何时在 Java 中使用 Kotlin 密封类?
【发布时间】:2021-09-20 09:02:08
【问题描述】:

考虑一下误用密封类,我们看看下面的设计。

有两个模块:

  • parser (Kotlin) - 负责从 String 中创建实例
  • processor (Java) - 将原始传入数据泵入强类型存储(即关系表)
  1. 字符串来自外部源到processor
  2. processor 将其认可委托给 parser
  3. parser 基于一些 rules X 创建不同类型的实例 [Banana, Nail, Shoe]
  4. processor 基于一些 rules Y 将每个实例持久化到适当的表中

parser 中使用这样的密封类是否合适,然后在processor 中根据每个实例的具体类型做出决定?

// parser module exposes Item and its subclasses

sealed interface Item {
    class Banana(/*state 1*/) : Item
    class Nail(/*state 2*/) : Item
    class Shoe(/*state 3*/) : Item
}

fun parse(value: String, rule: ParseRule): Item {
    return when (true) {
        rule.canParseBanana(value) -> rule.makeBananaFrom(value)
        rule.canParseNail(value) -> rule.makeNailFrom(value)
        rule.canParseShoe(value) -> rule.makeShoeFrom(value)
        else -> throw RuntimeException("cannot parse")
    }
}    

// processor module makes decisions based on class 

void process(String value){
  Item item = parser.parse(value);

  if (item instance of Item.Banana){
    persistBanana((Item.Banana) item)
  } else if ( ... )
    // etc         
  } else {
     throw new RuntimeException("Unknown subclass of Item : " + item.getClass())
  }
}

我发现这种方法有问题,因为越来越多的 Item 子类可能会导致设计灾难,但无法弄清楚是否存在与这个完全不同的密封类的“规范”用例。

什么是密封类适用性的限制,当系统设计者应该更喜欢“较少类型”的东西时,如下所示:

class Item{
  Object marker; // String or Enum
  Map<String, Object> attributes;
}

// basically it is the same, but without dancing with types
void process(String value){
    Item item = parser.parse(value);

    if ("BANANA".equals(item.marker)){
      persistBanana(item.attributes)
    } else if (...){
      // etc
    }
}

【问题讨论】:

  • 我相信您提出的问题适用于 Kotlin 和 Java。您应该始终尝试坚持正确的 OOP,因此重载/实现方法而不是使用 when/switch/if 链。那么有多少子类并不重要。有时,最好使用枚举/密封接口,但通常会发生在可能元素的集合有限且它们在未来不会发生太大变化的情况下。如果when 代码知道所有可能的子类型是合乎逻辑的,因为通常它确实不应该。
  • 我很难为您的案例提出具体的解决方案。让项目自己持久化可能是有意义的,例如将函数添加到Itemfun persist(&lt;whatever is needed to persist&gt;)。另一方面,根据定义,数据库层知道所有实体,因此有时创建这样的 if 链可能是有意义的。我绝对不鼓励像您上一个示例中那样使用字符串。对于这种情况,密封接口或枚举要好得多。
  • 从这个例子中删除sealed关键字并没有改变任何东西,这似乎回答了这里是否合适的问题。它没有增加任何价值。这里真正的问题似乎根本与sealed 无关。而是 OOP 中最基本的问题:如何用不同的具体实现对抽象进行建模。
  • @jaco0646 你是对的。我猜你比我更能表达我的想法。事实上,问题是关于密封类的限制。我故意滥用它们(因为它们的移除不会影响设计)。问题是我看不到何时应用密封类是合适的(或者可能是不可避免的)。看着答案,我发现我并不孤单。
  • 你有没有读过一些关于密封类的热门话题,例如Why seal a class?When and why would you seal a class? 很多语言都有这个功能,所以这个问题或多或少与语言无关。

标签: java kotlin design-patterns sealed-class


【解决方案1】:

您可以使用访问者模式为 Java 提供 when..is 样式方法。

// Kotlin
abstract class ItemVisitor<OUT> {
    operator fun invoke(item: Item) = when (item) {
        is Banana -> visitBanana(item)
        is Shoe -> visitShoe(item)
        is Nail -> visitNail(item)
    }
    abstract fun visitBanana(item: Banana): OUT
    abstract fun visitShoe(item: Shoe): OUT
    abstract fun visitNail(item: Nail): OUT
}

因为 Item 是密封的,所以在 when 中不需要 else 案例,然后 Java 代码可以创建访问者,而不是使用 instanceof 进行自己的检查 @987654328 @,如果你添加一个变体,你添加一个方法并被提醒访问者的 Java 实现需要这个新方法。

【讨论】:

  • 访问者模式用于避免类型检查,而不是合并它。 invoke 方法与模式相矛盾。
  • @jaco0646 这将类型检查合并到 Kotlin 中的一个地方,编译器可以识别所有情况都被覆盖,这样你就可以避免在编译器没有的 Java 中进行。
  • 同意,这可能是惯用的 Kotlin;但它与访问者模式相反。访问者完全避免类型检查。 (但在相关说明中,Java 正在使用 Pattern Matching for switch 实现类似的类型检查功能。)
【解决方案2】:

如果您选择使用密封类,则每当引入新的子类型时,您都会自行扩展层次结构。鉴于您具有特定于子类型的规则和持久性,我不确定您可以做多少来避免这种情况。

话虽如此,但在您已经确定了项目类型之后再做instanceOf 是一种耻辱。 下面的代码或多或少是访问者模式(这是有争议的,见 cmets)。我认为需要分离到 Java 和 Kotlin。

// Kotlin
sealed interface Item {
    class Banana(/*state 1*/) : Item
    class Nail(/*state 2*/) : Item
    class Shoe(/*state 3*/) : Item
}

class Parser(val persistor: Persistor, val rule: ParseRule) {
    fun parse(value: String): Item =
        when (true) {
            rule.canParseBanana(value) -> rule.makeBananaFrom(value).also { persistor.persist(it) }
            rule.canParseNail(value) -> rule.makeNailFrom(value).also { persistor.persist(it) }
            rule.canParseShoe(value) -> rule.makeShoeFrom(value).also { persistor.persist(it) }
            else -> throw RuntimeException("cannot parse: $value")
        }
}

class ParseRule {
    fun canParseBanana(value: String): Boolean = ...
    fun makeBananaFrom(value: String): Item.Banana = ...
    ...
}
// Java
public class Persistor {
    void persist(Item.Banana item) {
        System.out.println("persisting " + item);
    }
    void persist(Item.Shoe item) {
        System.out.println("persisting " + item);
    }
    void persist(Item.Nail item) {
        System.out.println("persisting " + item);
    }
}


public class Processor {
    private final Parser parser;

    public Processor(Parser parser) {
        this.parser = parser;
    }

    void process(String value) {
        parser.parse(value);
    }
    
    public static void main(String[] args) {
        Parser parser = new Parser(new Persistor(), new ParseRule());
        Processor p = new Processor(parser);
        p.process("banana");
        p.process("nail");
        p.process("zap");
    }
}


【讨论】:

  • 此解决方案中没有访客模式,这很好,因为此解决方案不需要它。
  • 你这么说是因为你错过了双重调度?我对此感觉并不强烈,但persistor.persist(it) 具有这种品质。
  • 在调用persist 方法时没有双重分派,因为没有类型歧义。不需要双重分派,因为rule 直接返回具体类型。换一种说法,调用重载方法不会像访问者模式那样自动模仿双重分派。访问者检索“丢失”类型信息。在这个解决方案中,没有任何损失。这实际上是一个更好的解决方案,因为通过保留解析器生成的类型信息,Visitor 的复杂性是不必要的。
  • 我相信双重调度是一个实现细节,而不是访问者的先决条件,至少根据 GoF。不管怎样,我会记录你的反对意见。
  • 100% 明确:访问者模式中没有实际的双重调度。双重调度是一种语言特性。仅访问者模仿该功能,语言不直接支持它。在支持它的语言中,没有理由使用该模式。该解决方案甚至不需要模仿该功能,因此根本不需要提及访问者。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-08-05
  • 2019-05-15
  • 2022-08-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多