Programmatic Enforcement Of Liskov Substitution Principle

Ovid on 2009-03-30T22:19:29

So you write your Employee class and some idiotic programmer writes the Employee::Volunteer subclass and it returns undef for the salary, but that violates the Liskov Substitution Principle. Languages like Perl make it hard to stop this because the subclass method calls the superclass method and can then munge it any way they want and you can't force them to respect Liskov.

Languages like Beta, however, can make this easier because its classes are patterns and a subclass is a subpattern and the patterns call the subpatterns and can guarantee Liskov by effectively reversing the direction in which methods are called via inheritance. Superclass methods call subclass methods instead of the other way around.

So I made this work in Perl.

#!/usr/bin/env perl -l

use strict;
use warnings;

{
    package Employee;
    use Acme::Liskov;
    use Scalar::Util 'looks_like_number';

    sub new {
        my ( $pattern, $payrate ) = @_;
        die unless looks_like_number($payrate);
        bless { payrate => $payrate } => $pattern;
    }

    sub salary {
        my $pattern = shift;
        if ( $pattern->is_augmented ) {
            my $salary = $pattern->inner;
            warn $@ if $@;
            die "not a number ($salary)" unless looks_like_number($salary);
            return $salary;
        }
        return $pattern->{payrate};
    }
}

{
    package Employee::Hourly;
    use base 'Employee';

    sub salary {
        my $subpattern = shift;
        return $subpattern->{payrate} * 40;
    }
}
{
    package Employee::Volunteer;
    use base 'Employee';

    sub salary {
        my $subpattern = shift;
        return undef;
    }
}

my $hourly = Employee::Hourly->new(5);

# prints 200
print $hourly->salary;

my $volunteer = Employee::Volunteer->new(4);

# blows up because it violates liskov
print $volunteer->salary;

The code is a massive hack (and not on the CPAN) and terribly fragile and uses some really, really nasty tricks. If you're tempted by this technique, use augment and inner from Moose.


typo?

zby on 2009-03-31T07:54:33

my $subpattern = shift; return $pattern->{payrate} * 40;

Re:typo?

Ovid on 2009-03-31T08:50:29

Fixed. Thank you!

Academic Nonsense

djberg96 on 2009-03-31T14:24:53

The Liskov Substitution Principal is academic nonsense. Do we need to have the Pathname debate again?

What I really need to do is write an article on why PhD's should never be trusted.

Re:Academic Nonsense

pdcawley on 2009-03-31T15:51:37

Your Turing Medal is in the post

Re:Academic Nonsense

Ovid on 2009-04-01T10:34:18

++ :)

Re:Academic Nonsense

btilly on 2009-03-31T16:09:56

I agree that treating it as an absolute rule is nonsense. But the basic principle - that you should be able to expect that objects of a subclass can be safely used in place of objects of a class - is sound. Furthermore in complex OO code it is common to have subclasses accidentally override methods of parent classes that they did not know about. And these cases area almost always real bugs.

Therefore the Liskov Substitution Principle is a rule of thumb to be aware of, and there is value in catching accidental violations of it.

Re:Academic Nonsense

Ovid on 2009-04-01T10:32:49

I can only assume you're referring to your response to my O'Reilly Liskov write-up. I didn't respond to you then, but since you've mentioned this twice, I feel compelled to respond: you've picked a most unfortunate counter-argument. I have a lot of respect for you and I'm sorry to be so blunt, but but consider what you wrote:

Is a Path a kind of String? Yes. Can I choose to implement a Path class as a subclass of String. If we follow the LSP then the answer to the second question is no and I must delegate all string-like methods...

A Path is a kind of String? By no stretch of the imagination is a path a type of string. A path represents a place on a disk. A directory structure is a tree and a path could be considered a limb on the tree, or a series of inodes, but a string? Now you're forcing an metaphor on people which may not be appropriate. When I cd into a directory, I'm not concatenating two paths. I might be pushing an inode onto a stack or moving to a new node on a graph or tree or something like that, but I'm not concatenating.

