Sponsored By Aspose - File Format APIs for .NET

Aspose are the market leader of .NET APIs for file business formats – natively work with DOCX, XLSX, PPT, PDF, MSG, MPP, images formats and many more!

Why SOLID? GIMME AN L!

.csharp { font-family: ‘Consolas’, ‘Courier New'; color: #33F;}

Thus far in my journey to explain the why of the so-called SOLID principles I’ve covered Single Responsibility Principle and Open/Closed Principle. This brings us to “L” for Liskov Substitution Principle which the originator, Barbara Liskov, describes as:

What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

Whoa! Math meets English and kicks its… well, you know. Robert Martin boils it down a bit:

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

If we have a consuming function that takes a Customer class, it shouldn’t matter if that gets a GoldCustomer specialization. We don’t want, in our consumer code, to rely on the is or as keywords to branch our consumer accordingly.

// For shame!

void Consumer(Customer customer) 
{
  if (customer is GoldCustomer) 
  {
     var goldCustomer = customer as GoldCustomer;
     goldCustomer.DoSomethingElse();
  }
  else
  {
     customer.DoSomething();
  }
}

Seems sensible, but recall: we’re dealing with the why and operating under the strict belief that it’s unacceptable to invoke these principles without understanding their deeper reasons and benefits. It’s particularly important that we be able and ready to articulate the reasons for these tried truisms as we mentor new developers and win entrenched developers over to known better ways of doing things.

Mind Your Coupling

A lot of these principles come down to coupling and cohesion. LSP is all about controlling coupling. Notice how in the code example we’ve touched on two types? Customer and GoldCustomer are both used by our (rather creatively named) Consumer method. So what if we add a PlatinumCustomer? We’ve created a permalink between our consumer and a wider-than-desired surface area of our customer model.

Respect the OCP

As Robert Martin points out, ignoring LSP is a clear violation of “the Open-Closed principle because [that function] must be modified whenever a new derivative of the base class is created.” So LSP and OCP are intrinsically linked, and, if you remember, the why behind OCP has to do with stability and moving forward by only ever adding new behavior. If I have to change the Consumer function every time I create a specialization of Customer is that function closed to modification? Clearly not.

Fragile APIs

Ignoring this has particularly nasty effects when we’re writing code or an API to be consumed by someone else. By “someone else” I mean either:

  1. Another team in the same timeline.
  2. Another developer that has to come along and maintain our code.
  3. Ourselves way down the timeline; we’d have to remember this bit of code exists!

LSP must be taken into account when we design our class hierarchies. We have to respect this principle upstream and within the context of our layered architectures. I believe LSP and everything it supports form part of the justification for another principle: favor composition over inheritance.

Sure inheritance seems conceptually useful when we’re inside the model or framework using it, but when we think about our system as a whole — external consumers, future dependents — and apply LSP, that initial perceived value diminishes quickly. Since it’s so hard to get these “is a” relationships right and they are fewer and further between than we might initially think, why not just avoid it and treat our classes as a kind of surgical tray? Small, single-purpose instruments that can collaborate to solve a number of problems…

Leveraging DbC

Design-by-Contract and LSP go hand-in-hand. DbC’s notion of preconditions and postconditions have import when we delve into the mire of class hierarchies. A subclass may override a parent method only under certain conditions:

  1. Preconditions can only be weaker.
  2. Postconditions can only be stronger.

If you want to take advantage of DbC features in the future — and I think a lot of us will — your class hierarchies need to meet this guideline. They should meet it already right now, but a DbC framework should prove this at compile time. DbC gets us closer to the promised power of static languages with verifiably correct software, which has the benefit of achieving a high bar of initial quality after the development process while lightening our testing and specification burden.

This entry was posted in oo, principles, solid. Bookmark the permalink. Follow any comments here with the RSS feed for this post.

3 Responses to Why SOLID? GIMME AN L!

  1. void ProcessCustomer(Customer customer)
    {
    if (!customer.IsGoldCustomer)

    Why Customer class could know about all inherit classes (GoldCustomer).

  2. Dave Laribee says:

    Good point, Neil. I think LSP applies in both cases. Thank you for bringing that up.

    In your example, you’re moving coupling to a factory, if I understand the scenario correctly. I think this is OK as long as that factory returns an interface that adheres to LSP. That is, you don’t need to inspect the type returned to do something custom/specific. If all that’s handled in the factory, you just have one place to go.

    Still, I’d consider the design. Perhaps there’s a better way to do it? The GoldCustomer/Customer example is probably a silly one as we can use a specification on one customer object to branch logic or ignore innapropriate Customers (just one way). I’d consider maybe doing this:

    class CustomerProcessor: ITheInterfaceYouMentioned
    {
    void ProcessCustomer(Customer customer)
    {
    // do what you must…
    if (!customer.IsGoldCustomer)
    // do something special…
    // using an extension method off of the customer entity
    // to determine applicability
    }
    }

    No inheritance necessary!

  3. Neil Mosafi says:

    Nice article… however I thought the Substitution principal meant that you shouldn’t change the semantic meaning of a function if you override it? Your example just talked about needing to downcast.

    Anyway, given your example, I wonder how the rule applies when you have different classes which act on the different types? For example, I might have an interface with a single method ProcessCustomer(Customer customer). I could then create a factory method which looks at the type of customer and returns the appropriate implementation. The implementation for GoldCustomers would obviously have to downcast to operate on the gold customer.

    This is essentially the same thing but dressed up differently! Is this still bad?

    Thanks
    Neil

Leave a Reply

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

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>