1

Liskov Substitution Principle states:

"if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program (correctness, task performed, etc.)"

  • Preconditions cannot be strengthened in the subtype
  • Postconditions cannot be weakened in the subtype
  • Invariants must be preserved in the subtype

Now, imagine the following relationship between Person and Employee:

export default class Person {
    age: number;

    constructor () {
        this.age = 0;
    }

    setAge (age: number) {
        if (age < 0) throw new Error("Age must be 0 or above");
        this.age = age;
    }
}
import Person from "./Person";

export default class Employee extends Person {
    salary: number;

    constructor () {
        super();
        this.salary = 0;
    }

    setAge (age: number) {
        if (age < 18) throw new Error("Age must not be under 18");
        super.setAge(age);
    }

    setSalary (salary: number) {
        this.salary = salary;
    }
}

When we call the method setAge using a Person instance, we can use any age above 0. However, if we substitute an instance of Person by an instance of Employee, depending on the age, the program will break because the age it must be above 18.

I have 3 questions:

  1. It means that the preconditions were strengthened in the subtype?
  2. Did we break the LSP?
  3. How to fix this relationship in order to not break the LSP?
1
  • After ingesting all of the fabulous answers to this question, also read the excellent essay Wizards and Warriors by Eric Lippert.
    – mtj
    Aug 24, 2021 at 4:59

6 Answers 6

8

Inheritance of Person to Employee, or Person to Student, or Person to Instructor is an inappropriate relationship.

Why?  Because Persons are real entities, but Employee, Student, Instructor are roles, which are not is-a relationships with Persons.  A person is long-lived, whereas Roles are by comparison temporary/ephemeral and changeable, and, a person can have multiple roles simultaneously.

Systems configured using inheritance, is-a, relationships for people and their roles have problems when a person continues in the system but takes on another role(s).  For example, an employee becomes a customer, or a student teaches a course, perhaps even while continuing as a student.


We should not setAge to an entity, b/c age is dependent upon the time-of-now, which is constantly changing.  Instead, capture birth date or some such.  (Birth date can be captured immutably at construction.)


If age < 18 is problematic as a rule or requirement for an Employee role, then that should be checked when attempting to attach the Employee role to the person.


If a subclass imposes a restriction that is not otherwise allowed for by the base class, then yes, it is an LSP violation.  However, if the base class publishes/documents/declares that an age-related exception may be thrown, then it is not an LSP violation — so sadly, this is a somewhat subjective issue depending on other factors than code itself.

2
  • Great Erik, thanks for answering, in despite of the example, imagine any other situation where the subclass overrides a superclass method in that way, is it a LSP violation? Aug 21, 2021 at 22:59
  • 1
    Rodrigo, you’d have to add that as an example.
    – gnasher729
    Aug 22, 2021 at 14:26
3

It depends on the contract: LSP is not about code, but about promises made about that code, and we don't know the promises you made.

Most of the answers here assume that the contract should not throw and the exception is result of a checking of the pre-conditions. But error handling can be part of a contract. If I'd take this assumption:

  • The preconditions did not change : you always accept an integer and check if it's ok before performing anything. This is a robust contract.
  • The postconditions were strengthened: while the age changes for any positive number in the super class, it changes for less numbers for employee.
  • The invariants are preserved in the subtype: any Employee is guaranteed to be a Person with an age between 0 and ..., even if we now that employee does not use the full range.
  • The history constraint is met (yes, you've forgotten this one, and it's part of LSP): you change the age properly, by invoking the operation of the supertype.

While this could be good news about LSP if you take the same view on the contract, there's nevertheless something confusing in the code: Employee breaks its own invariants: when you construct a new employee its age is 0 and no exception is thrown, whereas you obviously expect above 18. This shows that there is a flaw in your design, independently of LSP.

To fix it, consider that Employee is not a Person but a role that a Person can have at some time (the person exist before being an employee, and continue to exist after quitting or losing the job; and the age changes independently of the employee status). So, prefer composition over inheritance: create an Employee with a reference to a Person. Provide this person to the constructor of the employee and throw an error if the age is not correct. There will be no ambiguity about LSP anymore.

1

It means that the preconditions were strengthened in the subtype? Did we break the LSP?

Yes and yes.

