Skip to content

Commit 5e05ca1

Browse files
authored
Merge pull request faif#215 from spookylukey/pythonic_abstract_factory
Simplified the Abstract Factory Pattern to be Pythonic
2 parents ac79516 + 6127f9d commit 5e05ca1

2 files changed

Lines changed: 33 additions & 126 deletions

File tree

creational/abstract_factory.py

Lines changed: 28 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,25 @@
33

44
"""
55
*What is this pattern about?
6-
The Abstract Factory Pattern serves to provide an interface for
6+
7+
In Java and other languages, the Abstract Factory Pattern serves to provide an interface for
78
creating related/dependent objects without need to specify their
89
actual class.
10+
911
The idea is to abstract the creation of objects depending on business
1012
logic, platform choice, etc.
1113
14+
In Python, we interface we use is simply a callable, which is "builtin" interface
15+
in Python, and in normal circumstances we can simply use the class itself as
16+
that callable, because classes are first class objects in Python.
17+
1218
*What does this example do?
1319
This particular implementation abstracts the creation of a pet and
14-
does so depending on the AnimalFactory we chose (Dog or Cat)
15-
This works because both Dog/Cat and their factories respect a common
16-
interface (.speak(), get_pet() and get_food()).
17-
Now my application can create pets (and feed them) abstractly and decide later,
20+
does so depending on the factory we chose (Dog or Cat, or random_animal)
21+
This works because both Dog/Cat and random_animal respect a common
22+
interface (callable for creation and .speak()).
23+
Now my application can create pets abstractly and decide later,
1824
based on my own criteria, dogs over cats.
19-
The second example allows us to create pets based on the string passed by the
20-
user, using cls.__subclasses__ (the list of sub classes for class cls)
21-
and sub_cls.__name__ to get its name.
2225
2326
*Where is the pattern used practically?
2427
@@ -30,9 +33,6 @@
3033
Provides a way to encapsulate a group of individual factories.
3134
"""
3235

33-
34-
import six
35-
import abc
3636
import random
3737

3838

@@ -48,14 +48,11 @@ def __init__(self, animal_factory=None):
4848
def show_pet(self):
4949
"""Creates and shows a pet using the abstract factory"""
5050

51-
pet = self.pet_factory.get_pet()
51+
pet = self.pet_factory()
5252
print("We have a lovely {}".format(pet))
5353
print("It says {}".format(pet.speak()))
54-
print("We also have {}".format(self.pet_factory.get_food()))
5554

5655

57-
# Stuff that our factory makes
58-
5956
class Dog(object):
6057

6158
def speak(self):
@@ -74,80 +71,38 @@ def __str__(self):
7471
return "Cat"
7572

7673

77-
# Factory classes
78-
79-
class DogFactory(object):
80-
81-
def get_pet(self):
82-
return Dog()
83-
84-
def get_food(self):
85-
return "dog food"
74+
# Additional factories:
8675

87-
88-
class CatFactory(object):
89-
90-
def get_pet(self):
91-
return Cat()
92-
93-
def get_food(self):
94-
return "cat food"
95-
96-
97-
# Create the proper family
98-
def get_factory():
76+
# Create a random animal
77+
def random_animal():
9978
"""Let's be dynamic!"""
100-
return random.choice([DogFactory, CatFactory])()
101-
102-
103-
# Implementation 2 of an abstract factory
104-
@six.add_metaclass(abc.ABCMeta)
105-
class Pet(object):
106-
107-
@classmethod
108-
def from_name(cls, name):
109-
for sub_cls in cls.__subclasses__():
110-
if name == sub_cls.__name__.lower():
111-
return sub_cls()
112-
113-
@abc.abstractmethod
114-
def speak(self):
115-
""""""
116-
117-
118-
class Kitty(Pet):
119-
def speak(self):
120-
return "Miao"
121-
122-
123-
class Duck(Pet):
124-
def speak(self):
125-
return "Quak"
79+
return random.choice([Dog, Cat])()
12680

