From d68190d2885393374607f03f1ef691cb31c9e264 Mon Sep 17 00:00:00 2001 From: Luke Plant Date: Wed, 31 Jan 2018 16:53:49 +0300 Subject: [PATCH 1/2] Simplified the Abstract Factory Pattern to be Pythonic Once we appreciate that: 1) classes are their own factories and 2) classes are first class objects in Python, this pattern is massively simpler in idiomatic Python. Additional factory classes are not needed. This change also removes the `get_food` method because: 1) This violates the single responsibility principle of the factory 2) It obscures the main point. If we want an association between animals and what food they eat we should either: 1) make it part of the Pet interface 2) or, have another callable which can make this decision (presumably it should be passed the animal instance in order to do so). Or, if we really need it, create an interface that does both creation and feeding as before, but to have that in the this pattern obscures the simplicity of how this can and should be implemented in Python. The second implementation was also deleted. This simply looks up the classes using a string name, which is pointless when you can just use the class itself (why use `"kitty"` when you can use `Cat`). --- creational/abstract_factory.py | 96 +++++++++------------------------- 1 file changed, 25 insertions(+), 71 deletions(-) diff --git a/creational/abstract_factory.py b/creational/abstract_factory.py index 564bcca96..2d9bea354 100644 --- a/creational/abstract_factory.py +++ b/creational/abstract_factory.py @@ -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? @@ -30,9 +33,6 @@ Provides a way to encapsulate a group of individual factories. """ - -import six -import abc import random @@ -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): @@ -74,80 +71,37 @@ def __str__(self): return "Cat" -# Factory classes - -class DogFactory(object): - - def get_pet(self): - return Dog() - - def get_food(self): - return "dog food" - - -class CatFactory(object): - - def get_pet(self): - return Cat() - - def get_food(self): - return "cat food" - +# Additional factories: -# 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 From 6127f9dbfdeed059de3aec1457bf3cbc27ce3808 Mon Sep 17 00:00:00 2001 From: Luke Plant Date: Wed, 31 Jan 2018 20:26:06 +0300 Subject: [PATCH 2/2] Fixed tests for abstract factory. This all removes all the low value tests which were testing things that were trivial, irrelevant to the point or now no longer exist. --- creational/abstract_factory.py | 7 ++-- tests/test_abstract_factory.py | 58 +++------------------------------- 2 files changed, 9 insertions(+), 56 deletions(-) diff --git a/creational/abstract_factory.py b/creational/abstract_factory.py index 2d9bea354..6c781b86a 100644 --- a/creational/abstract_factory.py +++ b/creational/abstract_factory.py @@ -93,15 +93,16 @@ def random_animal(): shop.show_pet() print("=" * 20) - ### OUTPUT ### # We have a lovely Cat # It says meow -# -# ==================== +# # We have a lovely Dog # It says woof # ==================== # We have a lovely Cat # It says meow # ==================== +# We have a lovely Cat +# It says meow +# ==================== diff --git a/tests/test_abstract_factory.py b/tests/test_abstract_factory.py index 8ad0838e8..0e64c7127 100644 --- a/tests/test_abstract_factory.py +++ b/tests/test_abstract_factory.py @@ -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: @@ -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)