如何避免重复的接口代码?

15

由于接口不能包含实现,这似乎会导致继承接口的类中存在代码重复。在下面的示例中,假设设置从流读取的前10行或者更多行是重复的。 不要过于关注措辞,而是专注于每个类之间容易创建重复代码的概念。

例如:

public interface IDatabaseProcessor
{
   void ProcessData(Stream stream);
}
public class SqlServerProcessor : IDatabaseProcessor
{
    void ProcessData(Stream stream)
    {
      // setting up logic to read the stream is duplicated code
    }
}
public class DB2Processor : IDatabaseProcessor
{
    void ProcessData(Stream stream)
    {
      // setting up logic to read the stream is duplicated code
    }
}

我意识到使用一个抽象基类来处理数据并添加非抽象成员是一种解决方案。但是,如果我真的非常想使用接口呢?


9
为什么不能同时使用接口和抽象基类? - Pavel Bakshy
我认为你想在不同的位置以不同的方式实现它时,应该使用“接口”。 - MartyE
@PavelBakshy - 有什么意义呢?我只需使用抽象类并将上面的接口成员定义为抽象即可。恐怕你错过了重点。 - O.O
@MartyE - 可能,但如果有很多类使用到这部分代码的话,我就必须实现一个非常复杂的接口,即使并不是所有的类都需要它。 - O.O
我的意思是,您可以定义抽象类,其中实现了像非抽象方法一样的方法。但是,如果您想提取一些常见逻辑并保存处理器的不同实现,则可以使用类似混合的东西。 - Pavel Bakshy
显示剩余2条评论
7个回答

19

这是一种情况,你需要同时使用接口和抽象基类。

唯一需要同时使用它们的原因是另一个类不会共享抽象基类的代码,但会遵循接口。举个例子:

public interface IDatabaseProcessor {
   void ProcessData(Stream stream);
}

public abstract class AbstractDatabaseProcessor : IDatabaseProcessor {
    public void ProcessData(Stream stream) {
      // setting up logic to read the stream is not duplicated
    }
}

public class SqlServerProcessor : AbstractDatabaseProcessor {
    //SqlServerProcessor specific methods go here
}

public class DB2Processor : AbstractDatabaseProcessor {
    // DB2Processor specific methods go here
}

public class NonSharedDbProcessor : IDatabaseProcessor {
    void ProcessData(Stream stream) {
      // set up logic that is different than that of AbstractDatabaseProcessor
    }
}

语法可能有点问题,我不是一个常规的C#用户。我是通过面向对象编程标签来到这里的。


1
语法可能有点问题,我不是一个经常使用C#的用户。我是通过OOP标签来到这里的。你的抽象类中的ProcessData方法应该标记为公共的。 - Konrad Morawski
我只是不明白为什么要创建两个,一个就足够了。(抽象类) - O.O
1
当一个类必须实现多个接口时,这就会出现问题。 - Asad Saeeduddin
将抽象类AbstractDatabaseProcessor改为protected是否比public更好? - Michel Keijzers
1
@MichelKeijzers 或许吧;正如我所提到的,我不是一个常规的C#用户,也不了解这些问题的最佳实践。 - Levi Morrison

14

通过无状态扩展方法是在接口间共享代码的最佳方式。你可以构建这些扩展一次,然后将其用于实现接口的所有类中,而不管它们的继承链是什么样的。这就是.NET在LINQ中对IEnumerable<T>所采用的方式,效果非常出色。虽然这种解决方案并不总是可行的,但你应该尽可能地优先选择它。

另一种分享逻辑的方式是创建一个内部的“辅助”类。在你的情况下,这似乎是正确的选择:实现可以调用内部共享的代码作为辅助方法,而无需复制任何代码。例如:

internal static class SqlProcessorHelper {
    public void StreamSetup(Stream toSetUp) {
        // Shared code to prepare the stream
    }
}
public class SqlServerProcessor : IDatabaseProcessor {
    void ProcessData(Stream stream) {
        SqlProcessorHelper.StreamSetup(stream);
    }
}
public class DB2Processor : IDatabaseProcessor {
    void ProcessData(Stream stream) {
        SqlProcessorHelper.StreamSetup(stream);
    }
}

帮助类不需要是静态的:如果你的共享方法需要状态,你可以将帮助类作为普通类,并在每个实现你想要共享代码的接口中放置一个它的实例。


