├── .DS_Store ├── .gitignore ├── README.md ├── design_patterns ├── .DS_Store ├── abstract_factory_pattern │ ├── good_solution │ │ ├── framework │ │ │ ├── mac_ui_component_factory.py │ │ │ ├── ui_component_factory.py │ │ │ └── windows_ui_component_factory.py │ │ ├── main.py │ │ └── user_settings_form.py │ └── naive_solution │ │ ├── app │ │ ├── main.py │ │ └── user_settings_form.py │ │ └── gui_framework │ │ ├── button.py │ │ ├── checkbox.py │ │ ├── mac │ │ ├── mac_button.py │ │ └── mac_checkbox.py │ │ ├── operating_system_type.py │ │ ├── ui_component.py │ │ └── windows │ │ ├── windows_button.py │ │ └── windows_checkbox.py ├── adapter_pattern │ ├── good_solution │ │ ├── main.py │ │ ├── rainbow_adapter.py │ │ └── rainbow_color.py │ └── naive_solution │ │ ├── black_and_white_color.py │ │ ├── color.py │ │ ├── main.py │ │ ├── midnight_color.py │ │ ├── third_party_color_library │ │ └── rainbow.py │ │ ├── video.py │ │ └── video_editor.py ├── facade_pattern │ ├── good_solution │ │ ├── main.py │ │ └── order_service.py │ └── naive_solution │ │ ├── authenticator.py │ │ ├── inventory.py │ │ ├── main.py │ │ ├── order_fulfillment.py │ │ ├── order_request.py │ │ └── payment.py ├── observer_pattern │ ├── good_solution.py │ └── naive_solution.py ├── prototype_pattern │ ├── good_solution.py │ └── naive_solution.py └── state_pattern │ ├── good_solution │ ├── document.py │ ├── main.py │ ├── states │ │ ├── draft_state.py │ │ ├── moderation_state.py │ │ ├── published_state.py │ │ └── state.py │ └── user_roles.py │ ├── naive_solution │ ├── document.py │ ├── document_states.py │ ├── main.py │ └── user_roles.py │ └── stopwatch │ └── stopwatch.py ├── oop_intro ├── classes_and_objects.py ├── data_access.py ├── static_attributes.py └── static_methods.py ├── oop_principles ├── abstraction │ ├── good_solution.py │ └── naive_solution.py ├── composition │ └── example.py ├── coupling │ ├── good_solution.py │ └── naive_solution.py ├── encapsulation │ ├── good_solution.py │ └── naive_solution.py ├── inheritance │ └── example.py └── polymorphism │ ├── good_solution.py │ └── naive_solution.py ├── solid ├── dependency_inversion_principle │ ├── good_solution.py │ └── naive_solution.py ├── interface_segregation_principle │ ├── good_solution.py │ └── naive_solution.py ├── liskov_substitution_principle │ ├── good_solution.py │ └── naive_solution.py ├── open_closed_principle │ ├── good_solution.py │ └── naive_solution.py └── single_responsibility_principle │ ├── good_solution.py │ └── naive_solution.py └── uml ├── README.md └── images ├── association.png ├── class.png ├── class_hand_drawn.png ├── composition.png ├── dependency.png └── inheritance.png /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DoableDanny/oop-in-python-course/29f0bc84dcda41cbb9d44261bf1de4956b5a4cc6/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.mypy_cache 2 | __pycache__ 3 | ~$intro-slides.pptx 4 | venv 5 | intro-slides.pptx -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python OOP: Object Oriented Programming From Beginner to Pro 2 | 3 | This is the code repo containing all of the examples covered in the course. I strongly recommend you to code out all of the examples as you follow this course. 4 | 5 | ## To download and use this repo: 6 | 7 | 1. Open up a terminal 8 | 2. Download the repo to your computer with `git clone https://github.com/DoableDanny/oop-in-python-course.git` 9 | 3. Open up the project with your text editor, e.g. VS Code 10 | 11 | Example commands for how to run the examples: 12 | 13 | - `python -m solid.single_responsibility_principle.naive_solution` 14 | - `python -m design_patterns.abstract_factory_pattern.naive_solution.app.main` 15 | 16 | ## Below is some extra material that I didn't add to the course that you may find useful as you advance through the course... 17 | 18 | ## Types 19 | 20 | Programming languages use different **type systems** to manage how data is classified and manipulated. The type system determines how strictly types are enforced, when they are checked, and how flexible they are. Below are the **different kinds of typing systems**, with examples. 21 | 22 | --- 23 | 24 | ### 1. Static vs. Dynamic Typing 25 | 26 | This distinction is about **when type checking** is performed—**at compile-time or runtime**. 27 | 28 | - **Static Typing**: Types are checked at **compile-time**. Errors are caught before the program runs. 29 | 30 | - **Examples**: Java, C#, C++, TypeScript, Swift 31 | - **Pro**: Catch type errors early, improving safety and performance. 32 | - **Con**: Requires more code to declare types. 33 | 34 | **Example (Java):** 35 | 36 | ```java 37 | int x = "hello"; // Compile-time error: incompatible types 38 | ``` 39 | 40 | - **Dynamic Typing**: Types are checked at **runtime**. Errors occur only when the program runs. 41 | 42 | - **Examples**: Python, JavaScript, Ruby, PHP 43 | - **Pro**: Code is easier to write and more flexible. 44 | - **Con**: Type errors may only appear during execution, which can cause bugs. 45 | 46 | **Example (Python):** 47 | 48 | ```python 49 | x = "hello" 50 | x = 123 # No error at assignment time, but bugs may appear later. 51 | ``` 52 | 53 | --- 54 | 55 | ### 2. Strong vs. Weak Typing 56 | 57 | This distinction deals with **how strictly** the language enforces types. 58 | 59 | - **Strong Typing**: The language does **not allow implicit type conversions** that might lead to unexpected behavior. Types must be explicitly converted. 60 | 61 | - **Examples**: Python, Java, Ruby 62 | - **Pro**: Reduces bugs caused by unintended type coercion. 63 | - **Con**: Requires more careful handling of types. 64 | 65 | **Example (Python):** 66 | 67 | ```python 68 | print("The number is: " + 123) # TypeError: cannot concatenate str and int 69 | ``` 70 | 71 | - **Weak Typing**: The language **allows implicit type conversions** (coercion), often without the developer’s awareness. 72 | 73 | - **Examples**: JavaScript, PHP, C 74 | - **Pro**: Code is more concise and flexible. 75 | - **Con**: Can cause subtle bugs due to unintended type conversions. 76 | 77 | **Example (JavaScript):** 78 | 79 | ```javascript 80 | console.log("The number is: " + 123); // Implicit conversion to string: "The number is: 123" 81 | ``` 82 | 83 | --- 84 | 85 | ### 3. Nominal vs. Structural Typing 86 | 87 | This distinction is about **how compatibility** between types is determined. 88 | 89 | - **Nominal Typing**: Compatibility is based on **explicit declarations**—types are compatible only if they are declared to be related (e.g., by inheritance or interfaces). 90 | 91 | - **Examples**: Java, C#, Swift 92 | - **Pro**: Ensures clear relationships between types. 93 | - **Con**: Can be verbose and rigid. 94 | 95 | **Example (Java):** 96 | 97 | ```java 98 | class Dog {} 99 | class Cat {} 100 | 101 | Dog d = new Cat(); // Compile-time error: incompatible types 102 | ``` 103 | 104 | - **Structural Typing**: Compatibility is based on **type structure**—two types are compatible if they have the same shape or properties, regardless of their declared relationships. 105 | 106 | - **Examples**: TypeScript, Go, Python (duck typing) 107 | - **Pro**: More flexible; focuses on what an object can do, rather than what it is. 108 | - **Con**: Can lead to less predictable code. 109 | 110 | **Example (TypeScript):** 111 | 112 | ```typescript 113 | type Animal = { sound: () => void }; 114 | const cat = { sound: () => console.log("Meow") }; 115 | 116 | let animal: Animal = cat; // Works because it has the same structure 117 | ``` 118 | 119 | --- 120 | 121 | ### 4. Manifest vs. Inferred Typing 122 | 123 | This distinction focuses on whether **types need to be explicitly declared** by the programmer or are inferred by the compiler. 124 | 125 | - **Manifest Typing**: Types must be **explicitly declared** by the programmer. 126 | 127 | - **Examples**: Java, C#, TypeScript (with strict mode) 128 | - **Pro**: Makes code more readable and predictable. 129 | - **Con**: Can result in verbose code. 130 | 131 | **Example (Java):** 132 | 133 | ```java 134 | int x = 10; // Type explicitly declared 135 | ``` 136 | 137 | - **Inferred Typing**: The compiler or interpreter **infers the type** based on the assigned value, so the programmer doesn’t need to declare it explicitly. 138 | 139 | - **Examples**: Python, JavaScript, TypeScript (without strict mode) 140 | - **Pro**: Less code to write. 141 | - **Con**: Can make the code harder to understand or debug. 142 | 143 | **Example (Python):** 144 | 145 | ```python 146 | x = 10 # Type inferred as int 147 | ``` 148 | 149 | --- 150 | 151 | ### 5. Duck Typing 152 | 153 | This is a type system where **an object’s compatibility** is determined by the presence of certain methods or properties, rather than its actual type. 154 | 155 | - **Examples**: Python, JavaScript, Ruby 156 | - **Pro**: Promotes flexibility. 157 | - **Con**: Errors might not surface until runtime. 158 | 159 | **Example (Python):** 160 | 161 | ```python 162 | class Dog: 163 | def sound(self): 164 | print("Woof") 165 | 166 | class Cat: 167 | def sound(self): 168 | print("Meow") 169 | 170 | def make_sound(animal): 171 | animal.sound() # Works as long as the object has a 'sound' method 172 | 173 | make_sound(Dog()) # Woof 174 | make_sound(Cat()) # Meow 175 | ``` 176 | 177 | --- 178 | 179 | ### 6. Gradual Typing 180 | 181 | This is a mix of **static and dynamic typing**. The programmer can choose whether to use types, and the language can optionally enforce type checking. 182 | 183 | - **Examples**: TypeScript, Python (with `mypy`), PHP (since v7) 184 | - **Pro**: Provides flexibility to add type checks incrementally. 185 | - **Con**: Can be harder to maintain consistency between typed and untyped parts. 186 | 187 | **Example (Python with `mypy`):** 188 | 189 | ```python 190 | def greet(name: str) -> str: 191 | return f"Hello, {name}" 192 | 193 | greet(123) # mypy will flag this as an error 194 | ``` 195 | 196 | --- 197 | 198 | ### Summary Table of Typing Systems 199 | 200 | | **Type System** | **Description** | **Examples** | 201 | | ---------------------- | --------------------------------------------------- | --------------------------------------- | 202 | | Static vs. Dynamic | When types are checked (compile-time vs. runtime) | Java (static), Python (dynamic) | 203 | | Strong vs. Weak | How strictly types are enforced | Python (strong), JS (weak) | 204 | | Nominal vs. Structural | How type compatibility is determined | Java (nominal), TypeScript (structural) | 205 | | Manifest vs. Inferred | Whether types must be explicitly declared | Java (manifest), Python (inferred) | 206 | | Duck Typing | Compatibility based on methods/properties, not type | Python, JavaScript | 207 | | Gradual Typing | Supports both static and dynamic typing | TypeScript, Python (`mypy`) | 208 | 209 | --- 210 | 211 | ### **Conclusion** 212 | 213 | Different languages adopt different **type systems** based on their design goals. Some languages prioritize **safety and predictability** (e.g., Java, C#), while others emphasize **flexibility and ease of use** (e.g., Python, JavaScript). Understanding these different type systems helps you pick the right tool for the job and write more effective code. 214 | 215 | ## **@abstractmethod -- What Happens Under the Hood?** 216 | 217 | When you use the **`@abstractmethod` decorator**, Python makes a few internal checks and changes. Here’s a step-by-step breakdown of what it does: 218 | 219 | 1. **Sets a special flag** on the method to mark it as abstract. 220 | 221 | - This is done by adding a special attribute **`__isabstractmethod__ = True`** to the method. This flag is used by Python to track whether the method is abstract. 222 | 223 | 2. **Marks the class as abstract** if it contains any `@abstractmethod`s. 224 | 225 | - When the class containing the `@abstractmethod` is defined, Python ensures that the class is marked as **abstract**. This means that you **cannot instantiate** the class directly. 226 | 227 | 3. **Checks for subclass implementations** at instantiation. 228 | - When you try to **instantiate a subclass** of an abstract class, Python checks whether **all abstract methods** have been implemented. If not, it raises a **`TypeError`**. 229 | 230 | --- 231 | 232 | ### Code Explanation of `@abstractmethod` Internals 233 | 234 | Here’s a minimal version of what **`@abstractmethod`** does internally. 235 | 236 | #### **How `@abstractmethod` Works Internally (Simplified)** 237 | 238 | ```python 239 | def abstractmethod(func): 240 | """Mark a method as abstract by setting a special attribute.""" 241 | func.__isabstractmethod__ = True 242 | return func 243 | ``` 244 | 245 | This **decorator sets the `__isabstractmethod__` attribute** on the method, marking it as abstract. 246 | 247 | --- 248 | 249 | ## How Python Checks Abstract Methods in the Class 250 | 251 | When Python defines a class that inherits from `ABC`, it checks **all the methods** to see if **`__isabstractmethod__` is `True`**. 252 | 253 | If any abstract methods are not implemented in a subclass, Python raises a **`TypeError`** when trying to instantiate the class. Internally, this is done using the **`ABCMeta` metaclass**, which ensures that **abstract methods must be implemented** before the class can be instantiated. 254 | 255 | --- 256 | 257 | ### **Example with Debugging: Inspect Abstract Methods** 258 | 259 | Here’s how you can **inspect the abstract methods** behind the scenes: 260 | 261 | ```python 262 | from abc import ABC, abstractmethod 263 | 264 | class Shape(ABC): 265 | @abstractmethod 266 | def calculate_area(self): 267 | pass 268 | 269 | print(Shape.calculate_area.__isabstractmethod__) # Output: True 270 | ``` 271 | 272 | - When the `@abstractmethod` decorator is applied, **`__isabstractmethod__`** is set to `True`. 273 | 274 | --- 275 | 276 | ### What Happens if the Abstract Method Is Not Implemented? 277 | 278 | If you try to **instantiate a subclass** without implementing the abstract method, Python raises a **`TypeError`**. 279 | 280 | ```python 281 | class Circle(Shape): 282 | pass # No implementation of calculate_area() 283 | 284 | circle = Circle() # TypeError: Can't instantiate abstract class Circle with abstract method calculate_area 285 | ``` 286 | 287 | This behavior is enforced by **`ABCMeta`**, the metaclass that `ABC` uses to define abstract classes. 288 | 289 | --- 290 | 291 | ### Summary 292 | 293 | - **`@abstractmethod`** marks a method with **`__isabstractmethod__ = True`**. 294 | - Python uses **`ABCMeta`** (a metaclass) to **ensure abstract methods are implemented** in subclasses. 295 | - If an abstract method is not implemented, Python raises a **`TypeError`** at **instantiation time**. 296 | 297 | This is how **Python enforces the concept of abstract methods and abstract base classes**, ensuring that certain methods are always implemented in subclasses. 298 | 299 | --- 300 | -------------------------------------------------------------------------------- /design_patterns/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DoableDanny/oop-in-python-course/29f0bc84dcda41cbb9d44261bf1de4956b5a4cc6/design_patterns/.DS_Store -------------------------------------------------------------------------------- /design_patterns/abstract_factory_pattern/good_solution/framework/mac_ui_component_factory.py: -------------------------------------------------------------------------------- 1 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.mac.mac_button import ( 2 | MacButton, 3 | ) 4 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.mac.mac_checkbox import ( 5 | MacCheckbox, 6 | ) 7 | from design_patterns.abstract_factory_pattern.good_solution.framework.ui_component_factory import ( 8 | UIComponentFactory, 9 | ) 10 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.button import ( 11 | Button, 12 | ) 13 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.checkbox import ( 14 | Checkbox, 15 | ) 16 | 17 | 18 | class MacUIComponentFactory(UIComponentFactory): 19 | def create_button(self) -> Button: 20 | return MacButton() 21 | 22 | def create_checkbox(self) -> Checkbox: 23 | return MacCheckbox() 24 | -------------------------------------------------------------------------------- /design_patterns/abstract_factory_pattern/good_solution/framework/ui_component_factory.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.button import ( 3 | Button, 4 | ) 5 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.checkbox import ( 6 | Checkbox, 7 | ) 8 | 9 | 10 | class UIComponentFactory(ABC): 11 | @abstractmethod 12 | def create_button(self) -> Button: 13 | pass 14 | 15 | @abstractmethod 16 | def create_checkbox(self) -> Checkbox: 17 | pass 18 | -------------------------------------------------------------------------------- /design_patterns/abstract_factory_pattern/good_solution/framework/windows_ui_component_factory.py: -------------------------------------------------------------------------------- 1 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.windows.windows_button import ( 2 | WindowsButton, 3 | ) 4 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.windows.windows_checkbox import ( 5 | WindowsCheckbox, 6 | ) 7 | from design_patterns.abstract_factory_pattern.good_solution.framework.ui_component_factory import ( 8 | UIComponentFactory, 9 | ) 10 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.button import ( 11 | Button, 12 | ) 13 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.checkbox import ( 14 | Checkbox, 15 | ) 16 | 17 | 18 | class WindowsUIComponentFactory(UIComponentFactory): 19 | def create_button(self) -> Button: 20 | return WindowsButton() 21 | 22 | def create_checkbox(self) -> Checkbox: 23 | return WindowsCheckbox() 24 | -------------------------------------------------------------------------------- /design_patterns/abstract_factory_pattern/good_solution/main.py: -------------------------------------------------------------------------------- 1 | # This exemplifies the entry point to our application, where app setup occurs. 2 | # Run with: `python -m design_patterns.abstract_factory_pattern.good_solution.main` 3 | 4 | from design_patterns.abstract_factory_pattern.good_solution.framework.mac_ui_component_factory import ( 5 | MacUIComponentFactory, 6 | ) 7 | from design_patterns.abstract_factory_pattern.good_solution.framework.ui_component_factory import ( 8 | UIComponentFactory, 9 | ) 10 | from design_patterns.abstract_factory_pattern.good_solution.framework.windows_ui_component_factory import ( 11 | WindowsUIComponentFactory, 12 | ) 13 | from design_patterns.abstract_factory_pattern.good_solution.user_settings_form import ( 14 | UserSettingsForm, 15 | ) 16 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.operating_system_type import ( 17 | OperatingSystemType, 18 | ) 19 | 20 | os = OperatingSystemType.MAC # Example operating system 21 | ui_component_factory: UIComponentFactory 22 | 23 | if os == OperatingSystemType.WINDOWS: 24 | ui_component_factory = WindowsUIComponentFactory() 25 | elif os == OperatingSystemType.MAC: 26 | ui_component_factory = MacUIComponentFactory() 27 | else: 28 | raise Exception("Unsupported operating system") 29 | 30 | # Render the User Settings Form 31 | UserSettingsForm().render(ui_component_factory) 32 | 33 | # Logs: 34 | # Mac: render button 35 | # Mac: render checkbox 36 | -------------------------------------------------------------------------------- /design_patterns/abstract_factory_pattern/good_solution/user_settings_form.py: -------------------------------------------------------------------------------- 1 | from design_patterns.abstract_factory_pattern.good_solution.framework.ui_component_factory import ( 2 | UIComponentFactory, 3 | ) 4 | 5 | 6 | class UserSettingsForm: 7 | # Polymorphism used here, so this client requires no knowledge of specific uiComponentFactory. 8 | def render(self, ui_component_factory: UIComponentFactory): 9 | ui_component_factory.create_button().render() 10 | ui_component_factory.create_checkbox().render() 11 | -------------------------------------------------------------------------------- /design_patterns/abstract_factory_pattern/naive_solution/app/main.py: -------------------------------------------------------------------------------- 1 | # Run with: `python -m design_patterns.abstract_factory_pattern.naive_solution.app.main` 2 | 3 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.operating_system_type import ( 4 | OperatingSystemType, 5 | ) 6 | from design_patterns.abstract_factory_pattern.naive_solution.app.user_settings_form import ( 7 | UserSettingsForm, 8 | ) 9 | 10 | os = OperatingSystemType.MAC # Set the operating system type 11 | 12 | user_settings_form = UserSettingsForm() 13 | user_settings_form.render(os) 14 | 15 | # Logs: 16 | # Mac: render button 17 | # Mac: render checkbox 18 | -------------------------------------------------------------------------------- /design_patterns/abstract_factory_pattern/naive_solution/app/user_settings_form.py: -------------------------------------------------------------------------------- 1 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.operating_system_type import ( 2 | OperatingSystemType, 3 | ) 4 | 5 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.windows.windows_button import ( 6 | WindowsButton, 7 | ) 8 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.windows.windows_checkbox import ( 9 | WindowsCheckbox, 10 | ) 11 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.mac.mac_button import ( 12 | MacButton, 13 | ) 14 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.mac.mac_checkbox import ( 15 | MacCheckbox, 16 | ) 17 | 18 | 19 | class UserSettingsForm: 20 | def render(self, os: OperatingSystemType): 21 | # PROBLEM: open-closed principle violated: if add new OS, we have to modify this class 22 | if os == OperatingSystemType.WINDOWS: 23 | # PROBLEM: too easy to make mistake -- e.g., easy to accidentally render a Mac button here. 24 | WindowsButton().render() 25 | # PROBLEM: UserSettingsForm is tightly coupled to many concrete implementations of widgets. 26 | WindowsCheckbox().render() 27 | elif os == OperatingSystemType.MAC: 28 | MacButton().render() 29 | MacCheckbox().render() 30 | -------------------------------------------------------------------------------- /design_patterns/abstract_factory_pattern/naive_solution/gui_framework/button.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.ui_component import ( 3 | UIComponent, 4 | ) 5 | 6 | 7 | class Button(UIComponent): 8 | """Interface for a button UI component""" 9 | 10 | @abstractmethod 11 | def on_click(self): 12 | """Handle the button click event""" 13 | pass 14 | 15 | # Other methods specific to buttons can be defined here... 16 | -------------------------------------------------------------------------------- /design_patterns/abstract_factory_pattern/naive_solution/gui_framework/checkbox.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.ui_component import ( 3 | UIComponent, 4 | ) 5 | 6 | 7 | class Checkbox(UIComponent): 8 | """Interface for a checkbox UI component""" 9 | 10 | @abstractmethod 11 | def on_select(self): 12 | """Handle the checkbox select event""" 13 | pass 14 | 15 | # Other methods specific to checkboxes can be defined here... 16 | -------------------------------------------------------------------------------- /design_patterns/abstract_factory_pattern/naive_solution/gui_framework/mac/mac_button.py: -------------------------------------------------------------------------------- 1 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.button import ( 2 | Button, 3 | ) 4 | 5 | 6 | class MacButton(Button): 7 | def render(self): 8 | print("Mac: render button") 9 | 10 | def on_click(self): 11 | print("Mac: button clicked") 12 | -------------------------------------------------------------------------------- /design_patterns/abstract_factory_pattern/naive_solution/gui_framework/mac/mac_checkbox.py: -------------------------------------------------------------------------------- 1 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.checkbox import ( 2 | Checkbox, 3 | ) 4 | 5 | 6 | class MacCheckbox(Checkbox): 7 | def render(self): 8 | print("Mac: render checkbox") 9 | 10 | def on_select(self): 11 | print("Mac: checkbox selected") 12 | -------------------------------------------------------------------------------- /design_patterns/abstract_factory_pattern/naive_solution/gui_framework/operating_system_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class OperatingSystemType(Enum): 4 | WINDOWS = "Windows" 5 | MAC = "Mac" 6 | # In the future, we may need to support Linux, Web, Android... 7 | -------------------------------------------------------------------------------- /design_patterns/abstract_factory_pattern/naive_solution/gui_framework/ui_component.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | class UIComponent(ABC): 4 | @abstractmethod 5 | def render(self): 6 | """Render the UI component""" 7 | pass 8 | -------------------------------------------------------------------------------- /design_patterns/abstract_factory_pattern/naive_solution/gui_framework/windows/windows_button.py: -------------------------------------------------------------------------------- 1 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.button import ( 2 | Button, 3 | ) 4 | 5 | 6 | class WindowsButton(Button): 7 | def render(self): 8 | print("Windows: render button") 9 | 10 | def on_click(self): 11 | print("Windows: button clicked") 12 | -------------------------------------------------------------------------------- /design_patterns/abstract_factory_pattern/naive_solution/gui_framework/windows/windows_checkbox.py: -------------------------------------------------------------------------------- 1 | from design_patterns.abstract_factory_pattern.naive_solution.gui_framework.checkbox import ( 2 | Checkbox, 3 | ) 4 | 5 | 6 | class WindowsCheckbox(Checkbox): 7 | def render(self): 8 | print("Windows: render checkbox") 9 | 10 | def on_select(self): 11 | print("Windows: checkbox selected") 12 | -------------------------------------------------------------------------------- /design_patterns/adapter_pattern/good_solution/main.py: -------------------------------------------------------------------------------- 1 | # Run with: `python -m design_patterns.adapter_pattern.good_solution.main` 2 | 3 | from design_patterns.adapter_pattern.naive_solution.video import Video 4 | from design_patterns.adapter_pattern.naive_solution.video_editor import VideoEditor 5 | from design_patterns.adapter_pattern.naive_solution.black_and_white_color import ( 6 | BlackAndWhiteColor, 7 | ) 8 | from design_patterns.adapter_pattern.naive_solution.third_party_color_library.rainbow import ( 9 | Rainbow, 10 | ) 11 | 12 | # Now we can import our adapter class 13 | from design_patterns.adapter_pattern.good_solution.rainbow_color import RainbowColor 14 | from design_patterns.adapter_pattern.good_solution.rainbow_adapter import RainbowAdapter 15 | 16 | video = Video() 17 | video_editor = VideoEditor(video) 18 | 19 | # Applying one of our own colors 20 | video_editor.apply_color(BlackAndWhiteColor()) 21 | # Logs: 22 | # Applying black and white color to video 23 | 24 | # We can now apply the rainbow color to our videos, just like the other colors: 25 | video_editor.apply_color(RainbowColor(Rainbow())) 26 | # Logs: 27 | # Setting up rainbow filter 28 | # Applying rainbow filter to video 29 | 30 | video_editor.apply_color(RainbowAdapter()) 31 | -------------------------------------------------------------------------------- /design_patterns/adapter_pattern/good_solution/rainbow_adapter.py: -------------------------------------------------------------------------------- 1 | # This is an alternative solution to RainbowColor, using inheritance instead of composition 2 | 3 | from design_patterns.adapter_pattern.naive_solution.third_party_color_library.rainbow import ( 4 | Rainbow, 5 | ) 6 | from design_patterns.adapter_pattern.naive_solution.color import Color 7 | from design_patterns.adapter_pattern.naive_solution.video import Video 8 | 9 | class RainbowAdapter(Rainbow, Color): 10 | def apply(self, video: Video): 11 | self.setup() 12 | self.update(video) 13 | -------------------------------------------------------------------------------- /design_patterns/adapter_pattern/good_solution/rainbow_color.py: -------------------------------------------------------------------------------- 1 | from design_patterns.adapter_pattern.naive_solution.color import Color 2 | from design_patterns.adapter_pattern.naive_solution.third_party_color_library.rainbow import ( 3 | Rainbow, 4 | ) 5 | from design_patterns.adapter_pattern.naive_solution.video import Video 6 | 7 | # Adapter class. Rainbow is the adaptee 8 | class RainbowColor(Color): 9 | def __init__(self, rainbow: Rainbow): 10 | # "composition" -- RainbowColor is composed of Rainbow. Protected, as clients should only be concerned with applying the rainbow color, not how it is applied (the implementation details). 11 | self._rainbow = rainbow 12 | 13 | def apply(self, video: Video): 14 | self._rainbow.setup() 15 | self._rainbow.update(video) 16 | -------------------------------------------------------------------------------- /design_patterns/adapter_pattern/naive_solution/black_and_white_color.py: -------------------------------------------------------------------------------- 1 | from design_patterns.adapter_pattern.naive_solution.color import Color 2 | 3 | 4 | class BlackAndWhiteColor(Color): 5 | def apply(self, video): 6 | print("Applying black and white color to video") 7 | -------------------------------------------------------------------------------- /design_patterns/adapter_pattern/naive_solution/color.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | class Color(ABC): 4 | @abstractmethod 5 | def apply(self, video): 6 | pass 7 | -------------------------------------------------------------------------------- /design_patterns/adapter_pattern/naive_solution/main.py: -------------------------------------------------------------------------------- 1 | # Run file with: `python -m design_patterns.adapter_pattern.naive_solution.main` 2 | 3 | from design_patterns.adapter_pattern.naive_solution.video import Video 4 | from design_patterns.adapter_pattern.naive_solution.video_editor import VideoEditor 5 | from design_patterns.adapter_pattern.naive_solution.black_and_white_color import ( 6 | BlackAndWhiteColor, 7 | ) 8 | from design_patterns.adapter_pattern.naive_solution.third_party_color_library.rainbow import ( 9 | Rainbow, 10 | ) 11 | 12 | video = Video() 13 | video_editor = VideoEditor(video) 14 | 15 | video_editor.apply_color(BlackAndWhiteColor()) 16 | # Output: Applying black and white color to video 17 | 18 | # Applying a third-party lib color: ERROR: Argument of type "Rainbow" cannot be assigned to parameter "color" of type "Color" in function "apply_color" 19 | # video_editor.apply_color(Rainbow()) 20 | -------------------------------------------------------------------------------- /design_patterns/adapter_pattern/naive_solution/midnight_color.py: -------------------------------------------------------------------------------- 1 | from design_patterns.adapter_pattern.naive_solution.color import Color 2 | 3 | 4 | class MidnightColor(Color): 5 | def apply(self, video): 6 | print("Applying midnight-purple color to video") 7 | -------------------------------------------------------------------------------- /design_patterns/adapter_pattern/naive_solution/third_party_color_library/rainbow.py: -------------------------------------------------------------------------------- 1 | class Rainbow: 2 | def setup(self): 3 | print("Setting up rainbow filter") 4 | 5 | def update(self, video): 6 | print("Applying rainbow filter to video") 7 | -------------------------------------------------------------------------------- /design_patterns/adapter_pattern/naive_solution/video.py: -------------------------------------------------------------------------------- 1 | class Video: 2 | def play(self): 3 | print("Playing video...") 4 | 5 | def stop(self): 6 | print("Stopping video...") 7 | -------------------------------------------------------------------------------- /design_patterns/adapter_pattern/naive_solution/video_editor.py: -------------------------------------------------------------------------------- 1 | from design_patterns.adapter_pattern.naive_solution.color import Color 2 | 3 | 4 | class VideoEditor: 5 | def __init__(self, video): 6 | self.video = video 7 | 8 | def apply_color(self, color: Color): 9 | color.apply(self.video) 10 | -------------------------------------------------------------------------------- /design_patterns/facade_pattern/good_solution/main.py: -------------------------------------------------------------------------------- 1 | # This file demonstrates how a client could create an order. It's now much simpler. 2 | # Run script with `python -m design_patterns.facade_pattern.good_solution.main` 3 | 4 | from design_patterns.facade_pattern.naive_solution.order_request import OrderRequest 5 | from design_patterns.facade_pattern.good_solution.order_service import OrderService 6 | 7 | # Clients can now create orders without needing any knowledge of the underlying implementation details. All of this has been abstracted away, and life is now simple. 8 | order_request = OrderRequest() 9 | order_service = OrderService() 10 | order_service.create_order(order_request) 11 | -------------------------------------------------------------------------------- /design_patterns/facade_pattern/good_solution/order_service.py: -------------------------------------------------------------------------------- 1 | from design_patterns.facade_pattern.naive_solution.authenticator import Authenticator 2 | from design_patterns.facade_pattern.naive_solution.inventory import Inventory 3 | from design_patterns.facade_pattern.naive_solution.payment import Payment 4 | from design_patterns.facade_pattern.naive_solution.order_fulfillment import ( 5 | OrderFulfillment, 6 | ) 7 | 8 | 9 | class OrderService: 10 | def create_order(self, order_req): 11 | auth = Authenticator() 12 | auth.authenticate() 13 | 14 | inventory = Inventory() 15 | for item_id in order_req.item_ids: 16 | inventory.check_inventory(item_id) 17 | 18 | payment = Payment(order_req.name, order_req.card_number, order_req.amount) 19 | payment.pay() 20 | 21 | order_fulfillment = OrderFulfillment(inventory) 22 | order_fulfillment.fulfill(order_req.name, order_req.address, order_req.item_ids) 23 | 24 | # We could also add other order service methods, like `cancel_order()` and `update_order()`... 25 | -------------------------------------------------------------------------------- /design_patterns/facade_pattern/naive_solution/authenticator.py: -------------------------------------------------------------------------------- 1 | class Authenticator: 2 | def authenticate(self) -> bool: 3 | return True # returning True to keep example simple 4 | -------------------------------------------------------------------------------- /design_patterns/facade_pattern/naive_solution/inventory.py: -------------------------------------------------------------------------------- 1 | class Inventory: 2 | def check_inventory(self, item_id: str) -> bool: 3 | return True # just return true to keep the example simple 4 | 5 | def reduce_inventory(self, item_id: str, amount: int): 6 | # In real app this would reduce the amount in a database 7 | print(f"Reducing inventory of {item_id} by {amount}") 8 | -------------------------------------------------------------------------------- /design_patterns/facade_pattern/naive_solution/main.py: -------------------------------------------------------------------------------- 1 | # This file shows all of the steps that every client would have to do to create an order. 2 | # Run this on the terminal: `python -m design_patterns.facade_pattern.naive_solution.main` 3 | 4 | from design_patterns.facade_pattern.naive_solution.order_request import OrderRequest 5 | from design_patterns.facade_pattern.naive_solution.authenticator import Authenticator 6 | from design_patterns.facade_pattern.naive_solution.inventory import Inventory 7 | from design_patterns.facade_pattern.naive_solution.payment import Payment 8 | from design_patterns.facade_pattern.naive_solution.order_fulfillment import ( 9 | OrderFulfillment, 10 | ) 11 | 12 | # Order request contains info that user has submitted when requesting to make an order 13 | order_req = OrderRequest() 14 | 15 | auth = Authenticator() 16 | auth.authenticate() 17 | 18 | inventory = Inventory() 19 | for item_id in order_req.item_ids: 20 | inventory.check_inventory(item_id) 21 | 22 | payment = Payment(order_req.name, order_req.card_number, order_req.amount) 23 | payment.pay() 24 | 25 | order_fulfillment = OrderFulfillment(inventory) 26 | order_fulfillment.fulfill(order_req.name, order_req.address, order_req.item_ids) 27 | 28 | # Logs: 29 | # Charging card with name danny 30 | # Inserting order into database 31 | # Reducing inventory of 123 by 1 32 | # Reducing inventory of 423 by 1 33 | # Reducing inventory of 555 by 1 34 | # Reducing inventory of 989 by 1 35 | -------------------------------------------------------------------------------- /design_patterns/facade_pattern/naive_solution/order_fulfillment.py: -------------------------------------------------------------------------------- 1 | from design_patterns.facade_pattern.naive_solution.inventory import Inventory 2 | 3 | 4 | class OrderFulfillment: 5 | def __init__(self, inventory: Inventory): 6 | # protected attribute to hold the inventory instance 7 | self._inventory = inventory 8 | 9 | def fulfill(self, name: str, address: str, items: list[str]): 10 | print("Inserting order into database") 11 | for item in items: 12 | self._inventory.reduce_inventory(item, 1) 13 | -------------------------------------------------------------------------------- /design_patterns/facade_pattern/naive_solution/order_request.py: -------------------------------------------------------------------------------- 1 | # User-submitted order request data 2 | class OrderRequest: 3 | def __init__(self): 4 | self.name = "danny" 5 | self.card_number = "1234" 6 | self.amount = 20.99 7 | self.address = "123 Springfield Way, Texas" 8 | # item IDs user wants to order 9 | self.item_ids = ["123", "423", "555", "989"] 10 | -------------------------------------------------------------------------------- /design_patterns/facade_pattern/naive_solution/payment.py: -------------------------------------------------------------------------------- 1 | class Payment: 2 | def __init__(self, name: str, card_number: str, amount: float): 3 | self._name = name 4 | self._card_number = card_number 5 | self._amount = amount 6 | 7 | def pay(self): 8 | print(f"Charging card with name {self._name}") 9 | -------------------------------------------------------------------------------- /design_patterns/observer_pattern/good_solution.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | class Observer(ABC): 4 | @abstractmethod 5 | def update() -> None: 6 | pass 7 | 8 | class Sheet2(Observer): 9 | def __init__(self, dataSource) -> None: 10 | self.total = 0 11 | self.dataSource = dataSource 12 | 13 | def update(self): 14 | self.total = self.calculate_total(self.dataSource.values) 15 | print(f"New total: {self.total}") 16 | 17 | def calculate_total(self, values: list[float]) -> float: 18 | sum = 0 19 | for value in values: 20 | sum += value 21 | 22 | self.total = sum 23 | return self.total 24 | 25 | class BarChart(Observer): 26 | def __init__(self, dataSource) -> None: 27 | self.dataSource = dataSource 28 | 29 | def update(self): 30 | print("Rendering bar chart with new values") 31 | 32 | # Our observer manager class 33 | class Subject: 34 | def __init__(self) -> None: 35 | self.observers: list[Observer] = [] 36 | 37 | def add_observer(self, observer: Observer): 38 | self.observers.append(observer) 39 | 40 | def remove_observer(self, observer: Observer): 41 | self.observers.remove(observer) 42 | 43 | def notify_observers(self): 44 | for obs in self.observers: 45 | obs.update() # polymorphism -- we can treat all observers the same 46 | 47 | class DataSource(Subject): 48 | def __init__(self) -> None: 49 | super().__init__() # Initialize observers from ObserverManager 50 | self._values: list[float] = [] 51 | 52 | @property 53 | def values(self) -> list[float]: 54 | return self._values 55 | 56 | @values.setter 57 | def values(self, values: list[float]) -> None: 58 | self._values = values 59 | super().notify_observers() 60 | 61 | dataSource = DataSource() 62 | sheetTotal = Sheet2(dataSource) 63 | barChart = BarChart(dataSource) 64 | 65 | dataSource.add_observer(sheetTotal) 66 | dataSource.add_observer(barChart) 67 | 68 | print(dataSource.values) # [] 69 | 70 | dataSource.values = [1, 2, 3, 4.1] 71 | # LOGS: 72 | # New total: 10.1 73 | # Rendering bar chart with new values 74 | -------------------------------------------------------------------------------- /design_patterns/observer_pattern/naive_solution.py: -------------------------------------------------------------------------------- 1 | # Spreadsheet 2: calculates the total 2 | class Sheet2: 3 | def __init__(self) -> None: 4 | self.total = 0 5 | 6 | def calculate_total(self, values: list[float]) -> float: 7 | sum = 0 8 | for value in values: 9 | sum += value 10 | 11 | self.total = sum 12 | print(f"New total: {sum}") 13 | return self.total 14 | 15 | 16 | # Spreadsheet 1: contains the data source and bar chart 17 | class BarChart: 18 | def render(self, values: list[float]) -> None: 19 | print("Rendering bar chart with new values") 20 | 21 | 22 | class DataSource: 23 | def __init__(self) -> None: 24 | self._values: list[float] = [] 25 | self.dependents: list[object] = [] 26 | 27 | @property 28 | def values(self) -> list[float]: 29 | return self._values 30 | 31 | @values.setter 32 | def values(self, values: list[float]) -> None: 33 | self._values = values 34 | 35 | # Update dependencies -- this is gonna get very messy as we add more dependencies! 36 | for dependent in self.dependents: 37 | if isinstance(dependent, Sheet2): 38 | dependent.calculate_total(values) 39 | elif isinstance(dependent, BarChart): 40 | dependent.render(values) 41 | 42 | def addDependent(self, dependent: object) -> None: 43 | self.dependents.append(dependent) 44 | 45 | def removeDependent(self, dependent: object) -> None: 46 | self.dependents.remove(dependent) 47 | 48 | 49 | # Example useage 50 | sheet = Sheet2() 51 | barChart = BarChart() 52 | 53 | dataSource = DataSource() 54 | dataSource.addDependent(sheet) 55 | dataSource.addDependent(barChart) 56 | 57 | # Setting the values triggers the total and bar chart to also be updated: 58 | dataSource.values = [1, 2, 3, 4.1] 59 | # Logs: 60 | # New total: 10.1 61 | # Rendering bar chart with new values 62 | 63 | print("Removing Bar chart...") 64 | dataSource.removeDependent(barChart) 65 | dataSource.values = [10, 1] 66 | # Logs: 67 | # New total: 11 68 | -------------------------------------------------------------------------------- /design_patterns/prototype_pattern/good_solution.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | class Shape(ABC): 4 | @abstractmethod 5 | def draw(): 6 | pass 7 | 8 | @abstractmethod 9 | def duplicate(): 10 | pass 11 | 12 | class Circle(Shape): 13 | def __init__(self, radius): 14 | self.radius = radius 15 | 16 | def draw(self): 17 | print(f"Drawing circle with radius {self.radius}") 18 | 19 | def duplicate(self): 20 | newCircle = Circle(self.radius) 21 | return newCircle 22 | 23 | class Rectangle(Shape): 24 | def __init__(self, width, height): 25 | self.width = width 26 | self.height = height 27 | 28 | def draw(self): 29 | print(f"Drawing rectangle with width {self.width} and height {self.height}") 30 | 31 | def duplicate(self): 32 | newRect = Rectangle(self.width, self.height) 33 | return newRect 34 | 35 | class ShapeActions: 36 | def duplicate(self, shape): 37 | newShape = shape.duplicate() 38 | newShape.draw() 39 | 40 | # Example useage 41 | shapeActions = ShapeActions() 42 | 43 | circle = Circle(5) 44 | circle.draw() # Drawing circle with radius 5 45 | 46 | rect = Rectangle(5, 10) 47 | rect.draw() # Drawing rectangle with width 5 and height 10 48 | 49 | shapeActions.duplicate(circle) # Drawing circle with radius 5 50 | shapeActions.duplicate(rect) # Drawing rectangle with width 5 and height 10 51 | -------------------------------------------------------------------------------- /design_patterns/prototype_pattern/naive_solution.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | class Shape(ABC): 4 | @abstractmethod 5 | def draw(): 6 | pass 7 | 8 | class Circle(Shape): 9 | def __init__(self, radius): 10 | self.radius = radius 11 | 12 | def draw(self): 13 | print(f"Drawing circle with radius {self.radius}") 14 | 15 | class Rectangle(Shape): 16 | def __init__(self, width, height): 17 | self.width = width 18 | self.height = height 19 | 20 | def draw(self): 21 | print(f"Drawing rectangle with width {self.width} and height {self.height}") 22 | 23 | class ShapeActions: 24 | def duplicate(self, shape): 25 | if isinstance(shape, Circle): 26 | copy = shape 27 | copy.radius = shape.radius 28 | copy.draw() 29 | elif isinstance(shape, Rectangle): 30 | copy = shape 31 | copy.width = shape.width 32 | copy.height = shape.height 33 | copy.draw() 34 | else: 35 | raise ValueError("Invalid shape provided") 36 | 37 | # Example useage 38 | shapeActions = ShapeActions() 39 | 40 | # User adds a circle to GUI 41 | circle = Circle(5) 42 | circle.draw() # Drawing circle with radius 5 43 | 44 | # User adds a rectangle to GUI 45 | rect = Rectangle(5, 10) 46 | rect.draw() # Drawing rectangle with width 5 and height 10 47 | 48 | # User right-clicks the shapes and clicks "duplicate" 49 | shapeActions.duplicate(circle) # Drawing circle with radius 5 50 | shapeActions.duplicate(rect) # Drawing rectangle with width 5 and height 10 51 | -------------------------------------------------------------------------------- /design_patterns/state_pattern/good_solution/document.py: -------------------------------------------------------------------------------- 1 | from design_patterns.state_pattern.good_solution.states.draft_state import ( 2 | DraftState, 3 | ) 4 | 5 | 6 | class Document: 7 | def __init__(self, current_user_role): 8 | self.state = DraftState(self) # New documents have draft state by default 9 | self.current_user_role = current_user_role 10 | 11 | def publish(self): 12 | self.state.publish() 13 | -------------------------------------------------------------------------------- /design_patterns/state_pattern/good_solution/main.py: -------------------------------------------------------------------------------- 1 | # Run with `python -m design_patterns.state_pattern.good_solution.main` 2 | 3 | from design_patterns.state_pattern.good_solution.document import Document 4 | from design_patterns.state_pattern.good_solution.user_roles import UserRoles 5 | from design_patterns.state_pattern.good_solution.states.draft_state import DraftState 6 | 7 | doc = Document(UserRoles.EDITOR) 8 | print(doc.state.__class__.__name__) # DraftState 9 | 10 | doc.publish() 11 | print(doc.state.__class__.__name__) # ModerationState 12 | 13 | doc.publish() 14 | print( 15 | doc.state.__class__.__name__ 16 | ) # ModerationState -- editors can't create published documents 17 | 18 | # Simulate Admin logging in and publishing the document 19 | doc.current_user_role = UserRoles.ADMIN 20 | doc.publish() 21 | print(doc.state.__class__.__name__) # PublishedState 22 | 23 | # Can also switch to any state like so: 24 | doc.state = DraftState(doc) 25 | print(doc.state.__class__.__name__) # DraftState 26 | -------------------------------------------------------------------------------- /design_patterns/state_pattern/good_solution/states/draft_state.py: -------------------------------------------------------------------------------- 1 | from design_patterns.state_pattern.good_solution.states.state import State 2 | from design_patterns.state_pattern.good_solution.states.moderation_state import ( 3 | ModerationState, 4 | ) 5 | 6 | 7 | class DraftState(State): 8 | def __init__(self, document): 9 | self._document = document 10 | 11 | def publish(self): 12 | self._document.state = ModerationState(self._document) 13 | -------------------------------------------------------------------------------- /design_patterns/state_pattern/good_solution/states/moderation_state.py: -------------------------------------------------------------------------------- 1 | from design_patterns.state_pattern.good_solution.states.state import State 2 | from design_patterns.state_pattern.good_solution.states.published_state import ( 3 | PublishedState, 4 | ) 5 | from design_patterns.state_pattern.good_solution.user_roles import UserRoles 6 | 7 | 8 | class ModerationState(State): 9 | def __init__(self, document): 10 | self._document = document 11 | 12 | def publish(self): 13 | if self._document.current_user_role == UserRoles.ADMIN: 14 | self._document.state = PublishedState(self._document) 15 | -------------------------------------------------------------------------------- /design_patterns/state_pattern/good_solution/states/published_state.py: -------------------------------------------------------------------------------- 1 | from design_patterns.state_pattern.good_solution.states.state import State 2 | 3 | 4 | class PublishedState(State): 5 | def __init__(self, document): 6 | self._document = document 7 | 8 | def publish(self): 9 | pass # do nothing 10 | -------------------------------------------------------------------------------- /design_patterns/state_pattern/good_solution/states/state.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | class State(ABC): 4 | @abstractmethod 5 | def publish(self): 6 | pass 7 | -------------------------------------------------------------------------------- /design_patterns/state_pattern/good_solution/user_roles.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class UserRoles(Enum): 4 | READER = 1 5 | EDITOR = 2 6 | ADMIN = 3 7 | -------------------------------------------------------------------------------- /design_patterns/state_pattern/naive_solution/document.py: -------------------------------------------------------------------------------- 1 | from design_patterns.state_pattern.naive_solution.document_states import DocumentStates 2 | from design_patterns.state_pattern.naive_solution.user_roles import UserRoles 3 | 4 | 5 | class Document: 6 | def __init__(self, state: DocumentStates, current_user_role: UserRoles): 7 | self.state = state 8 | self.current_user_role = current_user_role 9 | 10 | def publish(self): 11 | if self.state == DocumentStates.DRAFT: 12 | self.state = DocumentStates.MODERATION 13 | elif self.state == DocumentStates.MODERATION: 14 | if self.current_user_role == UserRoles.ADMIN: 15 | self.state = DocumentStates.PUBLISHED 16 | elif self.state == DocumentStates.PUBLISHED: 17 | # Do nothing 18 | pass 19 | -------------------------------------------------------------------------------- /design_patterns/state_pattern/naive_solution/document_states.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class DocumentStates(Enum): 4 | DRAFT = 1 5 | MODERATION = 2 6 | PUBLISHED = 3 7 | -------------------------------------------------------------------------------- /design_patterns/state_pattern/naive_solution/main.py: -------------------------------------------------------------------------------- 1 | # Run with `python -m design_patterns.state_pattern.naive_solution.main` 2 | 3 | from design_patterns.state_pattern.naive_solution.document_states import DocumentStates 4 | from design_patterns.state_pattern.naive_solution.user_roles import UserRoles 5 | from design_patterns.state_pattern.naive_solution.document import Document 6 | 7 | # Create a document in the DRAFT state with an EDITOR role 8 | doc = Document(DocumentStates.DRAFT, UserRoles.EDITOR) 9 | print(f"Initial state: {doc.state.name}") 10 | 11 | # Try publishing as an EDITOR 12 | doc.publish() 13 | print(f"State after publishing: {doc.state.name}") 14 | 15 | # Change user role to ADMIN and publish again 16 | doc.current_user_role = UserRoles.ADMIN 17 | doc.publish() 18 | print(f"State after admin publishes: {doc.state.name}") 19 | -------------------------------------------------------------------------------- /design_patterns/state_pattern/naive_solution/user_roles.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class UserRoles(Enum): 4 | READER = 1 5 | EDITOR = 2 6 | ADMIN = 3 7 | -------------------------------------------------------------------------------- /design_patterns/state_pattern/stopwatch/stopwatch.py: -------------------------------------------------------------------------------- 1 | # Run with `python -m design_patterns.state_pattern.stopwatch.stopwatch` 2 | 3 | 4 | class Stopwatch: 5 | def __init__(self): 6 | self._is_running = False # Initialize as not running 7 | 8 | def click(self): 9 | if self._is_running: 10 | self._is_running = False 11 | print("Stopped") 12 | else: 13 | self._is_running = True 14 | print("Running") 15 | 16 | 17 | stopwatch = Stopwatch() 18 | stopwatch.click() # Output: Running 19 | stopwatch.click() # Output: Stopped 20 | stopwatch.click() # Output: Running 21 | -------------------------------------------------------------------------------- /oop_intro/classes_and_objects.py: -------------------------------------------------------------------------------- 1 | # Everything that you create in Python is an object. Check this out: 2 | 3 | name = "Danny" 4 | age = 29 5 | 6 | print(type(name)) # 7 | print(type(age)) # 8 | 9 | # Objects are made by classes. Classes are the blueprints for objects, meaning they describe what an object should look like. So, above, let's look at the `name` variable: 10 | 11 | # - The `name` variable is assigned to a string object 12 | # - the `str` class defines what each string object looks like 13 | 14 | # These classes demonstrate some of Python's "built-in" classes, that we can use to create data structures like strings and ints. But we can also create our own classes, then create objects (also known as instances) from those classes: 15 | 16 | # Dog class describes what information ("attributes" or "data") and behaviours ("methods") every dog should have. 17 | # First, lets create a very simple Dog class that has no data and just one behaviour 18 | class Dog: 19 | def bark(self): 20 | print("Whoof whoof") 21 | 22 | # # Define a variable called `dog` and assign it to an instance of (or "an object made from") the Dog class. 23 | dog = Dog() 24 | dog.bark() # Whoof whoof 25 | 26 | # Now let's add some data to the Dog class, so each dog object (or "instance") can have a name and a breed: 27 | class Dog2: 28 | 29 | def __init__(self, first_name, last_name, breed): 30 | self.first_name = first_name 31 | self.last_name = last_name 32 | self.breed = breed 33 | 34 | def bark(self): 35 | print("Whoof whoof") 36 | 37 | def get_full_name(self): 38 | return self.first_name + " " + self.last_name 39 | 40 | # __init__ is a special method that is ran only once when an object is instanciated (created). We can setup our object data in here 41 | 42 | scottyDog = Dog2("Angus", "Biggsby", "Scottish Terrier") 43 | scottyDog.bark() 44 | print(scottyDog.get_full_name()) 45 | print(scottyDog.breed) 46 | 47 | houndDog = Dog2("Elvis", "Presley", "Basset Hound") 48 | houndDog.bark() 49 | print(houndDog.get_full_name()) 50 | print(houndDog.breed) 51 | 52 | # Combining objects 53 | class Owner: 54 | def __init__(self, name, address, contact_number): 55 | self.name = name 56 | self.address = address 57 | self.phone_number = contact_number 58 | 59 | class Dog3: 60 | # __init__ is a special method that is ran only once when an object is instanciated (created). We can setup our object data in here 61 | def __init__(self, first_name, last_name, breed, owner): 62 | self.first_name = first_name 63 | self.last_name = last_name 64 | self.breed = breed 65 | self.owner = owner 66 | 67 | def bark(self): 68 | print("Whoof whoof") 69 | 70 | def get_full_name(self): 71 | return self.first_name + " " + self.last_name 72 | 73 | owner = Owner("Danny", "122 Springfield Way, Uk", "8888 888 888") 74 | scottyDog = Dog3("Bruce", "Biggs", "Scottish Terrier", owner) 75 | print(scottyDog.owner.name) 76 | 77 | # What is self? 78 | # In Python, self is a special parameter that refers to the instance of the class (the object) you're working with. When you define a method within a class, the first parameter of that method is always self, by convention. This helps Python know that the method belongs to an instance of the class. 79 | 80 | # Think of self as a way to refer to "this object" — the specific object that is calling the method. It gives each object its own set of attributes and allows access to methods that belong to it. 81 | 82 | # Why Do We Need self? 83 | # Without self, Python wouldn’t know which object you’re referring to when you use attributes or methods within a class. self ensures that each object can keep its own data separate and gives you a way to work with an object's attributes and methods. 84 | -------------------------------------------------------------------------------- /oop_intro/data_access.py: -------------------------------------------------------------------------------- 1 | # Accessing and setting data in classes 2 | 3 | # Let's say we have a User class that defines what data a user should have 4 | 5 | 6 | class User: 7 | def __init__(self, username, email, password): 8 | self.username = username 9 | self.email = email 10 | self.password = password 11 | 12 | def sayHiToUser(self, user): 13 | print( 14 | f"Sending message to {user.username}: Hi {user.username}, it's {self.username} ;)" 15 | ) 16 | 17 | 18 | user1 = User("dantheman", "dan@gmail.com", "123") 19 | user2 = User("batman", "bat@gmail.com", "abc") 20 | user1.sayHiToUser(user2) 21 | 22 | print(user1.email) 23 | 24 | user1.email = "danoutlook.com" # PROBLEM: we can set email to anything! 25 | 26 | print(user1.email) 27 | 28 | # SOLUTION: we need a way of controlling the way we can get and set data. Let me show you two ways: one traditional "Java"-style, and one the more modern "Python" (and C#) style. 29 | 30 | # 1. The traditional way: make the data private and use getters and setters: 31 | 32 | from datetime import datetime 33 | 34 | 35 | class User2: 36 | def __init__(self, username, email, isAdmin=False): 37 | self.username = username 38 | self._email = email 39 | self.isAdmin = isAdmin 40 | 41 | # convention: get + attr name 42 | def getEmail(self): 43 | # Advantage of getter: if we need to make changes to way data is accessed, we can do it just here -- not everywhere we are accessing email 44 | if self.isAdmin: 45 | print(f"Email accessed at {datetime.now()}") 46 | return self._email 47 | return None # Explicitly returns None to indicate no access 48 | 49 | # convention: set + attr name 50 | def setEmail(self, newEmail): 51 | if "@" in newEmail: 52 | self._email = newEmail 53 | 54 | 55 | user1 = User2("dantheman", "dan@gmail.com", True) 56 | print(user1._email) # NAUGHTY!! As responsible Python devs, we are expected to do this: 57 | print(user1.getEmail()) # Controlled access 58 | 59 | user1.setEmail("dan@outlook.com") 60 | print(user1.getEmail()) 61 | 62 | # Python’s Take on Access Modifiers 63 | # Unlike languages such as Java or C++, which enforce strict access control (like private or protected), Python takes a more relaxed approach. In Python: 64 | 65 | # A single underscore (_) before a name (e.g., _attribute) is a convention indicating that something is intended for internal use within the class or module. This means it’s not part of the public API, and external code shouldn’t access it directly. 66 | # However, Python doesn’t enforce this restriction. The attribute or method is still accessible from outside the class, but it signals to developers that it’s meant to be “protected” or “internal.” 67 | 68 | # The “Consenting Adults” Philosophy 69 | # Guido van Rossum’s "consenting adults" philosophy highlights Python’s emphasis on developer responsibility rather than strict rules. This philosophy suggests that: 70 | 71 | # Developers are trusted to respect the convention of not accessing underscore-prefixed attributes or methods. 72 | # Access is not prevented, as Python assumes that developers will act responsibly and won’t misuse or access “protected” members unless absolutely necessary. 73 | 74 | # 2. Using properties 75 | 76 | # This is the recommended approach in python. let's see why... 77 | 78 | 79 | class User3: 80 | def __init__(self, username, email, isAdmin=False): 81 | self.username = username 82 | self.email = email # this triggers the setter property 83 | self.isAdmin = isAdmin 84 | 85 | # Getter property 86 | @property 87 | def email(self): 88 | if self.isAdmin: 89 | return self._email 90 | print("Not admin, so can't access email") 91 | 92 | @email.setter 93 | def email(self, newEmail): 94 | if "@" in newEmail: 95 | self._email = newEmail 96 | else: 97 | raise ValueError("Invalid email: no '@'") 98 | 99 | 100 | user1 = User3("dantheman", "dan@gmail.com", True) 101 | print(user1.email) 102 | try: 103 | user1.email = "dayyn@gmail.com" 104 | except ValueError as e: 105 | print(f"Error: {e}") 106 | 107 | print(user1.email) 108 | 109 | # public vs protected vs private attributes (and methods) 110 | 111 | # static attributes and methods 112 | 113 | # Let's say that we want to keep track of the total number of user objects that have been created. To do that, we can create a "static" attribute on the User class: 114 | 115 | 116 | class User4: 117 | total_users_created = ( 118 | 0 # Create a static attribute called total_users and initialise it to 0 119 | ) 120 | 121 | def __init__(self, username, email, isAdmin=False): 122 | self.username = username 123 | self.email = email 124 | self.isAdmin = isAdmin 125 | 126 | # Whenever a new user is created, we increment the value 127 | User4.total_users_created += 1 128 | 129 | @property 130 | def email(self): 131 | if self.isAdmin: 132 | return self._email 133 | print("Not admin, so can't access email") 134 | 135 | @email.setter 136 | def email(self, newEmail): 137 | if "@" in newEmail: 138 | self._email = newEmail 139 | else: 140 | raise ValueError("Invalid email: no '@'") 141 | 142 | 143 | print(User4.total_users_created) 144 | user = User4("dantheman", "dan@gmail.com", True) 145 | print(User4.total_users_created) 146 | user2 = User4("joetheman", "joe@gmail.com", False) 147 | print(User4.total_users_created) 148 | user4 = User4("sally123", "sal@gmail.com", False) 149 | print(User4.total_users_created) 150 | -------------------------------------------------------------------------------- /oop_intro/static_attributes.py: -------------------------------------------------------------------------------- 1 | class User: 2 | # Static attribute to count all users 3 | user_count = 0 4 | 5 | def __init__(self, username, email): 6 | self.username = username # Instance attribute 7 | self.email = email # Instance attribute 8 | User.user_count += 1 # Increment the static attribute 9 | 10 | def display_user(self): 11 | print(f"Username: {self.username}, Email: {self.email}") 12 | 13 | 14 | # Creating user instances 15 | user1 = User("dantheman", "dan@gmail.com") 16 | user2 = User("sally123", "sally@gmail.com") 17 | 18 | # Accessing instance attributes 19 | print(user1.username) # Output: dantheman 20 | print(user2.username) # Output: sally123 21 | 22 | # Accessing static attribute 23 | print(User.user_count) # Output: 2 24 | print( 25 | user1.user_count 26 | ) # Output: 2 (but it's still shared, as this accesses User.user_count) 27 | print( 28 | user2.user_count 29 | ) # Output: 2 (but it's still shared, as this accesses User.user_count) 30 | -------------------------------------------------------------------------------- /oop_intro/static_methods.py: -------------------------------------------------------------------------------- 1 | class BankAccount: 2 | MIN_BALANCE = 100 # Class/static attribute, minimum balance requirement 3 | 4 | def __init__(self, owner, balance=0): 5 | self.owner = owner # Instance attribute 6 | self.balance = balance # Instance attribute 7 | 8 | # Instance method 9 | def deposit(self, amount): 10 | """Add amount to the account balance.""" 11 | if amount > 0: 12 | self.balance += amount 13 | print(f"{self.owner}'s new balance: ${self.balance}") 14 | else: 15 | print("Deposit amount must be positive.") 16 | 17 | # Static method 18 | @staticmethod 19 | def is_valid_interest_rate(rate): 20 | """Check if the interest rate is within a valid range (0 to 5%).""" 21 | return 0 <= rate <= 5 22 | 23 | 24 | # Example usage 25 | account = BankAccount("Alice", 500) 26 | 27 | # Using instance method 28 | account.deposit(200) # Output: Alice's new balance: $700 29 | 30 | # Using static method 31 | print(BankAccount.is_valid_interest_rate(3)) # Output: True 32 | print(BankAccount.is_valid_interest_rate(10)) # Output: False 33 | 34 | # Example: 35 | 36 | 37 | class Person: 38 | def __init__(self, name, email, address) -> None: 39 | self.name = name # public attribute 40 | self._email = email # protected attribute 41 | self.__home_address = address # private attribute 42 | 43 | def print_details(self): 44 | print( 45 | f"Name: {self.name}; Email: {self._email}; Address: {self.__home_address}" 46 | ) 47 | 48 | 49 | person = Person("danny", "danny@gmail.com", "200 Springfield way, UK") 50 | person.print_details() # Name: danny; Email: danny@gmail.com; Address: 200 Springfield way, UK 51 | 52 | print(person.name) # danny 53 | print(person._email) # danny@gmail.com (but we are not supposed to do this!) 54 | print( 55 | person.__home_address 56 | ) # AttributeError: 'Person' object has no attribute '__home_address' 57 | -------------------------------------------------------------------------------- /oop_principles/abstraction/good_solution.py: -------------------------------------------------------------------------------- 1 | class EmailService: 2 | def send_email(self): 3 | # Call private methods within the class 4 | self._connect() 5 | self._authenticate() 6 | print("Sending email...") 7 | self._disconnect() 8 | 9 | def _connect(self): 10 | print("Connecting to email server...") 11 | 12 | def _authenticate(self): 13 | print("Authenticating...") 14 | 15 | def _disconnect(self): 16 | print("Disconnecting from email server...") 17 | 18 | 19 | email = EmailService() 20 | email.send_email() 21 | 22 | # LOGS: 23 | # Sending email... 24 | # Connecting to email server... 25 | # Authenticating... 26 | # Disconnecting from email server... 27 | -------------------------------------------------------------------------------- /oop_principles/abstraction/naive_solution.py: -------------------------------------------------------------------------------- 1 | class BadEmailService: 2 | # def send_email(self): 3 | # self.connect() 4 | # self.authenticate() 5 | # print("Sending email...") 6 | # self.disconnect() 7 | 8 | def connect(self): 9 | print("Connecting to email server...") 10 | 11 | def authenticate(self): 12 | print("Authenticating...") 13 | 14 | # We could also force clients to call connect, authenticate, send_email, and disconnect to send an email. That wouldn't be very nice tho! No abstraction means more effort for client/dev. 15 | def send_email(self): 16 | print("Sending email...") 17 | 18 | def disconnect(self): 19 | print("Disconnecting from email server...") 20 | 21 | email = BadEmailService() 22 | 23 | email.connect() 24 | email.authenticate() 25 | email.send_email() 26 | email.disconnect() 27 | 28 | # LOGS: 29 | # Connecting to email server... 30 | # Authenticating... 31 | # Sending email... 32 | # Disconnecting from email server... 33 | 34 | # Oh no, I don't have such a simple API to just send an email (this thing I actually want to do). Much easier to make mistakes -- I might forget to disconnect after sending. 35 | # What happens if implementation details change? Client code has to change. 36 | -------------------------------------------------------------------------------- /oop_principles/composition/example.py: -------------------------------------------------------------------------------- 1 | # Composition 2 | 3 | # The "low-level" components that make up a car (the "high-level" component) 4 | class Engine: 5 | def start(self): 6 | print("Starting engine") 7 | 8 | class Wheels: 9 | def rotate(self): 10 | print("Rotate") 11 | 12 | class Chassis: 13 | def support(self): 14 | print("Chassis supporting the car") 15 | 16 | class Seats: 17 | def sit(self): 18 | print("Sitting on seats") 19 | 20 | # Car is composed (made up from) of the above components 21 | class Car: 22 | def __init__(self): 23 | # Private attributes (by convention, using a leading underscore) -- user of car doesn't want to think about all the low-level complexities ("implementation details") when starting the car. They just want to start and drive. 24 | self._engine = Engine() 25 | self._wheels = Wheels() 26 | self._chassis = Chassis() 27 | self._seats = Seats() 28 | 29 | def start(self): 30 | self._engine.start() 31 | self._wheels.rotate() 32 | self._chassis.support() 33 | self._seats.sit() 34 | print("Car started.") 35 | 36 | car = Car() 37 | car.start() -------------------------------------------------------------------------------- /oop_principles/coupling/good_solution.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | # Interface for notifications so all notification services provide consistent api for clients (polymorphism and dependency injection = FLEXIBLE!) 4 | class NotificationService(ABC): 5 | """Interface-like base class for notification services.""" 6 | 7 | @abstractmethod 8 | def send_notification(self, message: str): 9 | """Abstract method to send a notification.""" 10 | pass 11 | 12 | # Concrete implementation 13 | class EmailService(NotificationService): 14 | def send_notification(self, message: str): 15 | print(f"Sending email: {message}") 16 | 17 | # Concrete implementation 18 | class MobileService(NotificationService): 19 | def send_notification(self, message: str): 20 | print(f"Sending text message: {message}") 21 | 22 | # In this class, we introduce dependency injection and Python's type hinting to the student. 23 | class Order: 24 | def __init__(self, notification_service: NotificationService): 25 | self.notification_service = notification_service 26 | 27 | def create(self): 28 | # Perform order creation logic, e.g. validate order details, check product stock, save to database... 29 | 30 | self.notification_service.send_notification( 31 | "Hi, your order was placed successfully and will be with you within 2-5 working days" 32 | ) 33 | 34 | order = Order(EmailService()) 35 | order.create() 36 | 37 | order2 = Order(MobileService()) 38 | order2.create() 39 | -------------------------------------------------------------------------------- /oop_principles/coupling/naive_solution.py: -------------------------------------------------------------------------------- 1 | # Coupling 2 | 3 | class EmailSender: 4 | def send(self, message): 5 | print(f"Sending email: {message}") 6 | 7 | class Order: 8 | def create(self): 9 | # Perform order creation logic, e.g. validate order details, check product stock, save to database... 10 | 11 | # Send email notification 12 | email = EmailSender() 13 | email.send( 14 | "Hi, your order was placed successfully and will be with you within 2-5 working days" 15 | ) 16 | 17 | order = Order() 18 | order.create() 19 | 20 | # LOGS: 21 | # Sending email to danny@gmail.com: Hi Danny, your order was placed successfully and will be with you within 2-5 working days 22 | 23 | # PROBLEM: Order class is tightly coupled to EmailSender. What if in future we need to send text message? We have to modify this code, violating open/closed. 24 | -------------------------------------------------------------------------------- /oop_principles/encapsulation/good_solution.py: -------------------------------------------------------------------------------- 1 | # This class shows how we can use deposit and withdraw methods to perform validation before updating balance. BUT, it still allows balance to be modified directly. BankAccount2 solves this by introducing a property getter and omitting the property setter method. 2 | class BankAccount: 3 | def __init__(self): 4 | self.balance = 0.0 5 | 6 | def deposit(self, amount): 7 | if amount <= 0: 8 | raise ValueError("Deposit amount must be positive") 9 | self.balance += amount 10 | 11 | def withdraw(self, amount): 12 | if amount <= 0: 13 | raise ValueError("Withdraw amount must be positive") 14 | if amount >= self.balance: 15 | raise ValueError("Insufficient funds") 16 | self.balance -= amount 17 | 18 | # Uncomment to test... 19 | account = BankAccount() 20 | print(account.balance) 21 | # account.balance = -1 22 | # print(account.balance) 23 | account.deposit(1.99) 24 | print(account.balance) 25 | account.withdraw(1) 26 | print(account.balance) 27 | account.withdraw(1) 28 | 29 | # Problem: we can still modify the balance attr directly to a -ve number. 30 | 31 | # Solution: add an underscore to balance attr to signify to clients that this is protected. In Python, a single underscore prefix (e.g., _balance) is a convention that indicates an attribute is protected, meaning it’s intended for internal use within the class or subclasses, but not enforced as private. It’s a hint to developers, not a strict rule. 32 | 33 | class BankAccount2: 34 | def __init__(self): 35 | self._balance = 0.0 36 | 37 | # "Getter" property provides controlled access to _balance attr. 38 | @property 39 | def balance(self): 40 | return self._balance 41 | 42 | # If we don't provide a setter property, then _balance cannot be set directly outside of the class, providing safety against the balance being set to an invalid amount. To modify _balance, the provided deposit and withdraw api methods must be used. 43 | # @balance.setter 44 | # def balance(self, amount): 45 | # self._balance = amount 46 | 47 | def deposit(self, amount): 48 | if amount <= 0: 49 | raise ValueError("Deposit amount must be positive") 50 | self._balance += amount 51 | 52 | def withdraw(self, amount): 53 | if amount <= 0: 54 | raise ValueError("Withdraw amount must be positive") 55 | if amount >= self._balance: 56 | raise ValueError("Insufficient funds") 57 | self._balance -= amount 58 | 59 | account = BankAccount2() 60 | print(account.balance) # 0.0 61 | # account.balance = -1 # This would give ERROR: Cannot assign to attribute "balance" for class "BankAccount" 62 | account.deposit(1.99) 63 | print(account.balance) # 1.99 64 | account.withdraw(1) 65 | print(account.balance) # 0.99 66 | -------------------------------------------------------------------------------- /oop_principles/encapsulation/naive_solution.py: -------------------------------------------------------------------------------- 1 | class BadBankAccount: 2 | def __init__(self, balance): 3 | self.balance = balance 4 | 5 | account = BadBankAccount(0.0) 6 | account.balance = -1 # Oh dear -- balance should not be allowed to be negative 7 | print(account.balance) 8 | 9 | # Why is this bad? The `balance` attribute can be set negative in the __init__ method and can be set directly. Setting a negative balance is not allowed in our banking app. -------------------------------------------------------------------------------- /oop_principles/inheritance/example.py: -------------------------------------------------------------------------------- 1 | # Base class representing a vehicle 2 | class Vehicle: 3 | def __init__(self, brand, model, year): 4 | self.brand = brand 5 | self.model = model 6 | self.year = year 7 | 8 | def start(self): 9 | print("Vehicle is starting.") 10 | 11 | def stop(self): 12 | print("Vehicle is stopping.") 13 | 14 | # Subclass representing a car, inheriting from Vehicle 15 | class Car(Vehicle): 16 | def __init__(self, brand, model, year, number_of_doors, number_of_wheels): 17 | super().__init__(brand, model, year) 18 | self.number_of_doors = number_of_doors 19 | self.number_of_wheels = number_of_wheels 20 | 21 | # Subclass representing a bike, inheriting from Vehicle 22 | class Bike(Vehicle): 23 | def __init__(self, brand, model, year, number_of_wheels): 24 | super().__init__(brand, model, year) 25 | self.number_of_wheels = number_of_wheels 26 | 27 | car = Car("Ford", "Focus", 2008, 5, 4) 28 | bike = Bike("Honda", "Scoopy", 2018, 2) 29 | print(car.__dict__) 30 | # Output: {'brand': 'Ford', 'model': 'Focus', 'year': 2008, 'number_of_doors': 5, 'number_of_wheels': 4} 31 | -------------------------------------------------------------------------------- /oop_principles/polymorphism/good_solution.py: -------------------------------------------------------------------------------- 1 | # Parent class ("Superclass") 2 | class Vehicle: 3 | def __init__(self, brand, model, year): 4 | self.brand = brand 5 | self.model = model 6 | self.year = year 7 | 8 | def start(self): 9 | print("Vehicle is starting.") 10 | 11 | def stop(self): 12 | print("Vehicle is stopping.") 13 | 14 | # Child class ("Subclass") of Vehicle superclass 15 | class Car(Vehicle): 16 | def __init__(self, brand, model, year, number_of_doors): 17 | super().__init__(brand, model, year) 18 | self.number_of_doors = number_of_doors 19 | 20 | # Below, we "override" the start and stop methods, inherited from Vehicle, to provide car-specific behaviour 21 | 22 | def start(self): 23 | print("Car is starting.") 24 | 25 | def stop(self): 26 | print("Car is stopping.") 27 | 28 | # Child class ("Subclass") of Vehicle superclass 29 | class Motorcycle(Vehicle): 30 | def __init__(self, brand, model, year): 31 | super().__init__(brand, model, year) 32 | 33 | # Below, we "override" the start and stop methods, inherited from Vehicle, to provide bike-specific behaviour 34 | 35 | def start(self): 36 | print("Motorcycle is starting.") 37 | 38 | def stop(self): 39 | print("Motorcycle is stopping.") 40 | 41 | # To deomonstrate adding a new vehicle type no longer requires client code business logic modification -- we EXTEND by adding new plane class 42 | class Plane(Vehicle): 43 | def __init__(self, brand, model, year, number_of_doors): 44 | super().__init__(brand, model, year) 45 | self.number_of_doors = number_of_doors 46 | 47 | def start(self): 48 | print("Plane is starting.") 49 | 50 | def stop(self): 51 | print("Plane is stopping.") 52 | 53 | # This class will be used to test that we deal with non-vehicles correctly 54 | class RandomClass: 55 | someAttribute = "Hello there" 56 | 57 | # Client code (in other words, inside some other class or script) 58 | # Create list of vehicles to inspect 59 | vehicles = [ 60 | Car("Ford", "Focus", 2008, 5), 61 | Motorcycle("Honda", "Scoopy", 2018), 62 | ########## ADD A PLANE TO THE LIST: ######### 63 | Plane("Boeing", "747", 2015, 16), 64 | ############################################ 65 | RandomClass(), 66 | ] 67 | 68 | # Loop through list of vehicles and inspect them 69 | for vehicle in vehicles: 70 | if isinstance(vehicle, Vehicle): 71 | print(f"Inspecting {vehicle.brand} {vehicle.model} ({type(vehicle).__name__})") 72 | vehicle.start() 73 | vehicle.stop() 74 | else: 75 | raise Exception("Object is not a valid vehicle") 76 | 77 | # LOGS: 78 | # Inspecting Ford Focus (Car) 79 | # Car is starting. 80 | # Car is stopping. 81 | # Inspecting Honda Scoopy (Motorcycle) 82 | # Motorcycle is starting. 83 | # Motorcycle is stopping. 84 | # Traceback (most recent call last): 85 | # File "/Users/danadams/Desktop/python-messing-about/play.py", line 64, in 86 | # raise Exception("Object is not a valid vehicle") 87 | # Exception: Object is not a valid vehicle 88 | -------------------------------------------------------------------------------- /oop_principles/polymorphism/naive_solution.py: -------------------------------------------------------------------------------- 1 | # Example with no polymorphism 2 | 3 | class Car: 4 | def __init__(self, brand, model, year, number_of_doors): 5 | self.brand = brand 6 | self.model = model 7 | self.year = year 8 | self.number_of_doors = number_of_doors 9 | 10 | def start(self): 11 | print("Car is starting.") 12 | 13 | def stop(self): 14 | print("Car is stopping.") 15 | 16 | class Motorcycle: 17 | def __init__(self, brand, model, year): 18 | self.brand = brand 19 | self.model = model 20 | self.year = year 21 | 22 | def start_bike(self): 23 | print("Motorcycle is starting.") 24 | 25 | def stop_bike(self): 26 | print("Motorcycle is stopping.") 27 | 28 | # This class will be used to test that we deal with non-vehicles correctly 29 | class RandomClass: 30 | someAttribute = "Hello there" 31 | 32 | # Client code (in other words, inside some other class or script) 33 | # Create list of vehicles to inspect 34 | vehicles = [ 35 | Car("Ford", "Focus", 2008, 5), 36 | Motorcycle("Honda", "Scoopy", 2018), 37 | RandomClass(), 38 | ] 39 | 40 | # Loop through list of vehicles and inspect them 41 | for vehicle in vehicles: 42 | if isinstance(vehicle, Car): 43 | print(f"Inspecting {vehicle.brand} {vehicle.model} ({type(vehicle).__name__})") 44 | vehicle.start() 45 | vehicle.stop() 46 | elif isinstance(vehicle, Motorcycle): 47 | print(f"Inspecting {vehicle.brand} {vehicle.model} ({type(vehicle).__name__})") 48 | vehicle.start_bike() 49 | vehicle.stop_bike() 50 | else: 51 | raise Exception("Object is not a valid vehicle") 52 | 53 | # LOGS: 54 | # Inspecting Ford Focus (Car) 55 | # Car is starting. 56 | # Car is stopping. 57 | # Inspecting Honda Scoopy (Motorcycle) 58 | # Motorcycle is starting. 59 | # Motorcycle is stopping. 60 | # Traceback (most recent call last): 61 | # File "/Users/danadams/Desktop/python-messing-about/play.py", line 50, in 62 | # raise Exception("Object is not a valid vehicle") 63 | # Exception: Object is not a valid vehicle 64 | -------------------------------------------------------------------------------- /solid/dependency_inversion_principle/good_solution.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | class Engine(ABC): 4 | @abstractmethod 5 | def start(self): 6 | pass 7 | 8 | # Engine is our "low-level" module 9 | class BasicEngine(Engine): 10 | def start(self): 11 | print("Basic engine started") 12 | 13 | # We can now create different types of engines and inject them into car via dependency injection 14 | class FastEngine(Engine): 15 | def start(self): 16 | print("Activated power boot!") 17 | print("Fast engine started") 18 | 19 | # Car is our "high-level" module 20 | class Car: 21 | def __init__(self, engine): 22 | self.engine = engine 23 | 24 | def start(self): 25 | self.engine.start() 26 | print("Car started") 27 | 28 | fastEngine = FastEngine() # concrete implementation to be "injected" into the car 29 | car = Car( 30 | fastEngine 31 | ) # we can now inject any class that inherits the abstract Engine class, making our code more flexible because it is no longer tightly coupled to a particular concrete engine. 32 | car.start() 33 | 34 | # LOGS: 35 | # Activated power boot! 36 | # Engine started 37 | # Car started 38 | -------------------------------------------------------------------------------- /solid/dependency_inversion_principle/naive_solution.py: -------------------------------------------------------------------------------- 1 | # DIP BAD 2 | 3 | # Engine is our "low-level" module 4 | class Engine: 5 | def start(self): 6 | print("Engine started") 7 | 8 | # An alternative engine class that is FASTER than the basic engine above! 9 | class FastEngine(Engine): 10 | def start(self): 11 | print("Activated power boot!") 12 | print("Fast engine started") 13 | 14 | # Car is our "high-level" module 15 | class Car: 16 | def __init__(self): 17 | self.engine = Engine() # car is tightly-coupled to this particular engine. If we want to give it a FastEngine, then we have to MODIFY this class, breaking the open/closed principle. 18 | 19 | def start(self): 20 | self.engine.start() 21 | print("Car started") 22 | 23 | car = Car() 24 | car.start() 25 | 26 | # LOGS: 27 | # Engine started 28 | # Car started -------------------------------------------------------------------------------- /solid/interface_segregation_principle/good_solution.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import math 3 | 4 | class Shape2D(ABC): 5 | @abstractmethod 6 | def area(self): 7 | pass 8 | 9 | class Shape3D(ABC): 10 | @abstractmethod 11 | def area(self): 12 | pass 13 | 14 | @abstractmethod 15 | def volume(self): 16 | pass 17 | 18 | class Circle(Shape2D): 19 | def __init__(self, radius): 20 | self.radius = radius 21 | 22 | def area(self): 23 | return math.pi * (self.radius**2) 24 | 25 | class Sphere(Shape3D): 26 | def __init__(self, radius): 27 | self.radius = radius 28 | 29 | def area(self): 30 | return 4 * math.pi * (self.radius**2) 31 | 32 | def volume(self): 33 | return (4 / 3) * math.pi * (self.radius**3) 34 | 35 | # Example usage 36 | circle = Circle(10) 37 | print(f"Circle area: {circle.area()}") # Should be: 314.1592653589793 38 | # print(f"Circle volume: {circle.volume()}") # If uncomment this, linter now puts a squiggly line under `volume()` method, as if to say "circle's don't have volume, what are you doing?!" 39 | 40 | sphere = Sphere(10) 41 | print(f"Sphere surface area: {sphere.area()}") # Should be: 1256.6370614359173 42 | print(f"Sphere volume: {sphere.volume()}") # Should be: 4188.790204786391 43 | -------------------------------------------------------------------------------- /solid/interface_segregation_principle/naive_solution.py: -------------------------------------------------------------------------------- 1 | # Interface segregation principle 2 | 3 | from abc import ABC, abstractmethod 4 | import math 5 | 6 | class Shape(ABC): 7 | @abstractmethod 8 | def area(self): 9 | pass 10 | 11 | @abstractmethod 12 | def volume(self): 13 | pass 14 | 15 | class Circle(Shape): 16 | def __init__(self, radius): 17 | self.radius = radius 18 | 19 | def area(self): 20 | return math.pi * (self.radius**2) 21 | 22 | def volume(self): 23 | raise NotImplementedError("Volume not applicable for 2D shapes.") 24 | 25 | class Sphere(Shape): 26 | def __init__(self, radius): 27 | self.radius = radius 28 | 29 | def area(self): 30 | return 4 * math.pi * (self.radius**2) 31 | 32 | def volume(self): 33 | return (4 / 3) * math.pi * (self.radius**3) 34 | 35 | # Example usage 36 | circle = Circle(10) 37 | print(f"Circle area: {circle.area()}") # Should be: 314.1592653589793 38 | print( 39 | f"Circle volume: {circle.volume()}" 40 | ) # My text editor's linter flags no issue... but run the code and we get an exception -- circle has been forced to implement a method that it doesn't need 41 | 42 | sphere = Sphere(10) 43 | print(f"Sphere surface area: {sphere.area()}") # Should be: 1256.6370614359173 44 | print(f"Sphere volume: {sphere.volume()}") # Should be: 4188.790204786391 45 | -------------------------------------------------------------------------------- /solid/liskov_substitution_principle/good_solution.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | # Abstract Shape class 4 | class Shape(ABC): 5 | @abstractmethod 6 | def area(self) -> float: 7 | """Return the area of the shape.""" 8 | pass 9 | 10 | # Rectangle class inheriting from Shape 11 | class Rectangle(Shape): 12 | def __init__(self, width: float = 0.0, height: float = 0.0): 13 | self.width = width 14 | self.height = height 15 | 16 | def area(self) -> float: 17 | """Calculate the area of the rectangle.""" 18 | return self.width * self.height 19 | 20 | class Square(Shape): 21 | def __init__(self, side: float = 0.0): 22 | self.side = side 23 | 24 | def area(self): 25 | return self.side * self.side 26 | 27 | # Example usage 28 | rect = Rectangle() 29 | rect.width = 5 30 | rect.height = 10 31 | print("Expected area = 10 * 5 = 50.") 32 | print("Calculated area = " + str(rect.area())) 33 | 34 | square = Square() 35 | square.side = 5 36 | print("Expected area = 5 * 5 = 25.") 37 | print("Calculated area = " + str(square.area())) 38 | 39 | # LOGS: 40 | # Expected area = 10 * 5 = 50. 41 | # Calculated area = 50 42 | 43 | # Expected area = 5 * 5 = 25. 44 | # Calculated area = 25 45 | -------------------------------------------------------------------------------- /solid/liskov_substitution_principle/naive_solution.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | # Abstract Shape class 4 | class Shape(ABC): 5 | @abstractmethod 6 | def area(self) -> float: 7 | """Return the area of the shape.""" 8 | pass 9 | 10 | # Rectangle class inheriting from Shape 11 | class Rectangle(Shape): 12 | def __init__(self, width: float = 0.0, height: float = 0.0): 13 | self._width = width 14 | self._height = height 15 | 16 | @property 17 | def width(self) -> float: 18 | return self._width 19 | 20 | @width.setter 21 | def width(self, value: float): 22 | self._width = value 23 | 24 | @property 25 | def height(self) -> float: 26 | return self._height 27 | 28 | @height.setter 29 | def height(self, value: float): 30 | self._height = value 31 | 32 | def area(self) -> float: 33 | return self.width * self.height 34 | 35 | # Square class inheriting from Rectangle (LSP violation) 36 | class Square(Rectangle): 37 | def __init__(self, side: float = 0.0): 38 | super().__init__(side, side) 39 | 40 | @Rectangle.width.setter 41 | def width(self, value: float): 42 | """Set both width and height to the same value.""" 43 | self._width = value 44 | self._height = value 45 | 46 | @Rectangle.height.setter 47 | def height(self, value: float): 48 | """Set both height and width to the same value.""" 49 | self._height = value 50 | self._width = value 51 | 52 | # Example usage (LSP violation in action) 53 | # rect = Rectangle() 54 | # rect.width = 5 55 | # rect.height = 10 56 | # print("Expected area = 10 * 5 = 50.") 57 | # print("Calculated area = " + str(rect.area())) 58 | 59 | # LOGS: 60 | # Expected area = 10 * 5 = 50. 61 | # Calculated area = 50 62 | 63 | # Substituting Rectangle with Square (LSP violation) 64 | rect = Square() 65 | rect.width = 5 # Setting width will also set height to 5 66 | rect.height = 10 # Setting height will also set width to 10 67 | print("Expected area = 5 * 5 = 25.") # This is misleading! 68 | print("Calculated area = " + str(rect.area())) # Output: 100 69 | 70 | # LOGS: 71 | # Expected area = 5 * 5 = 25. 72 | # Calculated area = 100 73 | -------------------------------------------------------------------------------- /solid/open_closed_principle/good_solution.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import math 3 | 4 | # Abstract Shape class to define what methods each concrete shape class should implement 5 | class Shape(ABC): 6 | @abstractmethod 7 | def calculate_area(self) -> float: 8 | pass 9 | 10 | class Circle(Shape): 11 | def __init__(self, radius: float): 12 | self.radius = radius 13 | 14 | def calculate_area(self) -> float: 15 | return math.pi * self.radius**2 16 | 17 | class Rectangle(Shape): 18 | def __init__(self, width: float, height: float): 19 | self.width = width 20 | self.height = height 21 | 22 | def calculate_area(self) -> float: 23 | return self.height * self.width 24 | 25 | # Example useage 26 | circle = Circle(10) 27 | rectangle = Rectangle(5, 10) 28 | 29 | print(f"Circle area: {circle.calculate_area()}") 30 | print(f"Rectangle area: {rectangle.calculate_area()}") 31 | 32 | # LOGS: 33 | # Circle area: 314.1592653589793 34 | # Rectangle area: 50 35 | -------------------------------------------------------------------------------- /solid/open_closed_principle/naive_solution.py: -------------------------------------------------------------------------------- 1 | # Example violating O/CP. Let's also showcase Python's type hints in this example. 2 | 3 | from enum import Enum 4 | import math 5 | 6 | class ShapeType(Enum): 7 | CIRCLE = "circle" 8 | RECTANGLE = "rectangle" 9 | 10 | class Shape: 11 | def __init__( 12 | self, 13 | shape_type: ShapeType, 14 | radius: float = 0, 15 | height: float = 0, 16 | width: float = 0, 17 | ): 18 | self.type = shape_type 19 | self.radius = radius 20 | self.height = height 21 | self.width = width 22 | 23 | def calculate_area(self) -> float: 24 | # Warning: ugly stuff below 🤢🤮 25 | if self.type == ShapeType.CIRCLE: 26 | return math.pi * self.radius**2 27 | elif self.type == ShapeType.RECTANGLE: 28 | return self.height * self.width 29 | else: 30 | raise ValueError("Unsupported shape type.") 31 | 32 | # Example usage 33 | circle = Shape(ShapeType.CIRCLE, radius=5) 34 | rectangle = Shape(ShapeType.RECTANGLE, height=4, width=6) 35 | 36 | print(f"Circle Area: {circle.calculate_area()}") # Circle Area: 78.53981633974483 37 | print(f"Rectangle Area: {rectangle.calculate_area()}") # Rectangle Area: 24 38 | -------------------------------------------------------------------------------- /solid/single_responsibility_principle/good_solution.py: -------------------------------------------------------------------------------- 1 | # Here, each class is responsible for just one thing, and only has one reason to change. 2 | 3 | class EmailSender: 4 | def send_email(self, subject, recipient): 5 | print(f"Sending email to {recipient}: {subject}") 6 | 7 | class User: 8 | def __init__(self, username, email): 9 | self.username = username # Public attributes 10 | self.email = email 11 | 12 | class UserService: 13 | # Note: see README for guidance on whether to pass `user` to each service method, or to use composition 14 | def register(self, user): 15 | """Register the user and send a welcome email.""" 16 | # Register user logic (could be saving to a database, etc.) 17 | print(f"Registering user: {user.username}") 18 | 19 | # Send email notification 20 | email_sender = EmailSender() 21 | email_sender.send_email("Welcome to our platform!", user.email) 22 | 23 | # Example usage 24 | user = User(username="john_doe", email="john.doe@example.com") 25 | user_service = UserService() 26 | user_service.register(user) 27 | -------------------------------------------------------------------------------- /solid/single_responsibility_principle/naive_solution.py: -------------------------------------------------------------------------------- 1 | class EmailSender: 2 | def send_email(self, subject, recipient): 3 | print(f"Sending email to {recipient}: {subject}") 4 | 5 | # This user class violates SRP: it does more than one thing: encapsulate user data AND register user 6 | class User: 7 | def __init__(self, username, email): 8 | self.username = username # Public attributes 9 | self.email = email 10 | 11 | def register(self): 12 | """Register the user and send a welcome email.""" 13 | # Register user logic (could be saving to a database, etc.) 14 | print(f"Registering user: {self.username}") 15 | 16 | # Send email notification 17 | email_sender = EmailSender() 18 | email_sender.send_email("Welcome to our platform!", self.email) 19 | 20 | # Example usage 21 | user = User(username="john_doe", email="john.doe@example.com") 22 | user.register() 23 | -------------------------------------------------------------------------------- /uml/README.md: -------------------------------------------------------------------------------- 1 | # UML 2 | 3 | ## A Class 4 | 5 | ```py 6 | class Dog: 7 | def __init__(self, name: str): 8 | self.__name = name # Private attribute 9 | 10 | def bark(self): 11 | """Simulate the dog barking.""" 12 | print(f"Woof woof, my name is {self.__name} ;)") 13 | ``` 14 | 15 | ![class uml diagram hand-drawn](./images/class_hand_drawn.png) 16 | 17 | ![class uml diagram](./images/class.png) 18 | 19 | ## Inheritance 20 | 21 | ```py 22 | class Animal: 23 | def move(self): 24 | print("Animal is moving") 25 | 26 | class Dog(Animal): 27 | def __init__(self, name: str): 28 | self.name = name # Public attribute 29 | 30 | def bark(self): 31 | """Simulate the dog barking.""" 32 | print(f"Woof woof, mi nombre es {self.name} ;)") 33 | ``` 34 | 35 | ![inheritance uml diagram](./images/inheritance.png) 36 | 37 | ## Composition 38 | 39 | ```py 40 | class Owner: 41 | def __init__(self, name: str, contact_info: str): 42 | self.name = name 43 | self.contact_info = contact_info 44 | 45 | class Dog: 46 | def __init__(self, name: str, owner: Owner): 47 | self.name = name 48 | self.owner = owner # Dog is composed of ("has a") owner 49 | 50 | def bark(self): 51 | """Simulate the dog barking.""" 52 | print(f"Woof woof, mi nombre es {self.name} ;)") 53 | 54 | # Example useage 55 | dog = Dog("Bruce", Owner(name="Danny", contact_info="Call 01723 444 123 or Email danny@doabledanny.com")) 56 | print(dog.owner.contact_info) 57 | ``` 58 | 59 | ![composition uml diagram](./images/composition.png) 60 | 61 | ## Dependency 62 | 63 | ```py 64 | class Toy: 65 | def __init__(self, name: str): 66 | self.name = name 67 | 68 | def make_noise(self) -> str: 69 | """Return the sound the toy makes.""" 70 | return f"{self.name} makes a squeaky sound!" 71 | 72 | class Dog: 73 | def __init__(self, name: str): 74 | self.name = name 75 | 76 | def play_with_toy(self, toy: Toy) -> None: 77 | """This method depends on a Toy instance.""" 78 | print(f"{self.name} is playing with {toy.name}.") 79 | print(toy.make_noise()) 80 | 81 | # Example usage 82 | toy = Toy("Squeaky Bone") 83 | dog = Dog("Max") 84 | 85 | # Dog interacts with the Toy object 86 | dog.play_with_toy(toy) 87 | 88 | # LOGS: 89 | # Max is playing with Squeaky Bone. 90 | # Squeaky Bone makes a squeaky sound! 91 | ``` 92 | 93 | ![dependency uml diagram](./images/dependency.png) 94 | -------------------------------------------------------------------------------- /uml/images/association.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DoableDanny/oop-in-python-course/29f0bc84dcda41cbb9d44261bf1de4956b5a4cc6/uml/images/association.png -------------------------------------------------------------------------------- /uml/images/class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DoableDanny/oop-in-python-course/29f0bc84dcda41cbb9d44261bf1de4956b5a4cc6/uml/images/class.png -------------------------------------------------------------------------------- /uml/images/class_hand_drawn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DoableDanny/oop-in-python-course/29f0bc84dcda41cbb9d44261bf1de4956b5a4cc6/uml/images/class_hand_drawn.png -------------------------------------------------------------------------------- /uml/images/composition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DoableDanny/oop-in-python-course/29f0bc84dcda41cbb9d44261bf1de4956b5a4cc6/uml/images/composition.png -------------------------------------------------------------------------------- /uml/images/dependency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DoableDanny/oop-in-python-course/29f0bc84dcda41cbb9d44261bf1de4956b5a4cc6/uml/images/dependency.png -------------------------------------------------------------------------------- /uml/images/inheritance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DoableDanny/oop-in-python-course/29f0bc84dcda41cbb9d44261bf1de4956b5a4cc6/uml/images/inheritance.png --------------------------------------------------------------------------------