针对多个接口进行编程

28

我非常喜欢这个建议:“针对接口编程,而不是实现”,并且我正在努力始终遵循它。但是当我必须将代码与必须从多个接口继承的对象解耦时,我不确定如何让这个原则继续发挥作用。一个典型的例子可能是:

namespace ProgramAgainstInterfaces
{
    interface IMean
    {
            void foo();
    }  

    class Meaning : IMean , IDisposable
    {
        public void Dispose()
        {
            Console .WriteLine("Disposing..." );
        }

        public void foo()
        {
            Console .WriteLine("Doing something..." );           
        }
    }

   class DoingSomething
   {
        static void Main( string[] args)
        {
            IMean ThisMeaning = (IMean ) new Meaning ();  // Here the issue: I am losing the IDisposable methods
            ThisMeaning.foo();
            ThisMeaning.Dispose();                     // Error: i cannot call this method losing functionality
        }
   }   
}

解决这个问题的一个可能方法是定义一个临时接口,该接口继承自这两个接口:

一个可能的解决方案是定义一个特定的接口,该接口同时继承这两个接口:

namespace ProgramAgainstInterfaces
{
    interface IMean
    {
            void foo();
    }

    interface ITry : IMean , IDisposable
    {
    }

    class Meaning : ITry
    {
        public void Dispose()
        {
            Console .WriteLine("Disposing..." );
        }

        public void foo()
        {
            Console .WriteLine("Doing something..." );           
        }
    }

   class DoingSomething
   {
        static void Main( string[] args)
        {
            ITry ThisMeaning = (ITry ) new Meaning ();  // This works
            ThisMeaning.foo();
            ThisMeaning.Dispose();   // The method is available
        }
   }   
}

但我不确定这是否是更紧凑和有效的解决方案:我可能会有更复杂的多重继承层次结构,并且这会增加复杂性,因为我必须创建仅充当容器的接口。是否有更好的设计解决方案?


4
这可能是一个有趣的问题,可以在http://programmers.stackexchange.com/上提出。 - glenatron
4
通常情况下,您会检查接口的实现方式 - 例如 var disposable = ThisMeaning as IDisposable; if(disposable != null) disposable.Dispose() - 这意味着您在运行时发现实现,而不必在编译时知道它们。 - Charleh
@Charleh:请把它转化为一个回答。 - Thilo
@Thilo 完成了!我更喜欢这种方法,因为它意味着您可以通过支持越来越多的接口逐步构建类型 - 当然要注意任何警告,但在类设计阶段,您应该知道您应该支持哪些接口。 - Charleh
6
我不喜欢在运行时检查对象是否实现了除你已知接口以外的其他接口的想法。为什么会这样呢?我认为你是在利用具体类(含义)的默契知识来影响你的代码,这会削弱编程到接口的整个意义。使用类型系统使您的假设明确。如果您需要一个可处理的类型,您应该编写针对扩展IDisposable接口的接口或直接针对具体类型的代码。 - Einar
一种更好的设计方案,不幸的是在C#中并不可用,就是在需要它们时在原地声明特定接口。例如,在Scala中,您可以指定一个接受IMean且还是IDisposable的方法def doSomething(m: IMean with IDisposable) {} - artem
7个回答

21
如果成为一个"IMean"对象意味着始终是可被处理的,那么您应该这样实现接口:
public interface IMean : IDisposable
{
    ...
}

然而,如果有一个实现IMean接口的对象没有必要被处理掉(disposable),那么我认为你提出的解决方案是最好的:创建一个中介接口,这样你就可以拥有:

public interface IMean
{
    ...
}

public interface IDisposableMean : IMean, IDisposable
{
    ...
}

10
你应该让接口(interface)实现IDisposable,而不是让Meaning类实现它。这样,在将其转换为interface时,你不会失去IDisposable能力(因为它在你的interface级别上定义)。
像这样:

namespace ProgramAgainstInterfaces
{
    interface IMean : IDisposable
    {
        void foo();
    }

    interface ITry : IMean
    {
    }

    class Meaning : ITry
    {
        public void Dispose()
        {
            Console .WriteLine("Disposing..." );
        }

        public void foo()
        {
            Console .WriteLine("Doing something..." );           
        }
    }

   class DoingSomething
   {
        static void Main( string[] args)
        {
            ITry ThisMeaning = (ITry ) new Meaning ();  // This works
            ThisMeaning.foo();
            ThisMeaning.Dispose();   // The method is available
        }
   }   
}

那么,如果不是所有提供 IMean 接口的对象都需要处理,您只需创建一个空的处理方法来满足接口要求吗?(并不是说这样做好还是不好,只是想理解一下。) - George Duckett
@GeorgeDuckett 为了这种情况,他必须在接口 IMean 上实现 IDisposable,纯粹是因为他试图在将其转换为 interface 后调用 Dispose。如果 IMean 接口中的所有对象都不需要处理(并且说在调用 dispose 之前他没有将其转换为接口),那么 Dispose 实现最好放回到 Meaning 类上。仅为满足接口而实现方法可能指向不应使用的接口或不属于该接口的方法。希望这样说得清楚。 - Mathew Thompson
3
实际上,这是.NET框架采用的方法:Stream实现了IDisposable接口,在MemoryStream中它是无操作的,但在FileStream中不是。DbCommand实现了IDisposable接口,在 SqlCommand中基本上是无操作的,但在OleDbCommand中不是。 - Heinzi

