diff --git a/pypatterns/behavioral/__init__.py b/pypatterns/behavioral/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pypatterns/behavioral/chain.py b/pypatterns/behavioral/chain.py new file mode 100644 index 0000000..019e5b2 --- /dev/null +++ b/pypatterns/behavioral/chain.py @@ -0,0 +1,79 @@ +from abc import ABCMeta, abstractmethod + + +class ChainException(Exception): + """ + Exception for when a chain link could not handle a request. + """ + pass + + +class ChainLink(object, metaclass=ABCMeta): + """ + Abstract ChainLink object as part of the Chain of Responsibility pattern. + """ + def __init__(self): + """ + Initialize a new ChainLink instance. + """ + self.successor = None + + def set_successor(self, successor): + """ + Set a chain link to call if this chain link fails. + + @param successor: The chain link to call if this chain link fails. + @type successor: ChainLink + """ + self.successor = successor + + def successor_handle(self, request): + """ + Have this chain links successor handle a request. + + @param request: The request to handle. + """ + try: + return self.successor.handle(request) + except AttributeError: + raise ChainException + + @abstractmethod + def handle(self, request): + """ + Handle a request. + + @param request: The request to handle. + """ + pass + + +class Chain(object, metaclass=ABCMeta): + """ + Abstract Chain class as part of the Chain of Responsibility pattern. + """ + def __init__(self, chainlink): + """ + Initialize a new Chain instance. + + @param chainlink: The starting chain link. + """ + self.chainlink = chainlink + + def handle(self, request): + """ + Handle a request. + + @param request: The request to handle. + """ + try: + return self.chainlink.handle(request) + except ChainException: + return self.fail() + + @abstractmethod + def fail(self): + """ + The method to call when the chain could not handle a request. + """ + pass \ No newline at end of file diff --git a/setup.py b/setup.py index 72ce479..2736ff4 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,9 @@ setup( description='Python Design Patterns', author='Tyler LaBerge', packages=[ - 'pypatterns', - 'pypatterns.creational' + 'pypatterns', + 'pypatterns.creational', + 'pypatterns.behavioral' ], ) + diff --git a/tests/behavioral_tests/__init__.py b/tests/behavioral_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/behavioral_tests/test_chain.py b/tests/behavioral_tests/test_chain.py new file mode 100644 index 0000000..d2e2935 --- /dev/null +++ b/tests/behavioral_tests/test_chain.py @@ -0,0 +1,140 @@ +from unittest import TestCase +from pypatterns.behavioral.chain import ChainException, ChainLink, Chain + + +class ChainLinkTestCase(TestCase): + """ + Unit testing class for the ChainLink class. + """ + def setUp(self): + """ + Initialize testing data. + """ + class ConcreteChainLinkThree(ChainLink): + + def handle(self, request): + if request == 'handle_three': + return "Handled in chain link three" + else: + return self.successor_handle(request) + + class ConcreteChainLinkTwo(ChainLink): + + def __init__(self): + super().__init__() + self.set_successor(ConcreteChainLinkThree()) + + def handle(self, request): + if request == 'handle_two': + return "Handled in chain link two" + else: + return self.successor_handle(request) + + class ConcreteChainLinkOne(ChainLink): + + def __init__(self): + super().__init__() + self.set_successor(ConcreteChainLinkTwo()) + + def handle(self, request): + if request == 'handle_one': + return "Handled in chain link one" + else: + return self.successor_handle(request) + + self.chain_link_one_class = ConcreteChainLinkOne + + def test_success_handle(self): + """ + Test the handle method with successful requests. + + @raise AssertionError: If the test fails. + """ + handler = self.chain_link_one_class() + + self.assertEquals("Handled in chain link one", handler.handle("handle_one")) + self.assertEquals("Handled in chain link two", handler.handle("handle_two")) + self.assertEquals("Handled in chain link three", handler.handle("handle_three")) + + def test_fail_handle(self): + """ + Test the handle method with unsuccessful requests. + + @raise AssertionError: If the test fails. + """ + handler = self.chain_link_one_class() + with self.assertRaises(ChainException): + handler.handle("foo") + + +class ChainTestCase(TestCase): + """ + Unit testing class for the Chain class. + """ + def setUp(self): + """ + Initialize testing data. + """ + class ConcreteChainLinkThree(ChainLink): + + def handle(self, request): + if request == 'handle_three': + return "Handled in chain link three" + else: + return self.successor_handle(request) + + class ConcreteChainLinkTwo(ChainLink): + + def __init__(self): + super().__init__() + self.set_successor(ConcreteChainLinkThree()) + + def handle(self, request): + if request == 'handle_two': + return "Handled in chain link two" + else: + return self.successor_handle(request) + + class ConcreteChainLinkOne(ChainLink): + + def __init__(self): + super().__init__() + self.set_successor(ConcreteChainLinkTwo()) + + def handle(self, request): + if request == 'handle_one': + return "Handled in chain link one" + else: + return self.successor_handle(request) + + class ConcreteChain(Chain): + + def __init__(self): + super().__init__(ConcreteChainLinkOne()) + + def fail(self): + return 'Fail' + + self.chain_class = ConcreteChain + + def test_success_handle(self): + """ + Test the handle method with a successful request + + @raise AssertionError: If the test fails. + """ + chain = self.chain_class() + + self.assertEquals("Handled in chain link one", chain.handle("handle_one")) + self.assertEquals("Handled in chain link two", chain.handle("handle_two")) + self.assertEquals("Handled in chain link three", chain.handle("handle_three")) + + def test_fail_handle(self): + """ + Test the handle method with unsuccessful requests. + + @raise AssertionError: If the test fails. + """ + chain = self.chain_class() + + self.assertEquals("Fail", chain.handle("foo"))