You presented the LSP from a theoretical perspective, but a more practical way to look at the LSP is from the point of view of the client of your API. Let's say you define a function like this:

function reinitializePerson(person: Person) {
    person.setAge(0);
}

Due to the implementation of Employee, this function may fail depending on the concrete instance of Person. This is the fundamental issue and the reason why the implementation of Employee.setAge violates the LSP: you must know the concrete type of the person object to ensure you can respect the contract of the setAge method.

In layman's terms, the Liskov Substitution Principle means "Can the child class be used in every context where the parent class is used?", which is no in your case.

How to fix this relationship in order to not break the LSP?

If I take your question in the literal sense, the fix would be not to enforce that the age must be at least 18. Of course, this may or may not make sense in your context. If you cannot loosen this precondition, it is a very strong hint that there is not an inheritance relationship between Person and Employee. There is possibly a common interface you can extract out of them, but the first step would be to ensure Employee does not inherit from Person.

2
  • 1
    "In layman's terms, the Liskov Substitution Principle means "Can the child class be used in every context where the parent class is used?"" – the LSP is not about classes. The LSP is about types. This is especially important in a language like TypeScript, where classes are not types, and there are many different kinds of types. Aug 22, 2021 at 10:02
  • @JörgWMittag That is correct, it is simply an approximate description to help the original poster transition from the theoretical definition of the LSP to a more practical approach for their use case. Aug 22, 2021 at 13:13
0

At least for this specific case, the correct strategy seems fairly simple to me.

Get rid of setAge entirely. At least in my opinion, it's a poorly chosen function to have in the first place.

Instead, I'd have the constructor take the person's birthday as a parameter. Assuming your intent is to represent only currently living people, the constructor for Person might verify that the specified birthday is no more than (say) 250 years ago (but if it's for people in general, probably doesn't even do that). For Employee it would obviously verify that it's at least 18 years ago.

Storing the birthday (rather than age) avoids a number of problems, including not only the one you pointed out, but also the need to constantly update ages, since they change annually.

But as an aside, even when you do use setAge, the problem you've specified shouldn't exist. Even when invoked via the base, when you call setAge, it should invoke the setAge for the derived class. As such, you're attempting to set an age of (say) 5 years on an Employee object should be rejected in any case.

2
  • 1
    Without the date of birth, you wouldn’t even know when to update the age.
    – gnasher729
    Aug 22, 2021 at 14:27
  • @gnasher729: That's true too. Aug 22, 2021 at 22:18
0

Yes it breaks LSP. Specifically the setAge method is a problem. The solution IMO is to remove the method or change its postcondition.

By that I mean, the implied postcondition of setAge(age: number) is assert(this.age == age). If you change that, then you can adjust the precondition so that both Person and Employee can keep the method.

For Person:

    setAge (age: number) {
        assert(true); // precondition
        if (age >= 0) { 
            this.age = age;
        }
        assert(this.age >= 0); // post-condition
    }

For Employee:

    setAge (age: number) {
        assert(true); // precondition
        if (age >= 18) { 
            this.age = age;
        }
        assert(this.age >= 18); // post-condition
    }

With the above I have:

  • Not strengthened the precondition (They are both the same.)
  • Not weakend the postcondition (in fact I have strengthend it.)
  • Preserved the invariant (the implied invariant is age >= 0

You will notice that the real change in the above is that setAge is no longer a demand, rather it is a suggestion and that makes all the difference.

In general, the way to ensure that your classes conform to LSP is to give them methods where you tell them about events that happened outside the object rather than telling them to perform actions inside. This is why in many systems you see methods called something like xDidHappen instead of doX.

When you tell an object that x happened, you are leaving it up to the object to decide what to do in response. This is exactly what objects are supposed to do. When you tell an object to "do X" (or in this case "set X") you are removing agency from the object. It is no longer in control of its own state. Something outside of the object now is calling all the shots. That's not OO; That's imperative programming in OO clothing.

0

How to fix this relationship in order to not break the LSP?

LSP violation is only a problem if you have client code that implements logic operating on polymorphic variables of the Person type (i.e. the concrete type could be any derivative of Person). If you instead have, say, a Company class that only ever works with the Employee type, and has really nothing to do with Person, then in the context of Company it doesn't matter that Employee breaks LSP or inherits from Person, because Company is not relying at all on the abstract behavior of Person. In this case, Employee inherits some implementation details from Person, but doesn't subtype it (as defined by LSP).

But if you have logic that polymorphically uses Person, then you care about LSP violations because they'll either break your code, or will force you to do type-checking, complicate your implementation, and introduce coupling that will tangle your code in various ways.

If Person specifies that ages 0 and above are allowed, then client code can rely on that fact - that's what types are for. The calling code can legitimately make this assumption, and can be written in a way that will cause it to break otherwise. So, since the Employee violates that guarantee, it is not a behavioral subtype of Person - even though an Employee is a Person when classified in a different way. There's generally more than one way to classify things in the world, depending on what you choose as your classification criteria. For subtyping, it is the abstract behavior (behavioral specification) that is the relevant classification criterion. Inheritance is often described as an "is-a" relationship, however "is-a" is too simplistic. A person is an employee. An employee is a person. Now what?


Aside: What's a bit confusing is that the same language mechanism (inheritance) can be used to implement two different kinds of relationships that are of interest to programmers: subtyping and implementation inheritance.


If you have code that does involve the Person type, then consider what that code actually does: if the calling code doesn't really need to set the age, then you can remove that method in the base type (this is under the assumption that there are other methods there that are not shown).

Or, you could reconceptualize the whole thing; you could pass the birth date to the constructor, and only have a getter for the age, that calculates the age based on the current date (that's a hidden dependency on a time-providing component, but it would eliminate this particular LSP violation). The details of the constructor are generally not a part of the abstract behavior, because clients that use polymorphic objects typically don't know the concrete type, as they are not the ones that create these objects.

Technically, you could also change the base type specification to say that setAge could throw an exception for any number; that would make Employee compatible, but it would also make setAge almost useless.

Finally, as others have pointed out, not everything has to be represented by inheritance. Composition is often a more flexible alternative to inheritance. Depending on your application, an Employee could be a distinct concept in your system, with a reference to a Person. Or maybe, depending on what your application is for, you don't need a separate Person class in your system at all. Or some other alternative.

Remember, the goal is not to identify concepts and classify them to create some inheritance hierarchy that reflects a preliminary understanding of these relationships that your mind happened to conjure at the time. The goal is to create a piece of software that serves the needs of its users, while making use of strategies that will make your job easier in terms of reasoning about what's happening in the code, and in terms of making changes and maintaining the code. LSP is one such strategy.

5
  • 1
    "What's a bit confusing is that the same language mechanism (inheritance) can be used to implement two different kinds of relationships that are of interest to programmers: subtyping and implementation inheritance." – One of my major pet peeves with Java. I have the feeling Java would be a much better language if classes weren't types. Aug 22, 2021 at 15:47
  • You could have a property minimumAge in the base class returning 0, and overridden in the subclass returning 18. The base class setAge throws an exception if age < minimumAge and needs no override.
    – gnasher729
    Aug 22, 2021 at 17:43
  • @gnasher729 - that's an option too (falls under my "or some other alternative") - by doing that you're changing the specification of the behavior associated with the base class to include the age >= minimumAge invariant. That may make perfect sense in certain contexts, and there could be a legitimate need for the setAge method, but the approach is not without potentially problematic tradeoffs (especially if the age is mutable), as you now need to check if you can safely call setAge. So the logic is split between the class and the caller (it's not cohesive). One has to weigh the pros and cons. Aug 22, 2021 at 18:29
  • @JörgWMittag IIRC you've pointed out a number of times that Java interfaces are a better representation of types (they at least don't bring any implementation with them); I agree, but I also think even in that case a lot rests on the design skills of the developer (team), as interfaces really only enforce a certain structure and method signatures, but only partially (if at all) specify type semantics (e.g. it's not evident from the signature of int Compare(T x, T y) what the return value means; it needs to be specified out-of-band, and this generalizes to multi-method interfaces). Aug 22, 2021 at 18:44
  • Yeah, even with only interfaces as types, Java's type system still leaves a lot to be desired. And unfortunately, the libraries don't even make proper use of the features that are there. E.g. in the example you mention, the return value of compareTo should be an enum ComparisonResult { LESS_THAN, EQUAL, GREATER_THAN }, not an int. This requires no changes to the type system, it is supported today. Aug 25, 2021 at 19:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.