7
您可以引入一个泛型类型 T,该类型必须实现多个接口。以下是使用 IFooIDisposable 的示例:
class Program
{
    static void Main(string[] args)
    {
    }

    interface IFoo
    {
        void Foo();
    }

    class Bar<T> where T : IFoo, IDisposable
    {
        public Bar(T foo)
        {
            foo.Foo();
            foo.Dispose();
        }
    }
}

这有点复杂。如果从设计角度来看IFoo : IDisposable不合适,它可能就合理了。


5
当您有需要一个类型实现多个不同接口的代码时,这通常是您必须要做的。但是,根据您的代码语义,可能会有很多变化。
例如,如果IMean不一定是IDisposable,但有许多消费者需要其IMean为可处理的,则您提出的解决方案是可以接受的。您还可以使用抽象基类来实现此目的--“以接口为程序”不使用“interface”作为“interface”关键字定义的语言构造,而是“对象的抽象版本”。
事实上,您可以要求您使用的任何类型都实现ITry(因此是可处理的),并简单地记录某些类型将Dispose实现为no-op是可以的。如果使用抽象基类,则也可以提供此no-op实现作为默认值。
另一种解决方案是使用泛型:
void UseDisposableMeaning<T>(T meaning) where T : IMean, IDisposable
{
    meaning.foo();
    meaning.Dispose();
}

// This allows you to transparently write UseDisposableMeaning(new Meaning());

另一个例子是,某个客户端严格要求只使用IMean,但同时也需要支持一次性使用。你可以通过筛选类型来解决这个问题:
IMean mean = new Meaning();
var disposable = mean as IDisposable;
if (disposable != null) disposable.Dispose();

虽然这是一个可接受的实际解决方案(特别是考虑到IDisposable并不是“任何”接口),但如果你发现自己一遍又一遍地这样做,你应该退一步思考;通常情况下,“类型转换”的任何形式都被认为是不好的实践。


4
为了进行组合编程,您可以通过强制转换来检查对象是否支持特定的功能(接口):
例如:
// Try and cast
var disposable = ThisMeaning as IDisposable; 

// If the cast succeeded you can safely call the interface methods
if(disposable != null) 
    disposable.Dispose();

这意味着您在运行时发现实现,而不必在编译时知道它们,并且您的类型不需要实现IDisposable。
这样可以满足可被处理的要求,而无需知道IMean是Meaning类型(仍然可以使用IMean引用)。

2

针对接口编程而非实现 - "接口"是一个通用术语,它并不是字面上的关键词interface。使用abstract类符合这个原则。如果需要一些实现,那么也许更好。此外,子类始终可以根据需要实现interface


我认为这不正确。编写抽象类的代码与编写接口的代码完全不同。编写接口的主要原因之一是,某些其他类可以使用完全独立的实现编写到相同的接口 - 扩展抽象类会更紧密地绑定类,并且这正是您在说“编写接口”时要避免的。 - Bill K
@Bill K,一个抽象类并不严格等同于实现一个接口。但它们都是多态的机制,并且它们都可以定义公共接口。它们并不是互斥的。在任何设计中,混合使用抽象(甚至具体)类与“接口”实现来表达业务/领域正是应该做的事情。很遗憾,“接口”的含义被重载了,但“..按接口编程..”绝对不意味着“不要使用抽象类”。 - radarbob
让我明确一点。按照定义,抽象类至少有一个方法被声明为抽象 - 这是子类必须实现的标志。此外,任何修饰符都可以应用于其方法,从而使子类具有不可变和/或可选的实现。然后,子类被引用为它们的基类,因此客户端代码具有一个接口来编写代码,同时保证a)所有所需的方法(即接口)都在子类中b)公共行为相同c)子类表现出适当的多态性。这就是抽象类是接口的原因和方式。 - radarbob
我理解它们的区别。问题不在于确切的定义,而是抽象类的目的是继承一些功能/实现。接口的目的是能够完全替换实现,只提供一个模板来访问您的功能。即使没有实现任何方法的抽象类在实现上与接口非常接近,但它们有非常不同的目的。 - Bill K
抱歉要继续讲下去,但更重要的是抽象类定义了它的子类,因为你只能有一个父类。这意味着,如果另一个系统(具有另一个继承树)想要使用你的库,如果你的库要求他们从抽象类继承,他们就不能成为自己继承树的一部分。这是一个巨大的问题,也是我在二十年的面向对象编程中减少了对抽象类的使用,从某些情况到几乎没有,转而使用接口的原因。 - Bill K

1
你在这种情况下提出的替代方案有点不自然。我的意思是“实现”IDisposable。针对类进行编程并不总是不好,这取决于具体情况。

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