您如何在方法的使用和定义之间灵活地保留有用的对象功能?

How do you flexibly retain useful object functionality between the usage and definition of a method?

说明:

假设我有这个简单的界面:

interface Y { Y f(); }

我可以用 3 种不同的方式实现它:

  1. 到处使用通用类型。

    class SubY_generalist implements Y
    {
        public Y f()
        {
            Y y = new SubY_generalist();
            ...
            return y;
        }
    }
    
  2. 使用特殊类型,但return相同的值隐式转换为通用类型。

    class SubY_mix implements Y
    {
        public Y f()
        {
            SubY_mix y = new SubY_mix();
            ...
            return y;
        }
    }
    
  3. 使用特殊类型和return一样。

    class SubY_specialist implements Y
    {
        public SubY_specialist f()
        {
            SubY_specialist y = new SubY_specialist();
            ...
            return y;
        }
    }
    

我的考虑:

关于“编程到 interface。最推的回答好像没有深入区分 argument 和 return 类型,实际上是 fundamentally distinct。因此发现 其他地方的讨论并没有给我一个明确的答案,我别无选择,只能 自己揣测——当然,除非那种reader能帮我一把。

我假设以下关于 Java 的基本事实:(它们正确吗?)

  1. 一个对象是在它最特殊的时候创建的。
  2. 随时可以隐式转换为更通用的类型(通用化)
  3. 当对象被泛化时,它会失去一些有用的属性,但会获得 none。

从这些简单的几点可以看出,一个特殊的对象更有用,但也更危险 比一般人。

例如,我可能有一个可变容器,我可以将其归纳为 不可变的。如果我确保容器在被冻结之前具有一些有用的属性,我可以 在正确的时间概括它,以防止用户不小心破坏不变量。但 这是正确的方法吗?还有另一种实现类似隔离的方法:我可能总是让 我的方法 package-private。概括似乎更灵活但容易遗漏和 为细微错误引入表面。

但是在某些语言中,例如 Python,实际上没有必要保护方法免受 外部访问;他们只是用下划线标记内部方法。熟练的用户可以 访问内部方法以获得一些收益,前提是他们知道所有的复杂性。

另一个结果是在方法定义中我应该更喜欢专门的对象。

我的问题:

这是我用来谈论这个话题的上下文。假设函数 f 有以下选项:

interface Y { ... }
class SubY implements Y { ... }
DEFINITION CHOICE:
public SubY f() { ... }
OR
public Y f() { ... }
USAGE CHOICE:
...
Y y = f();
OR
SubY y = f(); //maybe with a cast
...

从技术上讲,所有选项都可以是正确的,具体取决于您是否打算向最终用户(下一位程序员)公开 SubY 的详细信息。如果向最终用户公开 SubY 看起来不是个好主意,请不要这样做。否则,做。推理如下:
从概念上讲,您应该始终 return 可能需要的最窄类型(更窄的类型在 class hierarchy 的下方 - 将其视为 "a narrower type is-a wider type")。例如,如果您 return a List,则意味着您希望最终用户仅根据界面与其进行交互。但是,如果您希望最终用户需要 ArrayList 中定义的交互,return 则相反。
通常,想法是使方法中的 return 类型尽可能窄,而不暴露最终用户不应该知道的血淋淋的细节。只要您适当地隐藏由 class SubY 处理的内部细节,return 和 SubY 就完全没问题。另一方面,考虑最终用户的责任:
最终用户的责任是负责任地使用您的 return 类型赋予他们的权力。您已经通过正确隐藏内部结构来防止他们做令人讨厌的事情。然后,当最终用户使用您的 class 时,他们应该根据自己的需要进行编程,例如:

// imported library provides:
public SubY f() { ... }
... // la la la
Y y = f();
useYFunctionality(y);
... // somewhere else that you need SubY functionality
SubY subY = f();
useSubYFunctionality(subY);

因此,程序员应将其函数定义为 return 精确类型,return 尽可能缩小安全的实现(它 可以 界面)。另一方面,service/functionality 的最终用户应该将他们的变量定义为仍然有效的最广泛的类型,以减少耦合并提高意图的清晰度(如果你只需要 getLicensePlateNumber,请使用Vehicle 而不是 VeryUnreliableCar)。这里的关键是选择 return 类型没有 definite 答案。相反,使用您的判断来决定什么应该和不应该暴露给最终用户。如果没有特殊原因拒绝访问 SubY,请牢记此原则:Functions Return Accurately, Users Define Variables As Necessary 或 FRAUDVAN。
将此应用于您的示例:

class SubY_generalist implements Y
{
    public Y f()
    {
        Y y = new SubY_generalist();
        ...
        return y;
    }
}
  1. 到处使用界面。当您只关心界面的细节时这是最好的,对于最终用户也是如此。例如,如果您只关心 getadd,则 Y 可能是 List,而 SubY_generalist() 可能是 ArrayList.
class SubY_mix implements Y
{
    public Y f()
    {
        SubY_mix y = new SubY_mix();
        ...
        return y;
    }
}
  1. 使用特定类型,但在 return 处隐式扩展为通用类型。当您实际需要为特定类型使用方法时,这是最好的,但您的最终用户 永远不会 需要这样做。使用前面示例中的 ArrayListList,假设 f 必须调用 trimToSize,一个未在 List 中定义的方法。那么,显然有必要将 y 声明为 ArrayList。但是,由于最终用户只需要 List,您应该 return y 并隐式加宽。
class SubY_specialist implements Y
{
    public SubY_specialist f()
    {
        SubY_specialist y = new SubY_specialist();
        ...
        return y;
    }
}
  1. 使用专用类型,return使用专用类型。这就是 FRAUDVAN 的 "essence"。 SubY_specialist 作为 return 类型允许用户灵活地处理 SubY_specialist returned,或者在接口方面使用 f() [=28] =].这是难以置信的常见,当接口不够具体而无法有效使用时(它们应该是,因为它们是抽象)。例如,在 Java Stream class, any time a method returns a Stream, this is happening. This is because Stream implements BaseStream 中。它 发生在你采取 StringBufferreverseappendreplacedeleteinsertStringBuffer 实现 CharSequence)。
    需要这种行为的原因是界面通常不包含足够的细节以供最终用户有效使用。将这些细节放在接口中会导致接口 bloat/pollution,这是不可取的。相反,直接让 class 负责使用这些细节保留了具有独特功能的 classes 的单一责任原则(避免在接口的未来实现者中被迫实现冗余)。毕竟,如果你的 class 实现了两个接口,你要么必须创建一个新的接口来扩展这两个接口和 return 那个(这很快就会变得丑陋),或者你可以 return实现者,并让最终用户负责他们需要的界面(不是丑陋的)。
    为什么这种行为如此普遍?这是因为,一旦你隐藏了最终用户不需要看到的东西(使用可见性修饰符、封装和信息隐藏),让最终用户实际上是一个 好主意以他们选择的方式与您的实现交互(即根据他们将变量声明为的接口)。事实上,我什至声称这里的所有示例都符合 FRAUDVAN,其中向用户公开的 "safest" 类型是 Y 而不是专用类型。在第一个示例中,接口 Y 对用户和最终用户来说已经足够专业了。在第二个示例中,Y 对用户来说不够专业,但对最终用户来说足够专业。最后,在这个例子中,Y 对用户和所有最终用户来说不够专业,尽管如果 Y 满足他们的需要,一些最终用户可能会选择声明 Y y = f()

所以,这是要点: