From befcee9df308e8d17f1c29611ba87c3a63fd20da Mon Sep 17 00:00:00 2001 From: M Date: Mon, 8 Apr 2024 14:03:42 -0700 Subject: [PATCH 1/5] Add docstring for Servant behavioral design pattern --- patterns/behavioral/servant.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 patterns/behavioral/servant.py diff --git a/patterns/behavioral/servant.py b/patterns/behavioral/servant.py new file mode 100644 index 00000000..d04a1fae --- /dev/null +++ b/patterns/behavioral/servant.py @@ -0,0 +1,21 @@ +""" +Implementation of the Servant design pattern. + +The Servant design pattern is a behavioral pattern used to offer functionality +to a group of classes without requiring them to inherit from a base class. + +This pattern involves creating a Servant class that provides certain services +or functionalities. These services are used by other classes which do not need +to be related through a common parent class. It is particularly useful in +scenarios where adding the desired functionality through inheritance is impractical +or would lead to a rigid class hierarchy. + +This pattern is characterized by the following: + +- A Servant class that provides specific services or actions. +- Client classes that need these services, but do not derive from the Servant class. +- The use of the Servant class by the client classes to perform actions on their behalf. + +References: +- https://en.wikipedia.org/wiki/Servant_(design_pattern) +""" From 4e004484dadde6284922240406cc5147c7b8abf8 Mon Sep 17 00:00:00 2001 From: M Date: Mon, 8 Apr 2024 14:21:28 -0700 Subject: [PATCH 2/5] Implement Servant class --- patterns/behavioral/servant.py | 84 ++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/patterns/behavioral/servant.py b/patterns/behavioral/servant.py index d04a1fae..ac478971 100644 --- a/patterns/behavioral/servant.py +++ b/patterns/behavioral/servant.py @@ -19,3 +19,87 @@ References: - https://en.wikipedia.org/wiki/Servant_(design_pattern) """ +import math + +class Position: + """Representation of a 2D position with x and y coordinates.""" + + def __init__(self, x, y): + self.x = x + self.y = y + +class Circle: + """Representation of a circle defined by a radius and a position.""" + + def __init__(self, radius, position: Position): + self.radius = radius + self.position = position + +class Rectangle: + """Representation of a rectangle defined by width, height, and a position.""" + + def __init__(self, width, height, position: Position): + self.width = width + self.height = height + self.position = position + + +class GeometryTools: + """ + Servant class providing geometry-related services, including area and + perimeter calculations and position updates. + """ + + @staticmethod + def calculate_area(shape): + """ + Calculate the area of a given shape. + + Args: + shape: The geometric shape whose area is to be calculated. + + Returns: + The area of the shape. + + Raises: + ValueError: If the shape type is unsupported. + """ + if isinstance(shape, Circle): + return math.pi * shape.radius ** 2 + elif isinstance(shape, Rectangle): + return shape.width * shape.height + else: + raise ValueError("Unsupported shape type") + + @staticmethod + def calculate_perimeter(shape): + """ + Calculate the perimeter of a given shape. + + Args: + shape: The geometric shape whose perimeter is to be calculated. + + Returns: + The perimeter of the shape. + + Raises: + ValueError: If the shape type is unsupported. + """ + if isinstance(shape, Circle): + return 2 * math.pi * shape.radius + elif isinstance(shape, Rectangle): + return 2 * (shape.width + shape.height) + else: + raise ValueError("Unsupported shape type") + + @staticmethod + def move_to(shape, new_position: Position): + """ + Move a given shape to a new position. + + Args: + shape: The geometric shape to be moved. + new_position: The new position to move the shape to. + """ + shape.position = new_position + print(f"Moved to ({shape.position.x}, {shape.position.y})") From 1e23255f52265afbc68f157698c0dce344c82e5e Mon Sep 17 00:00:00 2001 From: M Date: Mon, 8 Apr 2024 14:30:16 -0700 Subject: [PATCH 3/5] Add docstest examples --- patterns/behavioral/servant.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/patterns/behavioral/servant.py b/patterns/behavioral/servant.py index ac478971..de939a60 100644 --- a/patterns/behavioral/servant.py +++ b/patterns/behavioral/servant.py @@ -103,3 +103,25 @@ def move_to(shape, new_position: Position): """ shape.position = new_position print(f"Moved to ({shape.position.x}, {shape.position.y})") + + +def main(): + """ + >>> servant = GeometryTools() + >>> circle = Circle(5, Position(0, 0)) + >>> rectangle = Rectangle(3, 4, Position(0, 0)) + >>> servant.calculate_area(circle) + 78.53981633974483 + >>> servant.calculate_perimeter(rectangle) + 14 + >>> servant.move_to(circle, Position(3, 4)) + Moved to (3, 4) + >>> servant.move_to(rectangle, Position(5, 6)) + Moved to (5, 6) + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() From afe45a6a3d32c515cfcff2caaea81370cc8d645f Mon Sep 17 00:00:00 2001 From: M Date: Mon, 8 Apr 2024 14:35:48 -0700 Subject: [PATCH 4/5] Add testing for Servant class --- tests/behavioral/test_servant.py | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tests/behavioral/test_servant.py diff --git a/tests/behavioral/test_servant.py b/tests/behavioral/test_servant.py new file mode 100644 index 00000000..45a75ca6 --- /dev/null +++ b/tests/behavioral/test_servant.py @@ -0,0 +1,35 @@ +from patterns.behavioral.servant import GeometryTools, Circle, Rectangle, Position +import pytest +import math + +def test_calculate_area(): + circle = Circle(3, Position(0, 0)) + assert GeometryTools.calculate_area(circle) == math.pi * 3 ** 2 + + rectangle = Rectangle(4, 5, Position(0, 0)) + assert GeometryTools.calculate_area(rectangle) == 4 * 5 + + with pytest.raises(ValueError): + GeometryTools.calculate_area("invalid shape") + +def test_calculate_perimeter(): + circle = Circle(3, Position(0, 0)) + assert GeometryTools.calculate_perimeter(circle) == 2 * math.pi * 3 + + rectangle = Rectangle(4, 5, Position(0, 0)) + assert GeometryTools.calculate_perimeter(rectangle) == 2 * (4 + 5) + + with pytest.raises(ValueError): + GeometryTools.calculate_perimeter("invalid shape") + + +def test_move_to(): + circle = Circle(3, Position(0, 0)) + new_position = Position(1, 1) + GeometryTools.move_to(circle, new_position) + assert circle.position == new_position + + rectangle = Rectangle(4, 5, Position(0, 0)) + new_position = Position(1, 1) + GeometryTools.move_to(rectangle, new_position) + assert rectangle.position == new_position From 828b967d23ff827f62954f5350afee00be36773e Mon Sep 17 00:00:00 2001 From: M Date: Mon, 8 Apr 2024 14:55:30 -0700 Subject: [PATCH 5/5] Use fixtures for circle and rectangle --- tests/behavioral/test_servant.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/behavioral/test_servant.py b/tests/behavioral/test_servant.py index 45a75ca6..e5edb70d 100644 --- a/tests/behavioral/test_servant.py +++ b/tests/behavioral/test_servant.py @@ -2,34 +2,36 @@ import pytest import math -def test_calculate_area(): - circle = Circle(3, Position(0, 0)) - assert GeometryTools.calculate_area(circle) == math.pi * 3 ** 2 - rectangle = Rectangle(4, 5, Position(0, 0)) +@pytest.fixture +def circle(): + return Circle(3, Position(0, 0)) + +@pytest.fixture +def rectangle(): + return Rectangle(4, 5, Position(0, 0)) + + +def test_calculate_area(circle, rectangle): + assert GeometryTools.calculate_area(circle) == math.pi * 3 ** 2 assert GeometryTools.calculate_area(rectangle) == 4 * 5 with pytest.raises(ValueError): GeometryTools.calculate_area("invalid shape") -def test_calculate_perimeter(): - circle = Circle(3, Position(0, 0)) +def test_calculate_perimeter(circle, rectangle): assert GeometryTools.calculate_perimeter(circle) == 2 * math.pi * 3 - - rectangle = Rectangle(4, 5, Position(0, 0)) assert GeometryTools.calculate_perimeter(rectangle) == 2 * (4 + 5) with pytest.raises(ValueError): GeometryTools.calculate_perimeter("invalid shape") -def test_move_to(): - circle = Circle(3, Position(0, 0)) +def test_move_to(circle, rectangle): new_position = Position(1, 1) GeometryTools.move_to(circle, new_position) assert circle.position == new_position - rectangle = Rectangle(4, 5, Position(0, 0)) new_position = Position(1, 1) GeometryTools.move_to(rectangle, new_position) assert rectangle.position == new_position