Movatterモバイル変換


[0]ホーム

URL:


— FREE Email Series —

🐍 Python Tricks 💌

Python Tricks Dictionary Merge

🔒 No spam. Unsubscribe any time.

Browse TopicsGuided Learning Paths
Basics Intermediate Advanced
aialgorithmsapibest-practicescareercommunitydatabasesdata-sciencedata-structuresdata-vizdevopsdjangodockereditorsflaskfront-endgamedevguimachine-learningnewsnumpyprojectspythonstdlibtestingtoolsweb-devweb-scraping

Table of Contents

Recommended Course

The Factory Method Pattern and Its Implementation in Python

Implementing the Factory Method Pattern in Python

2h 7m · 27 lessons

The Factory Method Pattern and Its Implementation in Python

The Factory Method Pattern and Its Implementation in Python

byIsaac RodriguezReading time estimate 34mintermediatebest-practices

Table of Contents

Remove ads

Recommended Course

Implementing the Factory Method Pattern in Python(2h 7m)

This article explores the Factory Method design pattern and its implementation in Python. Design patterns became a popular topic in late 90s after the so-called Gang of Four (GoF: Gamma, Helm, Johson, and Vlissides) published their bookDesign Patterns: Elements of Reusable Object-Oriented Software.

The book describes design patterns as a core design solution to reoccurring problems in software and classifies each design pattern intocategories according to the nature of the problem. Each pattern is given a name, a problem description, a design solution, and an explanation of the consequences of using it.

The GoF book describes Factory Method as a creational design pattern. Creational design patterns are related to the creation of objects, and Factory Method is a design pattern that creates objects with a commoninterface.

This is a recurrent problem thatmakes Factory Method one of the most widely used design patterns, and it’s very important to understand it and know how apply it.

By the end of this article, you will:

  • Understand the components of Factory Method
  • Recognize opportunities to use Factory Method in your applications
  • Learn to modify existing code and improve its design by using the pattern
  • Learn to identify opportunities where Factory Method is the appropriate design pattern
  • Choose an appropriate implementation of Factory Method
  • Know how to implement a reusable, general purpose solution of Factory Method

Free Bonus:5 Thoughts On Python Mastery, a free course for Python developers that shows you the roadmap and the mindset you’ll need to take your Python skills to the next level.

Introducing Factory Method

Factory Method is a creational design pattern used to create concrete implementations of a common interface.

It separates the process of creating an object from the code that depends on the interface of the object.

For example, an application requires an object with a specific interface to perform its tasks. The concrete implementation of the interface is identified by some parameter.

Instead of using a complexif/elif/else conditional structure to determine the concrete implementation, the application delegates that decision to a separate component that creates the concrete object. With this approach, the application code is simplified, making it more reusable and easier to maintain.

Imagine an application that needs to convert aSong object into itsstring representation using a specified format. Converting an object to a different representation is often called serializing. You’ll often see these requirements implemented in a single function or method that contains all the logic and implementation, like in the following code:

Python
# In serializer_demo.pyimportjsonimportxml.etree.ElementTreeasetclassSong:def__init__(self,song_id,title,artist):self.song_id=song_idself.title=titleself.artist=artistclassSongSerializer:defserialize(self,song,format):ifformat=='JSON':song_info={'id':song.song_id,'title':song.title,'artist':song.artist}returnjson.dumps(song_info)elifformat=='XML':song_info=et.Element('song',attrib={'id':song.song_id})title=et.SubElement(song_info,'title')title.text=song.titleartist=et.SubElement(song_info,'artist')artist.text=song.artistreturnet.tostring(song_info,encoding='unicode')else:raiseValueError(format)

In the example above, you have a basicSong class to represent a song and aSongSerializer class that can convert asong object into itsstring representation according to the value of theformat parameter.

The.serialize() method supports two different formats:JSON andXML. Any otherformat specified is not supported, so aValueError exception is raised.

Let’s use the Python interactive shell to see how the code works:

Python
>>>importserializer_demoassd>>>song=sd.Song('1','Water of Love','Dire Straits')>>>serializer=sd.SongSerializer()>>>serializer.serialize(song,'JSON')'{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}'>>>serializer.serialize(song,'XML')'<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>'>>>serializer.serialize(song,'YAML')Traceback (most recent call last):  File"<stdin>", line1, in<module>  File"./serializer_demo.py", line30, inserializeraiseValueError(format)ValueError:YAML

You create asong object and aserializer, and you convert the song to its string representation by using the.serialize() method. The method takes thesong object as a parameter, as well as a string value representing the format you want. The last call usesYAML as the format, which is not supported by theserializer, so aValueError exception is raised.

