Skip to content

Commit

Permalink
Merge pull request faif#215 from spookylukey/pythonic_abstract_factory
Browse files Browse the repository at this point in the history
Simplified the Abstract Factory Pattern to be Pythonic
  • Loading branch information
faif authored Feb 1, 2018
2 parents ac79516 + 6127f9d commit 5e05ca1
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 126 deletions.
101 changes: 28 additions & 73 deletions creational/abstract_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@

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


import six
import abc
import random


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

pet = self.pet_factory.get_pet()
pet = self.pet_factory()
print("We have a lovely {}".format(pet))
print("It says {}".format(pet.speak()))
print("We also have {}".format(self.pet_factory.get_food()))


# Stuff that our factory makes

class Dog(object):

def speak(self):
Expand All @@ -74,80 +71,38 @@ def __str__(self):
return "Cat"


# Factory classes

class DogFactory(object):

def get_pet(self):
return Dog()

def get_food(self):
return "dog food"
# Additional factories:


class CatFactory(object):

def get_pet(self):
return Cat()

def get_food(self):
return "cat food"


# Create the proper family
def get_factory():
# Create a random animal
def random_animal():
"""Let's be dynamic!"""
return random.choice([DogFactory, CatFactory])()


# Implementation 2 of an abstract factory
@six.add_metaclass(abc.ABCMeta)
class Pet(object):

@classmethod
def from_name(cls, name):
for sub_cls in cls.__subclasses__():
if name == sub_cls.__name__.lower():
return sub_cls()

@abc.abstractmethod
def speak(self):
""""""


class Kitty(Pet):
def speak(self):
return "Miao"


class Duck(Pet):
def speak(self):
return "Quak"
return random.choice([Dog, Cat])()


# Show pets with various factories
if __name__ == "__main__":

# A Shop that sells only cats
cat_shop = PetShop(Cat)
cat_shop.show_pet()
print("")

# A shop that sells random animals
shop = PetShop(random_animal)
for i in range(3):
shop = PetShop(get_factory())
shop.show_pet()
print("=" * 20)

for name0 in ["kitty", "duck"]:
pet = Pet.from_name(name0)
print("{}: {}".format(name0, pet.speak()))

### OUTPUT ###
# We have a lovely Cat
# It says meow
# We also have cat food
# ====================
#
# We have a lovely Dog
# It says woof
# We also have dog food
# ====================
# We have a lovely Cat
# It says meow
# We also have cat food
# ====================
# kitty: Miao
# duck: Quak
# We have a lovely Cat
# It says meow
# ====================
58 changes: 5 additions & 53 deletions tests/test_abstract_factory.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import unittest
from creational.abstract_factory import PetShop,\
Dog, Cat, DogFactory, CatFactory, Pet
from creational.abstract_factory import PetShop, Dog
try:
from unittest.mock import patch
except ImportError:
Expand All @@ -12,54 +11,7 @@
class TestPetShop(unittest.TestCase):

def test_dog_pet_shop_shall_show_dog_instance(self):
f = DogFactory()
with patch.object(f, 'get_pet') as mock_f_get_pet,\
patch.object(f, 'get_food') as mock_f_get_food:
ps = PetShop(f)
ps.show_pet()
self.assertEqual(mock_f_get_pet.call_count, 1)
self.assertEqual(mock_f_get_food.call_count, 1)

def test_cat_pet_shop_shall_show_cat_instance(self):
f = CatFactory()
with patch.object(f, 'get_pet') as mock_f_get_pet,\
patch.object(f, 'get_food') as mock_f_get_food:
ps = PetShop(f)
ps.show_pet()
self.assertEqual(mock_f_get_pet.call_count, 1)
self.assertEqual(mock_f_get_food.call_count, 1)


class TestCat(unittest.TestCase):

@classmethod
def setUpClass(cls):
cls.c = Cat()

def test_cat_shall_meow(cls):
cls.assertEqual(cls.c.speak(), 'meow')

def test_cat_shall_be_printable(cls):
cls.assertEqual(str(cls.c), 'Cat')


class TestDog(unittest.TestCase):

@classmethod
def setUpClass(cls):
cls.d = Dog()

def test_dog_shall_woof(cls):
cls.assertEqual(cls.d.speak(), 'woof')

def test_dog_shall_be_printable(cls):
cls.assertEqual(str(cls.d), 'Dog')


class PetTest(unittest.TestCase):

def test_from_name(self):
test_cases = [("kitty", "Miao"), ("duck", "Quak")]
for name, expected_speech in test_cases:
pet = Pet.from_name(name)
self.assertEqual(pet.speak(), expected_speech)
dog_pet_shop = PetShop(Dog)
with patch.object(Dog, 'speak') as mock_Dog_speak:
dog_pet_shop.show_pet()
self.assertEqual(mock_Dog_speak.call_count, 1)

0 comments on commit 5e05ca1

Please sign in to comment.