关于c#:Interface与抽象类有一个空方法

Interface vs Abstract Class with an empty method

我试图理解什么时候应该使用接口和抽象类。我在研究如何改进我的MVC应用程序设计,并看到了这篇文章:http://www.codeproject.com/articles/822791/developing-mvc-applications-using-solid-principles

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
    }

}

他的解决方案如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
    }
}

查看此解决方案时,我的问题是:

  • 为什么在这里使用抽象类而不是接口?
  • 如果将此更改为接口,会有什么区别?这真的很重要吗?
  • 谢谢你帮我理解这个


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

    1
    2
    3
    getAuthor()
    getISBN()
    isFiction()

    如果这本书是学生或公司的话,这些行为可能不会改变,所以我们有一个有很多标准行为的基础班。所以这本书真的是一个类,因为它有一些由派生类共享的有用行为。

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

    在实践中,我发现接口比抽象基类多。我将接口定义为我的面向外部的契约。也就是说,我编写了一些代码来处理调用方提供给我的对象。我告诉他们我需要满足这个接口的东西。我并没有说明这是怎么做到的,只要给我一些可以计算价格的东西。

    AbstractClass更适合于实现某些代码的人。我们实际上是给一个部分编写的类,然后要求编码人员"填补空白"。我们能够有效地做到这一点的情况往往更加罕见。


    在您给出的示例中,接口可能是最好的选择,但是如果扩展示例,这可能会改变。

    一般来说,接口和抽象类都可以用来执行契约——即,实现契约的类型应该以某种方式运行。主要的区别在于,一个接口仅仅说明了实现类型应该能够做什么,而抽象类除了契约之外还能够共享功能。

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

    限制是您只能在任何给定类型上实现一个抽象类,但是您可以实现任意多的接口。

    我已经看到了一些例子,其中抽象类实现了接口,具体类实现了抽象类——这样,您就可以充分利用这两个领域;第三方不必与您在抽象类上实现getISBN()相耦合。

    另一个要点是,一些模拟库将与模拟非虚拟方法作斗争,这包括抽象类上的方法——但是,它们将与接口完美结合。

    作为一个tldr:interfaces是为那些根本不关心如何实现的类型而设计的,您只关心类型具有某些特性。当您关心类的某些部分是如何实现的而不是其他部分时,请使用抽象类。


    在您的情况下,使用interface比使用abstract class更合适。我这么说是因为您没有提供方法的任何实现,而这些实现以后可能会被继承您的abstract class的类所覆盖。你只想让一个CorporateBookStudentBook有一个名为CalculatePrice的方法具有相同的签名。因此,可以定义一个名为

    1
    2
    3
    4
    public interface IPriceCalculator
    {
        public double CalculatePrice(double price);
    }

    然后让您的类实现这个interface

    1
    2
    3
    4
    5
    6
    7
    class StudentBook : Book, IPriceCalculator
    {
        public double CalculatePrice(double price)
        {
            return price - (price * 20);
        }
    }

    1
    2
    3
    4
    5
    6
    7
    class CorporateBook : Book, IPriceCalculator
    {
        public override double CalculatePrice(double price)
        {
            return price - (price * 10);
        }
    }

    另一方面,我建议采用另一种方法来计算该值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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构造函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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);
        }
    }

    最后,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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的建设者。


    答案取决于一些因素,如常见行为和可扩展性级别。我将在这里解释它创造了一个虚拟的社会网络概念,所以对我们来说,社会网络是一种可以用图像发布信息并保存已发布信息历史的东西。然后我们的社交网络将共享行为,所以我将创建一个基类(抽象类)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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基类是我们知道的一个正常的社会网络,但是我们需要一个超抽象来定义一个社会网络做什么,而不定义它的任何行为,所以我们需要定义一个接口。

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

    现在,我们将进行socialNetwork类来实现isocialNetwork:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public abstract class SocialNetwork : ISocialNetwork
    {
        ...    
        public void Post(string comment, byte[] image)
        {
            ...
        }
        ...
    }

    下面是新的Snapchat类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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(abstract类)的公共行为,并实现了isocialnetwork(interface)。Snapchat类不与Facebook和Twitter共享任何行为,但它也是一个社交网络,因此它直接实现等对话网络接口。

    你可以从这里阅读全文:http://www.theagilethinker.com/2015/08/22/an-interest-example-of-convivence-between-abstract-classes-and-interfaces/


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

    我知道这是一个高度简化的例子,但希望这本书能说明,计算出的书的价格根本不属于这本书。S.O.L.I.D.的第一个原则是单一责任。这是迄今为止最重要的。计算其价格的Book类(和派生类)为Book增加了第二个责任(我假设包含内容是书籍的另一个责任,也是主要责任)。这违反了第一条原则。[它还违反了其他OOP"规则",如高级内聚,但这是另一个主题]。

    如果要提供对Book类的价格计算的访问权限,则可以使用Book中的单独计算类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    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);
        }
    }