This example is short and simplified, but it still has a lot of complexity. There are three logical or execution paths depending on the value of theformat parameter. This may not seem like a big deal, and you’ve probably seen code with more complexity than this, but the above example is still pretty hard to maintain.

The Problems With Complex Conditional Code

The example above exhibits all the problems you’ll find in complex logical code. Complex logical code usesif/elif/else structures to change the behavior of an application. Usingif/elif/else conditional structures makes the code harder to read, harder to understand, and harder to maintain.

The code above might not seem hard to read or understand, but wait till you see the final code in this section!

Nevertheless, the code above is hard to maintain because it is doing too much. Thesingle responsibility principle states that amodule, a class, or even a method should have a single, well-defined responsibility. It should do just one thing and have only one reason to change.

The.serialize() method inSongSerializer will require changes for many different reasons. This increases the risk of introducing new defects or breaking existing functionality when changes are made. Let’s take a look at all the situations that will require modifications to the implementation:

  • When a new format is introduced: The method will have to change to implement the serialization to that format.

  • When theSong object changes: Adding or removing properties to theSong class will require the implementation to change in order to accommodate the new structure.

  • When the string representation for a format changes (plainJSON vsJSON API): The.serialize() method will have to change if the desired string representation for a format changes because the representation is hard-coded in the.serialize() method implementation.

The ideal situation would be if any of those changes in requirements could be implemented without changing the.serialize() method. Let’s see how you can do that in the following sections.

Looking for a Common Interface

The first step when you see complex conditional code in an application is to identify the common goal of each of the execution paths (or logical paths).

Code that usesif/elif/else usually has a common goal that is implemented in different ways in each logical path. The code above converts asong object to itsstring representation using a different format in each logical path.

Based on the goal, you look for a common interface that can be used to replace each of the paths. The example above requires an interface that takes asong object and returns astring.

Once you have a common interface, you provide separate implementations for each logical path. In the example above, you will provide an implementation to serialize to JSON and another for XML.

Then, you provide a separate component that decides the concrete implementation to use based on the specifiedformat. This component evaluates the value offormat and returns the concrete implementation identified by its value.

In the following sections, you will learn how to make changes to existing code without changing the behavior. This is referred to asrefactoring the code.

Martin Fowler in his bookRefactoring: Improving the Design of Existing Code defines refactoring as “the process of changing a software system in such a way that does not alter the external behavior of the code yet improves its internal structure.” If you’d like to see refactoring in action, check out the Real Python Code ConversationRefactoring: Prepare Your Code to Get Help.

Let’s begin refactoring the code to achieve the desired structure that uses the Factory Method design pattern.

Refactoring Code Into the Desired Interface

The desired interface is an object or a function that takes aSong object and returns astring representation.

The first step is to refactor one of the logical paths into this interface. You do this by adding a new method._serialize_to_json() and moving the JSON serialization code to it. Then, you change the client to call it instead of having the implementation in the body of theif statement:

Python
classSongSerializer:defserialize(self,song,format):ifformat=='JSON':returnself._serialize_to_json(song)# The rest of the code remains the samedef_serialize_to_json(self,song):payload={'id':song.song_id,'title':song.title,'artist':song.artist}returnjson.dumps(payload)

Once you make this change, you can verify that the behavior has not changed. Then, you do the same for the XML option by introducing a new method._serialize_to_xml(), moving the implementation to it, and modifying theelif path to call it.

The following example shows the refactored code:

Python
classSongSerializer:defserialize(self,song,format):ifformat=='JSON':returnself._serialize_to_json(song)elifformat=='XML':returnself._serialize_to_xml(song)else:raiseValueError(format)def_serialize_to_json(self,song):payload={'id':song.song_id,'title':song.title,'artist':song.artist}returnjson.dumps(payload)def_serialize_to_xml(self,song):song_element=et.Element('song',attrib={'id':song.song_id})title=et.SubElement(song_element,'title')title.text=song.titleartist=et.SubElement(song_element,'artist')artist.text=song.artistreturnet.tostring(song_element,encoding='unicode')

The new version of the code is easier to read and understand, but it can still be improved with a basic implementation of Factory Method.

Basic Implementation of Factory Method

The central idea in Factory Method is to provide a separate component with the responsibility to decide which concrete implementation should be used based on some specified parameter. That parameter in our example is theformat.

To complete the implementation of Factory Method, you add a new method._get_serializer() that takes the desiredformat. This method evaluates the value offormat and returns the matching serialization function:

Python
classSongSerializer:def_get_serializer(self,format):ifformat=='JSON':returnself._serialize_to_jsonelifformat=='XML':returnself._serialize_to_xmlelse:raiseValueError(format)