12781

12882
# Show pets with various factories
12983
if __name__ == "__main__":
84+
85+
# A Shop that sells only cats
86+
cat_shop = PetShop(Cat)
87+
cat_shop.show_pet()
88+
print("")
89+
90+
# A shop that sells random animals
91+
shop = PetShop(random_animal)
13092
for i in range(3):
131-
shop = PetShop(get_factory())
13293
shop.show_pet()
13394
print("=" * 20)
13495

135-
for name0 in ["kitty", "duck"]:
136-
pet = Pet.from_name(name0)
137-
print("{}: {}".format(name0, pet.speak()))
138-
13996
### OUTPUT ###
14097
# We have a lovely Cat
14198
# It says meow
142-
# We also have cat food
143-
# ====================
99+
#
144100
# We have a lovely Dog
145101
# It says woof
146-
# We also have dog food
147102
# ====================
148103
# We have a lovely Cat
149104
# It says meow
150-
# We also have cat food
151105
# ====================
152-
# kitty: Miao
153-
# duck: Quak
106+
# We have a lovely Cat
107+
# It says meow
108+
# ====================

tests/test_abstract_factory.py

Lines changed: 5 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
33
import unittest
4-
from creational.abstract_factory import PetShop,\
5-
Dog, Cat, DogFactory, CatFactory, Pet
4+
from creational.abstract_factory import PetShop, Dog
65
try:
76
from unittest.mock import patch
87
except ImportError:
@@ -12,54 +11,7 @@
1211
class TestPetShop(unittest.TestCase):
1312

1413
def test_dog_pet_shop_shall_show_dog_instance(self):
15-
f = DogFactory()
16-
with patch.object(f, 'get_pet') as mock_f_get_pet,\
17-
patch.object(f, 'get_food') as mock_f_get_food:
18-
ps = PetShop(f)
19-
ps.show_pet()
20-
self.assertEqual(mock_f_get_pet.call_count, 1)
21-
self.assertEqual(mock_f_get_food.call_count, 1)
22-
23-
def test_cat_pet_shop_shall_show_cat_instance(self):
24-
f = CatFactory()
25-
with patch.object(f, 'get_pet') as mock_f_get_pet,\
26-
patch.object(f, 'get_food') as mock_f_get_food:
27-
ps = PetShop(f)
28-
ps.show_pet()
29-
self.assertEqual(mock_f_get_pet.call_count, 1)
30-
self.assertEqual(mock_f_get_food.call_count, 1)
31-
32-
33-
class TestCat(unittest.TestCase):
34-
35-
@classmethod
36-
def setUpClass(cls):
37-
cls.c = Cat()
38-
39-
def test_cat_shall_meow(cls):
40-
cls.assertEqual(cls.c.speak(), 'meow')
41-
42-
def test_cat_shall_be_printable(cls):
43-
cls.assertEqual(str(cls.c), 'Cat')
44-
45-
46-
class TestDog(unittest.TestCase):
47-
48-
@classmethod
49-
def setUpClass(cls):
50-
cls.d = Dog()
51-
52-
def test_dog_shall_woof(cls):
53-
cls.assertEqual(cls.d.speak(), 'woof')
54-
55-
def test_dog_shall_be_printable(cls):
56-
cls.assertEqual(str(cls.d), 'Dog')
57-
58-
59-
class PetTest(unittest.TestCase):
60-
61-
def test_from_name(self):
62-
test_cases = [("kitty", "Miao"), ("duck", "Quak")]
63-
for name, expected_speech in test_cases:
64-
pet = Pet.from_name(name)
65-
self.assertEqual(pet.speak(), expected_speech)
14+
dog_pet_shop = PetShop(Dog)
15+
with patch.object(Dog, 'speak') as mock_Dog_speak:
16+
dog_pet_shop.show_pet()
17+
self.assertEqual(mock_Dog_speak.call_count, 1)

0 commit comments

Comments
 (0)