使用空方法的接口与抽象类

时间:2014-11-28 16:14:42

标签: c# oop solid-principles

我试图了解何时应该使用接口与抽象类。我正在考虑改进我的MVC应用程序设计并遇到了这篇文章:http://www.codeproject.com/Articles/822791/Developing-MVC-applications-using-SOLID-principles

在关于OCP的部分中,作者给出了一个关于计算书籍价格的例子。原始代码如下所示:

enum Category
{
    student,
    corporate
}

class Book
{
    public double CalculatePrice(double price,Category category)
    {
        if (category == Category.corporate)
        {
            price = price- (price * 10);
        }
        else if (category == Category.student)
        {
            price = price - (price * 20);
        }

        return price;
    }

}

他的解决方案看起来像这样:

abstract class Book
{
    public abstract double CalculatePrice(double price);
}

class StudentBook : Book
{
    public override double CalculatePrice(double price)
    {
        return price - (price * 20);
    }
}

class CorporateBook : Book
{
    public override double CalculatePrice(double price)
    {
        return price - (price * 10);
    }
}

我在查看此解决方案时遇到的问题是:

  1. 为什么在这里使用抽象类而不是接口?
  2. 如果要将其更改为界面,会有什么不同?这真的很重要吗?
  3. 感谢您帮助理解这个

5 个答案:

答案 0 :(得分:4)

该示例是人为的,因为Book基类没有行为,它也可以是一个接口。然而,一个更现实的例子会有许多其他方法,如

getAuthor()
getISBN()
isFiction()
如果这本书是学生或公司,那么这些行为可能不会改变,所以我们有一个有很多标准行为的基类。所以Book真的会成为一个类,因为它具有由派生类共享的有用行为。

当你有几组行为时,事情会变得复杂一些,例如,图书馆书是一本书,但它也是一个LendableThing,在Java中,你不能从两个不同的基类继承。

在实践中,我发现有比我做Abstract Base类更多的接口。我将Interfaces定义为我面向外的合同。那就是我写了一些代码,这些代码适用于我的调用者给我的对象。我告诉他们我需要一些能够满足这个界面的东西。我没有对如何做任何陈述,只要给我一些可以计算价格的东西。

AbstractClass更有利于实现某些代码的人。我们有效地提供了一个部分写的课程,然后要求编码员“填补空白”。我们可以做到这一点的情况往往更为罕见。

答案 1 :(得分:2)

在您的情况下,使用interface而不是abstract class会更合适。我这样说,因为你没有提供你的方法的任何实现,以后可能被继承你abstract class的类覆盖。您希望CorporateBookStudentBook拥有一个名为CalculatePrice的方法具有相同的签名。因此,您可以定义一个名为<{p}的interface

public interface IPriceCalculator
{
    public double CalculatePrice(double price);
}

以后再让你的类实现这个interface

class StudentBook : Book, IPriceCalculator
{
    public double CalculatePrice(double price)
    {
        return price - (price * 20);
    }
}

class CorporateBook : Book, IPriceCalculator
{
    public override double CalculatePrice(double price)
    {
        return price - (price * 10);
    }
}

另一方面,我建议另一种计算价值的方法:

public interface IPriceCalculator
{
     public double CalculatePrice(double price);
}

public class PriceCalculator
{
    public double Discount { get; private set; }

    public PriceCalculator(double discount)
    {
        Discount = discount;
    }

    public double CalculatePrice(double price)
    {
        return price - (price*Discount)
    }
}

然后将IPriceCalculator类型的对象注入Book构造函数。

public class Book
{
    // The initial price.
    public double Price { get; private set; }
    public IPriceCalculator PriceCalculator { get; private set; } 

    public Book(double price, IPriceCalculator priceCalculator)
    {
        Price = price;
        PriceCalculator = priceCalculator;
    }

    public double CalculatePrice()
    {
        return PriceCalculator.CalculatePrice(Price);
    }
}

最后,

class StudentBook : Book
{
    public StudentBook(double price, IPriceCalculator priceCalculator) : 
        base(double price, IPriceCalculator priceCalculator)
    {

    }
}

class CorporateBook : Book
{
    public CorporateBook(double price, IPriceCalculator priceCalculator) : 
        base(double price, IPriceCalculator priceCalculator)
    {

    }
}

然后创建您选择的PriceCalculator并将其传递给StudentBookCorporateBook的构造函数。

答案 2 :(得分:2)

接口可能是您给出的示例中的最佳选项,但如果示例扩展可能会更改。

从广义上讲,接口和抽象类都可以用来强制执行合同 - 即,实现合同的类型应该以某种方式运行。主要区别在于界面只是说实现类型应该能够做什么,而抽象类除了合同之外还能够共享功能。

您的书籍示例可以扩展为具有在所有类型的Book中具有相同实现的额外功能,在这种情况下,您可能希望使用抽象类。例如,如果我想共享一个getISBN()方法,但实现没有实现合同的类型,那么使用抽象类可能更有意义。

限制是您只能在任何给定类型上实现单个抽象类,但您可以根据需要实现任意数量的接口。