Note: The._get_serializer() method does not call the concrete implementation, and it just returns the function object itself.

Now, you can change the.serialize() method ofSongSerializer to use._get_serializer() to complete the Factory Method implementation. The next example shows the complete code:

Python
classSongSerializer:defserialize(self,song,format):serializer=self._get_serializer(format)returnserializer(song)def_get_serializer(self,format):ifformat=='JSON':returnself._serialize_to_jsonelifformat=='XML':returnself._serialize_to_xmlelse:raiseValueError(format)def_serialize_to_json(self,song):payload={'id':song.song_id,'title':song.title,'artist':song.artist}returnjson.dumps(payload)def_serialize_to_xml(self,song):song_element=et.Element('song',attrib={'id':song.song_id})title=et.SubElement(song_element,'title')title.text=song.titleartist=et.SubElement(song_element,'artist')artist.text=song.artistreturnet.tostring(song_element,encoding='unicode')

The final implementation shows the different components of Factory Method. The.serialize() method is the application code that depends on an interface to complete its task.

This is referred to as theclient component of the pattern. The interface defined is referred to as theproduct component. In our case, the product is a function that takes aSong and returns a string representation.

The._serialize_to_json() and._serialize_to_xml() methods are concrete implementations of the product. Finally, the._get_serializer() method is thecreator component. The creator decides which concrete implementation to use.

Because you started with some existing code, all the components of Factory Method are members of the same classSongSerializer.

Usually, this is not the case and, as you can see, none of the added methods use theself parameter. This is a good indication that they should not be methods of theSongSerializer class, and they can become external functions:

Python
classSongSerializer:defserialize(self,song,format):serializer=get_serializer(format)returnserializer(song)defget_serializer(format):ifformat=='JSON':return_serialize_to_jsonelifformat=='XML':return_serialize_to_xmlelse:raiseValueError(format)def_serialize_to_json(song):payload={'id':song.song_id,'title':song.title,'artist':song.artist}returnjson.dumps(payload)def_serialize_to_xml(song):song_element=et.Element('song',attrib={'id':song.song_id})title=et.SubElement(song_element,'title')title.text=song.titleartist=et.SubElement(song_element,'artist')artist.text=song.artistreturnet.tostring(song_element,encoding='unicode')

Note: The.serialize() method inSongSerializer does not use theself parameter.

The rule above tells us it should not be part of the class. This is correct, but you are dealing with existing code.

If you removeSongSerializer and change the.serialize() method to a function, then you’ll have to change all the locations in the application that useSongSerializer and replace the calls to the new function.

Unless you have a very high percentage of code coverage with your unit tests, this is not a change that you should be doing.

The mechanics of Factory Method are always the same. A client (SongSerializer.serialize()) depends on a concrete implementation of an interface. It requests the implementation from a creator component (get_serializer()) using some sort of identifier (format).

The creator returns the concrete implementation according to the value of the parameter to the client, and the client uses the provided object to complete its task.

You can execute the same set of instructions in the Python interactive interpreter to verify that the application behavior has not changed:

Python
>>>importserializer_demoassd>>>song=sd.Song('1','Water of Love','Dire Straits')>>>serializer=sd.SongSerializer()>>>serializer.serialize(song,'JSON')'{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}'>>>serializer.serialize(song,'XML')'<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>'>>>serializer.serialize(song,'YAML')Traceback (most recent call last):  File"<stdin>", line1, in<module>  File"./serializer_demo.py", line13, inserializeserializer=get_serializer(format)  File"./serializer_demo.py", line23, inget_serializerraiseValueError(format)ValueError:YAML

You create asong and aserializer, and use theserializer to convert the song to itsstring representation specifying aformat. SinceYAML is not a supported format,ValueError is raised.

Recognizing Opportunities to Use Factory Method

Factory Method should be used in every situation where an application (client) depends on an interface (product) to perform a task and there are multiple concrete implementations of that interface. You need to provide a parameter that can identify the concrete implementation and use it in the creator to decide the concrete implementation.

There is a wide range of problems that fit this description, so let’s take a look at some concrete examples.

Replacing complex logical code: Complex logical structures in the formatif/elif/else are hard to maintain because new logical paths are needed as requirements change.

Factory Method is a good replacement because you can put the body of each logical path into separate functions or classes with a common interface, and the creator can provide the concrete implementation.

The parameter evaluated in the conditions becomes the parameter to identify the concrete implementation. The example above represents this situation.

Constructing related objects from external data: Imagine an application that needs to retrieve employee information from a database or other external source.

