- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Guided: Implementing a Design Pattern in Python
In this hands-on lab, you’ll implement the Factory Method design pattern in Python by refactoring a simple command-line tool that sends messages via Email or SMS. You’ll start with tightly coupled procedural code and gradually introduce abstraction through interfaces, concrete classes, and a factory method. Along the way, you'll see how this pattern supports cleaner, more maintainable code that follows object-oriented design principles.
Lab Info
Table of Contents
-
Challenge
Design Patterns
Design Patterns
Design patterns are proven solutions to common software design problems. They help developers create code that is more maintainable, flexible, and reusable.
A common design pattern is the Factory Method. This pattern allows you to create objects without specifying the exact class of object that will be created. Instead, you define an interface for creating an object, and let subclasses decide which class to instantiate.
Why Use the Factory Method Pattern?
When object creation is tightly coupled with business logic, applications become rigid and hard to extend. Mixing object instantiation with core functionality makes it challenging and error-prone to add new features or modify existing ones.
The Factory Method pattern solves this problem by separating object creation from the main application logic. This separation leads to code that is easier to maintain, extend, and adapt as requirements change.
What You Will Accomplish
In this lab, you'll discover the value of the Factory Method pattern and how it supports scalable software design.
You'll refactor a simple command-line messaging application to use the Factory Method pattern in Python.
To get started, click the right arrow below to review the current code and identify areas for improvement.
-
Challenge
Review the Starting Code
Review the Starting Code
Before you start refactoring, it's important to understand the limitations of the current code.
Open the /main.py file in your editor. This script prompts the user to choose a channel—either email or SMS—and then uses a conditional statement to determine which channel to use for sending the message. If the user enters an invalid channel, the script raises an error.
To try it out, run this command in your terminal:
python3 main.pyIssues with the Current Implementation
Using conditional statements to select the message channel works for simple scripts, but it quickly becomes problematic as your application grows.
The current approach tightly couples message-sending logic to user input, so adding a new channel—like push notifications—requires editing
main.pydirectly, increasing the risk of bugs and making maintenance harder.Responsibilities for handling user input, selecting the channel, and sending messages are all mixed together, leading to less organized and harder-to-understand code.
Refactoring to use the Factory Method design pattern will make your application more modular, extensible, and easier to maintain.
Implementing the Factory Method Design Pattern
To help you get started, a
/channelsdirectory has been created with all the files you'll need to implement the Factory Method design pattern.Tip: In the activity bar to the left, you can click on the double page icon to switch to the explorer view to see these files. When done you can click on the book icon to return to this instructional pane.
Here’s a quick summary of the files you’ll find in the
/channelsdirectory:__init__.py: Marks the directory as a Python package.interface.py: Defines a common interface for all message channels.email_channel.pyandsms_channel.py: Provide concrete implementations for each channel.factory.py: Contains the factory method for creating channel instances.This structure keeps your code organized and makes it easy to add new channels in the future—just create a new file that implements the shared interface and include in the factory method of the
factory.pyfile.
The first task is to define a shared interface that all channels will implement.
Click the right arrow below to see how to create this interface.
-
Challenge
Define a Common Interface
Define a Common Interface
To begin, establish a shared interface that all message channels will follow. This ensures your code remains consistent and easy to extend as you add new channel types.
Python’s
abcmodule lets you define an abstract base class for this purpose.Open the /channels/interface.py file and add the following interface definition:
from abc import ABC, abstractmethod class Channel(ABC): @abstractmethod def send(self, recipient: str, message: str): passTip: If you need guidance or want to compare your work, you can find a complete example implementation in the
/solutiondirectory.Benefits of a Shared Interface
A shared interface ensures all channel classes implement the same methods, like
send, making your codebase consistent and easier to maintain. It also allows you to add new channels without modifying existing code, following the Open/Closed Principle for better scalability and flexibility.
Next, you'll create classes that inherit from
Channeland implement thesendmethod. Each class will define how messages will be delivered for its specific channel type. -
Challenge
Implement Concrete Channel Classes
Implement Concrete Channel Classes
With the
Channelinterface defined, it's time to create concrete classes for each communication channel:EmailChannelandSMSChannel.These classes will inherit from
Channeland provide their own implementation of thesendmethod, allowing each to handle message delivery in a way that's specific to the channel.EmailChannel Implementation
To get started, open the /channels/email_channel.py file.
Begin by importing the
Channelinterface frominterface.pyusing a relative import:from .interface import ChannelNext, define the
EmailChannelclass that inherits fromChannel:class EmailChannel(Channel):Then, implement the
sendmethod to mock the process of sending an email by printing a message indicating that an email is being sent.def send(self, recipient: str, message: str): print(f"Sending email to {recipient}: {message}")SMSChannel Implementation
Following the
EmailChannelexample, create theSMSChannelclass in the /channels/sms_channel.py file:The final implementation of the
SMSChannelclass should look like this:Solution
from .interface import Channel class SMSChannel(Channel): def send(self, recipient: str, message: str): print(f"Sending SMS to {recipient}: {message}")By following this structure, you ensure that both channels adhere to the same interface, making your codebase more consistent and extensible.
With the concrete channel classes in place, your next task is to implement a factory method.
-
Challenge
The Factory Method
The Factory Method
Now that you have your concrete channel classes, it's time to implement a factory method. This method will create and return the correct channel object based on user input.
By moving object creation out of your main application logic, you make your code cleaner and more flexible. Adding or updating channels in the future becomes much simpler.
Writing the Factory Method
Open the /channels/factory.py file. In this file, you'll define a
get_channel()function. This function will accept a channel type as an argument and return an instance of the appropriate channel class.Begin by importing the channel interface and the concrete channel classes:
from .interface import Channel from .email_channel import EmailChannel from .sms_channel import SMSChannelNow, define the
get_channel()function. Move the conditional logic that selects the correct channel from the /main.py file into this function.The
get_channel()function should return an instance of the appropriate channel class based on the input, or raise an error if the channel type is unknown.def get_channel(channel: str) -> Channel: if channel == "email": return EmailChannel() elif channel == "sms": return SMSChannel() else: raise ValueError(f"Unknown channel type: {channel}")Benefits of Delegating Object Creation
By moving the logic for creating channel instances into the
get_channel()function, you separate the concerns of object creation from your main application code.This approach makes your main application simpler and more focused on its core task—sending messages—while the factory method handles the details of which channel to use and how to create it.
Next, you'll modify your main application to use the factory method for creating channel instances.
-
Challenge
Refactor the Main Application Logic
Refactor the Main Application Logic
Now that you have implemented the factory method and channel classes, it's time to update the main application logic to use the factory method for channel selection.
Using the Factory Method
Open the /main.py file. In the
mainfunction, find the section where the code chooses which channel to use based on the user's input.To begin refactoring, add the following import statement at the top of your
main.pyfile:from channels.factory import get_channelNext, remove the existing conditional statements that select the channel based on user input. Instead, use the factory method to obtain the correct channel instance and call its
.send()method.Your updated
mainfunction should look like this:try: channel = get_channel(channel_type) channel.send(recipient, message) except ValueError as e: print(e)Advantages of the Refactored Design
This refactoring brings several key benefits:
Decoupling: The main application logic is no longer tied to specific channel implementations. This separation makes it much easier to add or modify channels in the future.
Maintainability: The codebase becomes cleaner and more readable. Channel selection is now handled by the factory method, reducing complexity in the main logic.
Extensibility: Adding new channels is straightforward—just create a new channel class and update the factory method. There's no need to change the main application logic.
Testing the Refactored Application
After refactoring the main logic, it's important to verify that everything works as intended.
To test your application:
python3 main.pyWhen you enter
emailorsmsas the channel type, the application should still correctly simulate sending messages using the appropriate channel class.If you enter an unsupported channel type, the application will raise a
ValueErrorand display an error message.
To experience the advantages of the factory method, try adding a new channel: implement a
PushChannelthat handles thepushchannel type.If you’d like step-by-step instructions, continue to the next section, where you’ll learn how to create this channel class and update the factory method to support it.
-
Challenge
Adding a New Channel
Adding a New Channel
With the factory method in place and the main application logic refactored, adding new channel types becomes straightforward.
To add push notifications as a new channel, follow these steps:
In the
/channelsdirectory, create a new file calledchannel_push.py. This file will define thePushChannelclass, responsible for handling push notification messages.Structure this file similarly to the other channel files, but implement logic specific to push notifications. For example:
from .interface import Channel class PushChannel(Channel): def send(self, recipient: str, message: str): print(f"Sending push notification to {recipient}")Next, open the /channels/factory.py file and update the
get_channelfunction to include the newPushChannel.First, import the new class at the top of the file:
from .channel_push import PushChannelThen, add a new condition to handle the push channel:
elif channel == "push": return PushChannel()Thanks to the Factory Method pattern, adding a new channel type is simple and does not require changes to existing code. Now, when a user enters
pushas the channel type, the factory method will return an instance ofPushChannel.To verify your changes, run
main.pyand enterpushwhen prompted for the channel type. You should see a message indicating that a push notification is being sent.
Congratulations! You have successfully extended the application to support a new channel type. Notice how you achieved this without altering the existing code structure—this demonstrates the flexibility and power of the Factory Method design pattern. By following this approach, you can easily add more channels in the future with minimal changes.
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.