0

Please see the Rules Design Pattern on this webpage: http://www.michael-whelan.net/rules-design-pattern/. It has classes like this:

public interface IDiscountRule
{
    decimal CalculateCustomerDiscount(Customer customer);
}

public class BirthdayDiscountRule : IDiscountRule
{
    public decimal CalculateCustomerDiscount(Customer customer)
    {
        return customer.IsBirthday() ? 0.10m : 0;
    }
}

Why would interfaces be used for this instead of inheritance i.e. should BirthdayDiscountRule inherit IDiscountRule (calling it DiscountRule instead)?.

w0051977
  • 7,139

5 Answers5

8

There are a number of reasons.

  1. In c# you can inherit many interfaces, but only one base class.

  2. Inheritance has lost popularity as a method of sharing code against composition.

Say we do have some base logic we want all discounts to apply and we put it in a BaseDiscount class as you suggest. But then we find that its not as universal as we thought and we don't want to use it for all discounts.

Well we can go back and add a BaseBaseDiscount and redefine any lists of discounts to be of that type instead of BaseDiscount. But the most flexible version of this would be to have a Base class with no code, just the exposed method/properties, which is essentially an interface.

Additionally it can be hard to determine what code is actually running if you have a deep inheritance chain.

With composition I could have:

public class BirthdayDiscount: IDiscount
{
    public BirthdayDiscount(IDiscount baseDiscount)
    {
         this.baseDiscount = baseDiscount;
    }

    public decimal GiveDiscount(decimal price)
    {
        return baseDiscount.GiveDiscount(price) * birthdayDiscount;
    }
}

Now I can chop and change the baseDiscount class I pass into the constructor at run time, rather than having the change the class inheritance at compile time.

Ewan
  • 83,178
4

Because interfaces declare a specific function signature rather than a specific algorithm to compute that function.

To elaborate: this is exactly what interfaces are for. Presumably there are various reasons to give discounts (maybe everyone gets 1% off on a holiday, or there's a promotion on energy drinks, or whatever), and they all have different conditions and discounts, so they would be modelled by different methods in different classes, all of which implement IDuscountRule. All these methods yield a decimal, so they fulfil the contract of CCD, but they do it in different ways.

Inheritance would mean that they all do it the same way, and that's not what the problem requires.

Kilian Foth
  • 110,899
0

I think of interfaces as a way of working around the limitation, in the two most popular modern languages, which is not to support multiple inheritance, e.g. a class cannot extend two classes.

In the example code, the interface is intended to be used to handle the computing of a discount based on some rule. Somewhere in the app there will be an object of type Person, Customer, etc. and that class will say "implements IDiscountRule" which means that the rest of the code in the app can call a "CalculateCustomerDiscount" method to obtain the discount.

There may be a discount because of birthday, a discount because of student status, or military status, etc. All of those would be coded as extending "IDiscountRule", but the "CalculateCustomerDiscount" in those classes would be specific to whatever discount is provided.

Lastly, the object that represents a customer does not "extend" but instead "implements "CalculateCustomerDiscount". If you did extend, your customer class could not extend anything else, and the discount logic is just one of probably many logical dimensions of customer.

0

Separate the things that change from the things that don't. Designed in such a way, code changes/new code becomes isolated with a defined way of creation. Monolithic inherited classes reduce the ability to maintain code effectively. Think about it as putting the work in now so you can be lazy in the future :D

-1

The other two answers, and the "possible duplicate question" link adequately address the problems of using base classes, rather than interfaces. But I'd like to address the code in the question itself.

The first thing to note is the return customer.IsBirthday() ? 0.10m : 0; line.

This code reveals the fact that the IsBirthday() method is likely tightly coupled to DateTime.Now. This makes testing harder than it need be, eg without changing the system time (or only running the test once every four years), there's no easy way to test that it correctly handles a customer born on 29th February. The current date should be passed to IsBirthday().

Secondly, if the current date is passed to it, CalculateCustomerDiscount becomes a pure function, thus can be made static:

public static decimal CalculateBirthdayDiscount(Customer customer, DateTime date) =>
    customer.IsBirthday(date) ? 0.10m : 0;

And it can be referenced via a Func<Customer, DateTime, decimal> delegate throughout the code. This removes the need to define the IDiscountRule interface.

Not only do we simplify the code by removing an unused interface, we remove the need to have to create mocks of that interface in test code. A simple mock method can be used instead, once again simplifying things, potentially even removing the need to use a mocking framework.

David Arno
  • 39,599
  • 9
  • 94
  • 129