The records represent employees with different roles or types: managers, office clerks, sales associates, and so on. The application may store an identifier representing the type of employee in the record and then use Factory Method to create each concreteEmployee object from the rest of the information on the record.

Supporting multiple implementations of the same feature: An image processing application needs to transform a satellite image from one coordinate system to another, but there are multiple algorithms with different levels of accuracy to perform the transformation.

The application can allow the user to select an option that identifies the concrete algorithm. Factory Method can provide the concrete implementation of the algorithm based on this option.

Combining similar features under a common interface: Following the image processing example, an application needs to apply a filter to an image. The specific filter to use can be identified by some user input, and Factory Method can provide the concrete filter implementation.

Integrating related external services: A music player application wants to integrate with multiple external services and allow users to select where their music comes from. The application can define a common interface for a music service and use Factory Method to create the correct integration based on a user preference.

All these situations are similar. They all define a client that depends on a common interface known as the product. They all provide a means to identify the concrete implementation of the product, so they all can use Factory Method in their design.

You can now look at the serialization problem from previous examples and provide a better design by taking into consideration the Factory Method design pattern.

An Object Serialization Example

The basic requirements for the example above are that you want to serializeSong objects into theirstring representation. It seems the application provides features related to music, so it is plausible that the application will need to serialize other type of objects likePlaylist orAlbum.

Ideally, the design should support adding serialization for new objects by implementing new classes without requiring changes to the existing implementation. The application requires objects to be serialized to multiple formats like JSON and XML, so it seems natural to define an interfaceSerializer that can have multiple implementations, one per format.

The interface implementation might look something like this:

Python
# In serializers.pyimportjsonimportxml.etree.ElementTreeasetclassJsonSerializer:def__init__(self):self._current_object=Nonedefstart_object(self,object_name,object_id):self._current_object={'id':object_id}defadd_property(self,name,value):self._current_object[name]=valuedefto_str(self):returnjson.dumps(self._current_object)classXmlSerializer:def__init__(self):self._element=Nonedefstart_object(self,object_name,object_id):self._element=et.Element(object_name,attrib={'id':object_id})defadd_property(self,name,value):prop=et.SubElement(self._element,name)prop.text=valuedefto_str(self):returnet.tostring(self._element,encoding='unicode')

Note: The example above doesn’t implement a fullSerializer interface, but it should be good enough for our purposes and to demonstrate Factory Method.

TheSerializer interface is an abstract concept due to the dynamic nature of thePython language. Static languages likeJava or C# require that interfaces be explicitly defined. In Python, any object that provides the desired methods or functions is said to implement the interface. The example defines theSerializer interface to be an object that implements the following methods or functions:

  • .start_object(object_name, object_id)
  • .add_property(name, value)
  • .to_str()

This interface is implemented by the concrete classesJsonSerializer andXmlSerializer.

The original example used aSongSerializer class. For the new application, you will implement something more generic, likeObjectSerializer:

Python
# In serializers.pyclassObjectSerializer:defserialize(self,serializable,format):serializer=factory.get_serializer(format)serializable.serialize(serializer)returnserializer.to_str()

The implementation ofObjectSerializer is completely generic, and it only mentions aserializable and aformat as parameters.

Theformat is used to identify the concrete implementation of theSerializer and is resolved by thefactory object. Theserializable parameter refers to another abstract interface that should be implemented on any object type you want to serialize.

Let’s take a look at a concrete implementation of theserializable interface in theSong class:

Python
# In songs.pyclassSong:def__init__(self,song_id,title,artist):self.song_id=song_idself.title=titleself.artist=artistdefserialize(self,serializer):serializer.start_object('song',self.song_id)serializer.add_property('title',self.title)serializer.add_property('artist',self.artist)

TheSong class implements theSerializable interface by providing a.serialize(serializer) method. In the method, theSong class uses theserializer object to write its own information without any knowledge of the format.

As a matter of fact, theSong class doesn’t even know the goal is to convert the data to a string. This is important because you could use this interface to provide a different kind ofserializer that converts theSong information to a completely different representation if needed. For example, your application might require in the future to convert theSong object to a binary format.

So far, we’ve seen the implementation of the client (ObjectSerializer) and the product (serializer). It is time to complete the implementation of Factory Method and provide the creator. The creator in the example is thevariablefactory inObjectSerializer.serialize().

Factory Method as an Object Factory

In the original example, you implemented the creator as a function. Functions are fine for very simple examples, but they don’t provide too much flexibility when requirements change.

Classes can provide additional interfaces to add functionality, and they can be derived to customize behavior. Unless you have a very basic creator that will never change in the future, you want to implement it as a class and not a function. These type of classes are called object factories.

