【问题标题】:Best practice for implementing a derived method that shouldn't be called [closed]实现不应调用的派生方法的最佳实践
【发布时间】:2025-12-05 14:05:01
【问题描述】:

我有一个 JAVA 类 A,它有一个方法 foo

abstract class A {
  abstract void foo();
}

我还有一个 A 的派生类 - MutableA。 MutableA 是一个单例对象,指示不需要更新,这对于重用代码流很有用。 永远不应该在 MutableA 上调用 foo()。 实现这一目标的最佳方法是什么:

  1. 抛出不支持的异常
  2. 什么都不做(空实现)
  3. 这是一个糟糕的设计。

有人可以推荐我在这种情况下的最佳做法是什么吗?

【问题讨论】:

  • 这真的取决于foo 应该做什么的细节。这些选项中的任何一个都可能适用。
  • 可以考虑做GuavaImmutable*类做的事情,就是标记这样的方法@Deprecated。这不会阻止你调用它们,但至少你的 IDE(如果你使用了)可以给你一个提示,提示你有什么事情发生了,只要你有对 MutableA 的特别引用。
  • 如果您想从 Google 的 Guava 中汲取灵感,如果您在不可变结构上调用变异集合方法,他们会抛出 UnsupportedOperationException。他们还将方法标记为@Deprecated
  • Best practice for implementing a derived method that shouldn't be called ... 是矛盾的。对于本质上是黑客攻击和违反良好继承设计的行为,没有“最佳实践”。要么正确实现继承,要么支持组合。也许您的设计过于粗粒度。当然,如果你被一些可怕的遗留代码库 / api 所困,所有的敏感性都被抛到窗外,你可以忽略我刚才所说的

标签: java inheritance object-oriented-analysis


【解决方案1】:

我会考虑重新考虑设计。

在超类中有一个抽象方法意味着子类应该实现该方法。除非您计划拥有更大的层次结构,在这种情况下,您应该考虑将 foo 方法在层次结构中向下移动到更靠近实现的位置。

如果您打算保留 foo 方法的位置,我也会将 MutableA 设为抽象。

【讨论】:

  • 我同意应该改变层次结构——也许应该引入一个中间层。如果一个方法在一个对象上可用,那么我希望我也可以使用它。
  • 这完全取决于用例,因为用例可能允许使用空实现调用此类方法。由于我们不知道细节,因此无法准确回答
【解决方案2】:

您应该考虑看看 Liskov 替换原则,这是良好设计的基本 SOLID 原则之一。

该原则指出派生对象的行为不应与其父对象不同。违反 Liskov 原则的一个常见例子是从椭圆推导圆。这在数学上是完全合理的,并且声音被认为是糟糕的设计,因为从椭圆派生的圆(例如)仍然会公开设置宽度和高度的方法,但在某些情况下它会表现得很奇怪。考虑以下

Something FooBar (Ellipse e)
{ 
    // do something with e
}

如果我们不知道调用者传递的是 Circle 而不是 Ellipse,并且我们同时设置了宽度和高度,我们的结果可能与我们预期的不同,因为 Circle 要么忽略 SetHeight,要么在 SetWidth 上设置宽度机器人和设置高度。无论哪种方式,如果我们期望在椭圆上操作,这种行为都是出乎意料的。因此,里氏替换原则。

【讨论】:

    【解决方案3】:

    既然你说“不需要更新”,看起来空的实现是要走的路。如果我这样做:

    A someA = createA();
    someA.foo();           // make sure it's "foo"ed
    

    我不知道我是否收到了MutableA,所以我打电话给foo 只是为了确定。

    如果禁止MutableA 上调用foo,我会选择UnsupportedOperationException 或重新考虑设计。也许您可以将A 包装在一个AHandler 中,该AHandler 知道何时以及如何在它包装的实例上调用foo

    【讨论】:

      【解决方案4】:

      这取决于该方法的用途。 您列出的所有替代方案实际上都很好。

      顺便说一句,如果你抛出一个UnsupportedOperation 异常,你就是在告诉你这个方法不应该被使用。使用异常始终是偏离“正常”的“消息”,因此您正在定义与A.foo() 的基本实现有关的事情,但您也将这个实现与子类分开。

      如果你使用一个空的实现,你会让子类在假设流中更有用,而不会与过去(超类A)分手,所以你可能不知道在你的每个上下文中使用子类想要。

      归根结底,您说MutableA 是单例的,所以我认为您会在特定的上下文中使用它,并特别关注它:所以我会采用“异常”解决方案。

      【讨论】:

        【解决方案5】:

        我会使用这样的东西:

        public abstract class ReadableA {
        }
        
        public abstract class A extends ReadableA{
          abstract void foo();
        }
        
        public class MutableA extends ReadableA {
        }
        

        在代码中您想要接收A 的任何地方,请改用ReadableA,除非您特别想调用foo()。然后将A 放入您的方法签名中。如果您想收到MutableA,请写一个MutableA 签名。

        例子:

        public class UsageOfA {
          public void bar(ReadableA a) {
            // Can use both A and MutableA but a.foo() cannot be invoked.
          }
          public void bar(A a) {
            a.foo();
          }
          public void bar(MutableA a) {
            // a.foo() cannot be invoked.
          }
        }
        

        【讨论】:

          【解决方案6】:

          我认为在这里投掷UnsupportedOperationException 是正确的选择。假设你有 3 个类扩展 A

          class X extends A{
          //foo allowed here
          }
          class Y extends A{
          //foo allowed here
          }
          class Z extends A{
          //foo allowed here
          }
          class MutableA extends A{
          //foo NOT allowed here
          }
          

          现在从设计的角度来看,X,Y,Z and MutableA 的行为方式应该与foo() 保持一致。我不鼓励引入另一个类层次结构只是为了使 MutableA foofree。一个简单的方法是抛出一个UnsupportedOperationException 并让调用者知道该方法不能被调用对象支持。另一方面,empty 实现 仍然有效,坦率地说,从 设计角度 不适合,因为它仍然 来自调用对象的有效调用

          PS:我也会考虑重新考虑设计。任何可用但不应该使用的方法都不是好的设计的一部分。

          【讨论】: