Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/design_principles/solid/liskov/detailed/Exceptions.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Detailed Liskov Substitution

_(X minute read)_
_(2 minute read)_

## Structure

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Detailed Liskov Substitution

_(2 minute read)_

## Structure

| File | Description |
| ----------- | ----------- |
| [`strengthen_preconditions.py`](strengthen_preconditions.py) | Code example containing a pattern and anti-pattern. |
| [`../tests/strengthen_preconditions_test.py`](../tests/strengthen_preconditions_test.py) | Unit tests to show code in action. |

## What are Preconditions?

The preconditions of a method are conditions that must be met in order for a method to
run without crashing. Sometimes they can be obvious, a `.get_user()` method
probably requires that the user exists first, but is not normally the case.

They are often hard to identify as they are not always explicitly specified in the
code. Perhaps a method requires some certain data to exist in a database.

When we talk about 'strengthening' the preconditions, this is referring to the idea that
we are either adding _more_ conditions that need to be met, or making the current
conditions _more specific_. The idea of 'strengthening' them comes from the idea that
code can be coupled, and so what you are 'strengthening' is actually the link between
the code and its preconditions. There is a stronger bond between the two, making it
harder to separate and expand off of.

As you'll see, this breaks the LSP and stops you from being able to interchange a
subclass with its parent class.

## The Example

So take a look at our small example. We have a simple `Parrot` class which can speak
a number:

```python
class Parrot:
def __init__(self, name: str) -> None:
self.name = name

def speak_number(self, number: int) -> str:
return f"Hey it's me, {self.name}! Your number is {number}"

```

And if we look at the tests, we can see that our parrot Peter, can speak back
any number.

The LSP suggests that we should be able to substitute `Parrot` for a subclass and still
be able to speak back numbers. But you can see there is a test that will
fail because our `FussyParrot` Percy will not accept negative numbers.

This is why it can be hard to spot the strengthening of these preconditions. The
signature of the function is still the same:

```python
def speak_number(self, number: int) -> str:
```

Yet the actual permitted values for the `number: int` argument have changed. This is
another example of how easy it can be to break the LSP.

## Conclusion

Although this is a small example it demonstrates just how easy it is to
break the LSP. A good way of finding where you've broken the LSP is to write some
tests which run through the exact same use-cases but are substituting one class for
another, much like we did in this example. You may need to assert for different
behaviour from the methods, but they should not fail when you use the same inputs.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class Parrot:
def __init__(self, name: str) -> None:
self.name = name

def speak_number(self, number: int) -> str:
return f"Hey it's me, {self.name}! Your number is {number}"


class FussyParrot(Parrot):
def speak_number(self, number: int) -> str:
if number < 0:
return "I don't deal with negatives! Give me a real number!"
return f"Hey it's me, {self.name}! Your number is {number}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import pytest

from src.design_principles.solid.liskov.detailed.strengthen_preconditions import (
FussyParrot,
Parrot,
)


def test_normal_parrot_can_repeat_number() -> None:
# given
my_pet = Parrot("Peter")
number = 5

# when
speech = my_pet.speak_number(number)

# then
assert str(number) in speech


def test_normal_parrot_can_repeat_negative_number() -> None:
# given
my_pet = Parrot("Peter")
number = -4

# when
speech = my_pet.speak_number(number)

# then
assert str(number) in speech


def test_fussy_parrot_can_repeat_number() -> None:
# given
my_pet = FussyParrot("Percy")
number = 5

# when
speech = my_pet.speak_number(number)

# then
assert str(number) in speech


@pytest.mark.xfail(reason="This test demonstrates an anti-pattern.")
def test_fussy_parrot_fails_to_repeat_negative_number() -> None:
# given
my_pet = FussyParrot("Percy")
number = -4

# when
speech = my_pet.speak_number(number)

# then
assert str(number) in speech