Skip to content

Commit 3c0725a

Browse files
Servant pattern (#413)
* Add docstring for Servant behavioral design pattern * Implement Servant class * Add docstest examples * Add testing for Servant class * Use fixtures for circle and rectangle
1 parent bee048e commit 3c0725a

File tree

2 files changed

+164
-0
lines changed

2 files changed

+164
-0
lines changed

patterns/behavioral/servant.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"""
2+
Implementation of the Servant design pattern.
3+
4+
The Servant design pattern is a behavioral pattern used to offer functionality
5+
to a group of classes without requiring them to inherit from a base class.
6+
7+
This pattern involves creating a Servant class that provides certain services
8+
or functionalities. These services are used by other classes which do not need
9+
to be related through a common parent class. It is particularly useful in
10+
scenarios where adding the desired functionality through inheritance is impractical
11+
or would lead to a rigid class hierarchy.
12+
13+
This pattern is characterized by the following:
14+
15+
- A Servant class that provides specific services or actions.
16+
- Client classes that need these services, but do not derive from the Servant class.
17+
- The use of the Servant class by the client classes to perform actions on their behalf.
18+
19+
References:
20+
- https://en.wikipedia.org/wiki/Servant_(design_pattern)
21+
"""
22+
import math
23+
24+
class Position:
25+
"""Representation of a 2D position with x and y coordinates."""
26+
27+
def __init__(self, x, y):
28+
self.x = x
29+
self.y = y
30+
31+
class Circle:
32+
"""Representation of a circle defined by a radius and a position."""
33+
34+
def __init__(self, radius, position: Position):
35+
self.radius = radius
36+
self.position = position
37+
38+
class Rectangle:
39+
"""Representation of a rectangle defined by width, height, and a position."""
40+
41+
def __init__(self, width, height, position: Position):
42+
self.width = width
43+
self.height = height
44+
self.position = position
45+
46+
47+
class GeometryTools:
48+
"""
49+
Servant class providing geometry-related services, including area and
50+
perimeter calculations and position updates.
51+
"""
52+
53+
@staticmethod
54+
def calculate_area(shape):
55+
"""
56+
Calculate the area of a given shape.
57+
58+
Args:
59+
shape: The geometric shape whose area is to be calculated.
60+
61+
Returns:
62+
The area of the shape.
63+
64+
Raises:
65+
ValueError: If the shape type is unsupported.
66+
"""
67+
if isinstance(shape, Circle):
68+
return math.pi * shape.radius ** 2
69+
elif isinstance(shape, Rectangle):
70+
return shape.width * shape.height
71+
else:
72+
raise ValueError("Unsupported shape type")
73+
74+
@staticmethod
75+
def calculate_perimeter(shape):
76+
"""
77+
Calculate the perimeter of a given shape.
78+
79+
Args:
80+
shape: The geometric shape whose perimeter is to be calculated.
81+
82+
Returns:
83+
The perimeter of the shape.
84+
85+
Raises:
86+
ValueError: If the shape type is unsupported.
87+
"""
88+
if isinstance(shape, Circle):
89+
return 2 * math.pi * shape.radius
90+
elif isinstance(shape, Rectangle):
91+
return 2 * (shape.width + shape.height)
92+
else:
93+
raise ValueError("Unsupported shape type")
94+
95+
@staticmethod
96+
def move_to(shape, new_position: Position):
97+
"""
98+
Move a given shape to a new position.
99+
100+
Args:
101+
shape: The geometric shape to be moved.
102+
new_position: The new position to move the shape to.
103+
"""
104+
shape.position = new_position
105+
print(f"Moved to ({shape.position.x}, {shape.position.y})")
106+
107+
108+
def main():
109+
"""
110+
>>> servant = GeometryTools()
111+
>>> circle = Circle(5, Position(0, 0))
112+
>>> rectangle = Rectangle(3, 4, Position(0, 0))
113+
>>> servant.calculate_area(circle)
114+
78.53981633974483
115+
>>> servant.calculate_perimeter(rectangle)
116+
14
117+
>>> servant.move_to(circle, Position(3, 4))
118+
Moved to (3, 4)
119+
>>> servant.move_to(rectangle, Position(5, 6))
120+
Moved to (5, 6)
121+
"""
122+
123+
124+
if __name__ == "__main__":
125+
import doctest
126+
127+
doctest.testmod()

tests/behavioral/test_servant.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from patterns.behavioral.servant import GeometryTools, Circle, Rectangle, Position
2+
import pytest
3+
import math
4+
5+
6+
@pytest.fixture
7+
def circle():
8+
return Circle(3, Position(0, 0))
9+
10+
@pytest.fixture
11+
def rectangle():
12+
return Rectangle(4, 5, Position(0, 0))
13+
14+
15+
def test_calculate_area(circle, rectangle):
16+
assert GeometryTools.calculate_area(circle) == math.pi * 3 ** 2
17+
assert GeometryTools.calculate_area(rectangle) == 4 * 5
18+
19+
with pytest.raises(ValueError):
20+
GeometryTools.calculate_area("invalid shape")
21+
22+
def test_calculate_perimeter(circle, rectangle):
23+
assert GeometryTools.calculate_perimeter(circle) == 2 * math.pi * 3
24+
assert GeometryTools.calculate_perimeter(rectangle) == 2 * (4 + 5)
25+
26+
with pytest.raises(ValueError):
27+
GeometryTools.calculate_perimeter("invalid shape")
28+
29+
30+
def test_move_to(circle, rectangle):
31+
new_position = Position(1, 1)
32+
GeometryTools.move_to(circle, new_position)
33+
assert circle.position == new_position
34+
35+
new_position = Position(1, 1)
36+
GeometryTools.move_to(rectangle, new_position)
37+
assert rectangle.position == new_position

0 commit comments

Comments
 (0)