我已经看到了一些抽象类实现接口的示例,具体类实现了抽象类 - 这样,您就可以获得两全其美的效果;第三方不必与您在抽象类上getISBN()的实现相关联。

另一个切入点是,一些模拟库将难以模拟非虚方法,这包括抽象类的方法 - 但是,它们可以很好地与接口一起工作。

作为TLDR:接口适用于您完全不了解如何实现的类型,您只关心某个类型具有某些功能。当您关心如何实现类的某些部分而不是其他部分时,请使用抽象类。

答案 3 :(得分:1)

在C#中,使用抽象类与接口之间的区别主要在于CLS语言中对多态性的限制。在您给出的示例中,因为CalculatePrice的两个实现非常简单,使用抽象类而不是接口会将多态约束添加到Book的所有派生,并且几乎不会带来任何收益。 / p>

我知道这是一个高度简化的例子,但希望这本书将展示这本书的价格计算根本不属于本书的内容。 S.O.L.I.D.的第一个原则是单一责任。这是迄今为止最重要的。计算其价格的书类(和衍生物)增加了对该书的第二责任(我假设包含内容是另一本书,主要是本书的责任)。这违反了第一个原则。 [它也违反了其他OOP“规则”,如高级凝聚力,但这是另一个主题]。

如果您想要提供书籍类价格计算的访问权限,您可以在书中使用单独的计算类:

public interface IBookPriceCalculator
{
    double CalculatePrice(double price);
}

public class StudentBookPriceCalculator : IBookPriceCalculator
{
    public double CalculatePrice(double price)
    {
        return price - (price * 0.20);
    }
}

public class StudentBook
{
    IBookPriceCalculator _priceCalculator;

    public StudentBook()
    {
        _priceCalculator = new StudentBookPriceCalculator();
    }

    public double BasePrice { get; set; }

    public double GetPrice()
    {
        return _priceCalculator.CalculatePrice(BasePrice);
    }
}

答案 4 :(得分:1)

答案取决于一些因素,如常见行为和可扩展性水平。我将解释它在这里创建一个虚构的社交网络概念,因此对于我们来说,社交网络可以发布带有图像的消息并保存已发布消息的历史记录。然后我们的社交网络将共享行为,因此我将创建一个基类(抽象类)。

public abstract class SocialNetwork
{
    public List<string> History { get; private set; }

    protected SocialNetwork()
    {
        History = new List<string>();
    }

    public void Post(string comment, byte[] image)
    {
        DoPost(comment, image);
        History.Add(comment);
    }

    protected virtual void DoPost(string comment, byte[] image)
    {
    }
}

现在我将创建我们的社交网络:facebook和twitter

public class Facebook : SocialNetwork
{
    protected override void DoPost(string comment, byte[] image)
    {
        //Logic to do a facebook post
    }
}

public class Twitter : SocialNetwork
{
    protected override void DoPost(string comment, byte[] image)
    {
        //Logic to do a twitter post
    }
}

到目前为止,一切都很好看。好吧,想象一下我们必须处理一种完全不同的社交网络,例如一些不存储消息历史的社交网络,比如Snapchat:

public class Snapchat : SocialNetwork
{
    private string _lastMessage;

    protected override void DoPost(string comment, byte[] image)
    {
        //Logic to do a snapchat post
        _lastMessage = comment;
        ProcessLastMessage();
        History.Clear();
    }

    private void ProcessLastMessage()
    {
        //Some logic here.
    }
}

如上所述,Snapchat类继承自SocialNetwork类,因此Snapchat类也会存储帖子的历史记录。但是我们不想要它,所以我们必须放置代码来清除历史列表。

接口实施 上面实现的问题是Snapchat有一个他不需要的东西,即历史所以我们需要更高层次的抽象,SocialNetwork基类是我们所知道的正常社交网络,但我们需要一个超级抽象来定义SocialNetwork没有定义任何行为,所以我们需要定义一个接口。

public interface ISocialNetwork
{
    void Post(string message, byte[] image);
}

现在我们将使用SocialNetwork类来实现ISocialNetwork:

public abstract class SocialNetwork : ISocialNetwork
{
    ...     
    public void Post(string comment, byte[] image)
    {
        ...
    }
    ...
}

现在这里是新的Snapchat类:

public class Snapchat : ISocialNetwork
{
    private string _lastMessage;

    public void Post(string message, byte[] image)
    {
        //Logic to do a snapchat post
        _lastMessage = message;
        ProcessLastMessage();
    }

    private void ProcessLastMessage()
    {
        //Some logic here.
    }
}

现在设计足够强大。 Facebook和Twitter分享来自SocialNetwork(抽象类)的常见行为,它实现了ISocialNetwork(接口)。 Snapchat类不与Facebook和Twitter分享任何行为,但它也是一个社交网络,因此它直接实现ISocialNetwork接口。

您可以阅读此处的完整文章:http://www.theagilethinker.com/2015/08/22/an-interesting-example-of-convivence-between-abstract-classes-and-interfaces/