C#中在基类/抽象类中实现部分接口的最佳方式

5

.Net不允许在基类中部分实现接口。为了缓解这个问题,我提出了三种备选方案。请帮我决定哪个更具通用性,以便于重构、编译/运行时错误和可读性。

  • 当然,您可以始终将对象强制转换为IFoo,并调用任何方法,而没有任何编译器警告。但这是不合逻辑的,您通常不会这样做。这个构造不会作为重构的结果出现。
  • 我希望最大限度地分离。直接类契约(公共方法和属性)应该与接口实现分开。我经常使用接口来分隔对象交互。

我的比较:

  1. BaseClass1/MyClass1:
    • 缺点: 必须为每个未实现的IFoo方法在BaseClass1中创建虚拟抽象方法。
    • 缺点: 额外的方法包装 - 运行时稍微有些影响生产率。
  2. BaseClass2/MyClass2:
    • 缺点: 如果MyClass2中没有实现Method2,则没有编译器警告,而是运行时异常。重构覆盖单元测试差的情况下可能会导致潜在的代码不稳定。
    • 缺点: 必须添加陈旧的构造来防止从子类中直接调用方法。
    • 缺点: Method2是BaseClass1的公共方法,因此现在它是类契约的一部分。必须使用"Obsolete"构造来防止直接调用,而不是通过IFoo调用。
  3. BaseClass3/MyClass3:
    • 优点: (与#2相比)更易读。您可以看到MyClass2.Method2是IFoo实现,而不仅仅是某个覆盖的方法。
public interface IFoo
{
    void Method1();
    void Method2();
}

public abstract class BaseClass1 : IFoo
{
    void IFoo.Method1()
    { 
        //some implementation
    }

    void IFoo.Method2()
    {
        IFooMethod2();
    }

    protected abstract void IFooMethod2();
}

public class MyClass1 : BaseClass1
{
    [Obsolete("Prohibited direct call from child classes. only inteface implementation")]
    protected override void IFooMethod2()
    {
        //some implementation
    }
}

public abstract class BaseClass2 : IFoo
{
    void IFoo.Method1()
    {
        //some implementation
    }

    [Obsolete("Prohibited direct call from child classes. only inteface implementation")]
    public virtual void Method2()
    {
        throw new NotSupportedException();
    }
}

public abstract class MyClass2 : BaseClass2
{
    public override void Method2()
    {
        //some implementation
    }
}

public abstract class BaseClass3 : IFoo
{
    void IFoo.Method1()
    {
        //some implementation
    }

    void IFoo.Method2()
    {
        throw new NotSupportedException();
    }
}

public abstract class MyClass3 : BaseClass3, IFoo
{
    void IFoo.Method2()
    {
        //some implementation
    }
}

6
你试图实现的模式非常尴尬。你说:“.NET不允许在基类中部分实现接口。” 那是有原因的。客户端代码期望实现接口的东西,可能会……实现接口。抛出异常来处理不支持的方法是一种非常糟糕的代码味道…… - Yuck
同意Yuck的看法。如果你有一个类型为IFoo的变量,你真的期望所有IFoo接口的方法都已经实现并可用。接口就是为此而设计的。 - ken2k
只有 MyClass1 必须完全实现接口。而它确实做到了。问题是有多个子类(我之前没有提到),每个子类都必须实现 IFoo 接口。如果没有基类,你必须复制/粘贴 Method1 的实现,这对于所有子类来说是相等的。这就是我试图避免的。但是在子类中,Method2 的实现是不同的,所以我不能只有一个类同时实现 Method1 和 Method2。 - user1194528
另一个澄清。有一组通信对象。如果所有东西都只是实现为公共方法,你可能会迷失在尝试理解这个方法做什么、谁使用它以及如何改变/重构它的过程中。为了减少复杂性,我使用接口。每个接口定义了两个类(以及它们所有子类)之间的交互。接口实现的一部分对于层次结构中的所有类都是相同的,其他部分则因子类而异。这是我的问题的根源。你必须在复制/粘贴、复杂的类合同和我之前提到的笨拙结构之间进行选择。 - user1194528
4个回答

9

我喜欢这个版本,基类不能被实例化因为它是抽象的,派生类必须在声明中列出IFoo,否则它不会实现接口,然后它就完全负责实现其余的接口方法。 我能看到的一个缺点是你无法在基类中显式地实现接口方法(即没有IFoo:Method1),但除此之外,这是一个相当低开销的版本。

public interface IFoo
{
    void Method1();
    void Method2();
}

public abstract class BaseClass1
{
    public void Method1()
    {
        //some implementation
    }
}

public class MyClass1 : BaseClass1, IFoo
{
    public void Method2()
    {
        //some implementation
    }
}

6

好的,由于BaseClass是抽象类,您可以尝试以下操作:

public interface IFoo
{
    void Method1();

    void Method2();
}

public abstract class BaseClass : IFoo
{
    public void Method1()
    {
        // Common stuff for all BaseClassX classes
    }

    // Abstract method: it ensures IFoo is fully implemented
    // by all classes that inherit from BaseClass, but doesn't provide
    // any implementation right here.
    public abstract void Method2();
}

public class MyClass1 : BaseClass
{
    public override void Method2()
    {
        // Specific stuff for MyClass1
        Console.WriteLine("Class1");
    }
}

public class MyClass2 : BaseClass
{
    public override void Method2()
    {
        // Specific stuff for MyClass2
        Console.WriteLine("Class2");
    }
}

private static void Main(string[] args)
{
    IFoo test1 = new MyClass1();
    IFoo test2 = new MyClass2();

    test1.Method2();
    test2.Method2();

    Console.ReadKey();
}

这是我的第二个变体,除了第一个缺点之外。仍然适用:1.必须放置额外的过时构造以防止子类直接调用方法。在您的情况下,没有过时,因此来自子类的直接调用不会生成编译时警告2.对于BaseClass1,Method2是公共的,因此它现在是类合同的一部分。必须放置“Obsolete”结构以防止直接调用,而不是通过IFoo。3.可读性较差。您看不到Method2是IFoo实现。 - user1194528
阅读问题下面的评论,这似乎是解决问题的正确方法。在BaseClass中对Method2()进行抽象实现确保了IFoo被完全实现,同时强制所有派生自BaseClass的类都要实现Method2()。还可以补充说明派生类将无法重写Method1()(但可以使用new关键词来隐藏它)。 - Julian
@user1194528,您对上面的代码有什么实际问题?为什么想要添加过时注释? - ken2k
2 user1194528:需要使用过时的注释来防止在MyClass1内部意外调用Method2。因为它只能被IFoo客户端调用。 - user1194528
2 Nailuj:你是对的。但是这种方法有我上面描述的一些小缺点。 - user1194528
2 user1194528:看起来可能过于保护,但当您拥有10多个类的层次结构,并且每个类都与3-4个其他类交互时,您会希望采用最小可访问性方法。声明所有方法为public,将代码留在经验不足的编码人员手中,这种混乱几乎不可能重构。 - user1194528

5
设计一个没有实现明确契约的类是极其糟糕的。这是因为您首先说一个类有能力做某事。您明确强调该类可以执行某些操作,但稍后在代码中,您会说“算了吧”,这个类没有实现也能运行。编译器非常聪明地要求您实现契约,但决定权却掌握在您手中。
以下是一些常见的解决方案: 糟糕的解决方案:
  • 抛出异常(NonImplementedException或NotSupportedException,请参见示例
  • 将其声明为过时(从一开始就进行良好的设计)
更好的解决方案:
  • 显式接口实现,但仍然需要实现它(只是隐藏起来)
最佳解决方案:
  • 使用接口隔离(将臃肿的接口拆分为更细和更可管理的接口)

1
我考虑过接口隔离,但问题在于接口设计来自客户端类的需求,而不是服务器类。客户端类不想知道实现细节。为什么它应该知道这个接口的某些部分是在基类中实现的,而其他部分是在子类中实现的呢?这也非常脆弱,可能有两个类层次结构实现了这个接口,它们在如何将实现分割在基类和子类之间方面是不同的。 - user1194528

0

我建议使用抽象基类实现接口,并调用protected abstract方法,就像你第一个示例中所示的那样,除了一些派生类可能不实现的方法(遵循“将所有内容都放入IList中,但并非所有方法都实际工作”的模式);这些可以是protected virtual存根,它们会抛出NotSupportedException

请注意,是否公开接口的特定成员作为同名公共成员取决于子类。

在VB.net中的正确模式应该是像MustOverride Sub IFoo_Method1() Implements IFoo.Method1这样的东西,这将避免额外的函数调用开销,但C#没有提供任何实现具有受保护成员的接口的手段。对于任何可能需要在子类中重写的方法,使用显式接口实现有点棘手,因为子类重新实现接口无法链接到父类的实现。


孩子的接口重新实现无法链接到父类的实现是不可能的。- 很好的观点。从这个角度来看,最好将所有方法声明为受保护的(对于那些未实现的方法使用抽象关键字)(ken2k变体)。感谢大家的想法。现在我可以做出更明智的决定了。
- user1194528

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接