从抽象基类返回规范表示子类是否可以接受?

时间:2017-10-28 03:02:31

标签: c# oop inheritance design-patterns

编辑2: TL; DR :有没有办法不破坏OO最佳实践,同时仍然满足一系列相同类型的东西必须可以转换为规范的东西的约束那种?

此外,请记住,我的问题是关于一般情况,而不是具体的例子。这不是一个家庭作业问题。

假设您有以下内容:

  • 实现通用功能的抽象基类;
  • 一个具体的派生类,用作规范表示。

现在假设您希望基类的任何继承者都可以转换为规范表示。实现此目的的一种方法是在基类中使用一个抽象方法,该方法旨在将继承者的转换作为规范派生类的实例返回。

然而,似乎普遍认为基类不应该知道它们的任何派生类,并且在一般情况下,我同意。但是,在这种情况下,这似乎是最好的解决方案,因为它允许任意数量的派生类,每个类都有自己的实现,我们不需要知道任何事情,通过转换为规范表示可以互操作每个派生类都必须实现。

你会以不同的方式做吗?为什么以及如何?

几何点的示例:

// an abstract point has no coordinate system information, so the values
// of X and Y are meaningless
public abstract class AbstractPoint {
    public int X;
    public int Y;

    public abstract ScreenPoint ToScreenPoint();
}

// a point in the predefined screen coordinate system; the meaning of X 
// and Y is known
public class ScreenPoint : AbstractPoint {
    public ScreenPoint(int x, int y) {
        X = x;
        Y = y;
    }

    public override ScreenPoint ToScreenPoint()
        => new ScreenPoint(X, Y);
}

// there can be any number of classes like this; we don't know anything
// about their coordinate systems and we don't care as long as we can
// convert them to `ScreenPoint`s
public class ArbitraryPoint : AbstractPoint {
    private int arbitraryTransformation;

    public ArbitraryPoint(int x, int y) {
        X = x;
        Y = y;
    }

    public override ScreenPoint ToScreenPoint()
        => new ScreenPoint(X * arbitraryTransformation, Y * arbitraryTransformation);

    // (other code)
}

编辑1:AbstractPointScreenPoint不是同一个类的原因是语义。 AbstractPoint没有已定义的坐标系,因此AbstractPoint实例中的X和Y值无意义。 ScreenPoint确实有一个已定义的坐标系,因此ScreenPoint实例中的X和Y值具有明确的含义。

如果ScreenPoint是基类,那么ArbitraryPoint将是ScreenPoint,情况并非如此。 ArbitraryPoint可以转换为ScreenPoint,但这并不意味着它is-a ScreenPoint

如果您仍然不相信,可以考虑将任意坐标系ACS1定义为对屏幕坐标系SCS具有动态偏移。这意味着两个坐标系之间的映射可能随时间变化,即点ACS1 (1, 1)可以在某一时刻映射到SCS (10, 10),在另一时刻映射到SCS (42, 877)

2 个答案:

答案 0 :(得分:1)

这种设计通常是代码气味。基类不应该知道它们的派生类,因为它创建了一个循环依赖。循环依赖通常会导致复杂的设计,很难推断出应该在哪些类中进行操作。在Java中,了解其派生类的基类甚至可能在极少数情况下导致死锁(我不知道C#)。

但是,如果您确切知道自己在做什么,特别是如果您想要实现的目标非常简单,那么您可以在特殊情况下违反一般规则。

你的情况似乎很简单。将AbstractPointScreenPoint作为不同的类是正确的。但事实上他们“一起工作”:所有AbstractPoint应该能够转换为ScreenPoint(这可能是AbstractPoint合同中最重要的功能?)。由于没有另一个人不能存在,AbstractPoint了解ScreenPoint并没有错。

<强>更新

在另一种设计中:创建一个名为CanonicalPoint的界面。 AbstractPoint有一个名为ToCanonicalPoint的方法,它返回CanonicalPointAbstractPoint的所有派生类都必须实现此类并返回CanonicalPointScreenPointAbstractPoint的派生类,它实现CanonicalPoint接口。 您甚至可以拥有多个实现CanonicalPoint的派生类。 注意:如果AbstractPointCanonicalPoint有共同的方法,则两者都可以实现另一种方法 称为Pointable的接口,它声明了所有这些方法。

答案 1 :(得分:1)

你有没有理由的复杂事情。如果一个类不能实现基类的规范,那么你违反了Liskovs Substitution原则。

另一种方法是声明任意对象可以表示为坐标的接口:

public interface IScreenPointProvider
{
    ScreenPoint ToPoint();
}

然后对象是什么或者它的内部坐标处理是什么并不重要。

另外不要忘记继承是关于is-a的关系。如果任何子类不支持基类提供的内容,那么它实际上不是is-a关系。这通常表明基类确实是一个实用程序类,或者定义的层次结构没有很好地设计。