Mark Needham

Thoughts on Software Development

Two controllers, type conformance and the Liskov Substitution Principle

with 6 comments

An interesting object orientation related problem that Raph and I were looking at recently revolved around the design of two controllers in the application we've been working on.

The two controllers in question look roughly like this:

public class GenericController extends Controller {
	private final SomeFactory someFactory;
 
	public GenericController(SomeFactory someFactory);
        this.someFactory = someFactory;
	}
 
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // do some stuff but never use 'request' or 'response'
    }
}
public class MoreSpecificController extends GenericController {
	private final SomeFactory someFactory;
 
	public MoreSpecificController(SomeFactory someFactory);
        super(someFactory);
	}
 
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
    		...    
		// do some stuff which does use 'request'
		String someValue = request.getParameter("someParameter");
    }
}

We noticed from the way that we wrote tests for these two classes that we seem to be breaking the principle of type conformance as defined in Meilir Page Jones' 'Fundamentals of Object Oriented Design in UML' and more commonly referred to as the Liskov Substitution Principle.

The principle states:

If S is a true subtype of T, then S must conform to T. In other words, an object of type S can be provided in any context where an object of T is expected and correctness is still preserved when any accessor operation of the object is executed

This means that wherever we make use of 'GenericController' in our code it should be possible to pass in an instance of 'MoreSpecificController' and it would adhere to the same contract.

Our tests for each of the controllers looked a bit like this:

@Test
public void someTest() {
	GenericController genericController = new GenericController();
 
	genericController.handleRequest(null, null);
 
	// and so on
}
@Test
public void someTest() {
	MoreSpecificController moreSpecificController = new MoreSpecificController();
 
	moreSpecificController.handleRequest(mock(HttpServletRequest.class), mock(HttpServletResponse.class));
 
	// and so on
}

In 'MoreSpecificController' we need to get a value from the request which means that we can't have it as null in the test. In 'GenericController' the request is actually irrelevant so we can pass in a null value.

This means that the pre condition for 'MoreSpecificController' is stronger than the pre condition for 'GenericController' which violates the principle of contravariance which states the following:

Every operation's precondition is no stronger than the corresponding operation in the superclass. The strength of operation preconditions in the subclass goes in the opposite direction to the strength of the class invariant. That is, the operations preconditions get, if anything, weaker

The reason this might cause a problem is because if a client had a reference to a 'GenericController' they should expect that they can treat an instance of 'MoreSpecificController' as if it was a 'GenericController' which should mean that we can pass null values for request and response.

We weren't ever referring to a 'GenericController' when we instantiated a 'MoreSpecificController' in our code base so it didn't prove to be a problem but in theory it seems like something we'd want to avoid.

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • HackerNews
  • StumbleUpon
  • Twitter

Written by Mark Needham

November 19th, 2009 at 12:08 am

Posted in Coding

Tagged with

6 Responses to 'Two controllers, type conformance and the Liskov Substitution Principle'

Subscribe to comments with RSS or TrackBack to 'Two controllers, type conformance and the Liskov Substitution Principle'.

  1. Not really. Quoting Wikipedia's more precise version of the Liskov Substitution Principle:

    Let q(x) be a property provable about objects x of type T. Then q(y) should be true for objects y of type S where S is a subtype of T.

    The word "provable" is important: It means that implementation details for the Base class, such as the fact that GenericController doesn't use request and response, need not be true for the Derived class as well. Unless not using request and response is by Design, at which point you obviously have a LSP violation.

    MauricioC

    19 Nov 09 at 1:25 am

  2. Or, in other words: Every code can be held to much stronger assumptions than what's explicitly stated as a pre-condition or post-condition, but that doesn't mean derived classes have to adhere to those implicit assumptions. That's why making pre-conditions and post-conditions explicit is a good idea.

    MauricioC

    19 Nov 09 at 1:28 am

  3. Couple of questions:

    If we had explicit pre and post conditions then would it be true that sub classes should adhere to those laid out in their super class?

    What would you say is the definition of a provable property? I think you're saying that it's just a coincidence that request and response aren't being used in the super class, and that's why it wouldn't be a violate. Is that right?

    Mark Needham

    19 Nov 09 at 1:33 am

  4. I think we're looking too closely at what the arguments are as opposed to what they can be(?)

    null's make this problem more obvious in that they're not actually instances of the argument as the method has specified.

    Saying that a sub class should be able to replace a super class wherever the super class is used doesn't mean it is to behave the same (clearly).

    So the fact the specific class snaps when given a null isn't a violation of LSP. It's just the fact that null's are evil and a change in behaviour.

    Having discussed the above with Pete (Gillard-Moss) he's schooled me some. He states two things I didn't consider.

    1. In a contract based language the acceptable values are part of the contract, which means the subclass indeed breaks the initial contract.

    2. Throwing an exception is beyond a change in behaviour, it too is a break in the contract given (1).

    That to me suggests that either the super class should check for nulls and throw an exception, or the subclass should check for nulls and handle that scenario gracefully.

    Zubair Khan

    20 Nov 09 at 1:21 am

  5. Yeh that's interesting. In reality neither of those is likely to be null but it seemed like something interesting!

    If I understand what you're saying correctly then we should make MoreSpecificController handle the null situation without throwing a Null Pointer Exception? Makes sense.

    Mark Needham

    20 Nov 09 at 3:23 am

  6. I think that if we agree on that arguments values are part of a precondition the situation is more clear:

    1. "Preconditions can be modified in redefined routines, but they may only be weakened[2]. That is, the redefined routine may lessen the obligation of the client, but not increase it."

    http://en.wikipedia.org/wiki/Precondition

    2. "* Preconditions cannot be strengthened in a subtype.
    * Postconditions cannot be weakened in a subtype.
    * Invariants of the supertype must be preserved in a subtype.
    * History constraint (the "history rule")… "

    http://en.wikipedia.org/wiki/Behavioral_subtyping

    So I agree with Zubair Khan and, personally, i would change the base class contract to throw an exception in presence of null arguments.

    domenico

    29 Nov 09 at 11:15 pm

Leave a Reply