In your argument, you are using a string as a metaphor for a path. If you want to do that, it's fine, but in this case, Liskov doesn't apply because your types don't match. You have forced a metaphor on people which may or may not be appropriate, but because you've no longer truly provided a subclass which is a more specialized version of the superclass, Liskov does not apply.

To summarize: you describe a "parent class as metaphor" approach and not the "parent class as basic type" approach and since Liskov works well with types and not with metaphors, it's not fair to say that Liskov is nonsense. It's just not appropriate in your case.

Re:Academic Nonsense

Ovid on 2009-04-01T10:36:56

I should point out that in the real world, hierarchical representations don't always work and that's part of the reason why Liskov can seem problematic. It's one of the many limitations of OO which causes people to misunderstand many fundamentals. That's why I'm so bullish on roles. I suspect they can help lead the way out of the OO mess we're in.

Re: Duck Typing!

systems on 2009-04-01T09:07:51

isn't this the problem that dynamic or duck typing solves!

where you declare a method you don't say what type you want to go in. the documentation though would have the attributes needed from the object to perform the task or work asked to it by the method

Ocaml, I believe have a way to declare the attributes the objects needs to have regardless of his class or type, so i think they are half way between static and dynamic

sub-classing in my opinion should be only used as a purely technical method to share code, not define types, natures or attributes!

Re: Duck Typing!

Ovid on 2009-04-01T10:05:15

Duck typing is a separate issue. The issue here is something like this:

while ( my $thing = $foo->next ) {
    $total += $thing->value;
}

You want value to always return something suitable for addition. In Perl, it's awfully tough to make this "just work". As Ben Tilly points out, this shouldn't be a global "you must always ..." debate, but the general principle is sound.

Re: Duck Typing!

systems on 2009-04-01T13:00:09

okay, I am not sure why making sure that value always returns something suitable for addition is a big deal.

first of all, you should not try to modify value directly, you should ask $think to modify value for you

second if $thing know how to perform addition on value, $thing will comply

there are so many ways to know if $thing can perform addition on value

1. ask $thing if he is of a certain type
2. ask $thing if one of his ancestors is of a certain type
3. according to your liskov, ask $thing if one of his derivatives is of a certain type
4. duck typing, ask thing to perform the addition, if $thing knows how, $thing will return the addition value, else raise an error (or do nothing)
5. ask $thing if he can add stuff regardless of his, his ancestors or hi derivatives types.

The problem i see with duck typing, is making sure that thing is always in a consistent state, that no request can validate things state, when an error is raise thing should make sure to return to a consistent state, thats all!

Re: Duck Typing!

btilly on 2009-04-02T04:29:48

The principle is that if an OO class supports a given internal and external API, then any subclasses should support that API. If the parent class promises that a given method returns something that can be added, then subclasses should do likewise. If the parent class makes errors available through a particular mechanism, subclasses should do likewise. And so on.

In short one should be able to use an object blessed into the subclass instead of an object blessed into the parent class and nothing should break. Put an alternate way, something of the subtype ISA thing of the parent type, and any guarantees that are supposed to hold for the parent type need to hold for the subtype.

Nothing technical enforces this. You are able to inherit from a parent class, override any method you want, and change the behavior to be what you want. But if you do this, then your inheritance relationships are now telling a lie. You have violated expectations in a way that is likely to cause code to break in nasty ways that is likely to be hard to debug.

So as a first rule, Don't Do That. For the experts I'll amend that to Don't Do That Without Careful Thought And Good Cause. If you're unclear on why this is important, then you're a novice who can't judge what good cause is, and therefore should follow the simpler version of the rule.

Re: Duck Typing!

chromatic on 2009-04-03T18:48:43

The principle is that if an OO class supports a given internal and external API, then any subclasses should support that API.

Nitpick: the principle talks about subtypes, not subclasses. This is an important distinction, as subtyping relationships do not require inheritance. (I see that someone has proposed merging the LSP page into the Inheritance page on Wikipedia. That disappoints but does not surprise me.)