├── 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"{result}" 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 | ![Screenshot](logo.png) 2 | 3 | [![made-with-python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/) 4 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 5 | [![Checked with pylint](https://img.shields.io/badge/pylint-checked-blue)](https://www.pylint.org) 6 | [![Build Status](https://www.travis-ci.com/vyahello/python-ood.svg?branch=master)](https://www.travis-ci.com/github/vyahello/python-ood) 7 | [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE.md) 8 | [![CodeFactor](https://www.codefactor.io/repository/github/vyahello/python-ood/badge)](https://www.codefactor.io/repository/github/vyahello/python-ood) 9 | [![EO principles respected here](https://www.elegantobjects.org/badge.svg)](https://www.elegantobjects.org) 10 | [![Docs](https://img.shields.io/badge/docs-github-orange)](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"{result}" 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 | --------------------------------------------------------------------------------