You can see the basic interface ofSerializerFactory in the implementation ofObjectSerializer.serialize(). The method usesfactory.get_serializer(format) to retrieve theserializer from the object factory.

You will now implementSerializerFactory to meet this interface:

Python
# In serializers.pyclassSerializerFactory:defget_serializer(self,format):ifformat=='JSON':returnJsonSerializer()elifformat=='XML':returnXmlSerializer()else:raiseValueError(format)factory=SerializerFactory()

The current implementation of.get_serializer() is the same you used in the original example. The method evaluates the value offormat and decides the concrete implementation to create and return. It is a relatively simple solution that allows us to verify the functionality of all the Factory Method components.

Let’s go to the Python interactive interpreter and see how it works:

Python
>>>importsongs>>>importserializers>>>song=songs.Song('1','Water of Love','Dire Straits')>>>serializer=serializers.ObjectSerializer()>>>serializer.serialize(song,'JSON')'{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}'>>>serializer.serialize(song,'XML')'<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>'>>>serializer.serialize(song,'YAML')Traceback (most recent call last):  File"<stdin>", line1, in<module>  File"./serializers.py", line39, inserializeserializer=factory.get_serializer(format)  File"./serializers.py", line52, inget_serializerraiseValueError(format)ValueError:YAML

The new design of Factory Method allows the application to introduce new features by adding new classes, as opposed to changing existing ones. You can serialize other objects by implementing theSerializable interface on them. You can support new formats by implementing theSerializer interface in another class.

The missing piece is thatSerializerFactory has to change to include the support for new formats. This problem is easily solved with the new design becauseSerializerFactory is a class.

Supporting Additional Formats

The current implementation ofSerializerFactory needs to be changed when a new format is introduced. Your application might never need to support any additional formats, but you never know.

You want your designs to be flexible, and as you will see, supporting additional formats without changingSerializerFactory is relatively easy.

The idea is to provide a method inSerializerFactory that registers a newSerializer implementation for the format we want to support:

Python
# In serializers.pyclassSerializerFactory:def__init__(self):self._creators={}defregister_format(self,format,creator):self._creators[format]=creatordefget_serializer(self,format):creator=self._creators.get(format)ifnotcreator:raiseValueError(format)returncreator()factory=SerializerFactory()factory.register_format('JSON',JsonSerializer)factory.register_format('XML',XmlSerializer)

The.register_format(format, creator) method allows registering new formats by specifying aformat value used to identify the format and acreator object. The creator object happens to be the class name of the concreteSerializer. This is possible because all theSerializer classes provide a default.__init__() to initialize the instances.

The registration information is stored in the_creatorsdictionary. The.get_serializer() method retrieves the registered creator and creates the desired object. If the requestedformat has not been registered, thenValueError is raised.

You can now verify the flexibility of the design by implementing aYamlSerializer and get rid of the annoyingValueError you saw earlier:

Python
# In yaml_serializer.pyimportyamlimportserializersclassYamlSerializer(serializers.JsonSerializer):defto_str(self):returnyaml.dump(self._current_object)serializers.factory.register_format('YAML',YamlSerializer)

Note: To implement the example, you need to installPyYAML in your environment usingpip install PyYAML.

JSON and YAML are very similar formats, so you can reuse most of the implementation ofJsonSerializer and overwrite.to_str() to complete the implementation. The format is then registered with thefactory object to make it available.

Let’s use the Python interactive interpreter to see the results:

Python
>>>importserializers>>>importsongs>>>importyaml_serializer>>>song=songs.Song('1','Water of Love','Dire Straits')>>>serializer=serializers.ObjectSerializer()>>>print(serializer.serialize(song,'JSON')){"id": "1", "title": "Water of Love", "artist": "Dire Straits"}>>>print(serializer.serialize(song,'XML'))<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>>>>print(serializer.serialize(song,'YAML')){artist: Dire Straits, id: '1', title: Water of Love}

By implementing Factory Method using an Object Factory and providing a registration interface, you are able to support new formats without changing any of the existing application code. This minimizes the risk of breaking existing features or introducing subtle bugs.

A General Purpose Object Factory

The implementation ofSerializerFactory is a huge improvement from the original example. It provides great flexibility to support new formats and avoids modifying existing code.

Still, the current implementation is specifically targeted to the serialization problem above, and it is not reusable in other contexts.

Factory Method can be used to solve a wide range of problems. An Object Factory gives additional flexibility to the design when requirements change. Ideally, you’ll want an implementation of Object Factory that can be reused in any situation without replicating the implementation.

There are some challenges to providing a general purpose implementation of Object Factory, and in the following sections you will look at those challenges and implement a solution that can be reused in any situation.

