The Nonconformity Principle

Interchangeability is a common goal for those of us who are OOP developers. When we create new classes, we often try to design them with simplified public interfaces which make them interchangeable with other classes which serve similar purposes.  In other words, we try to make our classes plug-and-playable.  It’s a good instinct to make interchangeable objects, but we often make the mistake of doing so too soon.  Interchangeability is a different kind of business logic altogether, so if we are smart, we will put that abstraction logic in separate higher-level classes rather than making each class interchangeable at its lowest level.

As an example, let’s say that we have an order form which needs to support any number of payment methods.  To accomplish this, we create a simple C# interface which can represent any type of payment method, like this:

public interface IPaymentMethod
{
    ///
    /// Short name for the payment method that should be displayed to users
    ///
    string Description { get; }

    ///
    /// Processes a new payment for the given amount
    ///
    /// Returns the transaction ID, if applicable
    ///
    string ProcessPayment(decimal amount, string purchaseDescription);
}

So far so good.  Once we have abstracted the different payment methods into a common interface, like that, we can easily make the order form display a list of them. Regardless of which payment method the user selects, we can call the ProcessPayment method, not caring about how the internals of that method are implemented.

That all makes sense on the face of it, but there are some subtle abstraction-specific things that are going on here.  For instance, the Description property is important when displaying a list of payment methods to the user, but it’s entirely unimportant if we have code that only uses one particular type of payment.  The ProcessPayment method returns a transaction ID, but for some payment methods, such as cash, the transaction ID will never be applicable, so it will always return null.

Choosing what to return for the transaction ID and choosing what to return for the description are fundamentally different kinds of logic from the business logic that actually performs the payment processing.  The payment processing logic is core logic which may be useful even in other places that are hard-coded to only work with one particular type of payment, but the rest of it is just abstraction logic for the purposes of making it interchangeable with other methods of payment for the order.  The abstraction logic is only there so that any number of payment methods can be plugged into the order form and they will all play nicely together, but none of it is necessary for any one of the payment methods to work in isolation.

In addition to that, some of the payment methods may require more functionality which is irrelevant to the order form.  Perhaps the order form only cares about being able to process a payment, but the credit card payment class may also need a way to cancel a payment.  In fact, there may be a payment cancellation form which needs to support any number of cancelable payment methods.  In that case, a credit card payment method class would need to implement two interfaces—one for payment processing, and another one for cancellation.  As more and more features are added, and the ways in which each class needs to be interchangeable with other classes increases, the code becomes increasingly complicated.

Rather than having a single class for each payment method which handles not only the core business logic but also all the abstraction logic for the different usages, it would be better to create separate classes which wrap the core logic for the different interchangeable interfaces.  In other words, rather than having a single class which implements all the interfaces necessary to make it interchangeable, like this:

public class CreditCardBusiness : IPaymentMethod, IPaymentCancelationMethod
{
    string Description { get { return "Credit Card"; } }
    void CancelPayment(string transactionId) { /* Do work... */ }
    string ProcessPayment(decimal amount, string purchaseDescription) { /* Do work... */ }
}

It would be better to make one business class which provides all the low-level functionality, and then make separate classes that wrap it in the various interchangeable interfaces:

public class CreditCardBusiness
{
    void CancelPayment(string transactionId) { /* Do work... */ }
    string ProcessPayment(decimal amount, string purchaseDescription) { /* Do work... */ }
}

public class CreditCardPaymentMethod : IPaymentMethod
{
    // Use CreditCardBusiness to perform necessary tasks...
}

public class CreditCardCancelationMethod : ICancellationMethod
{
    // Use CreditCardBusiness to perform necessary tasks...
}

public class CashBusiness
{
    void LogPayment(decimal amount, string purchaseDescription) { /* Do work... */ }
}

public class CashPaymentMethod : IPaymentMethod
{
    // Use CashBusiness to perform necessary tasks...
}

Doing so requires a little more effort, but it leads to simpler and cleaner code which is easier to maintain.  It divides the logic into smaller organized classes, neatly dividing the responsibilities by kind and purpose.  I have dubbed this The Nonconformity Principle.  Simply put, The Nonconformity Principle states:

No business class should conform to a common interface unless it is absolutely necessary. When a common interface is necessary, the abstraction logic should be entrusted to a wrapper class which implements the interface for interchangeability.

In other words, don’t mix abstraction logic with core business logic.

That’s not to say that core business classes should not implement interfaces.  It’s just that the interfaces for the core business classes should be narrowly tailored for their specific task and should not be reused by other core business classes.  Just because two core business classes happen to have the same public members, that does not mean that they should implement the same interface.  If two classes really do represent the exact same thing (e.g. one wraps or mocks the other), then sharing an interface makes sense.  But if they are two different things, you shouldn’t pretend that they are the same thing. At some point in the future, their interfaces may need to deviate and it will be difficult to decouple them.

As a big proponent of dependency-injection, it took me a while to teach myself The Nonconformity Principle through trial and error.  With dependency-injection, all business classes are supposed to implement an interface.  Since I had always learned that the purpose of interfaces was to make classes interchangeable, it was only natural that, as I was creating all of those interfaces for DI-purposes, I would try to make them a bit abstracted so that I could reuse them in multiple classes.  I learned the hard way that my instinct was a mistake.  It breaks The Nonconformity Principle and leads to problems in the long-run.  I came to realize that interfaces aren’t just for interchangeability. They actually serve two different purposes, interchangeability and injectability.  When creating a new interface, I need to decide which purpose it is intended to serve and then stick with it.  One interface should rarely serve both purposes.

So, next time you create a class, don’t be afraid to let its individuality shine.  Each class is unique.  There’s no shame in that.  If you need several different classes to all don the same uniform so that they can all look and act alike, you can always do that later.  You can wrap the same unique class inside any number of dull gray uniforms, but underneath, it will still have its own unique personality that will never be compromised by societal pressures.

Share This:
Facebooktwitterredditpinterestlinkedintumblr

Steven is a senior software engineer with experience developing stable, mission-critical applications for the public-safety market since 1998. He has been awarded the highest level as a Trusted User by his peers on StackOverflow. He is the author of Herding Ocelots, a blog aimed at providing help to software development managers.

Posted in Best Practices Tagged with:

Leave a Reply

Your email address will not be published. Required fields are marked *

*