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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
pip install -r requirements-dev.txt
pip install pytest-cov ruff
- name: Lint with ruff
run: ruff check .
- name: Run Tests
run: pytest --cov=patterns --cov-report=xml
189 changes: 11 additions & 178 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,181 +1,14 @@
# python-patterns
# Python Design Patterns - Modern Edition

A collection of design patterns and idioms in Python.
Este repositorio contiene implementaciones de patrones de diseño clásicos, refactorizados integralmente para **Python 3.12**.

Remember that each pattern has its own trade-offs. And you need to pay attention more to why you're choosing a certain pattern than to how to implement it.
## 🚀 Mejoras de Ingeniería Realizadas
- **Tipado Estricto:** Implementación de `typing.Protocol`, `Self`, y `Generics`.
- **Arquitectura:** Uso de Clases Base Abstractas (`abc.ABC`) para asegurar contratos de interfaz.
- **Gestión de Memoria:** Optimización de patrones estructurales mediante `weakref`.
- **CI/CD:** Pipeline automatizado con GitHub Actions para ejecución de tests y linting con Ruff.

## Creational Patterns

> Patterns that deal with **object creation** — abstracting and controlling how instances are made.

```mermaid
graph LR
Client -->|requests object| AbstractFactory
AbstractFactory -->|delegates to| ConcreteFactory
ConcreteFactory -->|produces| Product

Builder -->|step-by-step| Director
Director -->|returns| BuiltObject

FactoryMethod -->|subclass decides| ConcreteProduct
Pool -->|reuses| PooledInstance
```

| Pattern | Description |
|:-------:| ----------- |
| [abstract_factory](patterns/creational/abstract_factory.py) | use a generic function with specific factories |
| [borg](patterns/creational/borg.py) | a singleton with shared-state among instances |
| [builder](patterns/creational/builder.py) | instead of using multiple constructors, builder object receives parameters and returns constructed objects |
| [factory](patterns/creational/factory.py) | delegate a specialized function/method to create instances |
| [lazy_evaluation](patterns/creational/lazy_evaluation.py) | lazily-evaluated property pattern in Python |
| [pool](patterns/creational/pool.py) | preinstantiate and maintain a group of instances of the same type |
| [prototype](patterns/creational/prototype.py) | use a factory and clones of a prototype for new instances (if instantiation is expensive) |

## Structural Patterns

> Patterns that define **how classes and objects are composed** to form larger, flexible structures.

```mermaid
graph TD
Client --> Facade
Facade --> SubsystemA
Facade --> SubsystemB
Facade --> SubsystemC

Client2 --> Adapter
Adapter --> LegacyService

Client3 --> Proxy
Proxy -->|controls access to| RealSubject

Component --> Composite
Composite --> Leaf1
Composite --> Leaf2
```

| Pattern | Description |
|:-------:| ----------- |
| [3-tier](patterns/structural/3-tier.py) | data<->business logic<->presentation separation (strict relationships) |
| [adapter](patterns/structural/adapter.py) | adapt one interface to another using a white-list |
| [bridge](patterns/structural/bridge.py) | a client-provider middleman to soften interface changes |
| [composite](patterns/structural/composite.py) | lets clients treat individual objects and compositions uniformly |
| [decorator](patterns/structural/decorator.py) | wrap functionality with other functionality in order to affect outputs |
| [facade](patterns/structural/facade.py) | use one class as an API to a number of others |
| [flyweight](patterns/structural/flyweight.py) | transparently reuse existing instances of objects with similar/identical state |
| [front_controller](patterns/structural/front_controller.py) | single handler requests coming to the application |
| [mvc](patterns/structural/mvc.py) | model<->view<->controller (non-strict relationships) |
| [proxy](patterns/structural/proxy.py) | an object funnels operations to something else |

## Behavioral Patterns

> Patterns concerned with **communication and responsibility** between objects.

```mermaid
graph LR
Sender -->|sends event| Observer1
Sender -->|sends event| Observer2

Request --> Handler1
Handler1 -->|passes if unhandled| Handler2
Handler2 -->|passes if unhandled| Handler3

Context -->|delegates to| Strategy
Strategy -->|executes| Algorithm

Originator -->|saves state to| Memento
Caretaker -->|holds| Memento
```

