├── tests
├── __init__.py
├── creational
│ ├── __init__.py
│ ├── test_builder.py
│ ├── test_factory_method.py
│ └── test_abstact_factory.py
└── marker.py
├── patterns
├── __init__.py
├── behavioral
│ ├── __init__.py
│ ├── iterator.py
│ ├── strategy.py
│ ├── chain_of_responsibility.py
│ ├── visitor.py
│ └── observer.py
├── creational
│ ├── __init__.py
│ ├── prototype.py
│ ├── factory_method.py
│ ├── builder.py
│ ├── singleton.py
│ └── abstract_factory.py
└── structural
│ ├── __init__.py
│ ├── proxy.py
│ ├── adapter.py
│ ├── composite.py
│ ├── decorator.py
│ ├── bridge.py
│ ├── facade.py
│ └── mvc.py
├── _config.yml
├── logo.png
├── pyproject.toml
├── .gitcommit.txt
├── requirements.txt
├── .gitignore
├── .travis.yml
├── pytest.ini
├── CHANGELOG.md
├── .github
└── workflows
│ └── code-assessment.yml
├── run-code-analysis.sh
├── LICENSE.md
├── .pylintrc
└── README.md
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/patterns/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/creational/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/patterns/behavioral/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/patterns/creational/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/patterns/structural/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vyahello/python-ood/HEAD/logo.png
--------------------------------------------------------------------------------
/tests/marker.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from _pytest.mark import MarkDecorator
3 |
4 | unittest: MarkDecorator = pytest.mark.unittest
5 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.black]
2 | line-length = 80
3 | target-version = ["py33", "py34", "py35" ,"py36", "py37", "py38"]
4 | exclude = '''
5 | /(
6 | \.pytest_cache
7 | )/
8 | '''
9 |
--------------------------------------------------------------------------------
/.gitcommit.txt:
--------------------------------------------------------------------------------
1 | Title
2 |
3 | [Problem]
4 | .
5 |
6 | [Solution]
7 | .
8 |
9 |
10 | # Please run the following command to activate commit message template:
11 | # > git config commit.template .gitcommit.txt
12 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pytest==7.0.1
2 | pytest-emoji-output==0.2.1
3 | pytest-html==2.0.1
4 | pytest-clarity==1.0.1
5 | pytest-cov==2.8.1
6 | pytest-instafail==0.4.1.post0
7 | coveralls==1.9.2
8 | coverage==4.5.4
9 | pylint==2.6.0
10 | black==24.3.0
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Pycharm trash
2 | .idea
3 | __pycache__/
4 |
5 |
6 | # virtualenv
7 | .venv
8 | venv/
9 | venv
10 |
11 | # Distribution
12 | .DS_Store
13 | .cache/
14 |
15 | # python identifiers
16 | .python-version
17 |
18 | # test trash
19 | test-report.html
20 | .coverage
21 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "3.6"
4 | - "3.7"
5 | - "3.8"
6 | - "3.9"
7 | install:
8 | - pip install pip==20.2.0
9 | - pip install -r requirements.txt
10 | script:
11 | - ./run-code-analysis.sh
12 | after_success:
13 | - coveralls
14 | notifications:
15 | email: false
16 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | markers =
3 | unittest: unittest tests marker
4 | testpaths=tests
5 | python_files=*.py
6 | python_functions=test_*
7 | addopts = -rsxX
8 | -v
9 | --self-contained-html
10 | --html=test-report.html
11 | --cov=patterns
12 | --emoji-out
13 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Versions
2 | ========
3 |
4 | * 0.2.1
5 | - Use unittest pytestmark for factory method tests
6 | - Use coverage package for CI
7 | - Fix docs style for github pages
8 | - Support python 3.9
9 | - Bump requirements packages
10 | - Use 80 chars for code
11 | * 0.2.0
12 | - Add unit tests coverage
13 | * 0.1.1
14 | - Polish documentation
15 | * 0.1.0
16 | - Distribute first version of a project
17 |
--------------------------------------------------------------------------------
/.github/workflows/code-assessment.yml:
--------------------------------------------------------------------------------
1 | name: Python source code assessment 🐍
2 | on: [push]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | strategy:
7 | max-parallel: 4
8 | matrix:
9 | python-version:
10 | - 3.9.20
11 | - 3.10.15
12 | steps:
13 | - uses: actions/checkout@v1
14 | - name: Set up Python ${{ matrix.python-version }}
15 | uses: actions/setup-python@v1
16 | with:
17 | python-version: ${{ matrix.python-version }}
18 | - name: Static code analysis
19 | run: |
20 | pip install pip==20.2.0
21 | pip install -r requirements.txt
22 | ./run-code-analysis.sh
23 |
--------------------------------------------------------------------------------
/patterns/behavioral/iterator.py:
--------------------------------------------------------------------------------
1 | from typing import Iterator, Tuple, List
2 |
3 |
4 | def count_to(count: int) -> Iterator[Tuple[int, str]]:
5 | """Our iterator implementation."""
6 | numbers_in_german: List[str] = ["einn", "zwei", "drei", "veir", "funf"]
7 | iterator: Iterator[Tuple[int, str]] = zip(
8 | range(1, count + 1), numbers_in_german
9 | )
10 | for position, number in iterator: # type: int, str
11 | yield position, number
12 |
13 |
14 | for number_ in count_to(3): # type: Tuple[int]
15 | print("{} in german is {}".format(*number_))
16 |
17 |
18 | class IteratorSequence:
19 | """Represent iterator sequence object."""
20 |
21 | def __init__(self, capacity: int) -> None:
22 | self._range: Iterator[int] = iter(range(capacity))
23 |
24 | def __next__(self) -> int:
25 | return next(self._range)
26 |
27 | def __iter__(self) -> Iterator[int]:
28 | return self
29 |
30 |
31 | iterator_: IteratorSequence = IteratorSequence(capacity=10)
32 | for _ in range(10): # type: int
33 | print(next(iterator_))
34 |
--------------------------------------------------------------------------------
/patterns/structural/proxy.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 |
4 | class Producer:
5 | """Defines the resource-intensive object to instantiate."""
6 |
7 | def produce(self) -> None:
8 | print("Producer is working hard!")
9 |
10 | def meet(self) -> None:
11 | print("Producer has time to meet you now")
12 |
13 |
14 | class Proxy:
15 | """Defines the less resource-intensive object to instantiate as a middleman."""
16 |
17 | def __init__(self):
18 | self._occupied: bool = False
19 |
20 | @property
21 | def occupied(self) -> bool:
22 | return self._occupied
23 |
24 | @occupied.setter
25 | def occupied(self, state: bool) -> None:
26 | if not isinstance(state, bool):
27 | raise ValueError(f'"{state}" value should be a boolean data type!')
28 | self._occupied = state
29 |
30 | def produce(self) -> None:
31 | print("Artist checking if producer is available ...")
32 | if not self.occupied:
33 | producer: Producer = Producer()
34 | time.sleep(2)
35 | producer.meet()
36 | else:
37 | time.sleep(2)
38 | print("Producer is busy!")
39 |
40 |
41 | proxy: Proxy = Proxy()
42 | proxy.produce()
43 | proxy.occupied = True
44 | proxy.produce()
45 |
--------------------------------------------------------------------------------
/patterns/behavioral/strategy.py:
--------------------------------------------------------------------------------
1 | import types
2 | from typing import Callable, Any
3 |
4 |
5 | class Strategy:
6 | """A strategy pattern class."""
7 |
8 | def __init__(self, func: Callable[["Strategy"], Any] = None) -> None:
9 | self._name: str = "Default strategy"
10 | if func:
11 | self.execute = types.MethodType(func, self)
12 |
13 | @property
14 | def name(self) -> str:
15 | return self._name
16 |
17 | @name.setter
18 | def name(self, name: str) -> None:
19 | if not isinstance(name, str):
20 | raise ValueError(f'"{name}" value should be a string data type!')
21 | self._name = name
22 |
23 | def execute(self):
24 | print(f"{self._name} is used")
25 |
26 |
27 | def strategy_function_one(strategy: Strategy) -> None:
28 | print(f"{strategy.name} is used to execute method one")
29 |
30 |
31 | def strategy_function_two(strategy: Strategy) -> None:
32 | print(f"{strategy.name} is used to execute method two")
33 |
34 |
35 | default_strategy = Strategy()
36 | default_strategy.execute()
37 |
38 | first_strategy = Strategy(func=strategy_function_one)
39 | first_strategy.name = "Strategy one"
40 | first_strategy.execute()
41 |
42 | second_strategy = Strategy(func=strategy_function_two)
43 | second_strategy.name = "Strategy two"
44 | second_strategy.execute()
45 |
--------------------------------------------------------------------------------
/patterns/structural/adapter.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Any
3 |
4 |
5 | class Speaker(ABC):
6 | """Abstract interface for some speaker."""
7 |
8 | @abstractmethod
9 | def type(self) -> str:
10 | pass
11 |
12 |
13 | class Korean(Speaker):
14 | """Korean speaker."""
15 |
16 | def __init__(self) -> None:
17 | self._type: str = "Korean"
18 |
19 | def type(self) -> str:
20 | return self._type
21 |
22 | def speak_korean(self) -> str:
23 | return "An-neyong?"
24 |
25 |
26 | class British(Speaker):
27 | """English speaker."""
28 |
29 | def __init__(self):
30 | self._type: str = "British"
31 |
32 | def type(self) -> str:
33 | return self._type
34 |
35 | def speak_english(self) -> str:
36 | return "Hello"
37 |
38 |
39 | class Adapter:
40 | """Changes the generic method name to individualized method names."""
41 |
42 | def __init__(self, obj: Any, **adapted_method: Any) -> None:
43 | self._object = obj
44 | self.__dict__.update(adapted_method)
45 |
46 | def __getattr__(self, item: Any) -> Any:
47 | return getattr(self._object, item)
48 |
49 |
50 | speakers: list = []
51 | korean = Korean()
52 | british = British()
53 | speakers.append(Adapter(korean, speak=korean.speak_korean))
54 | speakers.append(Adapter(british, speak=british.speak_english))
55 |
56 | for speaker in speakers:
57 | print(f"{speaker.type()} says '{speaker.speak()}'")
58 |
--------------------------------------------------------------------------------
/tests/creational/test_builder.py:
--------------------------------------------------------------------------------
1 | # pylint:disable=protected-access
2 | import pytest
3 |
4 | from patterns.creational.builder import (
5 | Builder,
6 | Car,
7 | Director,
8 | Machine,
9 | SkyLarkBuilder,
10 | )
11 |
12 |
13 | @pytest.fixture
14 | def car() -> Machine:
15 | return Car()
16 |
17 |
18 | @pytest.fixture
19 | def skylark_builder() -> Builder:
20 | return SkyLarkBuilder()
21 |
22 |
23 | @pytest.fixture
24 | def director(skylark_builder: Builder) -> Director:
25 | return Director(skylark_builder)
26 |
27 |
28 | def test_car_summary(car: Machine) -> None:
29 | assert car.summary() == "Car details: None | None | None"
30 |
31 |
32 | def test_skylark_builder_machine(skylark_builder: Builder) -> None:
33 | assert isinstance(skylark_builder.machine(), Machine)
34 |
35 |
36 | def test_skylark_builder_add_model(skylark_builder: Builder) -> None:
37 | skylark_builder.add_model()
38 | assert skylark_builder._car.model == "SkyBuilder model"
39 |
40 |
41 | def test_skylark_builder_add_tires(skylark_builder: Builder) -> None:
42 | skylark_builder.add_tires()
43 | assert skylark_builder._car.tires == "Motosport tires"
44 |
45 |
46 | def test_skylark_builder_add_engine(skylark_builder: Builder) -> None:
47 | skylark_builder.add_engine()
48 | assert skylark_builder._car.engine == "GM Motors engine"
49 |
50 |
51 | def test_director_release_machine(director: Director) -> None:
52 | assert isinstance(director.release_machine(), Machine)
53 |
--------------------------------------------------------------------------------
/patterns/creational/prototype.py:
--------------------------------------------------------------------------------
1 | import copy
2 | from abc import ABC, abstractmethod
3 | from typing import Dict, Any
4 |
5 |
6 | class Machine(ABC):
7 | """Abstract machine interface."""
8 |
9 | @abstractmethod
10 | def summary(self) -> str:
11 | pass
12 |
13 |
14 | class Car(Machine):
15 | """A car object."""
16 |
17 | def __init__(self) -> None:
18 | self._name: str = "Skylar"
19 | self._color: str = "Red"
20 | self._options: str = "Ex"
21 |
22 | def summary(self) -> str:
23 | return "Car details: {} | {} | {}".format(
24 | self._name, self._color, self._options
25 | )
26 |
27 |
28 | class Prototype:
29 | """A prototype object."""
30 |
31 | def __init__(self) -> None:
32 | self._elements: Dict[Any, Any] = {}
33 |
34 | def register_object(self, name: str, machine: Machine) -> None:
35 | self._elements[name] = machine
36 |
37 | def unregister_object(self, name: str) -> None:
38 | del self._elements[name]
39 |
40 | def clone(self, name: str, **attr: Any) -> Car:
41 | obj: Any = copy.deepcopy(self._elements[name])
42 | obj.__dict__.update(attr)
43 | return obj
44 |
45 |
46 | # prototypical car object to be cloned
47 | primary_car: Machine = Car()
48 | print(primary_car.summary())
49 | prototype: Prototype = Prototype()
50 | prototype.register_object("skylark", primary_car)
51 |
52 | # clone a car object
53 | cloned_car: Machine = prototype.clone("skylark")
54 | print(cloned_car.summary())
55 |
--------------------------------------------------------------------------------
/patterns/behavioral/chain_of_responsibility.py:
--------------------------------------------------------------------------------
1 | from abc import abstractmethod
2 | from typing import List
3 |
4 |
5 | class Handler:
6 | """Abstract handler."""
7 |
8 | def __init__(self, successor: "Handler") -> None:
9 | self._successor: Handler = successor
10 |
11 | def handler(self, request: int) -> None:
12 | if not self.handle(request):
13 | self._successor.handler(request)
14 |
15 | @abstractmethod
16 | def handle(self, request: int) -> bool:
17 | pass
18 |
19 |
20 | class ConcreteHandler1(Handler):
21 | """Concrete handler 1."""
22 |
23 | def handle(self, request: int) -> bool:
24 | if 0 < request <= 10:
25 | print(f"Request {request} handled in handler 1")
26 | return True
27 | return False
28 |
29 |
30 | class DefaultHandler(Handler):
31 | """Default handler."""
32 |
33 | def handle(self, request: int) -> bool:
34 | """If there is no handler available."""
35 | print(f"End of chain, no handler for {request}")
36 | return True
37 |
38 |
39 | class Client:
40 | """Using handlers."""
41 |
42 | def __init__(self) -> None:
43 | self._handler: Handler = ConcreteHandler1(DefaultHandler(None))
44 |
45 | def delegate(self, request: List[int]) -> None:
46 | for next_request in request:
47 | self._handler.handler(next_request)
48 |
49 |
50 | # Create a client
51 | client: Client = Client()
52 |
53 | # Create requests
54 | requests: List[int] = [2, 5, 30]
55 |
56 | # Send the request
57 | client.delegate(requests)
58 |
--------------------------------------------------------------------------------
/tests/creational/test_factory_method.py:
--------------------------------------------------------------------------------
1 | from typing import Type
2 | import pytest
3 | from patterns.creational.factory_method import (
4 | Shape,
5 | Circle,
6 | Square,
7 | ShapeFactory,
8 | ShapeError,
9 | Dog,
10 | Cat,
11 | Pet,
12 | get_pet,
13 | )
14 | from tests.marker import unittest
15 |
16 | pytestmark = unittest
17 |
18 |
19 | @pytest.fixture(scope="module")
20 | def circle() -> Shape:
21 | return Circle()
22 |
23 |
24 | @pytest.fixture(scope="module")
25 | def square() -> Shape:
26 | return Square()
27 |
28 |
29 | def test_draw_circle(circle: Shape) -> None:
30 | assert circle.draw() == "Circle.draw"
31 |
32 |
33 | def test_draw_square(square: Shape) -> None:
34 | assert square.draw() == "Square.draw"
35 |
36 |
37 | @pytest.mark.parametrize(
38 | "shape, instance", (("circle", Circle), ("square", Square))
39 | )
40 | def test_shape_factory(shape: str, instance: Type[Shape]) -> None:
41 | assert isinstance(ShapeFactory(shape).get_shape(), instance)
42 |
43 |
44 | def test_shape_error() -> None:
45 | with pytest.raises(ShapeError):
46 | ShapeFactory("fooo").get_shape()
47 |
48 |
49 | def test_dog_speak() -> None:
50 | assert Dog("Spike").speak() == "Spike says Woof!"
51 |
52 |
53 | def test_cat_speak() -> None:
54 | assert Cat("Miya").speak() == "Miya says Meow!"
55 |
56 |
57 | @pytest.mark.parametrize("pet, instance", (("dog", Dog), ("cat", Cat)))
58 | def test_get_pet(pet: str, instance: Type[Pet]) -> None:
59 | assert isinstance(get_pet(pet), instance)
60 |
61 |
62 | def test_get_wrong_pet() -> None:
63 | with pytest.raises(KeyError):
64 | get_pet("foo")
65 |
--------------------------------------------------------------------------------
/patterns/behavioral/visitor.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 |
4 | class Visitor(ABC):
5 | """Abstract visitor."""
6 |
7 | @abstractmethod
8 | def visit(self, house: "House") -> None:
9 | pass
10 |
11 | def __str__(self) -> str:
12 | return self.__class__.__name__
13 |
14 |
15 | class House(ABC):
16 | """Abstract house."""
17 |
18 | @abstractmethod
19 | def accept(self, visitor: Visitor) -> None:
20 | pass
21 |
22 | @abstractmethod
23 | def work_on_hvac(self, specialist: Visitor) -> None:
24 | pass
25 |
26 | @abstractmethod
27 | def work_on_electricity(self, specialist: Visitor) -> None:
28 | pass
29 |
30 | def __str__(self) -> str:
31 | return self.__class__.__name__
32 |
33 |
34 | class ConcreteHouse(House):
35 | """Represent concrete house."""
36 |
37 | def accept(self, visitor: Visitor) -> None:
38 | visitor.visit(self)
39 |
40 | def work_on_hvac(self, specialist: Visitor) -> None:
41 | print(self, "worked on by", specialist)
42 |
43 | def work_on_electricity(self, specialist: Visitor) -> None:
44 | print(self, "worked on by", specialist)
45 |
46 |
47 | class HvacSpecialist(Visitor):
48 | """Concrete visitor: HVAC specialist."""
49 |
50 | def visit(self, house: House) -> None:
51 | house.work_on_hvac(self)
52 |
53 |
54 | class Electrician(Visitor):
55 | """Concrete visitor: electrician."""
56 |
57 | def visit(self, house: House) -> None:
58 | house.work_on_electricity(self)
59 |
60 |
61 | hvac: Visitor = HvacSpecialist()
62 | electrician: Visitor = Electrician()
63 | home: House = ConcreteHouse()
64 | home.accept(hvac)
65 | home.accept(electrician)
66 |
--------------------------------------------------------------------------------
/patterns/structural/composite.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Sequence, List
3 |
4 |
5 | class Component(ABC):
6 | """Abstract interface of some component."""
7 |
8 | @abstractmethod
9 | def function(self) -> None:
10 | pass
11 |
12 |
13 | class Child(Component):
14 | """Concrete child component."""
15 |
16 | def __init__(self, *args: str) -> None:
17 | self._args: Sequence[str] = args
18 |
19 | def name(self) -> str:
20 | return self._args[0]
21 |
22 | def function(self) -> None:
23 | print(f'"{self.name()}" component')
24 |
25 |
26 | class Composite(Component):
27 | """Concrete class maintains the tree recursive structure."""
28 |
29 | def __init__(self, *args: str) -> None:
30 | self._args: Sequence[str] = args
31 | self._children: List[Component] = []
32 |
33 | def name(self) -> str:
34 | return self._args[0]
35 |
36 | def append_child(self, child: Component) -> None:
37 | self._children.append(child)
38 |
39 | def remove_child(self, child: Component) -> None:
40 | self._children.remove(child)
41 |
42 | def function(self) -> None:
43 | print(f'"{self.name()}" component')
44 | for child in self._children: # type: Component
45 | child.function()
46 |
47 |
48 | top_menu = Composite("top_menu")
49 |
50 | submenu_one = Composite("submenu one")
51 | child_submenu_one = Child("sub_submenu one")
52 | child_submenu_two = Child("sub_submenu two")
53 | submenu_one.append_child(child_submenu_one)
54 | submenu_one.append_child(child_submenu_two)
55 |
56 | submenu_two = Child("submenu two")
57 | top_menu.append_child(submenu_one)
58 | top_menu.append_child(submenu_two)
59 | top_menu.function()
60 |
--------------------------------------------------------------------------------
/patterns/structural/decorator.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 | from typing import Callable
3 | from abc import ABC, abstractmethod
4 |
5 |
6 | def make_blink(function: Callable[[str], str]) -> Callable[..., str]:
7 | """Defines the decorator function."""
8 |
9 | @wraps(function)
10 | def decorator(*args, **kwargs) -> str:
11 | result: str = function(*args, **kwargs)
12 | return f""
13 |
14 | return decorator
15 |
16 |
17 | @make_blink
18 | def hello_world(name: str) -> str:
19 | """Original function."""
20 | return f'Hello World said "{name}"!'
21 |
22 |
23 | print(hello_world(name="James"))
24 | print(hello_world.__name__)
25 | print(hello_world.__doc__)
26 |
27 |
28 | class Number(ABC):
29 | """Abstraction of a number object."""
30 |
31 | @abstractmethod
32 | def value(self) -> int:
33 | pass
34 |
35 |
36 | class Integer(Number):
37 | """A subclass of a number."""
38 |
39 | def __init__(self, value: int) -> None:
40 | self._value = value
41 |
42 | def value(self) -> int:
43 | return self._value
44 |
45 |
46 | class Float(Number):
47 | """Decorator object converts `int` datatype into `float` datatype."""
48 |
49 | def __init__(self, number: Number) -> None:
50 | self._number: Number = number
51 |
52 | def value(self) -> float:
53 | return float(self._number.value())
54 |
55 |
56 | class SumOfFloat(Number):
57 | """Sum of two `float` numbers."""
58 |
59 | def __init__(self, one: Number, two: Number) -> None:
60 | self._one: Float = Float(one)
61 | self._two: Float = Float(two)
62 |
63 | def value(self) -> float:
64 | return self._one.value() + self._two.value()
65 |
66 |
67 | integer_one: Number = Integer(value=5)
68 | integer_two: Number = Integer(value=6)
69 | sum_float: Number = SumOfFloat(integer_one, integer_two)
70 | print(sum_float.value())
71 |
--------------------------------------------------------------------------------
/patterns/structural/bridge.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 |
4 | class DrawApi(ABC):
5 | """Provides draw interface."""
6 |
7 | @abstractmethod
8 | def draw_circle(self, x: int, y: int, radius: int) -> None:
9 | pass
10 |
11 |
12 | class Circle(ABC):
13 | """Provides circle shape interface."""
14 |
15 | @abstractmethod
16 | def draw(self) -> None:
17 | pass
18 |
19 | @abstractmethod
20 | def scale(self, percentage: int) -> None:
21 | pass
22 |
23 |
24 | class DrawApiOne(DrawApi):
25 | """Implementation-specific abstraction: concrete class one."""
26 |
27 | def draw_circle(self, x: int, y: int, radius: int) -> None:
28 | print(f"API 1 drawing a circle at ({x}, {y} with radius {radius}!)")
29 |
30 |
31 | class DrawApiTwo(DrawApi):
32 | """Implementation-specific abstraction: concrete class two."""
33 |
34 | def draw_circle(self, x: int, y: int, radius: int) -> None:
35 | print(f"API 2 drawing a circle at ({x}, {y} with radius {radius}!)")
36 |
37 |
38 | class DrawCircle(Circle):
39 | """Implementation-independent abstraction: e.g there could be a rectangle class!."""
40 |
41 | def __init__(self, x: int, y: int, radius: int, draw_api: DrawApi) -> None:
42 | self._x: int = x
43 | self._y: int = y
44 | self._radius: int = radius
45 | self._draw_api: DrawApi = draw_api
46 |
47 | def draw(self) -> None:
48 | self._draw_api.draw_circle(self._x, self._y, self._radius)
49 |
50 | def scale(self, percentage: int) -> None:
51 | if not isinstance(percentage, int):
52 | raise ValueError(
53 | f'"{percentage}" value should be an integer data type!'
54 | )
55 | self._radius *= percentage
56 |
57 |
58 | circle_one: Circle = DrawCircle(1, 2, 3, DrawApiOne())
59 | circle_one.draw()
60 | circle_two: Circle = DrawCircle(3, 4, 6, DrawApiTwo())
61 | circle_two.draw()
62 |
--------------------------------------------------------------------------------
/patterns/behavioral/observer.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 |
4 | class Subject:
5 | """Represents what is being observed. Needs to be monitored."""
6 |
7 | def __init__(self, name: str = "") -> None:
8 | self._observers: List["TempObserver"] = []
9 | self._name: str = name
10 | self._temperature: int = 0
11 |
12 | def attach(self, observer: "TempObserver") -> None:
13 | if observer not in self._observers:
14 | self._observers.append(observer)
15 |
16 | def detach(self, observer: "TempObserver") -> None:
17 | try:
18 | self._observers.remove(observer)
19 | except ValueError:
20 | pass
21 |
22 | def notify(self, modifier=None) -> None:
23 | for observer in self._observers:
24 | if modifier != observer:
25 | observer.update(self)
26 |
27 | @property
28 | def name(self) -> str:
29 | return self._name
30 |
31 | @property
32 | def temperature(self) -> int:
33 | return self._temperature
34 |
35 | @temperature.setter
36 | def temperature(self, temperature: int) -> None:
37 | if not isinstance(temperature, int):
38 | raise ValueError(
39 | f'"{temperature}" value should be an integer data type!'
40 | )
41 | self._temperature = temperature
42 |
43 |
44 | class TempObserver:
45 | """Represents an observer class. Needs to be notified."""
46 |
47 | def update(self, subject: Subject) -> None:
48 | print(
49 | f"Temperature Viewer: {subject.name} has Temperature {subject.temperature}"
50 | )
51 |
52 |
53 | subject_one = Subject("Subject One")
54 | subject_two = Subject("Subject Two")
55 |
56 | observer_one = TempObserver()
57 | observer_two = TempObserver()
58 |
59 | subject_one.attach(observer_one)
60 | subject_one.attach(observer_two)
61 |
62 | subject_one.temperature = 80
63 | subject_one.notify()
64 |
65 | subject_one.temperature = 90
66 | subject_one.notify()
67 |
--------------------------------------------------------------------------------
/patterns/creational/factory_method.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 |
4 | class Shape(ABC):
5 | """Interface that defines the shape."""
6 |
7 | @abstractmethod
8 | def draw(self) -> str:
9 | pass
10 |
11 |
12 | class ShapeError(Exception):
13 | """Represent shape error message."""
14 |
15 | pass
16 |
17 |
18 | class Circle(Shape):
19 | """Concrete shape subclass."""
20 |
21 | def draw(self) -> str:
22 | return "Circle.draw"
23 |
24 |
25 | class Square(Shape):
26 | """Concrete shape subclass."""
27 |
28 | def draw(self) -> str:
29 | return "Square.draw"
30 |
31 |
32 | class ShapeFactory:
33 | """Concrete shape factory."""
34 |
35 | def __init__(self, shape: str) -> None:
36 | self._shape: str = shape
37 |
38 | def get_shape(self) -> Shape:
39 | if self._shape == "circle":
40 | return Circle()
41 | if self._shape == "square":
42 | return Square()
43 | raise ShapeError(f'Could not find shape "{self._shape}"')
44 |
45 |
46 | class Pet(ABC):
47 | """Abstraction of a pet."""
48 |
49 | @abstractmethod
50 | def speak(self) -> str:
51 | """Interface for a pet to speak."""
52 | pass
53 |
54 |
55 | class Dog(Pet):
56 | """A simple dog class."""
57 |
58 | def __init__(self, name: str) -> None:
59 | self._dog_name: str = name
60 |
61 | def speak(self) -> str:
62 | return f"{self._dog_name} says Woof!"
63 |
64 |
65 | class Cat(Pet):
66 | """A simple cat class."""
67 |
68 | def __init__(self, name: str) -> None:
69 | self._cat_name: str = name
70 |
71 | def speak(self) -> str:
72 | return f"{self._cat_name} says Meow!"
73 |
74 |
75 | def get_pet(pet: str) -> Pet:
76 | """The factory method."""
77 | return {"dog": Dog("Hope"), "cat": Cat("Faith")}[pet]
78 |
79 |
80 | if __name__ == "__main__":
81 | factory: ShapeFactory = ShapeFactory(shape="circle")
82 | circle: Shape = factory.get_shape() # returns our shape
83 | circle.draw() # draw a circle
84 |
85 | # returns Cat class object
86 | get_pet("cat")
87 |
--------------------------------------------------------------------------------
/patterns/creational/builder.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 |
4 | class Machine(ABC):
5 | """Abstract machine interface."""
6 |
7 | @abstractmethod
8 | def summary(self) -> str:
9 | pass
10 |
11 |
12 | class Builder(ABC):
13 | """Abstract builder interface."""
14 |
15 | @abstractmethod
16 | def add_model(self) -> None:
17 | pass
18 |
19 | @abstractmethod
20 | def add_tires(self) -> None:
21 | pass
22 |
23 | @abstractmethod
24 | def add_engine(self) -> None:
25 | pass
26 |
27 | @abstractmethod
28 | def machine(self) -> Machine:
29 | pass
30 |
31 |
32 | class Car(Machine):
33 | """A car product."""
34 |
35 | def __init__(self) -> None:
36 | self.model: str = None
37 | self.tires: str = None
38 | self.engine: str = None
39 |
40 | def summary(self) -> str:
41 | return "Car details: {} | {} | {}".format(
42 | self.model, self.tires, self.engine
43 | )
44 |
45 |
46 | class SkyLarkBuilder(Builder):
47 | """Provides parts and tools to work on the car parts."""
48 |
49 | def __init__(self) -> None:
50 | self._car: Machine = Car()
51 |
52 | def add_model(self) -> None:
53 | self._car.model = "SkyBuilder model"
54 |
55 | def add_tires(self) -> None:
56 | self._car.tires = "Motosport tires"
57 |
58 | def add_engine(self) -> None:
59 | self._car.engine = "GM Motors engine"
60 |
61 | def machine(self) -> Machine:
62 | return self._car
63 |
64 |
65 | class Director:
66 | """A director. Responsible for `Car` assembling."""
67 |
68 | def __init__(self, builder_: Builder) -> None:
69 | self._builder: Builder = builder_
70 |
71 | def construct_machine(self) -> None:
72 | self._builder.add_model()
73 | self._builder.add_tires()
74 | self._builder.add_engine()
75 |
76 | def release_machine(self) -> Machine:
77 | return self._builder.machine()
78 |
79 |
80 | builder: Builder = SkyLarkBuilder()
81 | director: Director = Director(builder)
82 | director.construct_machine()
83 | car: Machine = director.release_machine()
84 | print(car.summary())
85 |
--------------------------------------------------------------------------------
/run-code-analysis.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | declare -a RESULT
4 |
5 | # specifies a set of variables to declare CLI output color
6 | FAILED_OUT="\033[0;31m"
7 | PASSED_OUT="\033[0;32m"
8 | NONE_OUT="\033[0m"
9 |
10 | # specifies a set of variables to declare files to be used for code assessment
11 | PROJECT_FILES="./"
12 |
13 | function store-failures {
14 | RESULT+=("$1")
15 | }
16 |
17 |
18 | function remove-pycache-trash {
19 | local PYCACHE_DIR="__pycache__"
20 | echo "Removing ${PYCACHE_DIR} directories if present ..."
21 | ( find . -d -name ${PYCACHE_DIR} | xargs rm -r ) || echo -e "No ${PYCACHE_DIR} found"
22 | }
23 |
24 |
25 | function remove-analysis-trash {
26 | local PYTEST_CACHE_DIR='.pytest_cache'
27 | local MYPY_CACHE_DIR='.mypy_cache'
28 | echo "Removing code analysis trash if present ..."
29 | [[ -d "$PYTEST_CACHE_DIR" ]] && rm -rf ${PYTEST_CACHE_DIR} && echo "pytest trash is removed"
30 | [[ -d "$MYPY_CACHE_DIR" ]] && rm -rf ${MYPY_CACHE_DIR} && echo "mypy trash is removed"
31 | }
32 |
33 |
34 | function install-dependencies {
35 | echo "Installing python code analysis packages ..." \
36 | && ( pip install --no-cache-dir --upgrade pip ) \
37 | && ( pip install --no-cache-dir -r requirements-dev.txt )
38 | }
39 |
40 |
41 | function run-unittests {
42 | echo "Running unittests ..." && ( pytest -m unittest )
43 | }
44 |
45 |
46 | function run-pylint-analysis() {
47 | echo "Running pylint analysis ..." && ( pylint $(find "${PROJECT_FILES}" -iname "*.py") )
48 | }
49 |
50 |
51 | function run-black-analysis() {
52 | echo "Running black analysis ..." && ( black --check "${PROJECT_FILES}" )
53 | }
54 |
55 |
56 | function run-code-analysis {
57 | echo "Running code analysis ..."
58 | remove-pycache-trash
59 | run-unittests || store-failures "Unittests are failed!"
60 | run-pylint-analysis || store-failures "pylint analysis is failed!"
61 | run-black-analysis || store-failures "black analysis is failed!"
62 |
63 | if [[ ${#RESULT[@]} -ne 0 ]];
64 | then echo -e "${FAILED_OUT}Some errors occurred while analysing the code quality.${NONE_OUT}"
65 | for failed_item in "${RESULT[@]}"; do
66 | echo -e "${FAILED_OUT}- ${failed_item}${NONE_OUT}"
67 | done
68 | remove-analysis-trash
69 | exit 1
70 | fi
71 | remove-analysis-trash
72 | echo -e "${PASSED_OUT}Code analysis is passed${NONE_OUT}"
73 | }
74 |
75 |
76 | function main() {
77 | if [[ "$1" == "install-dependencies" ]];
78 | then install-dependencies || store-failures "Python packages installation is failed!";
79 | fi
80 | run-code-analysis
81 | }
82 |
83 |
84 | main "$@"
--------------------------------------------------------------------------------
/patterns/creational/singleton.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict
2 |
3 |
4 | class SingletonMeta(type):
5 | """Singleton metaclass implementation."""
6 |
7 | def __init__(cls, cls_name: str, bases: tuple, namespace: dict):
8 | cls.__instance = None
9 | super().__init__(cls_name, bases, namespace)
10 |
11 | def __call__(cls, *args, **kwargs):
12 | if cls.__instance is None:
13 | cls.__instance = super().__call__(*args, **kwargs)
14 | return cls.__instance
15 | return cls.__instance
16 |
17 |
18 | class Single(metaclass=SingletonMeta):
19 | """Singleton object."""
20 |
21 | pass
22 |
23 |
24 | class Singleton:
25 | """Makes all instances as the same object."""
26 |
27 | _instance: "Singleton"
28 |
29 | def __new__(cls) -> "Singleton":
30 | if not hasattr(cls, "_instance"):
31 | cls._instance = super().__new__(cls)
32 | return cls._instance
33 |
34 |
35 | def singleton(cls: Any) -> Any:
36 | """A singleton decorator."""
37 | instances: Dict[Any, Any] = {}
38 |
39 | def get_instance() -> Any:
40 | if cls not in instances:
41 | instances[cls] = cls()
42 | return instances[cls]
43 |
44 | return get_instance
45 |
46 |
47 | @singleton
48 | class Bar:
49 | """A fancy object."""
50 |
51 | pass
52 |
53 |
54 | print(Single() is Single())
55 |
56 | singleton_one: Singleton = Singleton()
57 | singleton_two: Singleton = Singleton()
58 |
59 | print(id(singleton_one))
60 | print(id(singleton_two))
61 | print(singleton_one is singleton_two)
62 |
63 | bar_one: Bar = Bar()
64 | bar_two: Bar = Bar()
65 | print(id(bar_one))
66 | print(id(bar_two))
67 | print(bar_one is bar_two)
68 |
69 |
70 | class Borg:
71 | """Borg class making class attributes global.
72 | Safe the same state of all instances but instances are all different."""
73 |
74 | _shared_state: Dict[Any, Any] = {}
75 |
76 | def __init__(self) -> None:
77 | self.__dict__ = self._shared_state
78 |
79 |
80 | class BorgSingleton(Borg):
81 | """This class shares all its attribute among its instances. Store the same state."""
82 |
83 | def __init__(self, **kwargs: Any) -> None:
84 | Borg.__init__(self)
85 | self._shared_state.update(kwargs)
86 |
87 | def __str__(self) -> str:
88 | return str(self._shared_state)
89 |
90 |
91 | # Create a singleton object and add out first acronym
92 | x: Borg = BorgSingleton(HTTP="Hyper Text Transfer Protocol")
93 | print(x)
94 |
95 | # Create another singleton which will add to the existent dict attribute
96 | y: Borg = BorgSingleton(SNMP="Simple Network Management Protocol")
97 | print(y)
98 |
--------------------------------------------------------------------------------
/tests/creational/test_abstact_factory.py:
--------------------------------------------------------------------------------
1 | from typing import Sequence
2 | import pytest
3 | from patterns.creational.abstract_factory import (
4 | Pet,
5 | PetFactory,
6 | Dog,
7 | Cat,
8 | DogFood,
9 | CatFood,
10 | DogFactory,
11 | CatFactory,
12 | FluffyStore,
13 | PetStore,
14 | )
15 | from tests.marker import unittest
16 |
17 |
18 | @pytest.fixture(scope="module")
19 | def dog() -> Pet:
20 | return Dog(name="Spike", type_="bulldog")
21 |
22 |
23 | @pytest.fixture(scope="module")
24 | def cat() -> Pet:
25 | return Cat(name="Miya", type_="persian")
26 |
27 |
28 | @pytest.fixture(scope="module")
29 | def dog_factory() -> PetFactory:
30 | return DogFactory()
31 |
32 |
33 | @pytest.fixture(scope="module")
34 | def cat_factory() -> PetFactory:
35 | return CatFactory()
36 |
37 |
38 | @unittest
39 | def test_dog_speak(dog: Pet) -> None:
40 | assert dog.speak() == '"Spike" says Woof!'
41 |
42 |
43 | @unittest
44 | def test_dog_type(dog: Pet) -> None:
45 | assert dog.type() == "bulldog dog"
46 |
47 |
48 | @unittest
49 | def test_cat_speak(cat: Pet) -> None:
50 | assert cat.speak() == '"Miya" says Moew!'
51 |
52 |
53 | @unittest
54 | def test_cat_type(cat: Pet) -> None:
55 | assert cat.type() == "persian cat"
56 |
57 |
58 | @unittest
59 | def test_dog_food() -> None:
60 | assert DogFood().show() == "Pedigree"
61 |
62 |
63 | @unittest
64 | def test_cat_food() -> None:
65 | assert CatFood().show() == "Whiskas"
66 |
67 |
68 | @unittest
69 | def test_dog_factory_pet(dog_factory: PetFactory) -> None:
70 | assert isinstance(dog_factory.pet(), Dog)
71 |
72 |
73 | @unittest
74 | def test_dog_factory_food(dog_factory: PetFactory) -> None:
75 | assert isinstance(dog_factory.food(), DogFood)
76 |
77 |
78 | @unittest
79 | def test_cat_factory_pet(dog_factory: PetFactory) -> None:
80 | assert isinstance(dog_factory.pet(), Dog)
81 |
82 |
83 | @unittest
84 | def test_cat_factory_food(dog_factory: PetFactory) -> None:
85 | assert isinstance(dog_factory.food(), DogFood)
86 |
87 |
88 | @unittest
89 | @pytest.mark.parametrize(
90 | "store, result",
91 | (
92 | (
93 | FluffyStore(CatFactory()),
94 | (
95 | "Our pet is persian cat",
96 | 'persian cat "Hope" says Moew!',
97 | "It eats Whiskas food",
98 | ),
99 | ),
100 | (
101 | FluffyStore(DogFactory()),
102 | (
103 | "Our pet is bulldog dog",
104 | 'bulldog dog "Spike" says Woof!',
105 | "It eats Pedigree food",
106 | ),
107 | ),
108 | ),
109 | )
110 | def test_fluffy_store(store: PetStore, result: Sequence[str]) -> None:
111 | assert tuple(store.show_pet()) == result
112 |
--------------------------------------------------------------------------------
/patterns/creational/abstract_factory.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Generator
3 |
4 |
5 | class Pet(ABC):
6 | """Abstract interface of a pet."""
7 |
8 | @abstractmethod
9 | def speak(self) -> str:
10 | pass
11 |
12 | @abstractmethod
13 | def type(self) -> str:
14 | pass
15 |
16 |
17 | class Food(ABC):
18 | """Abstract interface of a food."""
19 |
20 | @abstractmethod
21 | def show(self) -> str:
22 | pass
23 |
24 |
25 | class PetFactory(ABC):
26 | """Abstract interface of a pet factory."""
27 |
28 | @abstractmethod
29 | def pet(self) -> Pet:
30 | pass
31 |
32 | @abstractmethod
33 | def food(self) -> Food:
34 | pass
35 |
36 |
37 | class PetStore(ABC):
38 | """Abstract interface of a pet store."""
39 |
40 | @abstractmethod
41 | def show_pet(self) -> str:
42 | pass
43 |
44 |
45 | class Dog(Pet):
46 | """A dog pet."""
47 |
48 | def __init__(self, name: str, type_: str) -> None:
49 | self._name: str = name
50 | self._type: str = type_
51 |
52 | def speak(self) -> str:
53 | return f'"{self._name}" says Woof!'
54 |
55 | def type(self) -> str:
56 | return f"{self._type} dog"
57 |
58 |
59 | class DogFood(Food):
60 | """A dog food."""
61 |
62 | def show(self) -> str:
63 | return "Pedigree"
64 |
65 |
66 | class DogFactory(PetFactory):
67 | """A dog factory."""
68 |
69 | def __init__(self) -> None:
70 | self._dog: Pet = Dog(name="Spike", type_="bulldog")
71 | self._food: Food = DogFood()
72 |
73 | def pet(self) -> Pet:
74 | return self._dog
75 |
76 | def food(self) -> Food:
77 | return self._food
78 |
79 |
80 | class Cat(Pet):
81 | """A cat pet."""
82 |
83 | def __init__(self, name: str, type_: str) -> None:
84 | self._name: str = name
85 | self._type: str = type_
86 |
87 | def speak(self) -> str:
88 | return f'"{self._name}" says Moew!'
89 |
90 | def type(self) -> str:
91 | return f"{self._type} cat"
92 |
93 |
94 | class CatFood(Food):
95 | """A cat food."""
96 |
97 | def show(self) -> str:
98 | return "Whiskas"
99 |
100 |
101 | class CatFactory(PetFactory):
102 | """A dog factory."""
103 |
104 | def __init__(self) -> None:
105 | self._cat: Pet = Cat(name="Hope", type_="persian")
106 | self._food: Food = CatFood()
107 |
108 | def pet(self) -> Pet:
109 | return self._cat
110 |
111 | def food(self) -> Food:
112 | return self._food
113 |
114 |
115 | class FluffyStore(PetStore):
116 | """Houses our abstract pet factory."""
117 |
118 | def __init__(self, pet_factory: PetFactory) -> None:
119 | self._pet: Pet = pet_factory.pet()
120 | self._pet_food: Food = pet_factory.food()
121 |
122 | def show_pet(self) -> Generator[str, None, None]:
123 | yield f"Our pet is {self._pet.type()}"
124 | yield f"{self._pet.type()} {self._pet.speak()}"
125 | yield f"It eats {self._pet_food.show()} food"
126 |
127 |
128 | if __name__ == "__main__":
129 | # cat factory
130 | cat_factory: PetFactory = CatFactory()
131 | store: PetStore = FluffyStore(cat_factory)
132 | print(tuple(store.show_pet()))
133 |
134 | # dog factory
135 | dog_factory: PetFactory = DogFactory()
136 | store: PetStore = FluffyStore(dog_factory)
137 | print(tuple(store.show_pet()))
138 |
--------------------------------------------------------------------------------
/patterns/structural/facade.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | import time
3 | from typing import List, Tuple, Iterator, Type
4 |
5 | _sleep_for: float = 0.2
6 |
7 |
8 | class TestCase(ABC):
9 | """Abstract test case interface."""
10 |
11 | @abstractmethod
12 | def run(self) -> None:
13 | pass
14 |
15 |
16 | class TestCaseOne(TestCase):
17 | """Concrete test case one."""
18 |
19 | def __init__(self, name: str) -> None:
20 | self._name: str = name
21 |
22 | def run(self) -> None:
23 | print("{:#^20}".format(self._name))
24 | time.sleep(_sleep_for)
25 | print("Setting up testcase one")
26 | time.sleep(_sleep_for)
27 | print("Running test")
28 | time.sleep(_sleep_for)
29 | print("Tearing down")
30 | time.sleep(_sleep_for)
31 | print("Test Finished\n")
32 |
33 |
34 | class TestCaseTwo(TestCase):
35 | """Concrete test case two."""
36 |
37 | def __init__(self, name: str) -> None:
38 | self._name: str = name
39 |
40 | def run(self) -> None:
41 | print("{:#^20}".format(self._name))
42 | time.sleep(_sleep_for)
43 | print("Setting up testcase two")
44 | time.sleep(_sleep_for)
45 | print("Running test")
46 | time.sleep(_sleep_for)
47 | print("Tearing down")
48 | time.sleep(_sleep_for)
49 | print("Test Finished\n")
50 |
51 |
52 | class TestCaseThree(TestCase):
53 | """Concrete test case three."""
54 |
55 | def __init__(self, name: str) -> None:
56 | self._name: str = name
57 |
58 | def run(self) -> None:
59 | print("{:#^20}".format(self._name))
60 | time.sleep(_sleep_for)
61 | print("Setting up testcase three")
62 | time.sleep(_sleep_for)
63 | print("Running test")
64 | time.sleep(_sleep_for)
65 | print("Tearing down")
66 | time.sleep(_sleep_for)
67 | print("Test Finished\n")
68 |
69 |
70 | class TestSuite:
71 | """Represents simpler unified interface to run all test cases.
72 |
73 | A facade class itself.
74 | """
75 |
76 | def __init__(self, testcases: List[TestCase]) -> None:
77 | self._testcases = testcases
78 |
79 | def run(self) -> None:
80 | for testcase in self._testcases: # type: TestCase
81 | testcase.run()
82 |
83 |
84 | test_cases: List[TestCase] = [
85 | TestCaseOne("TC1"),
86 | TestCaseTwo("TC2"),
87 | TestCaseThree("TC3"),
88 | ]
89 | test_suite = TestSuite(test_cases)
90 | test_suite.run()
91 |
92 |
93 | class Interface(ABC):
94 | """Abstract interface."""
95 |
96 | @abstractmethod
97 | def run(self) -> str:
98 | pass
99 |
100 |
101 | class A(Interface):
102 | """Implement interface."""
103 |
104 | def run(self) -> str:
105 | return "A.run()"
106 |
107 |
108 | class B(Interface):
109 | """Implement interface."""
110 |
111 | def run(self) -> str:
112 | return "B.run()"
113 |
114 |
115 | class C(Interface):
116 | """Implement interface."""
117 |
118 | def run(self) -> str:
119 | return "C.run()"
120 |
121 |
122 | class Facade(Interface):
123 | """Facade object."""
124 |
125 | def __init__(self):
126 | self._all: Tuple[Type[Interface], ...] = (A, B, C)
127 |
128 | def run(self) -> Iterator[Interface]:
129 | yield from self._all
130 |
131 |
132 | if __name__ == "__main__":
133 | print(*(cls().run() for cls in Facade().run()))
134 |
--------------------------------------------------------------------------------
/patterns/structural/mvc.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import List, Dict, Iterator, Any
3 |
4 |
5 | class Model(ABC):
6 | """Abstract model defines interfaces."""
7 |
8 | @abstractmethod
9 | def __iter__(self) -> Iterator[str]:
10 | pass
11 |
12 | @abstractmethod
13 | def get(self, item: str) -> Dict[str, int]:
14 | """Returns an object with a .items() call method
15 | that iterates over key,value pairs of its information."""
16 | pass
17 |
18 | @property
19 | @abstractmethod
20 | def item_type(self) -> str:
21 | pass
22 |
23 |
24 | class View(ABC):
25 | """Abstract view defines interfaces."""
26 |
27 | @abstractmethod
28 | def show_item_list(self, item_type: str, item_list: List[str]) -> None:
29 | pass
30 |
31 | @abstractmethod
32 | def show_item_information(
33 | self, item_type: str, item_name: str, item_info: List[str]
34 | ) -> None:
35 | """
36 | Will look for item information by iterating over
37 | key,value pairs yielded by item_info.items().
38 | """
39 | pass
40 |
41 | @abstractmethod
42 | def item_not_found(self, item_type: str, item_name: str) -> None:
43 | pass
44 |
45 |
46 | class Controller(ABC):
47 | """Abstract controller defines interfaces."""
48 |
49 | @abstractmethod
50 | def show_items(self):
51 | pass
52 |
53 | @abstractmethod
54 | def show_item_information(self, item_name: str) -> None:
55 | pass
56 |
57 |
58 | class ProductModel(Model):
59 | """Concrete product model."""
60 |
61 | class Price(float):
62 | """A polymorphic way to pass a float with a particular
63 | __str__ functionality."""
64 |
65 | def __str__(self) -> str:
66 | first_digits_str: str = str(round(self, 2))
67 | try:
68 | dot_location: int = first_digits_str.index(".")
69 | except ValueError:
70 | return f"{first_digits_str}.00"
71 | return f"{first_digits_str}{'0' * (3 + dot_location - len(first_digits_str))}"
72 |
73 | products: Dict[str, Dict[str, Any]] = {
74 | "milk": {"price": Price(1.50), "quantity": 10},
75 | "eggs": {"price": Price(0.20), "quantity": 100},
76 | "cheese": {"price": Price(2.00), "quantity": 10},
77 | }
78 |
79 | @property
80 | def item_type(self) -> str:
81 | return "product"
82 |
83 | def __iter__(self) -> Iterator[str]:
84 | for item in self.products: # type: str
85 | yield item
86 |
87 | def get(self, item: str) -> Dict[str, int]:
88 | try:
89 | return self.products[item]
90 | except KeyError as error:
91 | raise KeyError(
92 | str(error) + " not in the model's item list."
93 | ) from error
94 |
95 |
96 | class ConsoleView(View):
97 | """Concrete console view."""
98 |
99 | def show_item_list(self, item_type: str, item_list: Dict[str, Any]) -> None:
100 | print("{} LIST:".format(item_type.upper()))
101 | for item in item_list:
102 | print(item)
103 | print("\n")
104 |
105 | @staticmethod
106 | def capitalizer(string: str) -> str:
107 | return f"{string[0].upper()}{ string[1:].lower()}"
108 |
109 | def show_item_information(
110 | self, item_type: str, item_name: str, item_info: Dict[str, int]
111 | ) -> None:
112 | print(f"{item_type.upper()} INFORMATION:")
113 | printout: str = f"Name: {item_name}"
114 | for key, value in item_info.items():
115 | printout += ", " + self.capitalizer(str(key)) + ": " + str(value)
116 | printout += "\n"
117 | print(printout)
118 |
119 | def item_not_found(self, item_type: str, item_name: str) -> None:
120 | print(f'That "{item_type}" "{item_name}" does not exist in the records')
121 |
122 |
123 | class ItemController(Controller):
124 | """Concrete item controller."""
125 |
126 | def __init__(self, item_model: Model, item_view: View) -> None:
127 | self._model = item_model
128 | self._view = item_view
129 |
130 | def show_items(self) -> None:
131 | items: List = list(self._model)
132 | item_type: str = self._model.item_type
133 | self._view.show_item_list(item_type, items)
134 |
135 | def show_item_information(self, item_name: str) -> None:
136 | try:
137 | item_info: Dict[str, Any] = self._model.get(item_name)
138 | except KeyError:
139 | item_type: str = self._model.item_type
140 | self._view.item_not_found(item_type, item_name)
141 | else:
142 | item_type: str = self._model.item_type
143 | self._view.show_item_information(item_type, item_name, item_info)
144 |
145 |
146 | if __name__ == "__main__":
147 | model: Model = ProductModel()
148 | view: View = ConsoleView()
149 | controller: ItemController = ItemController(model, view)
150 | controller.show_items()
151 | controller.show_item_information("cheese")
152 | controller.show_item_information("eggs")
153 | controller.show_item_information("milk")
154 | controller.show_item_information("arepas")
155 |
156 |
157 | # OUTPUT #
158 | # PRODUCT LIST:
159 | # cheese
160 | # eggs
161 | # milk
162 | #
163 | # PRODUCT INFORMATION:
164 | # Name: Cheese, Price: 2.00, Quantity: 10
165 | #
166 | # PRODUCT INFORMATION:
167 | # Name: Eggs, Price: 0.20, Quantity: 100
168 | #
169 | # PRODUCT INFORMATION:
170 | # Name: Milk, Price: 1.50, Quantity: 10
171 | #
172 | # That product "arepas" does not exist in the records
173 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2018 Volodymyr Yahello
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [MASTER]
2 |
3 | # Specify a configuration file.
4 | #rcfile=
5 |
6 | # Python code to execute, usually for sys.path manipulation such as
7 | # pygtk.require().
8 | #init-hook=
9 |
10 | # Add files or directories to the blacklist. They should be base names, not
11 | # paths.
12 | ignore=CVS
13 |
14 | # Pickle collected data for later comparisons.
15 | persistent=yes
16 |
17 | # List of plugins (as comma separated values of python modules names) to load,
18 | # usually to register additional checkers.
19 | load-plugins=
20 |
21 | # Use multiple processes to speed up Pylint.
22 | jobs=1
23 |
24 | # Allow loading of arbitrary C extensions. Extensions are imported into the
25 | # active Python interpreter and may run arbitrary code.
26 | unsafe-load-any-extension=no
27 |
28 | # A comma-separated list of package or module names from where C extensions may
29 | # be loaded. Extensions are loading into the active Python interpreter and may
30 | # run arbitrary code
31 | extension-pkg-whitelist=
32 |
33 |
34 | [MESSAGES CONTROL]
35 |
36 | # Only show warnings with the listed confidence levels. Leave empty to show
37 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
38 | confidence=
39 |
40 | # Enable the message, report, category or checker with the given id(s). You can
41 | # either give multiple identifier separated by comma (,) or put this option
42 | # multiple time. See also the "--disable" option for examples.
43 | #enable=
44 |
45 | # Disable the message, report, category or checker with the given id(s). You
46 | # can either give multiple identifiers separated by comma (,) or put this
47 | # option multiple times (only on the command line, not in the configuration
48 | # file where it should appear only once).You can also use "--disable=all" to
49 | # disable everything first and then reenable specific checks. For example, if
50 | # you want to run only the similarities checker, you can use "--disable=all
51 | # --enable=similarities". If you want to run only the classes checker, but have
52 | # no Warning level messages displayed, use"--disable=all --enable=classes
53 | # --disable=W"
54 |
55 | disable=invalid-name, too-few-public-methods, missing-docstring,
56 | unnecessary-pass, no-self-use, not-callable, method-hidden,
57 | redefined-outer-name, useless-object-inheritance, duplicate-code
58 |
59 |
60 | [REPORTS]
61 |
62 | # Set the output format. Available formats are text, parseable, colorized, msvs
63 | # (visual studio) and html. You can also give a reporter class, eg
64 | # mypackage.mymodule.MyReporterClass.
65 | output-format=text
66 |
67 | # Put messages in a separate file for each module / package specified on the
68 | # command line instead of printing them on stdout. Reports (if any) will be
69 | # written in a file name "pylint_global.[txt|html]".
70 | files-output=no
71 |
72 | # Tells whether to display a full report or only the messages
73 | reports=yes
74 |
75 | # Python expression which should return a note less than 10 (10 is the highest
76 | # note). You have access to the variables errors warning, statement which
77 | # respectively contain the number of errors / warnings messages and the total
78 | # number of statements analyzed. This is used by the global evaluation report
79 | # (RP0004).
80 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
81 |
82 | # Template used to display messages. This is a python new-style format string
83 | # used to format the message information. See doc for all details
84 | #msg-template=
85 |
86 |
87 | [LOGGING]
88 |
89 | # Logging modules to check that the string format arguments are in logging
90 | # function parameter format
91 | logging-modules=logging
92 |
93 |
94 | [MISCELLANEOUS]
95 |
96 | # List of note tags to take in consideration, separated by a comma.
97 | notes=FIXME,XXX,TODO
98 |
99 |
100 | [SIMILARITIES]
101 |
102 | # Minimum lines number of a similarity.
103 | min-similarity-lines=4
104 |
105 | # Ignore comments when computing similarities.
106 | ignore-comments=yes
107 |
108 | # Ignore docstrings when computing similarities.
109 | ignore-docstrings=yes
110 |
111 | # Ignore imports when computing similarities.
112 | ignore-imports=no
113 |
114 |
115 | [VARIABLES]
116 |
117 | # Tells whether we should check for unused import in __init__ files.
118 | init-import=no
119 |
120 | # A regular expression matching the name of dummy variables (i.e. expectedly
121 | # not used).
122 | dummy-variables-rgx=_$|dummy
123 |
124 | # List of additional names supposed to be defined in builtins. Remember that
125 | # you should avoid to define new builtins when possible.
126 | additional-builtins=
127 |
128 | # List of strings which can identify a callback function by name. A callback
129 | # name must start or end with one of those strings.
130 | callbacks=cb_,_cb
131 |
132 |
133 | [FORMAT]
134 |
135 | # Maximum number of characters on a single line.
136 | max-line-length=120
137 |
138 | # Regexp for a line that is allowed to be longer than the limit.
139 | ignore-long-lines=^\s*(# )??$
140 |
141 | # Allow the body of an if to be on the same line as the test if there is no
142 | # else.
143 | single-line-if-stmt=no
144 |
145 | # List of optional constructs for which whitespace checking is disabled
146 | no-space-check=trailing-comma,dict-separator
147 |
148 | # Maximum number of lines in a module
149 | max-module-lines=1000
150 |
151 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
152 | # tab).
153 | indent-string=' '
154 |
155 | # Number of spaces of indent required inside a hanging or continued line.
156 | indent-after-paren=4
157 |
158 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
159 | expected-line-ending-format=
160 |
161 |
162 | [BASIC]
163 |
164 | # List of builtins function names that should not be used, separated by a comma
165 | bad-functions=map,filter,input
166 |
167 | # Good variable names which should always be accepted, separated by a comma
168 | good-names=i,j,k,ex,Run,_
169 |
170 | # Bad variable names which should always be refused, separated by a comma
171 | bad-names=foo,bar,baz,toto,tutu,tata
172 |
173 | # Colon-delimited sets of names that determine each other's naming style when
174 | # the name regexes allow several styles.
175 | name-group=
176 |
177 | # Include a hint for the correct naming format with invalid-name
178 | include-naming-hint=no
179 |
180 | # Regular expression matching correct function names
181 | function-rgx=[a-z_][a-z0-9_]{2,30}$
182 |
183 | # Naming hint for function names
184 | function-name-hint=[a-z_][a-z0-9_]{2,30}$
185 |
186 | # Regular expression matching correct variable names
187 | variable-rgx=[a-z_][a-z0-9_]{2,30}$
188 |
189 | # Naming hint for variable names
190 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$
191 |
192 | # Regular expression matching correct constant names
193 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
194 |
195 | # Naming hint for constant names
196 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
197 |
198 | # Regular expression matching correct attribute names
199 | attr-rgx=[a-z_][a-z0-9_]{2,30}$
200 |
201 | # Naming hint for attribute names
202 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$
203 |
204 | # Regular expression matching correct argument names
205 | argument-rgx=[a-z_][a-z0-9_]{2,30}$
206 |
207 | # Naming hint for argument names
208 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$
209 |
210 | # Regular expression matching correct class attribute names
211 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
212 |
213 | # Naming hint for class attribute names
214 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
215 |
216 | # Regular expression matching correct inline iteration names
217 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
218 |
219 | # Naming hint for inline iteration names
220 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
221 |
222 | # Regular expression matching correct class names
223 | class-rgx=[A-Z_][a-zA-Z0-9]+$
224 |
225 | # Naming hint for class names
226 | class-name-hint=[A-Z_][a-zA-Z0-9]+$
227 |
228 | # Regular expression matching correct module names
229 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
230 |
231 | # Naming hint for module names
232 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
233 |
234 | # Regular expression matching correct method names
235 | method-rgx=[a-z_][a-z0-9_]{2,30}$
236 |
237 | # Naming hint for method names
238 | method-name-hint=[a-z_][a-z0-9_]{2,30}$
239 |
240 | # Regular expression which should only match function or class names that do
241 | # not require a docstring.
242 | no-docstring-rgx=__.*__
243 |
244 | # Minimum line length for functions/classes that require docstrings, shorter
245 | # ones are exempt.
246 | docstring-min-length=-1
247 |
248 | # List of decorators that define properties, such as abc.abstractproperty.
249 | property-classes=abc.abstractproperty
250 |
251 |
252 | [TYPECHECK]
253 |
254 | # Tells whether missing members accessed in mixin class should be ignored. A
255 | # mixin class is detected if its name ends with "mixin" (case insensitive).
256 | ignore-mixin-members=yes
257 |
258 | # List of module names for which member attributes should not be checked
259 | # (useful for modules/projects where namespaces are manipulated during runtime
260 | # and thus existing member attributes cannot be deduced by static analysis
261 | ignored-modules=
262 |
263 | # List of classes names for which member attributes should not be checked
264 | # (useful for classes with attributes dynamically set).
265 | ignored-classes=SQLObject, optparse.Values, thread._local, _thread._local
266 |
267 | # List of members which are set dynamically and missed by pylint inference
268 | # system, and so shouldn't trigger E1101 when accessed. Python regular
269 | # expressions are accepted.
270 | generated-members=REQUEST,acl_users,aq_parent
271 |
272 | # List of decorators that create context managers from functions, such as
273 | # contextlib.contextmanager.
274 | contextmanager-decorators=contextlib.contextmanager
275 |
276 |
277 | [SPELLING]
278 |
279 | # Spelling dictionary name. Available dictionaries: none. To make it working
280 | # install python-enchant package.
281 | spelling-dict=
282 |
283 | # List of comma separated words that should not be checked.
284 | spelling-ignore-words=
285 |
286 | # A path to a file that contains private dictionary; one word per line.
287 | spelling-private-dict-file=
288 |
289 | # Tells whether to store unknown words to indicated private dictionary in
290 | # --spelling-private-dict-file option instead of raising a message.
291 | spelling-store-unknown-words=no
292 |
293 |
294 | [DESIGN]
295 |
296 | # Maximum number of arguments for function / method
297 | max-args=5
298 |
299 | # Argument names that match this expression will be ignored. Default to name
300 | # with leading underscore
301 | ignored-argument-names=_.*
302 |
303 | # Maximum number of locals for function / method body
304 | max-locals=15
305 |
306 | # Maximum number of return / yield for function / method body
307 | max-returns=6
308 |
309 | # Maximum number of branch for function / method body
310 | max-branches=12
311 |
312 | # Maximum number of statements in function / method body
313 | max-statements=50
314 |
315 | # Maximum number of parents for a class (see R0901).
316 | max-parents=7
317 |
318 | # Maximum number of attributes for a class (see R0902).
319 | max-attributes=7
320 |
321 | # Minimum number of public methods for a class (see R0903).
322 | min-public-methods=2
323 |
324 | # Maximum number of public methods for a class (see R0904).
325 | max-public-methods=20
326 |
327 |
328 | [CLASSES]
329 |
330 | # List of method names used to declare (i.e. assign) instance attributes.
331 | defining-attr-methods=__init__,__new__,setUp
332 |
333 | # List of valid names for the first argument in a class method.
334 | valid-classmethod-first-arg=cls
335 |
336 | # List of valid names for the first argument in a metaclass class method.
337 | valid-metaclass-classmethod-first-arg=mcs
338 |
339 | # List of member names, which should be excluded from the protected access
340 | # warning.
341 | exclude-protected=_asdict,_fields,_replace,_source,_make
342 |
343 |
344 | [IMPORTS]
345 |
346 | # Deprecated modules which should not be used, separated by a comma
347 | deprecated-modules=regsub,TERMIOS,Bastion,rexec
348 |
349 | # Create a graph of every (i.e. internal and external) dependencies in the
350 | # given file (report RP0402 must not be disabled)
351 | import-graph=
352 |
353 | # Create a graph of external dependencies in the given file (report RP0402 must
354 | # not be disabled)
355 | ext-import-graph=
356 |
357 | # Create a graph of internal dependencies in the given file (report RP0402 must
358 | # not be disabled)
359 | int-import-graph=
360 |
361 |
362 | [EXCEPTIONS]
363 |
364 | # Exceptions that will emit a warning when being caught. Defaults to
365 | # "Exception"
366 | overgeneral-exceptions=Exception
367 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://www.python.org/)
4 | [](https://github.com/psf/black)
5 | [](https://www.pylint.org)
6 | [](https://www.travis-ci.com/github/vyahello/python-ood)
7 | [](LICENSE.md)
8 | [](https://www.codefactor.io/repository/github/vyahello/python-ood)
9 | [](https://www.elegantobjects.org)
10 | [](https://vyahello.github.io/python-ood)
11 |
12 | # Python OOD
13 | > The project describes the architecture of the most useful python object oriented design patterns.
14 |
15 | ## Tools
16 |
17 | ### Language(s)
18 | - python 3.9, 3.10
19 |
20 | ### Development
21 | - [pylint](https://www.pylint.org/) code analyser
22 | - [black](https://black.readthedocs.io/en/stable/) code formatter
23 | - [travis](https://travis-ci.org/) CI
24 | - [pytest](https://docs.pytest.org/en/stable/) framework
25 |
26 | ## Table of contents
27 | - [Creational](#creational)
28 | - [Factory method](#factory-method)
29 | - [Abstract factory](#abstract-factory)
30 | - [Singleton](#singleton)
31 | - [Builder](#builder)
32 | - [Prototype](#prototype)
33 | - [Structural](#structural)
34 | - [Decorator](#decorator)
35 | - [Proxy](#proxy)
36 | - [Adapter](#adapter)
37 | - [Composite](#composite)
38 | - [Bridge](#bridge)
39 | - [Facade](#facade)
40 | - [Behavioral](#behavioral)
41 | - [MVC](#mvc)
42 | - [Observer](#observer)
43 | - [Visitor](#visitor)
44 | - [Iterator](#iterator)
45 | - [Strategy](#strategy)
46 | - [Chain of responsibility](#chain-of-responsibility)
47 | - [Development notes](#development-notes)
48 | - [Code analysis](#code-analysis)
49 | - [Release notes](#release-notes)
50 | - [Meta](#meta)
51 | - [Contributing](#contributing)
52 |
53 | ## Creational
54 | Creational types of patterns used to create objects in a systematic way. Supports flexibility and different subtypes of objects from the same class at runtime.
55 | Here **polymorphism** is often used.
56 |
57 | ### Factory method
58 | Factory method defines an interface for creating an object but defers object instantiation to run time.
59 |
60 | ```python
61 | from abc import ABC, abstractmethod
62 |
63 |
64 | class Shape(ABC):
65 | """Defines a shape interface."""
66 |
67 | @abstractmethod
68 | def draw(self) -> str:
69 | """Draws a shape."""
70 | pass
71 |
72 |
73 | class ShapeError(Exception):
74 | """Represents shape error message."""
75 |
76 | pass
77 |
78 |
79 | class Circle(Shape):
80 | """A shape subclass."""
81 |
82 | def draw(self) -> str:
83 | """Draws a circle."""
84 | return "Circle.draw"
85 |
86 |
87 | class Square(Shape):
88 | """A shape subclass."""
89 |
90 | def draw(self) -> str:
91 | """Draws a square."""
92 | return "Square.draw"
93 |
94 |
95 | class ShapeFactory:
96 | """A shape factory."""
97 |
98 | def __init__(self, shape: str) -> None:
99 | self._shape: str = shape
100 |
101 | def shape(self) -> Shape:
102 | """Returns a shape."""
103 | if self._shape == "circle":
104 | return Circle()
105 | if self._shape == "square":
106 | return Square()
107 | raise ShapeError(f'Could not find "{self._shape}" shape!')
108 |
109 |
110 | # circle shape
111 | factory: ShapeFactory = ShapeFactory(shape="circle")
112 | circle: Shape = factory.shape()
113 | print(circle.__class__.__name__)
114 | print(circle.draw())
115 |
116 | # square shape
117 | factory: ShapeFactory = ShapeFactory(shape="square")
118 | square: Shape = factory.shape()
119 | print(square.__class__.__name__)
120 | print(square.draw())
121 | ```
122 |
123 | Factory encapsulates objects creation. Factory is an object that is specialized in creation of other objects.
124 | - Benefits:
125 | - Useful when you are not sure what kind of object you will be needed eventually.
126 | - Application need to decide what class it has to use.
127 | - Exercise:
128 | - Pet shop is selling dogs but now it sells cats too.
129 |
130 | ```python
131 | from abc import ABC, abstractmethod
132 |
133 |
134 | class Pet(ABC):
135 | """Abstraction of a pet."""
136 |
137 | @abstractmethod
138 | def speak(self) -> str:
139 | """Interface for a pet to speak."""
140 | pass
141 |
142 |
143 | class Dog(Pet):
144 | """A simple dog class."""
145 |
146 | def __init__(self, name: str) -> None:
147 | self._dog_name: str = name
148 |
149 | def speak(self) -> str:
150 | return f"{self._dog_name} says Woof!"
151 |
152 |
153 | class Cat(Pet):
154 | """A simple cat class."""
155 |
156 | def __init__(self, name: str) -> None:
157 | self._cat_name: str = name
158 |
159 | def speak(self) -> str:
160 | return f"{self._cat_name} says Meow!"
161 |
162 |
163 | def get_pet(pet: str) -> Pet:
164 | """The factory method."""
165 | return {"dog": Dog("Hope"), "cat": Cat("Faith")}[pet]
166 |
167 |
168 | # returns Cat class object
169 | get_pet("cat")
170 | ```
171 |
172 | **[⬆ back to top](#table-of-contents)**
173 |
174 | ### Abstract factory
175 | In abstract factory a client expects to receive family related objects. But don't have to know which family it is until run time. Abstract factory is related to factory method and concrete product are singletons.
176 | - Implementation idea:
177 | - Abstract factory: pet factory
178 | - Concrete factory: dog factory and cat factory
179 | - Abstract product
180 | - Concrete product: dog and dog food, cat and cat food
181 | - Exercise:
182 | - We have a Pet factory (which includes Dog and Cat factory and both factories produced related products such as Dog and Cat food and we have a PetFactory which gets Cat or Dog factory).
183 |
184 | ```python
185 | from abc import ABC, abstractmethod
186 | from typing import Generator
187 |
188 |
189 | class Pet(ABC):
190 | """Abstract interface of a pet."""
191 |
192 | @abstractmethod
193 | def speak(self) -> str:
194 | pass
195 |
196 | @abstractmethod
197 | def type(self) -> str:
198 | pass
199 |
200 |
201 | class Food(ABC):
202 | """Abstract interface of a food."""
203 |
204 | @abstractmethod
205 | def show(self) -> str:
206 | pass
207 |
208 |
209 | class PetFactory(ABC):
210 | """Abstract interface of a pet factory."""
211 |
212 | @abstractmethod
213 | def pet(self) -> Pet:
214 | pass
215 |
216 | @abstractmethod
217 | def food(self) -> Food:
218 | pass
219 |
220 |
221 | class PetStore(ABC):
222 | """Abstract interface of a pet store."""
223 |
224 | @abstractmethod
225 | def show_pet(self) -> str:
226 | pass
227 |
228 |
229 | class Dog(Pet):
230 | """A dog pet."""
231 |
232 | def __init__(self, name: str, type_: str) -> None:
233 | self._name: str = name
234 | self._type: str = type_
235 |
236 | def speak(self) -> str:
237 | return f'"{self._name}" says Woof!'
238 |
239 | def type(self) -> str:
240 | return f"{self._type} dog"
241 |
242 |
243 | class DogFood(Food):
244 | """A dog food."""
245 |
246 | def show(self) -> str:
247 | return "Pedigree"
248 |
249 |
250 | class DogFactory(PetFactory):
251 | """A dog factory."""
252 |
253 | def __init__(self) -> None:
254 | self._dog: Pet = Dog(name="Spike", type_="bulldog")
255 | self._food: Food = DogFood()
256 |
257 | def pet(self) -> Pet:
258 | return self._dog
259 |
260 | def food(self) -> Food:
261 | return self._food
262 |
263 |
264 | class Cat(Pet):
265 | """A cat pet."""
266 |
267 | def __init__(self, name: str, type_: str) -> None:
268 | self._name: str = name
269 | self._type: str = type_
270 |
271 | def speak(self) -> str:
272 | return f'"{self._name}" says Moew!'
273 |
274 | def type(self) -> str:
275 | return f"{self._type} cat"
276 |
277 |
278 | class CatFood(Food):
279 | """A cat food."""
280 |
281 | def show(self) -> str:
282 | return "Whiskas"
283 |
284 |
285 | class CatFactory(PetFactory):
286 | """A dog factory."""
287 |
288 | def __init__(self) -> None:
289 | self._cat: Pet = Cat(name="Hope", type_="persian")
290 | self._food: Food = CatFood()
291 |
292 | def pet(self) -> Pet:
293 | return self._cat
294 |
295 | def food(self) -> Food:
296 | return self._food
297 |
298 |
299 | class FluffyStore(PetStore):
300 | """Houses our abstract pet factory."""
301 |
302 | def __init__(self, pet_factory: PetFactory) -> None:
303 | self._pet: Pet = pet_factory.pet()
304 | self._pet_food: Food = pet_factory.food()
305 |
306 | def show_pet(self) -> Generator[str, None, None]:
307 | yield f"Our pet is {self._pet.type()}"
308 | yield f"{self._pet.type()} {self._pet.speak()}"
309 | yield f"It eats {self._pet_food.show()} food"
310 |
311 |
312 |
313 | # cat factory
314 | cat_factory: PetFactory = CatFactory()
315 | store: PetStore = FluffyStore(cat_factory)
316 | print(tuple(store.show_pet()))
317 |
318 | # dog factory
319 | dog_factory: PetFactory = DogFactory()
320 | store: PetStore = FluffyStore(dog_factory)
321 | print(tuple(store.show_pet()))
322 | ```
323 |
324 | **[⬆ back to top](#table-of-contents)**
325 |
326 | ### Singleton
327 | Python has global variables and modules which are **_singletons_**. Singleton allows only one object to be instantiated from a class template.
328 | Useful if you want to share cached information to multiple objects.
329 |
330 | **Classic singleton**
331 |
332 | ```python
333 | from typing import Any, Dict
334 |
335 |
336 | class SingletonMeta(type):
337 | """Singleton metaclass implementation."""
338 |
339 | def __init__(cls, cls_name: str, bases: tuple, namespace: dict):
340 | cls.__instance = None
341 | super().__init__(cls_name, bases, namespace)
342 |
343 | def __call__(cls, *args, **kwargs):
344 | if cls.__instance is None:
345 | cls.__instance = super().__call__(*args, **kwargs)
346 | return cls.__instance
347 | return cls.__instance
348 |
349 |
350 | class Singleton:
351 | """Makes all instances as the same object."""
352 |
353 | def __new__(cls) -> "Singleton":
354 | if not hasattr(cls, "_instance"):
355 | cls._instance = super().__new__(cls)
356 | return cls._instance
357 |
358 |
359 | def singleton(cls: Any) -> Any:
360 | """A singleton decorator."""
361 | instances: Dict[Any, Any] = {}
362 |
363 | def get_instance() -> Any:
364 | if cls not in instances:
365 | instances[cls] = cls()
366 | return instances[cls]
367 |
368 | return get_instance
369 |
370 |
371 | @singleton
372 | class Bar:
373 | """A fancy object."""
374 |
375 | pass
376 |
377 |
378 | singleton_one: Singleton = Singleton()
379 | singleton_two: Singleton = Singleton()
380 |
381 | print(id(singleton_one))
382 | print(id(singleton_two))
383 | print(singleton_one is singleton_two)
384 |
385 | bar_one: Bar = Bar()
386 | bar_two: Bar = Bar()
387 | print(id(bar_one))
388 | print(id(bar_two))
389 | print(bar_one is bar_two)
390 | ```
391 |
392 | **Borg singleton**
393 |
394 | ```python
395 | from typing import Dict, Any
396 |
397 |
398 | class Borg:
399 | """Borg class making class attributes global.
400 | Safe the same state of all instances but instances are all different."""
401 |
402 | _shared_state: Dict[Any, Any] = {}
403 |
404 | def __init__(self) -> None:
405 | self.__dict__ = self._shared_state
406 |
407 |
408 | class BorgSingleton(Borg):
409 | """This class shares all its attribute among its instances. Store the same state."""
410 |
411 | def __init__(self, **kwargs: Any) -> None:
412 | Borg.__init__(self)
413 | self._shared_state.update(kwargs)
414 |
415 | def __str__(self) -> str:
416 | return str(self._shared_state)
417 |
418 |
419 | # Create a singleton object and add out first acronym
420 | x: Borg = BorgSingleton(HTTP="Hyper Text Transfer Protocol")
421 | print(x)
422 |
423 | # Create another singleton which will add to the existent dict attribute
424 | y: Borg = BorgSingleton(SNMP="Simple Network Management Protocol")
425 | print(y)
426 | ```
427 |
428 | **[⬆ back to top](#table-of-contents)**
429 |
430 | ### Builder
431 | Builder reduces complexity of building objects.
432 | - Participants:
433 | - Director
434 | - Abstract Builder: interfaces
435 | - Concrete Builder: implements the interfaces
436 | - Product: object being built
437 | - Exercise:
438 | - Build a car object
439 |
440 | ```python
441 | from abc import ABC, abstractmethod
442 |
443 |
444 | class Machine(ABC):
445 | """Abstract machine interface."""
446 |
447 | @abstractmethod
448 | def summary(self) -> str:
449 | pass
450 |
451 |
452 | class Builder(ABC):
453 | """Abstract builder interface."""
454 |
455 | @abstractmethod
456 | def add_model(self) -> None:
457 | pass
458 |
459 | @abstractmethod
460 | def add_tires(self) -> None:
461 | pass
462 |
463 | @abstractmethod
464 | def add_engine(self) -> None:
465 | pass
466 |
467 | @abstractmethod
468 | def machine(self) -> Machine:
469 | pass
470 |
471 |
472 | class Car(Machine):
473 | """A car product."""
474 |
475 | def __init__(self) -> None:
476 | self.model: str = None
477 | self.tires: str = None
478 | self.engine: str = None
479 |
480 | def summary(self) -> str:
481 | return "Car details: {} | {} | {}".format(
482 | self.model, self.tires, self.engine
483 | )
484 |
485 |
486 | class SkyLarkBuilder(Builder):
487 | """Provides parts and tools to work on the car parts."""
488 |
489 | def __init__(self) -> None:
490 | self._car: Machine = Car()
491 |
492 | def add_model(self) -> None:
493 | self._car.model = "SkyBuilder model"
494 |
495 | def add_tires(self) -> None:
496 | self._car.tires = "Motosport tires"
497 |
498 | def add_engine(self) -> None:
499 | self._car.engine = "GM Motors engine"
500 |
501 | def machine(self) -> Machine:
502 | return self._car
503 |
504 |
505 | class Director:
506 | """A director. Responsible for `Car` assembling."""
507 |
508 | def __init__(self, builder_: Builder) -> None:
509 | self._builder: Builder = builder_
510 |
511 | def construct_machine(self) -> None:
512 | self._builder.add_model()
513 | self._builder.add_tires()
514 | self._builder.add_engine()
515 |
516 | def release_machine(self) -> Machine:
517 | return self._builder.machine()
518 |
519 |
520 | builder: Builder = SkyLarkBuilder()
521 | director: Director = Director(builder)
522 | director.construct_machine()
523 | car: Machine = director.release_machine()
524 | print(car.summary())
525 | ```
526 |
527 | **[⬆ back to top](#table-of-contents)**
528 |
529 | ### Prototype
530 | Prototype patterns are related to abstract factory pattern.
531 | - Ideas:
532 | - Clone objects according to prototypical instance.
533 | - Creating many identical objects individually.
534 | - Clone individual objects
535 | - Create a prototypical instance first
536 | - Exercise:
537 | - Use the same car if car has same color or options, you can clone objects instead of creating individual objects
538 |
539 | ```python
540 | import copy
541 | from abc import ABC, abstractmethod
542 | from typing import Dict, Any
543 |
544 |
545 | class Machine(ABC):
546 | """Abstract machine interface."""
547 |
548 | @abstractmethod
549 | def summary(self) -> str:
550 | pass
551 |
552 |
553 | class Car(Machine):
554 | """A car object."""
555 |
556 | def __init__(self) -> None:
557 | self._name: str = "Skylar"
558 | self._color: str = "Red"
559 | self._options: str = "Ex"
560 |
561 | def summary(self) -> str:
562 | return "Car details: {} | {} | {}".format(
563 | self._name, self._color, self._options
564 | )
565 |
566 |
567 | class Prototype:
568 | """A prototype object."""
569 |
570 | def __init__(self) -> None:
571 | self._elements: Dict[Any, Any] = {}
572 |
573 | def register_object(self, name: str, machine: Machine) -> None:
574 | self._elements[name] = machine
575 |
576 | def unregister_object(self, name: str) -> None:
577 | del self._elements[name]
578 |
579 | def clone(self, name: str, **attr: Any) -> Car:
580 | obj: Any = copy.deepcopy(self._elements[name])
581 | obj.__dict__.update(attr)
582 | return obj
583 |
584 |
585 | # prototypical car object to be cloned
586 | primary_car: Machine = Car()
587 | print(primary_car.summary())
588 | prototype: Prototype = Prototype()
589 | prototype.register_object("skylark", primary_car)
590 |
591 | # clone a car object
592 | cloned_car: Machine = prototype.clone("skylark")
593 | print(cloned_car.summary())
594 | ```
595 |
596 | **[⬆ back to top](#table-of-contents)**
597 |
598 | ## Structural
599 | Structural type of patterns establish useful relationships between software components. Here **_inheritance_** is often used.
600 | - Ideas:
601 | - Route maps the user request to a `Controller` which...
602 | - Uses the `Model` to retrieve all of the necessary data, organizes it and send it off to the...
603 | - View, which then uses that data to render the web page
604 |
605 | **[⬆ back to top](#table-of-contents)**
606 |
607 | ### MVC
608 | MVC (Model-View-Controller) is a UI pattern intended to separate internal representation of data from ways it is presented to/from the user.
609 |
610 | ```python
611 | from abc import ABC, abstractmethod
612 | from typing import List, Dict, Iterator, Any
613 |
614 |
615 | class Model(ABC):
616 | """Abstract model defines interfaces."""
617 |
618 | @abstractmethod
619 | def __iter__(self) -> Iterator[str]:
620 | pass
621 |
622 | @abstractmethod
623 | def get(self, item: str) -> Dict[str, int]:
624 | """Returns an object with a .items() call method
625 | that iterates over key,value pairs of its information."""
626 | pass
627 |
628 | @property
629 | @abstractmethod
630 | def item_type(self) -> str:
631 | pass
632 |
633 |
634 | class View(ABC):
635 | """Abstract view defines interfaces."""
636 |
637 | @abstractmethod
638 | def show_item_list(self, item_type: str, item_list: List[str]) -> None:
639 | pass
640 |
641 | @abstractmethod
642 | def show_item_information(
643 | self, item_type: str, item_name: str, item_info: List[str]
644 | ) -> None:
645 | """
646 | Will look for item information by iterating
647 | over key,value pairs yielded by item_info.items().
648 | """
649 | pass
650 |
651 | @abstractmethod
652 | def item_not_found(self, item_type: str, item_name: str) -> None:
653 | pass
654 |
655 |
656 | class Controller(ABC):
657 | """Abstract controller defines interfaces."""
658 |
659 | @abstractmethod
660 | def show_items(self):
661 | pass
662 |
663 | @abstractmethod
664 | def show_item_information(self, item_name: str) -> None:
665 | pass
666 |
667 |
668 | class ProductModel(Model):
669 | """Concrete product model."""
670 |
671 | class Price(float):
672 | """A polymorphic way to pass a float with a particular
673 | __str__ functionality."""
674 |
675 | def __str__(self) -> str:
676 | first_digits_str: str = str(round(self, 2))
677 | try:
678 | dot_location: int = first_digits_str.index(".")
679 | except ValueError:
680 | return f"{first_digits_str}.00"
681 | return f"{first_digits_str}{'0' * (3 + dot_location - len(first_digits_str))}"
682 |
683 | products: Dict[str, Dict[str, Any]] = {
684 | "milk": {"price": Price(1.50), "quantity": 10},
685 | "eggs": {"price": Price(0.20), "quantity": 100},
686 | "cheese": {"price": Price(2.00), "quantity": 10},
687 | }
688 |
689 | @property
690 | def item_type(self) -> str:
691 | return "product"
692 |
693 | def __iter__(self) -> Iterator[str]:
694 | for item in self.products: # type: str
695 | yield item
696 |
697 | def get(self, item: str) -> Dict[str, int]:
698 | try:
699 | return self.products[item]
700 | except KeyError as error:
701 | raise KeyError(
702 | str(error) + " not in the model's item list."
703 | ) from error
704 |
705 |
706 | class ConsoleView(View):
707 | """Concrete console view."""
708 |
709 | def show_item_list(self, item_type: str, item_list: Dict[str, Any]) -> None:
710 | print("{} LIST:".format(item_type.upper()))
711 | for item in item_list:
712 | print(item)
713 | print("\n")
714 |
715 | @staticmethod
716 | def capitalizer(string: str) -> str:
717 | return f"{string[0].upper()}{ string[1:].lower()}"
718 |
719 | def show_item_information(
720 | self, item_type: str, item_name: str, item_info: Dict[str, int]
721 | ) -> None:
722 | print(f"{item_type.upper()} INFORMATION:")
723 | printout: str = f"Name: {item_name}"
724 | for key, value in item_info.items():
725 | printout += ", " + self.capitalizer(str(key)) + ": " + str(value)
726 | printout += "\n"
727 | print(printout)
728 |
729 | def item_not_found(self, item_type: str, item_name: str) -> None:
730 | print(f'That "{item_type}" "{item_name}" does not exist in the records')
731 |
732 |
733 | class ItemController(Controller):
734 | """Concrete item controller."""
735 |
736 | def __init__(self, item_model: Model, item_view: View) -> None:
737 | self._model = item_model
738 | self._view = item_view
739 |
740 | def show_items(self) -> None:
741 | items: List = list(self._model)
742 | item_type: str = self._model.item_type
743 | self._view.show_item_list(item_type, items)
744 |
745 | def show_item_information(self, item_name: str) -> None:
746 | try:
747 | item_info: Dict[str, Any] = self._model.get(item_name)
748 | except KeyError:
749 | item_type: str = self._model.item_type
750 | self._view.item_not_found(item_type, item_name)
751 | else:
752 | item_type: str = self._model.item_type
753 | self._view.show_item_information(item_type, item_name, item_info)
754 |
755 |
756 | if __name__ == "__main__":
757 | model: Model = ProductModel()
758 | view: View = ConsoleView()
759 | controller: ItemController = ItemController(model, view)
760 | controller.show_items()
761 | controller.show_item_information("cheese")
762 | controller.show_item_information("eggs")
763 | controller.show_item_information("milk")
764 | controller.show_item_information("arepas")
765 |
766 |
767 | # OUTPUT #
768 | # PRODUCT LIST:
769 | # cheese
770 | # eggs
771 | # milk
772 | #
773 | # PRODUCT INFORMATION:
774 | # Name: Cheese, Price: 2.00, Quantity: 10
775 | #
776 | # PRODUCT INFORMATION:
777 | # Name: Eggs, Price: 0.20, Quantity: 100
778 | #
779 | # PRODUCT INFORMATION:
780 | # Name: Milk, Price: 1.50, Quantity: 10
781 | #
782 | # That product "arepas" does not exist in the records
783 | ```
784 |
785 | **[⬆ back to top](#table-of-contents)**
786 |
787 | ### Decorator
788 | Decorator type of patterns add new feature to an existing object. Supports dynamic changes.
789 | - Exercise:
790 | - Add additional message to an existing function
791 |
792 | **Decorator function**
793 | ```python
794 | from functools import wraps
795 | from typing import Callable
796 |
797 |
798 | def make_blink(function: Callable[[str], str]) -> Callable[..., str]:
799 | """Defines the decorator function."""
800 |
801 | @wraps(function)
802 | def decorator(*args, **kwargs) -> str:
803 | result: str = function(*args, **kwargs)
804 | return f""
805 |
806 | return decorator
807 |
808 |
809 | @make_blink
810 | def hello_world(name: str) -> str:
811 | """Original function."""
812 | return f'Hello World said "{name}"!'
813 |
814 |
815 | print(hello_world(name="James"))
816 | print(hello_world.__name__)
817 | print(hello_world.__doc__)
818 | ```
819 |
820 | **Decorator class**
821 | ```python
822 | from abc import ABC, abstractmethod
823 |
824 |
825 | class Number(ABC):
826 | """Abstraction of a number object."""
827 |
828 | @abstractmethod
829 | def value(self) -> int:
830 | pass
831 |
832 |
833 | class Integer(Number):
834 | """A subclass of a number."""
835 |
836 | def __init__(self, value: int) -> None:
837 | self._value = value
838 |
839 | def value(self) -> int:
840 | return self._value
841 |
842 |
843 | class Float(Number):
844 | """Decorator object converts `int` datatype into `float` datatype."""
845 |
846 | def __init__(self, number: Number) -> None:
847 | self._number: Number = number
848 |
849 | def value(self) -> float:
850 | return float(self._number.value())
851 |
852 |
853 | class SumOfFloat(Number):
854 | """Sum of two `float` numbers."""
855 |
856 | def __init__(self, one: Number, two: Number) -> None:
857 | self._one: Float = Float(one)
858 | self._two: Float = Float(two)
859 |
860 | def value(self) -> float:
861 | return self._one.value() + self._two.value()
862 |
863 |
864 | integer_one: Number = Integer(value=5)
865 | integer_two: Number = Integer(value=6)
866 | sum_float: Number = SumOfFloat(integer_one, integer_two)
867 | print(sum_float.value())
868 | ```
869 |
870 | **[⬆ back to top](#table-of-contents)**
871 |
872 | ### Proxy
873 | Proxy patterns postpones object creation unless it is necessary. Object is too expensive (resource intensive) to create that's why we have to create it once it is needed.
874 | - Participants:
875 | - Producer
876 | - Artist
877 | - Guest
878 | - Clients interact with a Proxy. Proxy is responsible for creating the resource intensive objects
879 |
880 | ```python
881 | import time
882 |
883 |
884 | class Producer:
885 | """Defines the resource-intensive object to instantiate."""
886 |
887 | def produce(self) -> None:
888 | print("Producer is working hard!")
889 |
890 | def meet(self) -> None:
891 | print("Producer has time to meet you now")
892 |
893 |
894 | class Proxy:
895 | """Defines the less resource-intensive object to instantiate as a middleman."""
896 |
897 | def __init__(self):
898 | self._occupied: bool = False
899 |
900 | @property
901 | def occupied(self) -> bool:
902 | return self._occupied
903 |
904 | @occupied.setter
905 | def occupied(self, state: bool) -> None:
906 | if not isinstance(state, bool):
907 | raise ValueError(f'"{state}" value should be a boolean data type!')
908 | self._occupied = state
909 |
910 | def produce(self) -> None:
911 | print("Artist checking if producer is available ...")
912 | if not self.occupied:
913 | producer: Producer = Producer()
914 | time.sleep(2)
915 | producer.meet()
916 | else:
917 | time.sleep(2)
918 | print("Producer is busy!")
919 |
920 |
921 | proxy: Proxy = Proxy()
922 | proxy.produce()
923 | proxy.occupied = True
924 | proxy.produce()
925 | ```
926 |
927 | **[⬆ back to top](#table-of-contents)**
928 |
929 | ### Adapter
930 | Adapter patterns convert interface of a class into another one that client is expecting.
931 | - Exercise:
932 | - Korean language: `speak_korean()`
933 | - British language: `speak_english()`
934 | - Client has to have uniform interface - `speak method`
935 | - Solution:
936 | - Use an adapter pattern that translates method name between client and the server code
937 |
938 | ```python
939 | from abc import ABC, abstractmethod
940 | from typing import Any
941 |
942 |
943 | class Speaker(ABC):
944 | """Abstract interface for some speaker."""
945 |
946 | @abstractmethod
947 | def type(self) -> str:
948 | pass
949 |
950 |
951 | class Korean(Speaker):
952 | """Korean speaker."""
953 |
954 | def __init__(self) -> None:
955 | self._type: str = "Korean"
956 |
957 | def type(self) -> str:
958 | return self._type
959 |
960 | def speak_korean(self) -> str:
961 | return "An-neyong?"
962 |
963 |
964 | class British(Speaker):
965 | """English speaker."""
966 |
967 | def __init__(self):
968 | self._type: str = "British"
969 |
970 | def type(self) -> str:
971 | return self._type
972 |
973 | def speak_english(self) -> str:
974 | return "Hello"
975 |
976 |
977 | class Adapter:
978 | """Changes the generic method name to individualized method names."""
979 |
980 | def __init__(self, obj: Any, **adapted_method: Any) -> None:
981 | self._object = obj
982 | self.__dict__.update(adapted_method)
983 |
984 | def __getattr__(self, item: Any) -> Any:
985 | return getattr(self._object, item)
986 |
987 |
988 | speakers: list = []
989 | korean = Korean()
990 | british = British()
991 | speakers.append(Adapter(korean, speak=korean.speak_korean))
992 | speakers.append(Adapter(british, speak=british.speak_english))
993 |
994 | for speaker in speakers:
995 | print(f"{speaker.type()} says '{speaker.speak()}'")
996 | ```
997 |
998 | **[⬆ back to top](#table-of-contents)**
999 |
1000 | ### Composite
1001 | - Exercise:
1002 | - Build recursive tree data structure. (Menu > submenu > sub-submenu > ...)
1003 | - Participants:
1004 | - Component - abstract 'class'
1005 | - Child - inherits from Component 'class'
1006 | - Composite - inherits from component 'class'. Maintain child objects by adding.removing them
1007 |
1008 | ```python
1009 | from abc import ABC, abstractmethod
1010 | from typing import Sequence, List
1011 |
1012 |
1013 | class Component(ABC):
1014 | """Abstract interface of some component."""
1015 |
1016 | @abstractmethod
1017 | def function(self) -> None:
1018 | pass
1019 |
1020 |
1021 | class Child(Component):
1022 | """Concrete child component."""
1023 |
1024 | def __init__(self, *args: str) -> None:
1025 | self._args: Sequence[str] = args
1026 |
1027 | def name(self) -> str:
1028 | return self._args[0]
1029 |
1030 | def function(self) -> None:
1031 | print(f'"{self.name()}" component')
1032 |
1033 |
1034 | class Composite(Component):
1035 | """Concrete class maintains the tree recursive structure."""
1036 |
1037 | def __init__(self, *args: str) -> None:
1038 | self._args: Sequence[str] = args
1039 | self._children: List[Component] = []
1040 |
1041 | def name(self) -> str:
1042 | return self._args[0]
1043 |
1044 | def append_child(self, child: Component) -> None:
1045 | self._children.append(child)
1046 |
1047 | def remove_child(self, child: Component) -> None:
1048 | self._children.remove(child)
1049 |
1050 | def function(self) -> None:
1051 | print(f'"{self.name()}" component')
1052 | for child in self._children: # type: Component
1053 | child.function()
1054 |
1055 |
1056 | top_menu = Composite("top_menu")
1057 |
1058 | submenu_one = Composite("submenu one")
1059 | child_submenu_one = Child("sub_submenu one")
1060 | child_submenu_two = Child("sub_submenu two")
1061 | submenu_one.append_child(child_submenu_one)
1062 | submenu_one.append_child(child_submenu_two)
1063 |
1064 | submenu_two = Child("submenu two")
1065 | top_menu.append_child(submenu_one)
1066 | top_menu.append_child(submenu_two)
1067 | top_menu.function()
1068 | ```
1069 |
1070 | **[⬆ back to top](#table-of-contents)**
1071 |
1072 | ### Bridge
1073 | Bridge pattern separates the abstraction into different class hierarchies.
1074 | Abstract factory and adapter patterns are related to Bridge design pattern.
1075 |
1076 | ```python
1077 | from abc import ABC, abstractmethod
1078 |
1079 |
1080 | class DrawApi(ABC):
1081 | """Provides draw interface."""
1082 |
1083 | @abstractmethod
1084 | def draw_circle(self, x: int, y: int, radius: int) -> None:
1085 | pass
1086 |
1087 |
1088 | class Circle(ABC):
1089 | """Provides circle shape interface."""
1090 |
1091 | @abstractmethod
1092 | def draw(self) -> None:
1093 | pass
1094 |
1095 | @abstractmethod
1096 | def scale(self, percentage: int) -> None:
1097 | pass
1098 |
1099 |
1100 | class DrawApiOne(DrawApi):
1101 | """Implementation-specific abstraction: concrete class one."""
1102 |
1103 | def draw_circle(self, x: int, y: int, radius: int) -> None:
1104 | print(f"API 1 drawing a circle at ({x}, {y} with radius {radius}!)")
1105 |
1106 |
1107 | class DrawApiTwo(DrawApi):
1108 | """Implementation-specific abstraction: concrete class two."""
1109 |
1110 | def draw_circle(self, x: int, y: int, radius: int) -> None:
1111 | print(f"API 2 drawing a circle at ({x}, {y} with radius {radius}!)")
1112 |
1113 |
1114 | class DrawCircle(Circle):
1115 | """Implementation-independent abstraction: e.g there could be a rectangle class!."""
1116 |
1117 | def __init__(self, x: int, y: int, radius: int, draw_api: DrawApi) -> None:
1118 | self._x: int = x
1119 | self._y: int = y
1120 | self._radius: int = radius
1121 | self._draw_api: DrawApi = draw_api
1122 |
1123 | def draw(self) -> None:
1124 | self._draw_api.draw_circle(self._x, self._y, self._radius)
1125 |
1126 | def scale(self, percentage: int) -> None:
1127 | if not isinstance(percentage, int):
1128 | raise ValueError(
1129 | f'"{percentage}" value should be an integer data type!'
1130 | )
1131 | self._radius *= percentage
1132 |
1133 |
1134 | circle_one: Circle = DrawCircle(1, 2, 3, DrawApiOne())
1135 | circle_one.draw()
1136 | circle_two: Circle = DrawCircle(3, 4, 6, DrawApiTwo())
1137 | circle_two.draw()
1138 | ```
1139 |
1140 | **[⬆ back to top](#table-of-contents)**
1141 |
1142 | ### Facade
1143 | The Facade pattern is a way to provide a simpler unified interface to a more complex system.
1144 | It provides an easier way to access functions of the underlying system by providing a single entry point.
1145 |
1146 | ```python
1147 | from abc import ABC, abstractmethod
1148 | import time
1149 | from typing import List, Tuple, Iterator, Type
1150 |
1151 | _sleep: float = 0.2
1152 |
1153 |
1154 | class TestCase(ABC):
1155 | """Abstract test case interface."""
1156 |
1157 | @abstractmethod
1158 | def run(self) -> None:
1159 | pass
1160 |
1161 |
1162 | class TestCaseOne(TestCase):
1163 | """Concrete test case one."""
1164 |
1165 | def __init__(self, name: str) -> None:
1166 | self._name: str = name
1167 |
1168 | def run(self) -> None:
1169 | print("{:#^20}".format(self._name))
1170 | time.sleep(_sleep)
1171 | print("Setting up testcase one")
1172 | time.sleep(_sleep)
1173 | print("Running test")
1174 | time.sleep(_sleep)
1175 | print("Tearing down")
1176 | time.sleep(_sleep)
1177 | print("Test Finished\n")
1178 |
1179 |
1180 | class TestCaseTwo(TestCase):
1181 | """Concrete test case two."""
1182 |
1183 | def __init__(self, name: str) -> None:
1184 | self._name: str = name
1185 |
1186 | def run(self) -> None:
1187 | print("{:#^20}".format(self._name))
1188 | time.sleep(_sleep)
1189 | print("Setting up testcase two")
1190 | time.sleep(_sleep)
1191 | print("Running test")
1192 | time.sleep(_sleep)
1193 | print("Tearing down")
1194 | time.sleep(_sleep)
1195 | print("Test Finished\n")
1196 |
1197 |
1198 | class TestCaseThree(TestCase):
1199 | """Concrete test case three."""
1200 |
1201 | def __init__(self, name: str) -> None:
1202 | self._name: str = name
1203 |
1204 | def run(self) -> None:
1205 | print("{:#^20}".format(self._name))
1206 | time.sleep(_sleep)
1207 | print("Setting up testcase three")
1208 | time.sleep(_sleep)
1209 | print("Running test")
1210 | time.sleep(_sleep)
1211 | print("Tearing down")
1212 | time.sleep(_sleep)
1213 | print("Test Finished\n")
1214 |
1215 |
1216 | class TestSuite:
1217 | """Represents simpler unified interface to run all test cases.
1218 |
1219 | A facade class itself.
1220 | """
1221 |
1222 | def __init__(self, testcases: List[TestCase]) -> None:
1223 | self._testcases = testcases
1224 |
1225 | def run(self) -> None:
1226 | for testcase in self._testcases: # type: TestCase
1227 | testcase.run()
1228 |
1229 |
1230 | test_cases: List[TestCase] = [
1231 | TestCaseOne("TC1"),
1232 | TestCaseTwo("TC2"),
1233 | TestCaseThree("TC3")
1234 | ]
1235 | test_suite = TestSuite(test_cases)
1236 | test_suite.run()
1237 |
1238 |
1239 | class Interface(ABC):
1240 | """Abstract interface."""
1241 |
1242 | @abstractmethod
1243 | def run(self) -> str:
1244 | pass
1245 |
1246 |
1247 | class A(Interface):
1248 | """Implement interface."""
1249 |
1250 | def run(self) -> str:
1251 | return "A.run()"
1252 |
1253 |
1254 | class B(Interface):
1255 | """Implement interface."""
1256 |
1257 | def run(self) -> str:
1258 | return "B.run()"
1259 |
1260 |
1261 | class C(Interface):
1262 | """Implement interface."""
1263 |
1264 | def run(self) -> str:
1265 | return "C.run()"
1266 |
1267 |
1268 | class Facade(Interface):
1269 | """Facade object."""
1270 |
1271 | def __init__(self):
1272 | self._all: Tuple[Type[Interface], ...] = (A, B, C)
1273 |
1274 | def run(self) -> Iterator[Interface]:
1275 | yield from self._all
1276 |
1277 |
1278 | if __name__ == "__main__":
1279 | print(*(cls().run() for cls in Facade().run()))
1280 | ```
1281 |
1282 | **[⬆ back to top](#table-of-contents)**
1283 |
1284 | ## Behavioral
1285 | Behavioral patterns provide best practices of objects interaction. Methods and signatures are often used.
1286 |
1287 | ### Observer
1288 | Observer pattern establishes one to many relationship between subject and multiple observers. Singleton is related to observer design pattern.
1289 | - Exercise:
1290 | - Subjects need to be monitored
1291 | - Observers need to be notified
1292 | - Participants:
1293 | - Subject: abstract class
1294 | - Attach
1295 | - Detach
1296 | - Notify
1297 | - Concrete Subjects
1298 |
1299 | ```python
1300 | from typing import List
1301 |
1302 |
1303 | class Subject:
1304 | """Represents what is being observed. Needs to be monitored."""
1305 |
1306 | def __init__(self, name: str = "") -> None:
1307 | self._observers: List["TempObserver"] = []
1308 | self._name: str = name
1309 | self._temperature: int = 0
1310 |
1311 | def attach(self, observer: "TempObserver") -> None:
1312 | if observer not in self._observers:
1313 | self._observers.append(observer)
1314 |
1315 | def detach(self, observer: "TempObserver") -> None:
1316 | try:
1317 | self._observers.remove(observer)
1318 | except ValueError:
1319 | pass
1320 |
1321 | def notify(self, modifier=None) -> None:
1322 | for observer in self._observers:
1323 | if modifier != observer:
1324 | observer.update(self)
1325 |
1326 | @property
1327 | def name(self) -> str:
1328 | return self._name
1329 |
1330 | @property
1331 | def temperature(self) -> int:
1332 | return self._temperature
1333 |
1334 | @temperature.setter
1335 | def temperature(self, temperature: int) -> None:
1336 | if not isinstance(temperature, int):
1337 | raise ValueError(f'"{temperature}" value should be an integer data type!')
1338 | self._temperature = temperature
1339 |
1340 |
1341 | class TempObserver:
1342 | """Represents an observer class. Needs to be notified."""
1343 |
1344 | def update(self, subject: Subject) -> None:
1345 | print(f"Temperature Viewer: {subject.name} has Temperature {subject.temperature}")
1346 |
1347 |
1348 | subject_one = Subject("Subject One")
1349 | subject_two = Subject("Subject Two")
1350 |
1351 | observer_one = TempObserver()
1352 | observer_two = TempObserver()
1353 |
1354 | subject_one.attach(observer_one)
1355 | subject_one.attach(observer_two)
1356 |
1357 | subject_one.temperature = 80
1358 | subject_one.notify()
1359 |
1360 | subject_one.temperature = 90
1361 | subject_one.notify()
1362 | ```
1363 |
1364 | **[⬆ back to top](#table-of-contents)**
1365 |
1366 | ### Visitor
1367 | Visitor pattern adds new features to existing hierarchy without changing it. Add new operations to existing classes dynamically.
1368 | Exercise:
1369 | - House class:
1370 | - HVAC specialist: Visitor type 1
1371 | - Electrician: Visitor type 2
1372 |
1373 | ```python
1374 | from abc import ABC, abstractmethod
1375 |
1376 |
1377 | class Visitor(ABC):
1378 | """Abstract visitor."""
1379 |
1380 | @abstractmethod
1381 | def visit(self, house: "House") -> None:
1382 | pass
1383 |
1384 | def __str__(self) -> str:
1385 | return self.__class__.__name__
1386 |
1387 |
1388 | class House(ABC):
1389 | """Abstract house."""
1390 |
1391 | @abstractmethod
1392 | def accept(self, visitor: Visitor) -> None:
1393 | pass
1394 |
1395 | @abstractmethod
1396 | def work_on_hvac(self, specialist: Visitor) -> None:
1397 | pass
1398 |
1399 | @abstractmethod
1400 | def work_on_electricity(self, specialist: Visitor) -> None:
1401 | pass
1402 |
1403 | def __str__(self) -> str:
1404 | return self.__class__.__name__
1405 |
1406 |
1407 | class ConcreteHouse(House):
1408 | """Represent concrete house."""
1409 |
1410 | def accept(self, visitor: Visitor) -> None:
1411 | visitor.visit(self)
1412 |
1413 | def work_on_hvac(self, specialist: Visitor) -> None:
1414 | print(self, "worked on by", specialist)
1415 |
1416 | def work_on_electricity(self, specialist: Visitor) -> None:
1417 | print(self, "worked on by", specialist)
1418 |
1419 |
1420 | class HvacSpecialist(Visitor):
1421 | """Concrete visitor: HVAC specialist."""
1422 |
1423 | def visit(self, house: House) -> None:
1424 | house.work_on_hvac(self)
1425 |
1426 |
1427 | class Electrician(Visitor):
1428 | """Concrete visitor: electrician."""
1429 |
1430 | def visit(self, house: House) -> None:
1431 | house.work_on_electricity(self)
1432 |
1433 |
1434 | hvac: Visitor = HvacSpecialist()
1435 | electrician: Visitor = Electrician()
1436 | home: House = ConcreteHouse()
1437 | home.accept(hvac)
1438 | home.accept(electrician)
1439 | ```
1440 |
1441 | **[⬆ back to top](#table-of-contents)**
1442 |
1443 | ### Iterator
1444 | Composite pattern is related to iterator pattern.
1445 | - Exercise:
1446 | - Our custom iterator based on a build-in python iterator: `zip()`
1447 | - Will iterate over a certain point based on client input
1448 |
1449 | **Iterator function**
1450 |
1451 | ```python
1452 | from typing import Iterator, Tuple, List
1453 |
1454 |
1455 | def count_to(count: int) -> Iterator[Tuple[int, str]]:
1456 | """Our iterator implementation."""
1457 | numbers_in_german: List[str] = ["einn", "zwei", "drei", "veir", "funf"]
1458 | iterator: Iterator[Tuple[int, str]] = zip(range(1, count + 1), numbers_in_german)
1459 | for position, number in iterator: # type: int, str
1460 | yield position, number
1461 |
1462 |
1463 | for number_ in count_to(3): # type: Tuple[int]
1464 | print("{} in german is {}".format(*number_))
1465 |
1466 |
1467 | class IteratorSequence:
1468 | """Represent iterator sequence object."""
1469 |
1470 | def __init__(self, capacity: int) -> None:
1471 | self._range: Iterator[int] = iter(range(capacity))
1472 |
1473 | def __next__(self) -> int:
1474 | return next(self._range)
1475 |
1476 | def __iter__(self) -> Iterator[int]:
1477 | return self
1478 |
1479 |
1480 | iterator_: IteratorSequence = IteratorSequence(capacity=10)
1481 | for _ in range(10): # type: int
1482 | print(next(iterator_))
1483 | ```
1484 |
1485 | **[⬆ back to top](#table-of-contents)**
1486 |
1487 | ### Strategy
1488 | Strategy patterns used to dynamically change the behavior of an object. Add dynamically objects with `types` module.
1489 | - Participants:
1490 | - Abstract strategy class with default set of behaviors
1491 | - Concrete strategy class with new behaviors
1492 |
1493 | ```python
1494 | import types
1495 | from typing import Callable, Any
1496 |
1497 |
1498 | class Strategy:
1499 | """A strategy pattern class."""
1500 |
1501 | def __init__(self, func: Callable[["Strategy"], Any] = None) -> None:
1502 | self._name: str = "Default strategy"
1503 | if func:
1504 | self.execute = types.MethodType(func, self)
1505 |
1506 | @property
1507 | def name(self) -> str:
1508 | return self._name
1509 |
1510 | @name.setter
1511 | def name(self, name: str) -> None:
1512 | if not isinstance(name, str):
1513 | raise ValueError(f'"{name}" value should be a string data type!')
1514 | self._name = name
1515 |
1516 | def execute(self):
1517 | print(f"{self._name} is used")
1518 |
1519 |
1520 | def strategy_function_one(strategy: Strategy) -> None:
1521 | print(f"{strategy.name} is used to execute method one")
1522 |
1523 |
1524 | def strategy_function_two(strategy: Strategy) -> None:
1525 | print(f"{strategy.name} is used to execute method two")
1526 |
1527 |
1528 | default_strategy = Strategy()
1529 | default_strategy.execute()
1530 |
1531 | first_strategy = Strategy(func=strategy_function_one)
1532 | first_strategy.name = "Strategy one"
1533 | first_strategy.execute()
1534 |
1535 | second_strategy = Strategy(func=strategy_function_two)
1536 | second_strategy.name = "Strategy two"
1537 | second_strategy.execute()
1538 | ```
1539 |
1540 | **[⬆ back to top](#table-of-contents)**
1541 |
1542 | ### Chain of responsibility
1543 | This type of pattern decouples responsibility. Composite is related to this design pattern.
1544 | - Exercise:
1545 | - Integer value
1546 | - Handlers
1547 | - Find out its range
1548 | - Participants:
1549 | - Abstract handler
1550 | - Successor
1551 | - Concrete Handler
1552 | - Checks if it can handle the request
1553 |
1554 | ```python
1555 | from abc import abstractmethod
1556 | from typing import List
1557 |
1558 |
1559 | class Handler:
1560 | """Abstract handler."""
1561 |
1562 | def __init__(self, successor: "Handler") -> None:
1563 | self._successor: Handler = successor
1564 |
1565 | def handler(self, request: int) -> None:
1566 | if not self.handle(request):
1567 | self._successor.handler(request)
1568 |
1569 | @abstractmethod
1570 | def handle(self, request: int) -> bool:
1571 | pass
1572 |
1573 |
1574 | class ConcreteHandler1(Handler):
1575 | """Concrete handler 1."""
1576 |
1577 | def handle(self, request: int) -> bool:
1578 | if 0 < request <= 10:
1579 | print(f"Request {request} handled in handler 1")
1580 | return True
1581 | return False
1582 |
1583 |
1584 | class DefaultHandler(Handler):
1585 | """Default handler."""
1586 |
1587 | def handle(self, request: int) -> bool:
1588 | """If there is no handler available."""
1589 | print(f"End of chain, no handler for {request}")
1590 | return True
1591 |
1592 |
1593 | class Client:
1594 | """Using handlers."""
1595 |
1596 | def __init__(self) -> None:
1597 | self._handler: Handler = ConcreteHandler1(DefaultHandler(None))
1598 |
1599 | def delegate(self, request: List[int]) -> None:
1600 | for next_request in request:
1601 | self._handler.handler(next_request)
1602 |
1603 |
1604 | # Create a client
1605 | client: Client = Client()
1606 |
1607 | # Create requests
1608 | requests: List[int] = [2, 5, 30]
1609 |
1610 | # Send the request
1611 | client.delegate(requests)
1612 | ```
1613 |
1614 | **[⬆ back to top](#table-of-contents)**
1615 |
1616 | ## Development notes
1617 |
1618 | ### Code analysis
1619 | From the root directory of your shell please run following command to start static code assessment (it will check code with linter rules and unit testing):
1620 |
1621 | ```bash
1622 | ./run-code-analysis.sh
1623 | ```
1624 |
1625 | ### Commit template
1626 |
1627 | Please use the following command to include gitcommit message template within the project:
1628 | ```bash
1629 | git config commit.template .gitcommit.txt
1630 | ```
1631 |
1632 | ### Release notes
1633 |
1634 | Please check [changelog](CHANGELOG.md) file to get more details about actual versions and it's release notes.
1635 |
1636 | ### Meta
1637 | Author – Volodymyr Yahello vyahello@gmail.com
1638 |
1639 | Distributed under the `MIT` license. See [license](LICENSE.md) for more information.
1640 |
1641 | You can reach out me at:
1642 | * [vyahello@gmail.com](vyahello@gmail.com)
1643 | * [https://twitter.com/vyahello](https://twitter.com/vyahello)
1644 | * [https://www.linkedin.com/in/volodymyr-yahello-821746127](https://www.linkedin.com/in/volodymyr-yahello-821746127)
1645 |
1646 | ### Contributing
1647 | I would highly appreciate any contribution and support. If you are interested to add your ideas into project please follow next simple steps:
1648 |
1649 | 1. Clone the repository
1650 | 2. Configure `git` for the first time after cloning with your `name` and `email`
1651 | 3. `pip install -r requirements.txt` to install all project dependencies
1652 | 4. Create your feature branch (git checkout -b feature/fooBar)
1653 | 5. Commit your changes (git commit -am 'Add some fooBar')
1654 | 6. Push to the branch (git push origin feature/fooBar)
1655 | 7. Create a new Pull Request
1656 |
1657 | ### What's next
1658 |
1659 | All recent activities and ideas are described at project [issues](https://github.com/vyahello/python-ood/issues) page.
1660 | If you have ideas you want to change/implement please do not hesitate and create an issue.
1661 |
1662 | **[⬆ back to top](#table-of-contents)**
1663 |
--------------------------------------------------------------------------------