Not All Objects Can Be Created Equal

The biggest challenge to implement a general purpose Object Factory is that not all objects are created in the same way.

Not all situations allow us to use a default.__init__() to create and initialize the objects. It is important that the creator, in this case the Object Factory, returns fully initialized objects.

This is important because if it doesn’t, then the client will have to complete the initialization and use complex conditional code to fully initialize the provided objects. This defeats the purpose of the Factory Method design pattern.

To understand the complexities of a general purpose solution, let’s take a look at a different problem. Let’s say an application wants to integrate with different music services. These services can be external to the application or internal in order to support a local music collection. Each of the services has a different set of requirements.

Note: The requirements I define for the example are for illustration purposes and do not reflect the real requirements you will have to implement to integrate with services likePandora orSpotify.

The intent is to provide a different set of requirements that shows the challenges of implementing a general purpose Object Factory.

Imagine that the application wants to integrate with a service provided by Spotify. This service requires an authorization process where a client key and secret are provided for authorization.

The service returns an access code that should be used on any further communication. This authorization process is very slow, and it should only be performed once, so the application wants to keep the initialized service object around and use it every time it needs to communicate with Spotify.

At the same time, other users want to integrate with Pandora. Pandora might use a completely different authorization process. It also requires a client key and secret, but it returns a consumer key and secret that should be used for other communications. As with Spotify, the authorization process is slow, and it should only be performed once.

Finally, the application implements the concept of a local music service where the music collection is stored locally. The service requires that the the location of the music collection in the local system be specified. Creating a new service instance is done very quickly, so a new instance can be created every time the user wants to access the music collection.

This example presents several challenges. Each service is initialized with a different set of parameters. Also, Spotify and Pandora require an authorization process before the service instance can be created.

They also want to reuse that instance to avoid authorizing the application multiple times. The local service is simpler, but it doesn’t match the initialization interface of the others.

In the following sections, you will solve this problems by generalizing the creation interface and implementing a general purpose Object Factory.

Separate Object Creation to Provide Common Interface

The creation of each concrete music service has its own set of requirements. This means a common initialization interface for each service implementation is not possible or recommended.

The best approach is to define a new type of object that provides a general interface and is responsible for the creation of a concrete service. This new type of object will be called aBuilder. TheBuilder object has all the logic to create and initialize a service instance. You will implement aBuilder object for each of the supported services.

Let’s start by looking at the application configuration:

Python
# In program.pyconfig={'spotify_client_key':'THE_SPOTIFY_CLIENT_KEY','spotify_client_secret':'THE_SPOTIFY_CLIENT_SECRET','pandora_client_key':'THE_PANDORA_CLIENT_KEY','pandora_client_secret':'THE_PANDORA_CLIENT_SECRET','local_music_location':'/usr/data/music'}

Theconfig dictionary contains all the values required to initialize each of the services. The next step is to define an interface that will use those values to create a concrete implementation of a music service. That interface will be implemented in aBuilder.

Let’s look at the implementation of theSpotifyService andSpotifyServiceBuilder:

Python
# In music.pyclassSpotifyService:def__init__(self,access_code):self._access_code=access_codedeftest_connection(self):print(f'Accessing Spotify with{self._access_code}')classSpotifyServiceBuilder:def__init__(self):self._instance=Nonedef__call__(self,spotify_client_key,spotify_client_secret,**_ignored):ifnotself._instance:access_code=self.authorize(spotify_client_key,spotify_client_secret)self._instance=SpotifyService(access_code)returnself._instancedefauthorize(self,key,secret):return'SPOTIFY_ACCESS_CODE'

Note: The music service interface defines a.test_connection() method, which should be enough for demonstration purposes.

The example shows aSpotifyServiceBuilder that implements.__call__(spotify_client_key, spotify_client_secret, **_ignored).

This method is used to create and initialize the concreteSpotifyService. It specifies the required parameters and ignores any additional parameters provided through**_ignored. Once theaccess_code is retrieved, it creates and returns theSpotifyService instance.

Notice thatSpotifyServiceBuilder keeps the service instance around and only creates a new one the first time the service is requested. This avoids going through the authorization process multiple times as specified in the requirements.

Let’s do the same for Pandora:

Python
# In music.pyclassPandoraService:def__init__(self,consumer_key,consumer_secret):self._key=consumer_keyself._secret=consumer_secretdeftest_connection(self):print(f'Accessing Pandora with{self._key} and{self._secret}')classPandoraServiceBuilder:def__init__(self):self._instance=Nonedef__call__(self,pandora_client_key,pandora_client_secret,**_ignored):ifnotself._instance:consumer_key,consumer_secret=self.authorize(pandora_client_key,pandora_client_secret)self._instance=PandoraService(consumer_key,consumer_secret)returnself._instancedefauthorize(self,key,secret):return'PANDORA_CONSUMER_KEY','PANDORA_CONSUMER_SECRET'