| Pattern | Description |
|:-------:| ----------- |
| [chain_of_responsibility](patterns/behavioral/chain_of_responsibility.py) | apply a chain of successive handlers to try and process the data |
| [catalog](patterns/behavioral/catalog.py) | general methods will call different specialized methods based on construction parameter |
| [chaining_method](patterns/behavioral/chaining_method.py) | continue callback next object method |
| [command](patterns/behavioral/command.py) | bundle a command and arguments to call later |
| [interpreter](patterns/behavioral/interpreter.py) | define a grammar for a language and use it to interpret statements |
| [iterator](patterns/behavioral/iterator.py) | traverse a container and access the container's elements |
| [iterator](patterns/behavioral/iterator_alt.py) (alt. impl.)| traverse a container and access the container's elements |
| [mediator](patterns/behavioral/mediator.py) | an object that knows how to connect other objects and act as a proxy |
| [memento](patterns/behavioral/memento.py) | generate an opaque token that can be used to go back to a previous state |
| [observer](patterns/behavioral/observer.py) | provide a callback for notification of events/changes to data |
| [publish_subscribe](patterns/behavioral/publish_subscribe.py) | a source syndicates events/data to 0+ registered listeners |
| [registry](patterns/behavioral/registry.py) | keep track of all subclasses of a given class |
| [servant](patterns/behavioral/servant.py) | provide common functionality to a group of classes without using inheritance |
| [specification](patterns/behavioral/specification.py) | business rules can be recombined by chaining the business rules together using boolean logic |
| [state](patterns/behavioral/state.py) | logic is organized into a discrete number of potential states and the next state that can be transitioned to |
| [strategy](patterns/behavioral/strategy.py) | selectable operations over the same data |
| [template](patterns/behavioral/template.py) | an object imposes a structure but takes pluggable components |
| [visitor](patterns/behavioral/visitor.py) | invoke a callback for all items of a collection |

## Design for Testability Patterns

| Pattern | Description |
|:-------:| ----------- |
| [dependency_injection](patterns/dependency_injection.py) | 3 variants of dependency injection |

## Fundamental Patterns

| Pattern | Description |
|:-------:| ----------- |
| [delegation_pattern](patterns/fundamental/delegation_pattern.py) | an object handles a request by delegating to a second object (the delegate) |

## Others

| Pattern | Description |
|:-------:| ----------- |
| [blackboard](patterns/other/blackboard.py) | architectural model, assemble different sub-system knowledge to build a solution, AI approach - non gang of four pattern |
| [graph_search](patterns/other/graph_search.py) | graphing algorithms - non gang of four pattern |
| [hsm](patterns/other/hsm/hsm.py) | hierarchical state machine - non gang of four pattern |

## 🚫 Anti-Patterns

This section lists some common design patterns that are **not recommended** in Python and explains why.

### 🧱 Singleton
**Why not:**
- Python modules are already singletons — every module is imported only once.
- Explicit singleton classes add unnecessary complexity.
- Better alternatives: use module-level variables or dependency injection.

### 🌀 God Object
**Why not:**
- Centralizes too much logic in a single class.
- Makes code harder to test and maintain.
- Better alternative: split functionality into smaller, cohesive classes.

### 🔁 Inheritance overuse
**Why not:**
- Deep inheritance trees make code brittle.
- Prefer composition and delegation.
- “Favor composition over inheritance.”

## Videos

* [Design Patterns in Python by Peter Ullrich](https://www.youtube.com/watch?v=bsyjSW46TDg)
* [Sebastian Buczyński - Why you don't need design patterns in Python?](https://www.youtube.com/watch?v=G5OeYHCJuv0)
* [You Don't Need That!](https://www.youtube.com/watch?v=imW-trt0i9I)
* [Pluggable Libs Through Design Patterns](https://www.youtube.com/watch?v=PfgEU3W0kyU)

## Contributing

When an implementation is added or modified, please review the following guidelines:

##### Docstrings
Add module level description in form of a docstring with links to corresponding references or other useful information.
Add "Examples in Python ecosystem" section if you know some. It shows how patterns could be applied to real-world problems.
[facade.py](patterns/structural/facade.py) has a good example of detailed description, but sometimes the shorter one as in [template.py](patterns/behavioral/template.py) would suffice.

##### Python 2 compatibility
To see Python 2 compatible versions of some patterns please check-out the [legacy](https://github.com/faif/python-patterns/tree/legacy) tag.

##### Update README
When everything else is done - update corresponding part of README.

##### Travis CI
Please run the following before submitting a patch:
- `black .` This lints your code.
- Either `tox` or `tox -e ci37` for unit tests.
- If you have a bash compatible shell, use `./lint.sh`.

## Contributing via issue triage [![Open Source Helpers](https://www.codetriage.com/faif/python-patterns/badges/users.svg)](https://www.codetriage.com/faif/python-patterns)
You can triage issues and pull requests on [CodeTriage](https://www.codetriage.com/faif/python-patterns).
## 🛠 Stack de Calidad
- **Linter:** Ruff (High-performance Python linter).
- **Type Checker:** Mypy (Static type analysis).
- **Testing:** Pytest con cobertura de código.
4 changes: 1 addition & 3 deletions patterns/behavioral/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
during initialization. Uses a single dictionary instead of multiple conditions.
"""


__author__ = "Ibrahim Diop <ibrahim@sikilabs.com>"


class Catalog:
"""catalog of multiple static methods that are executed depending on an init parameter
"""
"""catalog of multiple static methods that are executed depending on an init parameter"""

def __init__(self, param: str) -> None:
# dictionary that will be used to determine which static method is
Expand Down
34 changes: 11 additions & 23 deletions patterns/behavioral/chaining_method.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,25 @@
from __future__ import annotations

from typing import Self


class Person:
def __init__(self, name: str) -> None:
self.name = name
self.age: int = 0

def do_action(self, action: Action) -> Action:
print(self.name, action.name, end=" ")
return action


class Action:
def __init__(self, name: str) -> None:
def set_name(self, name: str) -> Self:
self.name = name

def amount(self, val: str) -> Action:
print(val, end=" ")
return self

def stop(self) -> None:
print("then stop")

def set_age(self, age: int) -> Self:
self.age = age
return self

def main():
"""
>>> move = Action('move')
>>> person = Person('Jack')
>>> person.do_action(move).amount('5m').stop()
Jack move 5m then stop
"""
def __str__(self) -> str:
return f"Name: {self.name}, Age: {self.age}"


if __name__ == "__main__":
import doctest

doctest.testmod()
person = Person("Jorge").set_age(28).set_name("Jorge Otero")
print(person)
61 changes: 26 additions & 35 deletions patterns/behavioral/iterator.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,38 @@
"""
http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/
Implementation of the iterator pattern with a generator
from __future__ import annotations

*TL;DR
Traverses a container and accesses the container's elements.
"""
from typing import Generic, Iterable, Iterator, List, TypeVar

T = TypeVar("T")

def count_to(count: int):
"""Counts by word numbers, up to a maximum of five"""
numbers = ["one", "two", "three", "four", "five"]
yield from numbers[:count]

class AlphabeticalOrderIterator(Iterator[T], Generic[T]):
def __init__(self, collection: List[T]) -> None:
self._collection = collection
self._position = 0

# Test the generator
def count_to_two() -> None:
return count_to(2)
def __next__(self) -> T:
try:
value = self._collection[self._position]
self._position += 1
except IndexError:
raise StopIteration()
return value


def count_to_five() -> None:
return count_to(5)
class WordsCollection(Iterable[T], Generic[T]):
def __init__(self, collection: List[T] = []) -> None:
self._collection = collection

def __iter__(self) -> AlphabeticalOrderIterator[T]:
return AlphabeticalOrderIterator(self._collection)

def main():
"""
# Counting to two...
>>> for number in count_to_two():
... print(number)
one
two

# Counting to five...
>>> for number in count_to_five():
... print(number)
one
two
three
four
five
"""
def add_item(self, item: T) -> None:
self._collection.append(item)


if __name__ == "__main__":
import doctest

doctest.testmod()
collection = WordsCollection[str]()
collection.add_item("First")
collection.add_item("Second")
collection.add_item("Third")
print("\n".join(collection))
Loading