diff --git a/README.md b/README.md
index 65f408c..461aa0a 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,11 @@
-# HeadFirstDesignPatterns_python
+# Head First Design Patterns python code
-
Example code from [Head First Design Patterns second edition](https://www.wickedlysmart.com/head-first-design-patterns/) translated to python to help me understand and memorise the patterns.
+
Example code from [Head First Design Patterns second edition](https://wickedlysmart.com/head-first-design-patterns/) translated to python to help me understand and memorise the patterns.
I have added examples of pattern usage in the Python standard library and pypi - I am starting to see patterns everywhere!
> **Note**
-> I am aiming for a reasonably literal translation whilst trying to make the code a little more pythonic by, e.g. using python conventions for `ClassNames` and `method_names` and putting all of the code in a single file where it makes sense to do so.
+> I am aiming for a mostly literal translation whilst trying to make the code a little more pythonic by, e.g. using python conventions for `ClassNames` and `method_names` and putting all of the code in a single file where it makes sense to do so.
## Patterns Implemented
@@ -15,6 +15,7 @@ I have added examples of pattern usage in the Python standard library and pypi -
- [ ] [Factory Method](chapter04_factory)
- [ ] [Simple Factory](chapter04_factory)
- [x] [Abstract Factory](chapter04_factory)
+- [ ] [Builder](chapter04_factory#builder-%EF%B8%8F%EF%B8%8F) _(Bonus Pattern)_
- [x] [Singleton](chapter05_singleton)
- [x] [Command](chapter06_command)
- [x] [Adapter](chapter07_adapter_facade)
@@ -25,6 +26,8 @@ I have added examples of pattern usage in the Python standard library and pypi -
- [x] [State](chapter10_state)
- [ ] Proxy
- [ ] [Model View Controller (MVC)](chapter12_compound)
+- [ ] [Manager](chapter14_leftover) _(Bonus Pattern)_
+- [ ] [Result](extra_result) _(Bonus Pattern)_
## Sample Code : Java
diff --git a/chapter01_strategy/duck.py b/chapter01_strategy/duck.py
index 3288c2b..0cb0199 100644
--- a/chapter01_strategy/duck.py
+++ b/chapter01_strategy/duck.py
@@ -42,60 +42,75 @@ class Duck:
_fly_behavior = None
_quack_behavior = None
- def set_fly_behavior(self, fly_behavior):
+ @property
+ def fly_behavior(self):
+ return self._fly_behavior
+
+ @fly_behavior.setter
+ def fly_behavior(self, fly_behavior):
self._fly_behavior = fly_behavior
- def set_quack_behavior(self, quack_behavior):
+ @property
+ def quack_behavior(self):
+ return self._quack_behavior
+
+ @quack_behavior.setter
+ def quack_behavior(self, quack_behavior):
self._quack_behavior = quack_behavior
def display(self):
raise NotImplementedError
def perform_fly(self):
- self._fly_behavior.fly()
+ self.fly_behavior.fly()
def perform_quack(self):
- self._quack_behavior.quack()
+ self.quack_behavior.quack()
def swim(self):
print("All ducks float, even decoys!")
class MallardDuck(Duck):
- _fly_behavior = FlyWithWings()
- _quack_behavior = Quack()
+ def __init__(self):
+ self.fly_behavior = FlyWithWings()
+ self.quack_behavior = Quack()
def display(self):
print("I'm a real Mallard duck")
class DecoyDuck(Duck):
- _fly_behavior = FlyNoWay()
- _quack_behavior = MuteQuack()
+ def __init__(self):
+ self.fly_behavior = FlyNoWay()
+ self.quack_behavior = MuteQuack()
def display(self):
print("I'm a duck Decoy")
class ModelDuck(Duck):
- _fly_behavior = FlyNoWay()
- _quack_behavior = Squeak()
+ def __init__(self):
+ self.fly_behavior = FlyNoWay()
+ self.quack_behavior = Squeak()
def display(self):
print("I'm a real Mallard duck")
class RedHeadDuck(Duck):
- _fly_behavior = FlyWithWings()
- _quack_behavior = Quack()
+ def __init__(self):
+ self.fly_behavior = FlyWithWings()
+ self.quack_behavior = Quack()
def display(self):
print("I'm a real Red Headed duck")
class RubberDuck(Duck):
- _fly_behavior = FlyNoWay()
- _quack_behavior = Squeak()
+ def __init__(self):
+ self.fly_behavior = FlyNoWay()
+ self.quack_behavior = Squeak()
def display(self):
print("I'm a rubber duckie")
@@ -108,7 +123,7 @@ def mini_duck_simulator():
model = ModelDuck()
model.perform_fly()
- model.set_fly_behavior(FlyRocketPowered())
+ model.fly_behavior = FlyRocketPowered()
model.perform_fly()
diff --git a/chapter02_observer/readme.md b/chapter02_observer/readme.md
index 45cebe3..6fd7bc9 100644
--- a/chapter02_observer/readme.md
+++ b/chapter02_observer/readme.md
@@ -9,6 +9,46 @@ for instance, in Django.
As I wrote out the code I found it very appealing that I did not need
to change the subject at all to add new observers.
+### Class Diagram
+
+```mermaid
+
+classDiagram
+
+ Subject --> Observer : observers
+ Subject1 <-- Observer1 : subject
+ Subject1 <-- Observer2 : subject
+ Subject <|-- Subject1
+ Observer <|-- Observer1
+ Observer <|-- Observer2
+ Subject : attach(o)
+ Subject : detach(o)
+ Subject: notify()
+ class Observer{
+ update()
+ }
+ class Subject1{
+ state
+ get_state()
+ set_state()
+ }
+ class Observer1{
+ state
+ update()
+ }
+ class Observer2{
+ state
+ update()
+ }
+
+```
+
+## Use in Python
+
+[Django signals](https://docs.djangoproject.com/en/4.2/topics/signals/)
+allow other parts of the code to sign up to receive updates, for instance when settings are changed
+or model instances are created and / or updated.
+
## Running the code
```bash
diff --git a/chapter03_decorator/readme.md b/chapter03_decorator/readme.md
index 11e26a3..b3154fe 100644
--- a/chapter03_decorator/readme.md
+++ b/chapter03_decorator/readme.md
@@ -3,15 +3,16 @@
> **Decorator**: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative
> to subclassing for extending functionality.
-Not quite the same as
-python [decorator syntax](https://docs.python.org/3/reference/compound_stmts.html#grammar-token-decorators)
-as in python you call the _decorated function_ and the decorating function
-is called first whereas the _decorating function_ must be called here.
-
I subclass `ABC` and used the `@abstractmethod` decorator from the
`abc` module here but do not use any of this functionality -
it just serves as documentation.
+## Use in Python
+
+The python [decorator syntax](https://docs.python.org/3/reference/compound_stmts.html#grammar-token-decorators)
+decorator syntax looks quite different as in python you call the _decorated function_ and the decorating function
+is automatically called first whereas the _decorating function_ must be called according to the pattern in the book.
+
## Running the code
```bash
diff --git a/chapter04_factory/readme.md b/chapter04_factory/readme.md
index 9361761..a0cc874 100644
--- a/chapter04_factory/readme.md
+++ b/chapter04_factory/readme.md
@@ -1,29 +1,46 @@
# Chapter 4: Factory patterns
-> **Simple Factory**: A class which chooses which product class to instantiate and return, based upon method parameters.
+## Simple Factory 🚧
-The Python standard library contains multiple references to factory objects, for instances
-in [dataclasses](https://docs.python.org/3/library/dataclasses.html?highlight=factory).
-The Factory Boy package provides easy object creation for Django
-and for other ORMs.
+> A class which chooses which product class to instantiate and return, based upon method parameters.
-> **Factory Method**: Defines an interface for creating an object, but lets subclasses decide which class to
+### Use in Python
+
+The Python standard library contains multiple references to factory objects, for instance
+[namedtuple](https://docs.python.org/3/library/collections.html#collections.namedtuple)
+and [dataclasses](https://docs.python.org/3/library/dataclasses.html#module-dataclasses)
+are factories for creating classes.
+
+The [Factory Boy](https://github.com/FactoryBoy/factory_boy) package provides easy object creation for Django and for other ORMs.
+
+## Factory Method 📋
+
+> Defines an interface for creating an object, but lets subclasses decide which class to
> instantiate. The Factory method lets a class defer instantiation to subclasses.
For instance the `PizzaStore` abstract class in this repo provides an abstract `create_pizza` interface for creating one
product.
+### Use in Python
+
The [python-qrcode](https://github.com/dancergraham/python-qrcode) module uses the factory method pattern nicely to
separate only the part of the code that changes (generating png, svg, etc.) from the underlying logic of the code
generation and to allow extension through the creation of new factory methods without modification of the existing code.
I took advantage of this to add a new class for the creation of 3D QR codes with my favourite NURBS modelling software
-Rhino.
+Rhino - it was super simple once I understood the pattern.
+
+## Abstract Factory 🏭
-> **Abstract Factory**: Provides an interface for creating families of related or dependent objects without specifying
+> Provides an interface for creating families of related or dependent objects without specifying
> their concrete classes.
For instance the `PizzaIngredientFactory` abstract class defines an interface for a family of products.
+## Builder 👷🏻♀️🏗️
+
+When the object creation gets more complex with a number of distinct steps then the Builder pattern comes in,
+esseantially using a Template method to put all of the creation steps together.
+
## Running the code
```bash
diff --git a/chapter07_adapter_facade/readme.md b/chapter07_adapter_facade/readme.md
index 7204670..36c6161 100644
--- a/chapter07_adapter_facade/readme.md
+++ b/chapter07_adapter_facade/readme.md
@@ -1,18 +1,23 @@
# Chapter 7: Adapter and Facade design patterns
-I find myself using the adapter and facade patterns a lot when refactoring existing code. I know how I want it to work
-and I know how it works now, but I don't have time _right now_ to fix it, so I add an adapter or facade to give it a clean
-interface and I only change the underlying code if I need to make significant changes to the underlying implementation.
-
> **Adapter**: Converts the interface of a class into another interface the clients expect. Adapter lets classes work
> together that couldn't otherwise because of incompatible interfaces.
Compiled C modules in Python could be seen as examples of the adapter pattern: the Python implementation provides an
interface for python code to work with the incompatible underlying C code.
+I find the adapter pattern useful when using external libraries like skipy spatial to support my own code.
+I want features from the external library but I want to use it with my own objects and the library api doesnt
+exactly fit my own. I write a small adapter to make the external library feel right and work well with my own
+objects.
+
> **Facade**: Provides a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level
> interface that makes the subsystem easier to use.
+I also find myself using the adapter and facade patterns a lot when refactoring existing code. I know how I want it to work
+and I know how it works now, but I don't have time _right now_ to fix it, so I add an adapter or facade to give it a clean
+interface and I make a plan to change the underlying code when it suits me.
+
The [pathlib](https://github.com/python/cpython/blob/main/Lib/pathlib.py) Python library provides a simplified
high-level cross-platform interface to methods and attributes from the `os`, `sys`, `io` and other modules.
@@ -20,7 +25,7 @@ high-level cross-platform interface to methods and attributes from the `os`, `sy
### Adapter
```bash
-python duck.py
+python duck_adapter.py
```
### Facade
diff --git a/chapter09_iterator_composite/composite.py b/chapter09_iterator_composite/composite.py
new file mode 100644
index 0000000..3d7e2e9
--- /dev/null
+++ b/chapter09_iterator_composite/composite.py
@@ -0,0 +1,178 @@
+# package headfirst.designpatterns.iterator.dinermergercafe
+class MenuComponent:
+ def add(self, menu_component):
+ raise NotImplementedError
+
+ def remove(self, menu_component):
+ raise NotImplementedError
+
+ def get_child(self, i: int):
+ raise NotImplementedError
+
+ def print(self):
+ raise NotImplementedError
+
+
+class MenuItem(MenuComponent):
+ def __init__(
+ self,
+ name: str,
+ description: str,
+ vegetarian: bool,
+ price: float,
+ ):
+ self.name = name
+ self.description = description
+ self.vegetarian = vegetarian
+ self.price = price
+
+ def print(self):
+ print(" " + self.name + "(v)" * self.vegetarian + ", " + str(self.price))
+ print(" -- " + self.description)
+
+
+class Menu(MenuComponent):
+ name: str
+ description: str
+
+ def __init__(self, name: str, description: str):
+ self.menu_components: list[MenuComponent] = []
+ self.name = name
+ self.description = description
+
+ def add(self, menu_component: MenuComponent):
+ self.menu_components.append(menu_component)
+
+ def remove(self, menu_component: MenuComponent):
+ self.menu_components.remove(menu_component)
+
+ def get_child(self, i: int):
+ return self.menu_components[i]
+
+ def print(self):
+ print('\n' + self.name + ", " + self.description)
+ print('--------------------')
+
+ for menu_component in self.menu_components:
+ menu_component.print()
+
+ def __iter__(self):
+ raise NotImplementedError
+
+
+class CafeMenu(Menu):
+ def __iter__(self):
+ return iter(self.menu_items.values())
+
+
+class DinerMenu(Menu):
+ def __iter__(self):
+ return iter(self.menu_items)
+
+
+class Waitress:
+ def __init__(self, all_menus: MenuComponent):
+ self.all_menus = all_menus
+
+ def print_menu(self):
+ self.all_menus.print()
+
+
+def menu_test_drive():
+ cafe_menu = CafeMenu('Cafe Menu', 'For Dinner')
+ diner_menu = DinerMenu('Diner Menu', 'For Lunch')
+ all_menus: MenuComponent = Menu('all menus', 'all menus')
+ all_menus.add(cafe_menu)
+ all_menus.add(diner_menu)
+ cafe_menu.add(
+ MenuItem(
+ "Veggie Burger and Air Fries",
+ "Veggie burger on a whole wheat bun, lettuce, tomato, and fries",
+ True,
+ 3.99,
+ )
+ )
+ cafe_menu.add(
+ MenuItem(
+ "Soup of the day",
+ "A cup of the soup of the day, with a side salad",
+ False,
+ 3.69,
+ )
+ )
+ cafe_menu.add(
+ MenuItem(
+ "Burrito",
+ "A large burrito, with whole pinto beans, salsa, guacamole",
+ True,
+ 4.29,
+ )
+ )
+ diner_menu.add(
+ MenuItem(
+ "Vegetarian BLT",
+ "(Fakin') Bacon with lettuce & tomato on whole wheat",
+ True,
+ 2.99,
+ )
+ )
+ diner_menu.add(
+ MenuItem(
+ "BLT",
+ "Bacon with lettuce & tomato on whole wheat",
+ False,
+ 2.99,
+ )
+ )
+ diner_menu.add(
+ MenuItem(
+ "Soup of the day",
+ "Soup of the day, with a side of potato salad",
+ False,
+ 3.29,
+ )
+ )
+ diner_menu.add(
+ MenuItem(
+ "Hotdog",
+ "A hot dog, with sauerkraut, relish, onions, topped with cheese",
+ False,
+ 3.05,
+ )
+ )
+ diner_menu.add(
+ MenuItem(
+ "Steamed Veggies and Brown Rice",
+ "A medly of steamed vegetables over brown rice",
+ True,
+ 3.99,
+ )
+ )
+ diner_menu.add(
+ MenuItem(
+ "Pasta",
+ "Spaghetti with Marinara Sauce, and a slice of sourdough bread",
+ True,
+ 3.89,
+ )
+ )
+ cafe_menu.print()
+ waitress = Waitress(all_menus)
+ waitress.print_menu()
+ # print("\nCustomer asks, is the Hotdog vegetarian?")
+ # print("Waitress says: ", end="")
+ # if waitress.is_item_vegetarian("Hotdog"):
+ # print("Yes")
+ # else:
+ # print("No")
+
+
+""" print("\nCustomer asks, are the Waffles vegetarian?") # Not implemented
+ print("Waitress says: ", end="")
+ if (waitress.is_item_vegetarian("Waffles")):
+ print("Yes")
+ else:
+ print("No")"""
+
+if __name__ == "__main__":
+ menu_test_drive()
diff --git a/chapter14_leftover/readme.md b/chapter14_leftover/readme.md
new file mode 100644
index 0000000..5fc6c34
--- /dev/null
+++ b/chapter14_leftover/readme.md
@@ -0,0 +1,15 @@
+# Chapter 14: Leftover Patterns
+
+> **Manager Pattern**: _(Bonus pattern)_ Manages multiple entities of the same or similar type.
+
+Unlike the factory patterns which handle object creation or the
+mediator pattern which handles interaction between related objects, the
+manager pattern has the role of managing multiple identical or related objects.
+
+For example in the Python standard library the [`Manager` object](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Manager)
+in the `multiprocessing` library manages shared objects and child processes.
+It keeps track of the running processes, shared objects and the available methods
+on those objects. Without a `Manager` object, multiple processes could try to access
+or modify shared objects at the same time with unpredictable results.
+It uses the `Proxy` design pattern, providing a proxy to the shared objects, to help
+achieve this, passing messages and data across process boundaries.
diff --git a/extra_result/broken_config.json b/extra_result/broken_config.json
new file mode 100644
index 0000000..d9df176
--- /dev/null
+++ b/extra_result/broken_config.json
@@ -0,0 +1,2 @@
+{
+ "broken file"
diff --git a/extra_result/company_config.json b/extra_result/company_config.json
new file mode 100644
index 0000000..fbfefb3
--- /dev/null
+++ b/extra_result/company_config.json
@@ -0,0 +1,4 @@
+{
+ "home_directory": "/usr/local/",
+ "company_name": "Wickedly Smart"
+}
\ No newline at end of file
diff --git a/extra_result/filereader.py b/extra_result/filereader.py
new file mode 100644
index 0000000..52d8993
--- /dev/null
+++ b/extra_result/filereader.py
@@ -0,0 +1,77 @@
+import json
+
+
+class JsonResult:
+ """
+ Represents the outcome of reading/parsing a JSON file:
+ - success: A boolean indicating if the operation succeeded.
+ - data: Holds the successfully parsed JSON as a dictionary, if any.
+ - error: An error message if the operation failed.
+ """
+
+ def __init__(self, success, data=None, error=None):
+ self.success = success
+ self.data = data
+ self.error = error
+
+ def __repr__(self):
+ if self.success:
+ return f"JsonResult(success=True, data={self.data})"
+ return f"JsonResult(success=False, error='{self.error}')"
+
+
+def read_json_file(file_path):
+ """
+ Attempts to open and parse a JSON file, returning a JsonResult object.
+ - If successful, contains the parsed data as a Python dictionary.
+ - If the file is missing or invalid JSON, it returns a failure with an error message.
+ """
+ try:
+ with open(file_path, 'r', encoding='utf-8') as file:
+ contents = file.read()
+ parsed_data = json.loads(contents)
+ return JsonResult(success=True, data=parsed_data)
+ except FileNotFoundError:
+ return JsonResult(success=False, error=f"File '{file_path}' not found.")
+ except json.JSONDecodeError:
+ return JsonResult(success=False, error=f"File '{file_path}' contains invalid JSON.")
+ except Exception as e:
+ return JsonResult(success=False, error=str(e))
+
+
+def file_result_simulator():
+ result_company = read_json_file("company_config.json")
+ if result_company.success:
+ print("[Company Config - Success]")
+ print("Parsed data:", result_company.data)
+ else:
+ print("[Company Config - Error]")
+ print("Issue:", result_company.error)
+
+ result_user = read_json_file("user_config.json")
+ if result_user.success:
+ print("[User Config - Success]")
+ print("Parsed data:", result_user.data)
+ else:
+ print("[User Config - Error]")
+ print("Issue:", result_user.error)
+
+ result_broken = read_json_file("broken_config.json")
+ if result_broken.success:
+ print("[User Config - Success]")
+ print("Parsed data:", result_broken.data)
+ else:
+ print("[Broken Config - Error]")
+ print("Issue:", result_broken.error)
+
+ result_missing = read_json_file("missing_config.json")
+ if result_missing.success:
+ print("[Missing Config - Success]")
+ print("Parsed data:", result_missing.data)
+ else:
+ print("[Missing Config - Error]")
+ print("Issue:", result_missing.error)
+
+
+if __name__ == "__main__":
+ file_result_simulator()
diff --git a/extra_result/readme.md b/extra_result/readme.md
new file mode 100644
index 0000000..562050a
--- /dev/null
+++ b/extra_result/readme.md
@@ -0,0 +1,68 @@
+# Result Design Pattern
+
+> **Result Pattern**: _(Bonus pattern not in the book)_ Defines an object representing
+> both the
+> status and the output of an operation.
+
+A result object is a container for the result of an operation, which can be either a
+success or a failure, together with the output of the operation. The result pattern
+is a fundamental part of the Rust programming language in the form of the [Result enum][rust_result].
+
+### Class Diagram
+
+```mermaid
+classDiagram
+ class Result {
+ status: bool
+ output: Any
+ }
+```
+
+## Example
+
+The filereader example uses the Result pattern to handle common errors reading and
+parsing json files. It puts all file reading and parsing logic into a single
+function, which returns a Result object. The calling code can check the status without
+needing to understand the details of the error.
+
+In this case we have correct files, a missing file and a bad file. All can be handled with the same
+approach, in this case using the LBYL philosophy.
+
+### Running the code
+
+```bash
+ python filereader.py
+```
+
+## Discussion
+
+People often say that
+in Python you should use exceptions - "Easier to Ask Forgiveness than Permission" (EAFP)
+over guard clauses - "Look Before You Leap" (LBYL),
+but the **Result design pattern** - returning an object which explicitly
+states whether the operation succeeded is a useful alternative where error handling is required and
+can be compatible with both approaches.
+
+## Use in Python
+
+The asyncio [Future object][asyncio_future] can be seen as a result object - it
+represents the result of an asynchronous operation but rather than a success or failure it is either done or not done.
+Confusingly it contains a `.result` attribute which is the output of the operation,
+but is not the same as the Result object in the Result pattern.
+
+The [requests library][requests] is often cited as a quintessentially
+pythonic library - its [response object][response] (representing
+the [HTTP response][http_response]) is essentially a **Result** object.
+The [`raise_for_status()`][raise_for_status] method lets you easily use it with the EAFP philosophy.
+
+[rust_result]: https://doc.rust-lang.org/std/result/enum.Result.html
+
+[asyncio_future]: https://docs.python.org/3/library/asyncio-future.html#future-object
+
+[raise_for_status]: https://docs.python-requests.org/en/latest/api/#requests.Response.raise_for_status
+
+[requests]: https://docs.python-requests.org/en/latest/index.html
+
+[response]: https://docs.python-requests.org/en/latest/api/#requests.Response
+
+[http_response]: https://developer.mozilla.org/en-US/docs/Web/API/Response
\ No newline at end of file
diff --git a/extra_result/user_config.json b/extra_result/user_config.json
new file mode 100644
index 0000000..5397d4e
--- /dev/null
+++ b/extra_result/user_config.json
@@ -0,0 +1,4 @@
+{
+ "home_directory": "/usr/local/my_files/",
+ "environment": "development"
+}
\ No newline at end of file