ThePandoraServiceBuilder implements the same interface, but it uses different parameters and processes to create and initialize thePandoraService. It also keeps the service instance around, so the authorization only happens once.

Finally, let’s take a look at the local service implementation:

Python
# In music.pyclassLocalService:def__init__(self,location):self._location=locationdeftest_connection(self):print(f'Accessing Local music at{self._location}')defcreate_local_music_service(local_music_location,**_ignored):returnLocalService(local_music_location)

TheLocalService just requires a location where the collection is stored to initialize theLocalService.

A new instance is created every time the service is requested because there is no slow authorization process. The requirements are simpler, so you don’t need aBuilder class. Instead, a function returning an initializedLocalService is used. This function matches the interface of the.__call__() methods implemented in the builder classes.

A Generic Interface to Object Factory

A general purpose Object Factory (ObjectFactory) can leverage the genericBuilder interface to create all kinds of objects. It provides a method to register aBuilder based on akey value and a method to create the concrete object instances based on thekey.

Let’s look at the implementation of our genericObjectFactory:

Python
# In object_factory.pyclassObjectFactory:def__init__(self):self._builders={}defregister_builder(self,key,builder):self._builders[key]=builderdefcreate(self,key,**kwargs):builder=self._builders.get(key)ifnotbuilder:raiseValueError(key)returnbuilder(**kwargs)

The implementation structure ofObjectFactory is the same you saw inSerializerFactory.

The difference is in the interface that exposes to support creating any type of object. The builder parameter can be any object that implements thecallable interface. This means aBuilder can be a function, a class, or an object that implements.__call__().

The.create() method requires that additional arguments are specified as keyword arguments. This allows theBuilder objects to specify the parameters they need and ignore the rest in no particular order. For example, you can see thatcreate_local_music_service() specifies alocal_music_location parameter and ignores the rest.

Let’s create the factory instance and register the builders for the services you want to support:

Python
# In music.pyimportobject_factory# Omitting other implementation classes shown abovefactory=object_factory.ObjectFactory()factory.register_builder('SPOTIFY',SpotifyServiceBuilder())factory.register_builder('PANDORA',PandoraServiceBuilder())factory.register_builder('LOCAL',create_local_music_service)

Themusic module exposes theObjectFactory instance through thefactory attribute. Then, the builders are registered with the instance. For Spotify and Pandora, you register an instance of their corresponding builder, but for the local service, you just pass the function.

Let’s write a small program that demonstrates the functionality:

Python
# In program.pyimportmusicconfig={'spotify_client_key':'THE_SPOTIFY_CLIENT_KEY','spotify_client_secret':'THE_SPOTIFY_CLIENT_SECRET','pandora_client_key':'THE_PANDORA_CLIENT_KEY','pandora_client_secret':'THE_PANDORA_CLIENT_SECRET','local_music_location':'/usr/data/music'}pandora=music.factory.create('PANDORA',**config)pandora.test_connection()spotify=music.factory.create('SPOTIFY',**config)spotify.test_connection()local=music.factory.create('LOCAL',**config)local.test_connection()pandora2=music.services.get('PANDORA',**config)print(f'id(pandora) == id(pandora2):{id(pandora)==id(pandora2)}')spotify2=music.services.get('SPOTIFY',**config)print(f'id(spotify) == id(spotify2):{id(spotify)==id(spotify2)}')

The application defines aconfig dictionary representing the application configuration. The configuration is used as the keyword arguments to the factory regardless of the service you want to access. The factory creates the concrete implementation of the music service based on the specifiedkey parameter.

You can now run our program to see how it works:

Shell
$pythonprogram.pyAccessing Pandora with PANDORA_CONSUMER_KEY and PANDORA_CONSUMER_SECRETAccessing Spotify with SPOTIFY_ACCESS_CODEAccessing Local music at /usr/data/musicid(pandora) == id(pandora2): Trueid(spotify) == id(spotify2): True

You can see that the correct instance is created depending on the specified service type. You can also see that requesting the Pandora or Spotify service always returns the same instance.

Specializing Object Factory to Improve Code Readability

General solutions are reusable and avoid code duplication. Unfortunately, they can also obscure the code and make it less readable.

The example above shows that, to access a music service,music.factory.create() is called. This may lead to confusion. Other developers might believe that a new instance is created every time and decide that they should keep around the service instance to avoid the slow initialization process.

