Created
February 24, 2022 22:00
-
-
Save francois-durand/15b22673405515dd441f12822ee0b343 to your computer and use it in GitHub Desktop.
solid_principles_in_oop.ipynb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"cells": [ | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "slide" | |
} | |
}, | |
"id": "8c467685", | |
"cell_type": "markdown", | |
"source": "# SOLID Principles in Object-Oriented Programming" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:12.944062Z", | |
"start_time": "2022-02-24T15:34:12.942050Z" | |
}, | |
"slideshow": { | |
"slide_type": "slide" | |
}, | |
"trusted": false | |
}, | |
"id": "0eac1af8", | |
"cell_type": "code", | |
"source": "import time\nfrom collections import defaultdict", | |
"execution_count": 1, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:13.285074Z", | |
"start_time": "2022-02-24T15:34:13.268933Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "1f80f94e", | |
"cell_type": "code", | |
"source": "# The following code hides the traceback when an exception is raised.\nimport sys\nipython = get_ipython()\n\ndef exception_handler(exception_type, exception, traceback):\n print(\"%s: %s\" % (exception_type.__name__, exception), file=sys.stderr)\n\nipython._showtraceback = exception_handler", | |
"execution_count": 2, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "25c98290", | |
"cell_type": "markdown", | |
"source": "### Why this talk?" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "-" | |
} | |
}, | |
"id": "c45202ad", | |
"cell_type": "markdown", | |
"source": "* Understanding tools of OOP is not so complicated (classes, methods, attributes, inheritance...).\n* The real challenge is how to use this tools to design the architecture of a given project." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "7c885b40", | |
"cell_type": "markdown", | |
"source": "### Why write good-quality code?" | |
}, | |
{ | |
"metadata": {}, | |
"id": "45b59d2e", | |
"cell_type": "markdown", | |
"source": "* Easy to **understand** the code: important because code is *more often read than edited*.\n * Improve the communication between developers and/or with users.\n* Easy to **maintain** the code.\n * Reduce \"technical debt\" (disorganized code and massive need of rework).\n* Easy to **extend** the code:\n * Reduce the time needed to implement changes and new features. \n * Minimum changing existing codebase, or not at all (= expand, but not change).\n* Easy to **test** the code, hence improving robustness and confidence in your code." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "80ef5ba8", | |
"cell_type": "markdown", | |
"source": "### SOLID principles" | |
}, | |
{ | |
"metadata": {}, | |
"id": "dc63a437", | |
"cell_type": "markdown", | |
"source": "The principles themselves were gathered by Robert C. Martin in his 2000 paper *Design Principles and Design Patterns* (which I have not read myself, I admit). The SOLID acronym was introduced later, around 2004, by Michael Feathers.\n\n* **S**ingle-Responsibility Principle.\n* **O**pen-Closed Principle.\n* **L**iskov Substitution Principle.\n* **I**nterface Segregation Principle.\n* **D**ependency Inversion Principle.\n\nTo be honest, even if the SOLID acronym is supposed to be a mnemonic device, I don't find it that telling... But the important part is what these principles actually mean!" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "c4843118", | |
"cell_type": "markdown", | |
"source": "### Disclaimers" | |
}, | |
{ | |
"metadata": {}, | |
"id": "bfc3a70d", | |
"cell_type": "markdown", | |
"source": "* Pedagogic exposition requires to take simple examples. Unfortunately, they are often not-so-convincing: for very simple code, even with bad architecture, you will manage to do what to want, maintain or expand your code, etc. Just keep in mind that for real-life code, the type of problems that we will show as resulting from bad architecture will be multiplied.\n* Historically, good coding practices were identified because people noted that when you do not respect them, you are more likely to encounter problems later in your project. As much as possible, I will try to illustrate that. But for simple examples, it is not always that easy...\n" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "0a45ca28", | |
"cell_type": "markdown", | |
"source": "* My goal is to to make understand what the principles mean in practice, not to define them precisely and theoretically (I would not be able to do it anyway). My apologies if the wording is sometimes not the most rigorous possible.\n* Code architecture results from a series of choices. Some are quite clearly good or bad, others are more debatable... Maybe we will not agree on all choices. But the interesting part is to discuss them and improve our general understanding of the consequences of these choices, in order to make more educated decisions.\n* Especially for exploratory projects like research-oriented code, even with reasonable initial architecture choices, there may be a moment when you will need to rewrite / refactor your whole codebase. But at least, good architecture should make these time-consuming moments less frequent (and, of course, ideally avoid them)." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "bb783dce", | |
"cell_type": "markdown", | |
"source": "### Material used for this presentation" | |
}, | |
{ | |
"metadata": {}, | |
"id": "7136e624", | |
"cell_type": "markdown", | |
"source": "Sources (and in parentheses, their examples I like the most):" | |
}, | |
{ | |
"metadata": {}, | |
"id": "6dd1e836", | |
"cell_type": "markdown", | |
"source": "* https://en.wikipedia.org/wiki/SOLID\n* https://towardsdatascience.com/solid-coding-in-python-1281392a6a94 (S, O, I)\n* https://www.linkedin.com/pulse/solid-design-principles-python-examples-hiral-amodia/ (O, I, D)\n* https://www.slideshare.net/DrTrucho/python-solid (S, O, D)" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "71c1ab6a", | |
"cell_type": "markdown", | |
"source": "* https://gist.github.com/dmmeteo/f630fa04c7a79d3c132b9e9e5d037bfd (O, D)\n* https://medium.com/@vubon.roy/solid-principles-with-python-examples-10e1f3d91259 (O, D)\n* https://www.hashbangcode.com/article/solid-principles-python (S, L)\n* https://stackoverflow.com/questions/56860/what-is-an-example-of-the-liskov-substitution-principle (L)\n* https://stackify.com/solid-design-liskov-substitution-principle/ (L)" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "slide" | |
} | |
}, | |
"id": "7a8d6335", | |
"cell_type": "markdown", | |
"source": "## S: Single-Responsibility Principle" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "-" | |
} | |
}, | |
"id": "cc16c824", | |
"cell_type": "markdown", | |
"source": "Two main wordings:\n\n* \"Every class (or function) should have only one responsibility.\"\n* \"A class should have **only one reason to change**.\"\n\nA more developed wording:\n\n\"When designing our classes, we should aim to put related features together, so whenever they tend to change they change for **the same reason**. And we should try to separate features if they will change for **different reasons**.\" (Steve Fenton)" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "27f27cb1", | |
"cell_type": "markdown", | |
"source": "Why?\n\n* Easier to reuse any part of the code in another part of the project.\n* Easier to localize errors.\n* Easier to create testing for each part of the code." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "ae70aab4", | |
"cell_type": "markdown", | |
"source": "Consider animals with a name and a method to \"dump\" them to json format:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:20.696169Z", | |
"start_time": "2022-02-24T15:34:20.684228Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "a6a9f9da", | |
"cell_type": "code", | |
"source": "class Animal:\n def __init__(self, name):\n self.name = name\n def json_dumps(self):\n return json.dumps({'name': self.name})", | |
"execution_count": 3, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:20.992434Z", | |
"start_time": "2022-02-24T15:34:20.981425Z" | |
}, | |
"trusted": false | |
}, | |
"id": "ae86d86d", | |
"cell_type": "code", | |
"source": "def save_to_files(animals: list[Animal]):\n for i, animal in enumerate(animals):\n print(f\"With file {i} opened...\")\n print(f\"Write {animal.json_dumps()}\")", | |
"execution_count": 4, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:21.304099Z", | |
"start_time": "2022-02-24T15:34:21.287099Z" | |
}, | |
"trusted": false | |
}, | |
"id": "7117b15c", | |
"cell_type": "code", | |
"source": "save_to_files([Animal(\"Dumbo\"), Animal(\"Simba\")])", | |
"execution_count": 5, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "With file 0 opened...\nWrite {\"name\": \"Dumbo\"}\nWith file 1 opened...\nWrite {\"name\": \"Simba\"}\n" | |
} | |
] | |
}, | |
{ | |
"metadata": {}, | |
"id": "6a8b67ab", | |
"cell_type": "markdown", | |
"source": "Do you see problems? With which consequences?" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "2268b3c3", | |
"cell_type": "markdown", | |
"source": "Problem: imagine that we want to add an html dump." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:23.003622Z", | |
"start_time": "2022-02-24T15:34:22.998688Z" | |
}, | |
"trusted": false | |
}, | |
"id": "7174483d", | |
"cell_type": "code", | |
"source": "class Animal:\n def __init__(self, name):\n self.name = name\n def json_dumps(self):\n return json.dumps({'name': self.name})\n def html_dumps(self):\n return f\"<p>Name: {self.name}.</p>\"", | |
"execution_count": 6, | |
"outputs": [] | |
}, | |
{ | |
"metadata": {}, | |
"id": "354ca4b4", | |
"cell_type": "markdown", | |
"source": "What should we do with ``save_to_files``?" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "ea6a74d6", | |
"cell_type": "markdown", | |
"source": "A possible (ugly) solution:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:24.798365Z", | |
"start_time": "2022-02-24T15:34:24.789365Z" | |
}, | |
"trusted": false | |
}, | |
"id": "3bd5b978", | |
"cell_type": "code", | |
"source": "def save_to_files(animals: list[Animal], method='json_dumps'):\n for i, animal in enumerate(animals):\n print(f\"With file {i} opened...\")\n dump_method = getattr(animal, method)\n print(f\"Write {dump_method()}\")", | |
"execution_count": 7, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:25.201684Z", | |
"start_time": "2022-02-24T15:34:25.184687Z" | |
}, | |
"trusted": false | |
}, | |
"id": "cb3a6111", | |
"cell_type": "code", | |
"source": "save_to_files([Animal(\"Dumbo\"), Animal(\"Simba\")])", | |
"execution_count": 8, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "With file 0 opened...\nWrite {\"name\": \"Dumbo\"}\nWith file 1 opened...\nWrite {\"name\": \"Simba\"}\n" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:25.760260Z", | |
"start_time": "2022-02-24T15:34:25.751266Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "ef7b988a", | |
"cell_type": "code", | |
"source": "save_to_files([Animal(\"Dumbo\"), Animal(\"Simba\")], method='html_dumps')", | |
"execution_count": 9, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "With file 0 opened...\nWrite <p>Name: Dumbo.</p>\nWith file 1 opened...\nWrite <p>Name: Simba.</p>\n" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "5bf43427", | |
"cell_type": "markdown", | |
"source": "Several drawbacks:\n\n* We lose auto-completion (prone to typos).\n* Makes the task more difficult for refactoring tools.\n* Each time we add a dump method, we need to edit the code of ``Animal``.\n* If a dump method has some additional arguments, how to incorporate them in ``save_to_files``?" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "b1cfa9ca", | |
"cell_type": "markdown", | |
"source": "To avoid these drawbacks, we will apply the single-responsability principle: a separate class ``AnimalDumper`` will be dedicated to dumping the animal as a string." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:33.389782Z", | |
"start_time": "2022-02-24T15:34:33.387781Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "a48d60e3", | |
"cell_type": "code", | |
"source": "class Animal:\n def __init__(self, name):\n self.name = name\n @property\n def characteristic_data(self):\n return vars(self)", | |
"execution_count": 10, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:33.997264Z", | |
"start_time": "2022-02-24T15:34:33.985269Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "e0005627", | |
"cell_type": "code", | |
"source": "def key_value_to_html(k, v, with_div=False):\n s = f\"<p>{k}: {v}.</p>\"\n return \"<div>\" + s + \"</div>\" if with_div else s", | |
"execution_count": 11, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:34.369722Z", | |
"start_time": "2022-02-24T15:34:34.365684Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "d4c3e781", | |
"cell_type": "code", | |
"source": "class AnimalDumper:\n def dumps(self, animal):\n raise NotImplementedError\nclass AnimalDumperJson(AnimalDumper):\n def dumps(self, animal):\n return json.dumps(animal.characteristic_data)\nclass AnimalDumperHtml(AnimalDumper):\n def __init__(self, with_div=False):\n self.with_div = with_div\n def dumps(self, animal):\n return \"\\n\".join([key_value_to_html(k, v, self.with_div)\n for k, v in animal.characteristic_data.items()])", | |
"execution_count": 12, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "32c750fc", | |
"cell_type": "markdown", | |
"source": "Now, ``save_to_files`` can be written in a clean way:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:47.276523Z", | |
"start_time": "2022-02-24T15:34:47.264523Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "2a211d01", | |
"cell_type": "code", | |
"source": "def save_to_files(animals: list[Animal], animal_dumper: AnimalDumper):\n for i, animal in enumerate(animals):\n print(f\"With file {i} opened...\")\n print(f\"Write {animal_dumper.dumps(animal)}\")", | |
"execution_count": 13, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:47.693901Z", | |
"start_time": "2022-02-24T15:34:47.685902Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "70b07bb1", | |
"cell_type": "code", | |
"source": "save_to_files([Animal(\"Dumbo\"), Animal(\"Simba\")],\n animal_dumper=AnimalDumperJson())", | |
"execution_count": 14, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "With file 0 opened...\nWrite {\"name\": \"Dumbo\"}\nWith file 1 opened...\nWrite {\"name\": \"Simba\"}\n" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:48.206373Z", | |
"start_time": "2022-02-24T15:34:48.193364Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "d6f9d6ac", | |
"cell_type": "code", | |
"source": "save_to_files([Animal(\"Dumbo\"), Animal(\"Simba\")],\n animal_dumper=AnimalDumperHtml(with_div=True))", | |
"execution_count": 15, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "With file 0 opened...\nWrite <div><p>name: Dumbo.</p></div>\nWith file 1 opened...\nWrite <div><p>name: Simba.</p></div>\n" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "b191883c", | |
"cell_type": "markdown", | |
"source": "Remark: in research-oriented code, you may know (or think) from the start that you will always use the same method (here a json dump), and always with the same options... Since you want to write code quickly and simply, the very first version of our code is actually fine:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:51.294251Z", | |
"start_time": "2022-02-24T15:34:51.286310Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "abf91df0", | |
"cell_type": "code", | |
"source": "class Animal:\n def __init__(self, name):\n self.name = name\n def json_dumps(self):\n return json.dumps({'name': self.name})", | |
"execution_count": 16, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "-" | |
} | |
}, | |
"id": "f94957ac", | |
"cell_type": "markdown", | |
"source": "If, later in your project, you need other variants (here, other kinds of dumps), it will be soon enough to apply the single responsability principle and write a dedicated class for this action (here, dumping).\n\nThis is an application of another programming principle: **\"You aren't gonna need it\" (YAGNI)**. In other words: do not use complicated solutions just to be ready for hypothetical use cases that you may never need. This is especially true for us researchers, because our code often has a short life expectancy." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "slide" | |
} | |
}, | |
"id": "5ef90b44", | |
"cell_type": "markdown", | |
"source": "## O: Open-Closed Principle" | |
}, | |
{ | |
"metadata": {}, | |
"id": "a0647003", | |
"cell_type": "markdown", | |
"source": "\"Software entities ... should be open for extension, but closed for modification.\"\n\nIn other words, you should design your code so that you will be able to:\n\n* **Modify** existing code only when you want to **modify** what it does (= change the initial premises or fix bugs),\n* **Extend** your code (= write new code, functions, classes) when you want to **extend** what it does (= add new functionalities)." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "ec7a4548", | |
"cell_type": "markdown", | |
"source": "Consider animals and the sounds they make:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:54.702409Z", | |
"start_time": "2022-02-24T15:34:54.690397Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "31fc92ab", | |
"cell_type": "code", | |
"source": "class Animal:\n def __init__(self, species):\n self.species=species\ndef animal_sounds(animals: list[Animal]):\n for animal in animals:\n if animal.species == 'lion':\n print('roar')\n elif animal.species == 'elephant':\n print('trumpet')", | |
"execution_count": 17, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:34:55.043673Z", | |
"start_time": "2022-02-24T15:34:55.029677Z" | |
}, | |
"trusted": false | |
}, | |
"id": "d5e5abd0", | |
"cell_type": "code", | |
"source": "animal_sounds([Animal('lion'), Animal('elephant')])", | |
"execution_count": 18, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "roar\ntrumpet\n" | |
} | |
] | |
}, | |
{ | |
"metadata": {}, | |
"id": "9a9d579b", | |
"cell_type": "markdown", | |
"source": "Do you see the problem? Which consequences later?" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "176bb665", | |
"cell_type": "markdown", | |
"source": "Problem: if I want to **extend** my project to add the \"snake\" feature, I will need to **modify** the code of ``animal_sounds``. If there are many functions with such conditional code, I will need to perform a \"treasure hunt\" to find all such cases (and I will probably forget some of them)." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "69323571", | |
"cell_type": "markdown", | |
"source": "Solution:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:00.964883Z", | |
"start_time": "2022-02-24T15:35:00.951924Z" | |
}, | |
"trusted": false | |
}, | |
"id": "63873fd2", | |
"cell_type": "code", | |
"source": "class Animal:\n def make_sound(self):\n raise NotImplementedError\nclass Lion(Animal):\n def make_sound(self):\n return 'roar'\nclass Elephant(Animal):\n def make_sound(self):\n return 'trumpet'\nclass Snake(Animal):\n def make_sound(self):\n return 'hiss'", | |
"execution_count": 19, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:01.169248Z", | |
"start_time": "2022-02-24T15:35:01.162238Z" | |
}, | |
"trusted": false | |
}, | |
"id": "ccc7afbd", | |
"cell_type": "code", | |
"source": "def animal_sounds(animals: list[Animal]):\n for animal in animals:\n print(animal.make_sound())", | |
"execution_count": 20, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:01.357119Z", | |
"start_time": "2022-02-24T15:35:01.341112Z" | |
}, | |
"trusted": false | |
}, | |
"id": "b6622b97", | |
"cell_type": "code", | |
"source": "animal_sounds([Lion(), Elephant(), Snake()])", | |
"execution_count": 21, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "roar\ntrumpet\nhiss\n" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "64f7169a", | |
"cell_type": "markdown", | |
"source": "Comments on the solution:\n\n* If I want to **extend** the project with a dog that barks, then I will **extend** the code by adding another subclass of ``Animal``.\n* If I want to **modify** the snake so that it rattles (instead of hissing), then I will **modify** the code of ``Snake``.\n\nThis is compliant with the Open-Closed Principle." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "slide" | |
} | |
}, | |
"id": "567e38c0", | |
"cell_type": "markdown", | |
"source": "## L: Liskov Substitution Principle" | |
}, | |
{ | |
"metadata": {}, | |
"id": "4e6362b5", | |
"cell_type": "markdown", | |
"source": "\"Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.\"\n\nWait... what is this supposed to mean?\n\nIn any place where you can use a object of a **parent** class, the code must still work if you use an object of a **subclass**.\n\nIntroduced by Barbara Liskov in her conference keynote “Data Abstraction” in 1987." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "c8ac297a", | |
"cell_type": "markdown", | |
"source": "This principle has the following consequences (and others which I will not cover):\n\n* The method of a subclass can be **more tolerant on its input** that the parent class, but not less tolerant.\n* The method of a subclass can be **more restrictive on its output** that the parent class, but not allow new kinds of outputs.\n* (More subtle:) A \"provable property\" on the parent class should still be true on the subclass." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "c49c935d", | |
"cell_type": "markdown", | |
"source": "### LSP: Consequence on the Inputs " | |
}, | |
{ | |
"metadata": {}, | |
"id": "8c9011ea", | |
"cell_type": "markdown", | |
"source": "Let us play hide and seek." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:05.463399Z", | |
"start_time": "2022-02-24T15:35:05.447390Z" | |
}, | |
"trusted": false | |
}, | |
"id": "79e757b6", | |
"cell_type": "code", | |
"source": "class Player:\n def count(self, n):\n print(f\"Counting up to {n}...\")\n def search(self):\n print(\"Searching for other hidden players.\")\nclass PlayerBaby(Player):\n def count(self, n):\n if n > 5:\n raise ValueError(\"Cannot count up to more than 5.\")\n print(f\"Counting up to {n}...\")", | |
"execution_count": 22, | |
"outputs": [] | |
}, | |
{ | |
"metadata": {}, | |
"id": "69dc4a4e", | |
"cell_type": "markdown", | |
"source": "Can you see the problem? Future consequences?" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:07.578586Z", | |
"start_time": "2022-02-24T15:35:07.562586Z" | |
}, | |
"slideshow": { | |
"slide_type": "subslide" | |
}, | |
"trusted": false | |
}, | |
"id": "bdd978c9", | |
"cell_type": "code", | |
"source": "def play_hide_and_seek(player: Player):\n player.count(100)\n player.search()", | |
"execution_count": 23, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:07.905826Z", | |
"start_time": "2022-02-24T15:35:07.896883Z" | |
}, | |
"trusted": false | |
}, | |
"id": "c73ccbbb", | |
"cell_type": "code", | |
"source": "play_hide_and_seek(Player())", | |
"execution_count": 24, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "Counting up to 100...\nSearching for other hidden players.\n" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:08.281130Z", | |
"start_time": "2022-02-24T15:35:08.192131Z" | |
}, | |
"trusted": false | |
}, | |
"id": "0eda4d42", | |
"cell_type": "code", | |
"source": "play_hide_and_seek(PlayerBaby())", | |
"execution_count": 25, | |
"outputs": [ | |
{ | |
"name": "stderr", | |
"output_type": "stream", | |
"text": "ValueError: Cannot count up to more than 5.\n" | |
} | |
] | |
}, | |
{ | |
"metadata": {}, | |
"id": "2507bcf3", | |
"cell_type": "markdown", | |
"source": "The possible inputs are more limited in ``PlayerBaby`` than in parent class ``Player``: this violates the LSP." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "613acbe2", | |
"cell_type": "markdown", | |
"source": "A possible solution:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:10.854289Z", | |
"start_time": "2022-02-24T15:35:10.836450Z" | |
}, | |
"trusted": false | |
}, | |
"id": "8a745c33", | |
"cell_type": "code", | |
"source": "class PlayerKnowingAtLeastFiveNumbers:\n def count(self, n):\n if n > 5:\n raise ValueError(\"Cannot count up to more than 5.\")\n print(f\"Counting up to {n}...\")\n def search(self):\n print(\"Searching for other hidden players.\")\nclass PlayerAdult(PlayerKnowingAtLeastFiveNumbers):\n def count(self, n):\n print(f\"Counting up to {n}...\")\nclass PlayerBaby(PlayerKnowingAtLeastFiveNumbers):\n def count(self, n):\n vocabulary = ['ann', 'doo', 'sree', 'fow', 'fie']\n print('... '.join([vocabulary[i] for i in range(n)]))", | |
"execution_count": 27, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:11.321055Z", | |
"start_time": "2022-02-24T15:35:11.306114Z" | |
}, | |
"trusted": false | |
}, | |
"id": "f2a84898", | |
"cell_type": "code", | |
"source": "def play_hide_and_seek(player: PlayerAdult):\n player.count(100)\n player.search()", | |
"execution_count": 28, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:11.785619Z", | |
"start_time": "2022-02-24T15:35:11.781623Z" | |
}, | |
"trusted": false | |
}, | |
"id": "e56f409f", | |
"cell_type": "code", | |
"source": "play_hide_and_seek(PlayerAdult())", | |
"execution_count": 29, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "Counting up to 100...\nSearching for other hidden players.\n" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "26a7315c", | |
"cell_type": "markdown", | |
"source": "Now we can also define a \"hide and seek junior\" mostly for babies, but working also for adults (and, in fact, for any player knowing how to count up to five):" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:13.438882Z", | |
"start_time": "2022-02-24T15:35:13.430883Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "f5781546", | |
"cell_type": "code", | |
"source": "def play_hide_and_seek_junior(player: PlayerKnowingAtLeastFiveNumbers):\n player.count(5)\n player.search()", | |
"execution_count": 30, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:13.766286Z", | |
"start_time": "2022-02-24T15:35:13.756296Z" | |
}, | |
"trusted": false | |
}, | |
"id": "e6555ab4", | |
"cell_type": "code", | |
"source": "play_hide_and_seek_junior(PlayerBaby())", | |
"execution_count": 31, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "ann... doo... sree... fow... fie\nSearching for other hidden players.\n" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:14.185358Z", | |
"start_time": "2022-02-24T15:35:14.176346Z" | |
}, | |
"trusted": false | |
}, | |
"id": "a385cbee", | |
"cell_type": "code", | |
"source": "play_hide_and_seek_junior(PlayerAdult())", | |
"execution_count": 32, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "Counting up to 5...\nSearching for other hidden players.\n" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "01fc9f18", | |
"cell_type": "markdown", | |
"source": "### LSP: Consequence on the Outputs " | |
}, | |
{ | |
"metadata": {}, | |
"id": "c1d6d1de", | |
"cell_type": "markdown", | |
"source": "Consider mammals that give birth and make sounds:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:16.317032Z", | |
"start_time": "2022-02-24T15:35:16.312022Z" | |
}, | |
"trusted": false | |
}, | |
"id": "ed24d1a5", | |
"cell_type": "code", | |
"source": "class Mammal:\n def give_birth(self):\n \"\"\"Return newborn and time of birth.\"\"\"\n return (self.__class__(), time.time())\n def make_sound(self):\n print('generic sound')\nclass Lion(Mammal):\n def make_sound(self):\n print('roar')\nclass Platypus(Mammal):\n def give_birth(self):\n \"\"\"Return newborn, time of birth (= hatching), time of laying egg.\"\"\"\n return (self.__class__(), time.time(), time.time() - 1)\n def make_sound(self):\n print('growl')", | |
"execution_count": 33, | |
"outputs": [] | |
}, | |
{ | |
"metadata": {}, | |
"id": "7777a6f6", | |
"cell_type": "markdown", | |
"source": "For the sake of simplicity, we assume here that each Mammal, male or female, can give birth, and to exactly one newborn. Apart from that, can you see the problem?" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:21.329677Z", | |
"start_time": "2022-02-24T15:35:21.321743Z" | |
}, | |
"slideshow": { | |
"slide_type": "subslide" | |
}, | |
"trusted": false | |
}, | |
"id": "f6efd522", | |
"cell_type": "code", | |
"source": "def listen_to_newborns(mammals: list[Mammal]):\n for mammal in mammals:\n offspring, _ = mammal.give_birth()\n offspring.make_sound() ", | |
"execution_count": 35, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:21.672419Z", | |
"start_time": "2022-02-24T15:35:21.659433Z" | |
}, | |
"trusted": false | |
}, | |
"id": "6d6fc8f5", | |
"cell_type": "code", | |
"source": "listen_to_newborns([Mammal(), Lion()])", | |
"execution_count": 36, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "generic sound\nroar\n" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:22.218589Z", | |
"start_time": "2022-02-24T15:35:22.198589Z" | |
}, | |
"scrolled": true, | |
"trusted": false | |
}, | |
"id": "bac2cb19", | |
"cell_type": "code", | |
"source": "listen_to_newborns([Mammal(), Lion(), Platypus()])", | |
"execution_count": 37, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "generic sound\nroar\n" | |
}, | |
{ | |
"name": "stderr", | |
"output_type": "stream", | |
"text": "ValueError: too many values to unpack (expected 2)\n" | |
} | |
] | |
}, | |
{ | |
"metadata": {}, | |
"id": "4d3614e3", | |
"cell_type": "markdown", | |
"source": "The return type of ``Platypus.give_birth`` is a 3-tuple, which is not consistent with ``Animal.give_birth`` returning a 2-tuple!" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "ec236cc2", | |
"cell_type": "markdown", | |
"source": "A possible solution:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:24.608342Z", | |
"start_time": "2022-02-24T15:35:24.600341Z" | |
}, | |
"trusted": false | |
}, | |
"id": "dc45d65a", | |
"cell_type": "code", | |
"source": "class Mammal:\n def give_birth(self):\n \"\"\"Return newborn and time of birth.\"\"\"\n additional_info = {'birth_time': time.time()}\n return (self.__class__(), additional_info)\n def make_sound(self):\n print('generic animal sound')\nclass Lion(Mammal):\n def make_sound(self):\n print('roar')\nclass Platypus(Mammal):\n def give_birth(self):\n \"\"\"Return newborn, time of birth (= hatching), time of laying egg.\"\"\"\n additional_info = {'birth_time': time.time(), 'lay_time': time.time() - 1}\n return (self.__class__(), additional_info)\n def make_sound(self):\n print('growl')", | |
"execution_count": 38, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:25.759139Z", | |
"start_time": "2022-02-24T15:35:25.751141Z" | |
}, | |
"trusted": false | |
}, | |
"id": "43f8f4d0", | |
"cell_type": "code", | |
"source": "listen_to_newborns([Mammal(), Lion(), Platypus()])", | |
"execution_count": 39, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "generic animal sound\nroar\ngrowl\n" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "12ce8fa3", | |
"cell_type": "markdown", | |
"source": "Gathering our observations about inputs and outputs, note that if $\\text{ParentClass.method}: E \\to F$ and $\\text{SubClass.method}: E' \\to F'$, then we must have:\n\n* $E' \\supseteq E$: any input accepted by the parent should be accepted by the subclass.\n* $F' \\subseteq F$: an output value of the subclass should not surprise (and make fail) someone who expects an output value from the parent." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "bd2015ec", | |
"cell_type": "markdown", | |
"source": "### LSP: Consequence on Provable Properties" | |
}, | |
{ | |
"metadata": {}, | |
"id": "4c9abebc", | |
"cell_type": "markdown", | |
"source": "We will consider rectangles and squares..." | |
}, | |
{ | |
"metadata": {}, | |
"id": "a23c2e72", | |
"cell_type": "markdown", | |
"source": "Oddly enough, this example is given in many places to illustrate LSP, but it is far from being the simplest and, in my opinion, the most common violation of the LSP! I find the consequences on inputs and outputs more important and more often violated (at least by beginners)." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "slide" | |
} | |
}, | |
"id": "3e5473ed", | |
"cell_type": "markdown", | |
"source": "First implementation of ``Rectangle``: " | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:30.830366Z", | |
"start_time": "2022-02-24T15:35:30.812716Z" | |
}, | |
"trusted": false | |
}, | |
"id": "0c881041", | |
"cell_type": "code", | |
"source": "class Rectangle:\n def __init__(self, width, height):\n self.width = width\n self.height = height\n def area(self):\n return self.width * self.height", | |
"execution_count": 40, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "161cb692", | |
"cell_type": "markdown", | |
"source": "But actually, we will use this one, and you will see why soon:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:32.300392Z", | |
"start_time": "2022-02-24T15:35:32.292409Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "22f0b858", | |
"cell_type": "code", | |
"source": "class Rectangle:\n def __init__(self, width, height):\n self._width = width\n self._height = height\n @property\n def width(self):\n return self._width\n @width.setter\n def width(self, value):\n self._width = value\n @property\n def height(self):\n return self._height\n @height.setter\n def height(self, value):\n self._height = value\n def area(self):\n return self.width * self.height", | |
"execution_count": 41, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "91aafb53", | |
"cell_type": "markdown", | |
"source": "Squares are a subset of rectangles, so it seems like ``Square`` should be a subclass of ``Rectangle``. Here we go:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:33.847186Z", | |
"start_time": "2022-02-24T15:35:33.832340Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "ebfb6c19", | |
"cell_type": "code", | |
"source": "class Square(Rectangle):\n def __init__(self, length):\n super().__init__(length, length)\n @property\n def width(self):\n return self._width\n @width.setter\n def width(self, value):\n self._width = value\n self._height = value\n @property\n def height(self):\n return self._height\n @height.setter\n def height(self, value):\n self._width = value\n self._height = value", | |
"execution_count": 42, | |
"outputs": [] | |
}, | |
{ | |
"metadata": {}, | |
"id": "8ca1416b", | |
"cell_type": "markdown", | |
"source": "Do you see the problem?" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "65fa83f3", | |
"cell_type": "markdown", | |
"source": "Let us define a function that modifies the width of a rectangle, so that its final area is equal to 1:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:35.998163Z", | |
"start_time": "2022-02-24T15:35:35.994163Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "ae0edaac", | |
"cell_type": "code", | |
"source": "def modify_width_to_normalize_area(rectangle: Rectangle):\n rectangle.width = 1 / rectangle.height", | |
"execution_count": 43, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:36.387055Z", | |
"start_time": "2022-02-24T15:35:36.368064Z" | |
}, | |
"trusted": false | |
}, | |
"id": "2c3b0298", | |
"cell_type": "code", | |
"source": "rectangle = Rectangle(width=2, height=10)\nmodify_width_to_normalize_area(rectangle)\nrectangle.area()", | |
"execution_count": 44, | |
"outputs": [ | |
{ | |
"data": { | |
"text/plain": "1.0" | |
}, | |
"execution_count": 44, | |
"metadata": {}, | |
"output_type": "execute_result" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:36.683194Z", | |
"start_time": "2022-02-24T15:35:36.677206Z" | |
}, | |
"trusted": false | |
}, | |
"id": "f9059a24", | |
"cell_type": "code", | |
"source": "square = Square(length=2)\nmodify_width_to_normalize_area(square)\nsquare.area()", | |
"execution_count": 45, | |
"outputs": [ | |
{ | |
"data": { | |
"text/plain": "0.25" | |
}, | |
"execution_count": 45, | |
"metadata": {}, | |
"output_type": "execute_result" | |
} | |
] | |
}, | |
{ | |
"metadata": {}, | |
"id": "fa5e68e4", | |
"cell_type": "markdown", | |
"source": "One way of seing the problem: actually here, the class ``Rectangle`` does not model rectangles, but \"reshapable rectangles\" (where width and height can be modified independently). And a square is obviously not a reshapable rectangle in that sense." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "3519a996", | |
"cell_type": "markdown", | |
"source": "Solution?\n\n* In that specific case, the simplest is probably to make the rectangles and squares immutable.\n* If we want to model \"reshapable rectangles\", then we could do only one class ``Rectangle`` and implement ``is_square``, a method testing whether the rectangle happens to be a square (at the time being).\n* Instead of having ``Square`` a subclass of ``Rectangle``, a common interface or superclass could also work, for example ``Shape`` or ``Quadrilateral``." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "6a43fac3", | |
"cell_type": "markdown", | |
"source": "Let us go with immutability:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:40.147986Z", | |
"start_time": "2022-02-24T15:35:40.139002Z" | |
}, | |
"trusted": false | |
}, | |
"id": "d92b9779", | |
"cell_type": "code", | |
"source": "class Rectangle:\n def __init__(self, width, height):\n self._width = width\n self._height = height\n @property\n def width(self):\n return self._width\n @property\n def height(self):\n return self._height\n def __repr__(self):\n return f\"{self.__class__.__name__}(width={self.width}, height={self.height})\"\n def area(self):\n return self.width * self.height", | |
"execution_count": 46, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:40.534622Z", | |
"start_time": "2022-02-24T15:35:40.520548Z" | |
}, | |
"trusted": false | |
}, | |
"id": "43315850", | |
"cell_type": "code", | |
"source": "class Square(Rectangle):\n def __init__(self, length):\n super().__init__(length, length)\n def __repr__(self):\n return f\"{self.__class__.__name__}(length={self.length})\"", | |
"execution_count": 47, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "3ad146bc", | |
"cell_type": "markdown", | |
"source": "If we want to normalize area, we cannot modify the rectangle in place anymore, so we need to return a new ``Rectangle`` object:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:41.757643Z", | |
"start_time": "2022-02-24T15:35:41.750648Z" | |
}, | |
"trusted": false | |
}, | |
"id": "c767e91e", | |
"cell_type": "code", | |
"source": "def rectangle_with_modified_width_to_normalize_area(rectangle: Rectangle):\n return Rectangle(width = 1 / rectangle.height, height=rectangle.height)", | |
"execution_count": 48, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:42.083148Z", | |
"start_time": "2022-02-24T15:35:42.069418Z" | |
}, | |
"trusted": false | |
}, | |
"id": "fb669c26", | |
"cell_type": "code", | |
"source": "rectangle = Rectangle(width=2, height=10)\nrectangle_with_modified_width_to_normalize_area(rectangle)", | |
"execution_count": 49, | |
"outputs": [ | |
{ | |
"data": { | |
"text/plain": "Rectangle(width=0.1, height=10)" | |
}, | |
"execution_count": 49, | |
"metadata": {}, | |
"output_type": "execute_result" | |
} | |
] | |
}, | |
{ | |
"metadata": {}, | |
"id": "17743a4c", | |
"cell_type": "markdown", | |
"source": "Which leads to a consistent behavior when applied to a square:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:42.859186Z", | |
"start_time": "2022-02-24T15:35:42.844191Z" | |
}, | |
"trusted": false | |
}, | |
"id": "3d8ac383", | |
"cell_type": "code", | |
"source": "square = Square(length=2)\nrectangle_with_modified_width_to_normalize_area(square)", | |
"execution_count": 50, | |
"outputs": [ | |
{ | |
"data": { | |
"text/plain": "Rectangle(width=0.5, height=2)" | |
}, | |
"execution_count": 50, | |
"metadata": {}, | |
"output_type": "execute_result" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "slide" | |
} | |
}, | |
"id": "2ff1e848", | |
"cell_type": "markdown", | |
"source": "## I: Interface Segregation Principle" | |
}, | |
{ | |
"metadata": {}, | |
"id": "80dde0cb", | |
"cell_type": "markdown", | |
"source": "\"Many client-specific interfaces are better than one general-purpose interface.\"\n\nAn important consequence is: \"A class should not be forced to implement an interface that it does not use”." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "c4a3f11b", | |
"cell_type": "markdown", | |
"source": "Let us implement a smartphone, which is a particular case of communication device:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:45.301513Z", | |
"start_time": "2022-02-24T15:35:45.283226Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "e9a7973b", | |
"cell_type": "code", | |
"source": "class CommunicationDevice:\n def make_call(self):\n print(\"Default implementation of making a call.\")\n def send_sms(self):\n print(\"Default implementation of sending an sms.\")\n def browse_internet(self):\n print(\"Default implementation of browsing the internet.\")\nclass SmartPhone(CommunicationDevice):\n def make_call(self):\n print(\"SmartPhone makes a call.\")\n def send_sms(self):\n print(\"SmartPhone sends an sms.\")\n def browse_internet(self):\n print(\"SmartPhone browses the internet.\")", | |
"execution_count": 51, | |
"outputs": [] | |
}, | |
{ | |
"metadata": {}, | |
"id": "c7358667", | |
"cell_type": "markdown", | |
"source": "Can you spot the problem? What could go wrong in the future?" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "0a7fd84a", | |
"cell_type": "markdown", | |
"source": "Imagine that we implement a landline phone:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:46.433423Z", | |
"start_time": "2022-02-24T15:35:46.427421Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "8b22263f", | |
"cell_type": "code", | |
"source": "class LandLinePhone(CommunicationDevice):\n def make_call(self):\n print(\"LandLinePhone makes a call.\")", | |
"execution_count": 52, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:46.778841Z", | |
"start_time": "2022-02-24T15:35:46.774836Z" | |
}, | |
"trusted": false | |
}, | |
"id": "2b78303e", | |
"cell_type": "code", | |
"source": "def each_device_browses_internet(communication_devices: list[CommunicationDevice]):\n for communication_device in communication_devices:\n communication_device.browse_internet()", | |
"execution_count": 53, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:47.278696Z", | |
"start_time": "2022-02-24T15:35:47.272695Z" | |
}, | |
"trusted": false | |
}, | |
"id": "731b8ce5", | |
"cell_type": "code", | |
"source": "each_device_browses_internet([SmartPhone()])", | |
"execution_count": 54, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "SmartPhone browses the internet.\n" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:48.041682Z", | |
"start_time": "2022-02-24T15:35:48.038667Z" | |
}, | |
"trusted": false | |
}, | |
"id": "2d5356e2", | |
"cell_type": "code", | |
"source": "each_device_browses_internet([SmartPhone(), LandLinePhone()])", | |
"execution_count": 55, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "SmartPhone browses the internet.\nDefault implementation of browsing the internet.\n" | |
} | |
] | |
}, | |
{ | |
"metadata": {}, | |
"id": "69854632", | |
"cell_type": "markdown", | |
"source": "Problem: our landline phone browses the internet! This is especially awful because it leads to a \"silent bug\" which can be very difficult to detect." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "bf437f55", | |
"cell_type": "markdown", | |
"source": "A first idea of solution:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:50.559931Z", | |
"start_time": "2022-02-24T15:35:50.547931Z" | |
}, | |
"trusted": false | |
}, | |
"id": "8b891498", | |
"cell_type": "code", | |
"source": "class LandLinePhone(CommunicationDevice):\n def make_call(self):\n print(\"LandLinePhone makes a call.\")\n def browse_internet(self):\n raise NotImplementedError", | |
"execution_count": 56, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:50.875331Z", | |
"start_time": "2022-02-24T15:35:50.861330Z" | |
}, | |
"trusted": false | |
}, | |
"id": "657bacdd", | |
"cell_type": "code", | |
"source": "each_device_browses_internet([SmartPhone(), LandLinePhone()])", | |
"execution_count": 57, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "SmartPhone browses the internet.\n" | |
}, | |
{ | |
"name": "stderr", | |
"output_type": "stream", | |
"text": "NotImplementedError: \n" | |
} | |
] | |
}, | |
{ | |
"metadata": {}, | |
"id": "f5a3c731", | |
"cell_type": "markdown", | |
"source": "This is better: at least, trying to browse the internet with a landline phone raises an error. But it violates the Liskov substitution principle: ``LandlinePhone.browse_internet`` raises an error that ``CommunicationDevice`` never raises. In other words, avoiding the error above relies only on human common sense (a landline phone cannot browse the internet), not on a syntactic information." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "bcea6b13", | |
"cell_type": "markdown", | |
"source": "Here is the standard solution, following the interface segregation principle:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:35:54.737640Z", | |
"start_time": "2022-02-24T15:35:54.719640Z" | |
}, | |
"trusted": false | |
}, | |
"id": "71d846c9", | |
"cell_type": "code", | |
"source": "class CallingDevice:\n def make_call(self):\n print(\"Default implementation of making a call.\")\nclass MessagingDevice:\n def send_sms(self):\n print(\"Default implementation of sending an sms.\")\nclass InternetBrowsingDevice:\n def browse_internet(self):\n print(\"Default implementation of browsing the internet.\")\nclass SmartPhone(CallingDevice, MessagingDevice, InternetBrowsingDevice):\n def make_call(self):\n print(\"SmartPhone makes a call.\")\n def send_sms(self):\n print(\"SmartPhone sends an sms.\")\n def browse_internet(self):\n print(\"SmartPhone browses the internet.\")\nclass LandlinePhone(CallingDevice):\n def make_call(self):\n print(\"LandlinePhone makes a call.\") ", | |
"execution_count": 58, | |
"outputs": [] | |
}, | |
{ | |
"metadata": {}, | |
"id": "b088b99d", | |
"cell_type": "markdown", | |
"source": "We have many client-specific interfaces (``CallingDevice``, ``MessagingDevice``, ``InternetBrowsingDevice``) instead of one general-purpose interface (``CommunicationDevice``)." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "7d1166fc", | |
"cell_type": "markdown", | |
"source": "Now, let us rewrite ``each_device_browses_internet``:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:36:03.407660Z", | |
"start_time": "2022-02-24T15:36:03.401649Z" | |
}, | |
"trusted": false | |
}, | |
"id": "445c0009", | |
"cell_type": "code", | |
"source": "def each_device_browses_internet(internet_browsing_devices: list[InternetBrowsingDevice]):\n for internet_browsing_device in internet_browsing_devices:\n internet_browsing_device.browse_internet()", | |
"execution_count": 59, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:36:03.902767Z", | |
"start_time": "2022-02-24T15:36:03.884825Z" | |
}, | |
"trusted": false | |
}, | |
"id": "169ae2c5", | |
"cell_type": "code", | |
"source": "each_device_browses_internet([SmartPhone()])\n# Now, syntactically, it makes no sense to put a LandlinePhone here because it is not \n# an InternetBrowsingDevice.", | |
"execution_count": 60, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "SmartPhone browses the internet.\n" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "slide" | |
} | |
}, | |
"id": "f375b06d", | |
"cell_type": "markdown", | |
"source": "## D: Dependency Inversion Principle" | |
}, | |
{ | |
"metadata": {}, | |
"id": "b08ffa88", | |
"cell_type": "markdown", | |
"source": "\"Depend upon abstractions, not concretions.\"\n\nTwo main avatars:\n\n* \"Abstractions should not depend on details. Details should depend on abstraction.\"\n* \"High-level modules should not depend on low-level modules. Both should depend on abstractions.\"" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "5b725c43", | |
"cell_type": "markdown", | |
"source": "### DSP: \"Abstractions should not depend on details. Details should depend on abstraction.\"" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "feb61b43", | |
"cell_type": "markdown", | |
"source": "We will record persons belonging to different teams." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:36:58.325515Z", | |
"start_time": "2022-02-24T15:36:58.318503Z" | |
}, | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"trusted": false | |
}, | |
"id": "cd12828a", | |
"cell_type": "code", | |
"source": "class TeamMemberships:\n def __init__(self):\n self.d_team_persons = defaultdict(set)\n def put_person_in_team(self, person: str, team: int):\n self.d_team_persons[team].add(person)", | |
"execution_count": 61, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:37:24.323570Z", | |
"start_time": "2022-02-24T15:37:24.315563Z" | |
}, | |
"trusted": false | |
}, | |
"id": "f498d87c", | |
"cell_type": "code", | |
"source": "class Analysis:\n def analyze(self, team_memberships, team):\n persons = team_memberships.d_team_persons[team]\n for person in persons:\n print(f\"{person} is in team {team}.\")", | |
"execution_count": 62, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:37:27.434423Z", | |
"start_time": "2022-02-24T15:37:27.426458Z" | |
}, | |
"trusted": false | |
}, | |
"id": "9ea7d051", | |
"cell_type": "code", | |
"source": "team_memberships = TeamMemberships()\nteam_memberships.put_person_in_team(\"Alice\", 1)\nteam_memberships.put_person_in_team(\"Bob\", 2)\nteam_memberships.put_person_in_team(\"Cate\", 2)\nAnalysis().analyze(team_memberships, team=2)", | |
"execution_count": 63, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "Bob is in team 2.\nCate is in team 2.\n" | |
} | |
] | |
}, | |
{ | |
"metadata": {}, | |
"id": "5b4499b5", | |
"cell_type": "markdown", | |
"source": "Can you see the problem?" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "5a2d6d1b", | |
"cell_type": "markdown", | |
"source": "Imagine that we change the implementation of ``TeamMemberships``:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:37:59.924674Z", | |
"start_time": "2022-02-24T15:37:59.918675Z" | |
}, | |
"trusted": false | |
}, | |
"id": "94a9dc68", | |
"cell_type": "code", | |
"source": "class TeamMemberships:\n def __init__(self):\n self.d_person_team = dict() # Instead of d_team_persons\n def put_person_in_team(self, person, team):\n self.d_person_team[person] = team", | |
"execution_count": 64, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:38:00.361433Z", | |
"start_time": "2022-02-24T15:38:00.353446Z" | |
}, | |
"trusted": false | |
}, | |
"id": "ee0168d8", | |
"cell_type": "code", | |
"source": "team_memberships = TeamMemberships()\nteam_memberships.put_person_in_team(\"Alice\", 1)\nteam_memberships.put_person_in_team(\"Bob\", 2)\nteam_memberships.put_person_in_team(\"Cate\", 2)\nAnalysis().analyze(team_memberships, team=2)", | |
"execution_count": 65, | |
"outputs": [ | |
{ | |
"name": "stderr", | |
"output_type": "stream", | |
"text": "AttributeError: 'TeamMemberships' object has no attribute 'd_team_persons'\n" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "4b095e44", | |
"cell_type": "markdown", | |
"source": "Whose fault is this?\n\n* In some other languages, ``Analysis``: by accessing the attribute ``d_team_persons``, it did not respect *encapsulation* (= you should not access the attributes of other object directly, but use methods instead).\n* In Python, it is not so clear: since ``TeamMemberships.d_team_persons`` is \"public\", it can considered part of the API, and ``Analysis`` can rely on it. So ``TeamMemberships.d_team_persons`` should not have broken this API when changing its implementation." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "40e2011f", | |
"cell_type": "markdown", | |
"source": "First good solution: when changing its implementation, ``TeamMemberships`` keeps its API." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:39:39.400910Z", | |
"start_time": "2022-02-24T15:39:39.395912Z" | |
}, | |
"trusted": false | |
}, | |
"id": "ee8bcd8b", | |
"cell_type": "code", | |
"source": "class TeamMemberships:\n def __init__(self):\n self.d_person_team = dict()\n def put_person_in_team(self, person, team):\n self.d_person_team[person] = team\n @property\n def d_team_persons(self):\n # For backward-compatibility\n return {team: {person for person, t in self.d_person_team.items() if t == team}\n for team in self.d_person_team.values()}", | |
"execution_count": 66, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:39:45.621060Z", | |
"start_time": "2022-02-24T15:39:45.617049Z" | |
}, | |
"trusted": false | |
}, | |
"id": "a9978a40", | |
"cell_type": "code", | |
"source": "team_memberships = TeamMemberships()\nteam_memberships.put_person_in_team(\"Alice\", 1)\nteam_memberships.put_person_in_team(\"Bob\", 2)\nteam_memberships.put_person_in_team(\"Cate\", 2)\nAnalysis().analyze(team_memberships, team=2)", | |
"execution_count": 67, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "Bob is in team 2.\nCate is in team 2.\n" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "33523cb1", | |
"cell_type": "markdown", | |
"source": "Second good solution: from the start, ``TeamMemberships`` decided that ``d_team_persons`` was not part of the API, and ``Analysis`` did not rely on it." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:40:39.927303Z", | |
"start_time": "2022-02-24T15:40:39.917325Z" | |
}, | |
"trusted": false | |
}, | |
"id": "ba6f2228", | |
"cell_type": "code", | |
"source": "class TeamMemberships:\n def __init__(self):\n self._d_team_persons = defaultdict(set)\n # Note the leading underscore: it is not part of the API,\n # I authorize myself to change the implementation later.\n def put_person_in_team(self, person: str, team: int):\n self._d_team_persons[team].add(person)\n def find_all_persons_in_team(self, team: int): # New method\n return self._d_team_persons[team]", | |
"execution_count": 68, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:40:40.175343Z", | |
"start_time": "2022-02-24T15:40:40.158335Z" | |
}, | |
"trusted": false | |
}, | |
"id": "8ef9d167", | |
"cell_type": "code", | |
"source": "class Analysis:\n def analyze(self, team_memberships, team):\n for person in team_memberships.find_all_persons_in_team(team): # rely on TeamMemberships's API\n print(f\"{person} is in team {team}.\")", | |
"execution_count": 69, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:40:40.499844Z", | |
"start_time": "2022-02-24T15:40:40.485844Z" | |
}, | |
"trusted": false | |
}, | |
"id": "3d35e711", | |
"cell_type": "code", | |
"source": "team_memberships = TeamMemberships()\nteam_memberships.put_person_in_team(\"Alice\", 1)\nteam_memberships.put_person_in_team(\"Bob\", 2)\nteam_memberships.put_person_in_team(\"Cate\", 2)\nAnalysis().analyze(team_memberships, team=2)", | |
"execution_count": 70, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "Bob is in team 2.\nCate is in team 2.\n" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "f00416cd", | |
"cell_type": "markdown", | |
"source": "### DSP: \"High-level modules should not depend on low-level modules. Both should depend on abstractions.\"" | |
}, | |
{ | |
"metadata": {}, | |
"id": "816f105e", | |
"cell_type": "markdown", | |
"source": "Consider a connected car wash that sends an SMS when the wash is completed:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T14:03:34.469021Z", | |
"start_time": "2022-02-24T14:03:34.452020Z" | |
}, | |
"trusted": false | |
}, | |
"id": "312266d4", | |
"cell_type": "code", | |
"source": "class SmsNotifier:\n \"\"\"This is a low-level module (it can exist independently).\"\"\"\n def send_sms(self, s):\n print(f\"Send SMS: {s}\")\nclass CarWashService:\n \"\"\"This is a high-level module (it needs low-level modules).\"\"\"\n def wash_completed(self):\n SmsNotifier().send_sms(\"Wash completed.\")", | |
"execution_count": null, | |
"outputs": [] | |
}, | |
{ | |
"metadata": {}, | |
"id": "6a791f4b", | |
"cell_type": "markdown", | |
"source": "Do you see the problem?" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "a4eef076", | |
"cell_type": "markdown", | |
"source": "Problem: later, if you want to add the option of using an email notifier instead of an SMS notifier, it will be difficult!" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "5bc552fb", | |
"cell_type": "markdown", | |
"source": "Solution:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:42:46.399246Z", | |
"start_time": "2022-02-24T15:42:46.392247Z" | |
}, | |
"trusted": false | |
}, | |
"id": "da209121", | |
"cell_type": "code", | |
"source": "class Notifier:\n def send_message(self, s):\n raise NotImplementedError\nclass SmsNotifier(Notifier):\n def send_message(self, s):\n print(f\"Send SMS: {s}\")\nclass CarWashService:\n def __init__(self, notifier: Notifier): # Depend on abstraction, not concretion\n self.notifier = notifier\n def wash_completed(self):\n self.notifier.send_message(\"Wash completed.\")", | |
"execution_count": 71, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-24T15:42:47.239707Z", | |
"start_time": "2022-02-24T15:42:47.224698Z" | |
}, | |
"trusted": false | |
}, | |
"id": "de387bfe", | |
"cell_type": "code", | |
"source": "car_wash_service = CarWashService(notifier=SmsNotifier())\ncar_wash_service.wash_completed()", | |
"execution_count": 72, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": "Send SMS: Wash completed.\n" | |
} | |
] | |
}, | |
{ | |
"metadata": {}, | |
"id": "810f393c", | |
"cell_type": "markdown", | |
"source": "Instead of having ``CarWashService`` (high-level module) depend on ``SmsNotifier`` (low-level module), both rely on ``Notifier`` (an abstraction from which the low-level module is a subclass). You will be able to add an ``EmailNotifier`` later." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "7ba67e25", | |
"cell_type": "markdown", | |
"source": "Once again, in a (reasonably short) research-oriented code project, it is not so bad to do a bit more \"quick and dirty\". But later, if you see that you need the option of an email notifier, it should ring a bell: you should refactor your code to implement to common interface ``Notifier`` and adapt the code of ``CarWashService``." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "slide" | |
} | |
}, | |
"id": "c300e776", | |
"cell_type": "markdown", | |
"source": "## Conclusion" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "c993386c", | |
"cell_type": "markdown", | |
"source": "Take-aways:\n\n* **Single-Responsibility Principle:** \"A class should have only one reason to change.\"\n* **Open-Closed Principle:** \"Software entities ... should be open for extension, but closed for modification.\"\n* **Liskov Substitution Principle:** In any place where you can use a object of a parent class, the code must still work if you use an object of a subclass.\n* **Interface Segregation Principle:** \"Many client-specific interfaces are better than one general-purpose interface.\"\n* **Dependency Inversion Principle:** \"Depend upon abstractions, not concretions.\"" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "253fc225", | |
"cell_type": "markdown", | |
"source": "How to spot when you do not apply SOLID principles?" | |
}, | |
{ | |
"metadata": {}, | |
"id": "538812d6", | |
"cell_type": "markdown", | |
"source": "* You're writing a lot of \"if\" statements to handle different situations in object code.\n* You're writing a lot of code that doesn't actually do anything just to satisfy interface design.\n* You keep opening the same class to change the code.\n* You are writing code in classes that don't really have anything to do with that class. For example, putting SQL queries in a class outside the database connection class." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "1569e89d", | |
"cell_type": "markdown", | |
"source": "Thank you! Questions?" | |
} | |
], | |
"metadata": { | |
"celltoolbar": "Diaporama", | |
"kernelspec": { | |
"name": "python3", | |
"display_name": "Python 3 (ipykernel)", | |
"language": "python" | |
}, | |
"language_info": { | |
"name": "python", | |
"version": "3.9.7", | |
"mimetype": "text/x-python", | |
"codemirror_mode": { | |
"name": "ipython", | |
"version": 3 | |
}, | |
"pygments_lexer": "ipython3", | |
"nbconvert_exporter": "python", | |
"file_extension": ".py" | |
}, | |
"toc": { | |
"nav_menu": {}, | |
"number_sections": true, | |
"sideBar": true, | |
"skip_h1_title": true, | |
"base_numbering": 1, | |
"title_cell": "Table of Contents", | |
"title_sidebar": "Contents", | |
"toc_cell": false, | |
"toc_position": { | |
"height": "calc(100% - 180px)", | |
"left": "10px", | |
"top": "150px", | |
"width": "279.273px" | |
}, | |
"toc_section_display": true, | |
"toc_window_display": false | |
}, | |
"gist": { | |
"id": "", | |
"data": { | |
"description": "solid_principles_in_oop.ipynb", | |
"public": true | |
} | |
} | |
}, | |
"nbformat": 4, | |
"nbformat_minor": 5 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment