From 649076cfb9e0e383788a54611e990b7b5b34bc63 Mon Sep 17 00:00:00 2001 From: Jamie McKernan Date: Sun, 27 Aug 2023 20:59:42 +0100 Subject: [PATCH 1/5] Add basic tutorial and tests This is probably sufficient, but will look at it again later to re-read and make sure this is a good example. --- .../detailed/strengthen_preconditions.py | 13 +++++ .../tests/strengthen_preconditions_test.py | 52 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 src/design_principles/solid/liskov/detailed/strengthen_preconditions.py create mode 100644 src/design_principles/solid/liskov/tests/strengthen_preconditions_test.py diff --git a/src/design_principles/solid/liskov/detailed/strengthen_preconditions.py b/src/design_principles/solid/liskov/detailed/strengthen_preconditions.py new file mode 100644 index 0000000..221db64 --- /dev/null +++ b/src/design_principles/solid/liskov/detailed/strengthen_preconditions.py @@ -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}" diff --git a/src/design_principles/solid/liskov/tests/strengthen_preconditions_test.py b/src/design_principles/solid/liskov/tests/strengthen_preconditions_test.py new file mode 100644 index 0000000..1c30859 --- /dev/null +++ b/src/design_principles/solid/liskov/tests/strengthen_preconditions_test.py @@ -0,0 +1,52 @@ +import pytest + +from src.design_principles.solid.liskov.detailed.strengthen_preconditions import Parrot, FussyParrot + + +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 From 54e7d096f74de4b6e3cad4487bbbf1f2cb083175 Mon Sep 17 00:00:00 2001 From: Jamie McKernan Date: Sun, 27 Aug 2023 21:00:15 +0100 Subject: [PATCH 2/5] Run isort --- .../solid/liskov/tests/strengthen_preconditions_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/design_principles/solid/liskov/tests/strengthen_preconditions_test.py b/src/design_principles/solid/liskov/tests/strengthen_preconditions_test.py index 1c30859..d9f03e8 100644 --- a/src/design_principles/solid/liskov/tests/strengthen_preconditions_test.py +++ b/src/design_principles/solid/liskov/tests/strengthen_preconditions_test.py @@ -1,6 +1,9 @@ import pytest -from src.design_principles.solid.liskov.detailed.strengthen_preconditions import Parrot, FussyParrot +from src.design_principles.solid.liskov.detailed.strengthen_preconditions import ( + FussyParrot, + Parrot, +) def test_normal_parrot_can_repeat_number() -> None: From 125e6a6abad613c54ae161357f48188186095450 Mon Sep 17 00:00:00 2001 From: Jamie-McKernan Date: Mon, 28 Aug 2023 10:41:45 +0100 Subject: [PATCH 3/5] Add missing read time --- src/design_principles/solid/liskov/detailed/Exceptions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/design_principles/solid/liskov/detailed/Exceptions.md b/src/design_principles/solid/liskov/detailed/Exceptions.md index 70a2d99..3d22374 100644 --- a/src/design_principles/solid/liskov/detailed/Exceptions.md +++ b/src/design_principles/solid/liskov/detailed/Exceptions.md @@ -1,6 +1,6 @@ # Detailed Liskov Substitution -_(X minute read)_ +_(2 minute read)_ ## Structure From 73736a268d9ef565a09992a65c60ef3933aeace8 Mon Sep 17 00:00:00 2001 From: Jamie-McKernan Date: Mon, 28 Aug 2023 11:00:35 +0100 Subject: [PATCH 4/5] Add introduction --- .../detailed/StrengthenPreconditions.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/design_principles/solid/liskov/detailed/StrengthenPreconditions.md diff --git a/src/design_principles/solid/liskov/detailed/StrengthenPreconditions.md b/src/design_principles/solid/liskov/detailed/StrengthenPreconditions.md new file mode 100644 index 0000000..c45f928 --- /dev/null +++ b/src/design_principles/solid/liskov/detailed/StrengthenPreconditions.md @@ -0,0 +1,28 @@ +# Detailed Liskov Substitution + +_(X 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 +even run without crashing. Sometimes they can be obvious, an `.update_user()` method +probably requires that the user exists first. But sometimes they are not. + +They can often be 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_. + +As you'll see, this breaks the LSP and stops you from being able to interchange a +subclass with its parent class. + +## Conclusion From 2887273a372733ad044a0a56d650b61d12fcab71 Mon Sep 17 00:00:00 2001 From: Jamie McKernan Date: Wed, 20 Sep 2023 13:28:44 +0100 Subject: [PATCH 5/5] Add rest of tutorial --- .../detailed/StrengthenPreconditions.md | 51 +++++++++++++++++-- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/src/design_principles/solid/liskov/detailed/StrengthenPreconditions.md b/src/design_principles/solid/liskov/detailed/StrengthenPreconditions.md index c45f928..ec11d6b 100644 --- a/src/design_principles/solid/liskov/detailed/StrengthenPreconditions.md +++ b/src/design_principles/solid/liskov/detailed/StrengthenPreconditions.md @@ -1,6 +1,6 @@ # Detailed Liskov Substitution -_(X minute read)_ +_(2 minute read)_ ## Structure @@ -12,17 +12,58 @@ _(X minute read)_ ## What are Preconditions? The preconditions of a method are conditions that must be met in order for a method to -even run without crashing. Sometimes they can be obvious, an `.update_user()` method -probably requires that the user exists first. But sometimes they are not. +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 can often be hard to identify as they are not always explicitly specified in the +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_. +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.