You know that this is not what happens because theBuilder class keeps the initialized instance and returns it for subsequent calls, but this isn’t clear from just reading the code.

A good solution is to specialize a general purpose implementation to provide an interface that is concrete to the application context. In this section, you will specializeObjectFactory in the context of our music services, so the application code communicates the intent better and becomes more readable.

The following example shows how to specializeObjectFactory, providing an explicit interface to the context of the application:

Python
# In music.pyclassMusicServiceProvider(object_factory.ObjectFactory):defget(self,service_id,**kwargs):returnself.create(service_id,**kwargs)services=MusicServiceProvider()services.register_builder('SPOTIFY',SpotifyServiceBuilder())services.register_builder('PANDORA',PandoraServiceBuilder())services.register_builder('LOCAL',create_local_music_service)

You deriveMusicServiceProvider fromObjectFactory and expose a new method.get(service_id, **kwargs).

This method invokes the generic.create(key, **kwargs), so the behavior remains the same, but the code reads better in the context of our application. You also renamed the previousfactory variable toservices and initialized it as aMusicServiceProvider.

As you can see, the updated application code reads much better now:

Python
importmusicconfig={'spotify_client_key':'THE_SPOTIFY_CLIENT_KEY','spotify_client_secret':'THE_SPOTIFY_CLIENT_SECRET','pandora_client_key':'THE_PANDORA_CLIENT_KEY','pandora_client_secret':'THE_PANDORA_CLIENT_SECRET','local_music_location':'/usr/data/music'}pandora=music.services.get('PANDORA',**config)pandora.test_connection()spotify=music.services.get('SPOTIFY',**config)spotify.test_connection()local=music.services.get('LOCAL',**config)local.test_connection()pandora2=music.services.get('PANDORA',**config)print(f'id(pandora) == id(pandora2):{id(pandora)==id(pandora2)}')spotify2=music.services.get('SPOTIFY',**config)print(f'id(spotify) == id(spotify2):{id(spotify)==id(spotify2)}')

Running the program shows that the behavior hasn’t changed:

Shell
$pythonprogram.pyAccessing Pandora with PANDORA_CONSUMER_KEY and PANDORA_CONSUMER_SECRETAccessing Spotify with SPOTIFY_ACCESS_CODEAccessing Local music at /usr/data/musicid(pandora) == id(pandora2): Trueid(spotify) == id(spotify2): True

Conclusion

Factory Method is a widely used, creational design pattern that can be used in many situations where multiple concrete implementations of an interface exist.

The pattern removes complex logical code that is hard to maintain, and replaces it with a design that is reusable and extensible. The pattern avoids modifying existing code to support new requirements.

This is important because changing existing code can introduce changes in behavior or subtle bugs.

In this article, you learned:

  • What the Factory Method design pattern is and what its components are
  • How to refactor existing code to leverage Factory Method
  • Situations in which Factory Method should be used
  • How Object Factories provide more flexibility to implement Factory Method
  • How to implement a general purpose Object Factory and its challenges
  • How to specialize a general solution to provide a better context

Further Reading

If you want to learn more about Factory Method and other design patterns, I recommendDesign Patterns: Elements of Reusable Object-Oriented Software by the GoF, which is a great reference for widely adopted design patterns.

Also,Heads First Design Patterns: A Brain-Friendly Guide by Eric Freeman and Elisabeth Robson provides a fun, easy-to-read explanation of design patterns.

Wikipedia has a good catalog ofdesign patterns with links to pages for the most common and useful patterns.

Recommended Course

Implementing the Factory Method Pattern in Python(2h 7m)

🐍 Python Tricks 💌

Get a short & sweetPython Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

AboutIsaac Rodriguez

Hi, I'm Isaac. I build, lead, and mentor software development teams, and for the past few years I've been focusing on cloud services and back-end applications using Python among other languages. Love to hear from you here at Real Python.

» More about Isaac

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

MasterReal-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

MasterReal-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students.Get tips for asking good questions andget answers to common questions in our support portal.


Looking for a real-time conversation? Visit theReal Python Community Chat or join the next“Office Hours” Live Q&A Session. Happy Pythoning!

Keep Learning

Related Topics:intermediatebest-practices

Related Learning Paths:

Related Courses:

Related Tutorials:

Keep reading Real Python by creating a free account or signing in:

Already have an account?Sign-In

Almost there! Complete this form and click the button below to gain instant access:

Python Logo

5 Thoughts On Python Mastery

🔒 No spam. We take your privacy seriously.


[8]ページ先頭

©2009-2026 Movatter.jp