在5个几乎相同的答案中,如果您能提供替代建议,我会给您+1分(我的意思是彼此之间相同,而不是与您的相同)。 - Konrad Morawski
@KonradMorawski 他们之间只有几秒钟的差别。这种情况时有发生。 - Levi Morrison
1
@O.O “难以测试”的规则是不完整的:确实,使用静态状态的静态方法很难测试。但是,像LINQ提供的无状态方法非常容易测试,因为您可以从外部提供所需的一切,并且您具有完全控制权,因为该方法没有任何状态。 - Sergey Kalinichenko
3
如果你确实需要状态,你可以将你的辅助类设为非静态。这样可以使你的继承层次结构不需要继承抽象基类,有效地用封装(private)替代了继承(public)。 - Sergey Kalinichenko
2
我认为这是最好的解决方案。抽象基类可能在当时看起来是个好主意... 直到你需要实现第二个接口。你就无法从另一个基类继承! - jocull
显示剩余9条评论

2

正如你所说,一种选择是使用基础抽象类(甚至可能是非抽象类)。另一个选择是创建另一个实体来运行公共代码。在你的情况下,它可以是DataProcessor

internal class DataProcessor
{
    public void Do(Stream stream) 
    {
        // common processing here
    }
}
public class SqlServerProcessor : IDatabaseProcessor
{
    void ProcessData(Stream stream)
    {
        new DataProcessor().Do(stream);
    }
}
public class DB2Processor : IDatabaseProcessor
{
    void ProcessData(Stream stream)
    {
        new DataProcessor().Do(stream);
    }
}

0

只需使用带有抽象基类的接口:

public interface IDatabaseProcessor
{
   void ProcessData(Stream stream);
}
public abstract class AbstractDatabaseProcessor : IDatabaseProcessor
{
    public virtual void ProcessData(Stream stream)
    {
      // setting up logic to read the stream is duplicated code
    }
}
public class SqlServerProcessor : AbstractDatabaseProcessor
{
    public void ProcessData(Stream stream)
    {
        base.ProcessData(stream);

        // Sql specific processing code
    }
}
public class DB2Processor : AbstractDatabaseProcessor
{
    public void ProcessData(Stream stream)
    {
        base.ProcessData(stream);

        // DB2 specific processing code
    }
}

我宁愿将ProcessData()设置为非虚函数,并提供一些实现(例如那些共享的10行代码),在某个时刻调用一个抽象方法,该方法必须由所有派生(非抽象)类实现。同时,将你的DatabaseProcessor标记为abstract,因为不应该允许实例化该类。 - Styxxy
1
应将 ProcessData 声明为虚函数以避免隐藏。 - Daniel Mann
但是为什么呢?使用两者的意义何在?我的意思是这不是绕过接口吗?如果我有选择,我永远不会这样做。我会从一开始就使用抽象类。 - O.O
你需要实现一个接口(不只是一个,而是多个实现),或者你公开一个接口,让其他人按照自己的方式来实现它。对于你的实现,你可以创建一个抽象基类来共享一些代码,否则你就必须复制它们。让我们以上面的例子为例:你设计了一个数据抽象层,并公开了一个特定方言的接口,但是你自己使用在抽象类中共享的代码来实现 SQL 和 DB2。其他人不依赖于你的抽象基类型,只需编写自己的内部逻辑即可。 - David Rettenbacher
接口描述契约,它是API构建的依赖项,并使用抽象类以避免重复。 - Pavel Bakshy

0

正如您所指出的,一个抽象类提供了解决方案。如果您“真的非常想要”,也可以使用接口;没有什么是不允许的。您的抽象类应该实现IDatabaseProcessor


0

如果您真的想在不使用基类的情况下仍然能够访问共享代码中的private和/或protected成员,那么唯一可用的选项是代码生成。它内置于VS中(已经有很长时间了)并且非常强大。


0

在实现接口共享接口的某些实现方面,具有一些类的层次结构是可以的。

例如,在您的情况下,您可以将共享的ProcessData代码移动到类似于ProcessorBase的东西中,并从中派生DB2ProcessorSqlServerProcessor。您可以决定哪个级别实现接口(即,您可以出于任何原因仅使SqlServerProcessor实现IDatabaseProcessor接口-它仍将从基类中选择ProcessData作为接口的实现)。


是的,我以前做过那个,但似乎多了一步。为什么不一开始就用一个抽象类呢?那样似乎是绕过了接口,有点像一个hack。 - O.O
如果您有多个不同的实现,接口会更加灵活。例如,如果您决定为使用“DatabaseProcessor”(作为基类实现)的类编写测试,则会发现很难实现基类的测试版本(与接口不同)。 - Alexei Levenkov

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