Achieving Decoupling and Simplicity in Python: Combining Factory Method and Facade Design Patterns

Ali Moussawi
6 min readOct 14, 2023

--

Retrieved from Freepik

Introduction

In the realm of software development, two essential principles stand out: decoupling and simplicity. Decoupling involves isolating code that performs specific tasks from other parts of the system, making it more maintainable and reusable. Simplicity refers to writing code that is easy to read, understand, and modify, which is crucial for creating scalable and adaptable software. In this article, I will explore how to achieve decoupling and simplicity and then demonstrate how these principles can be combined using Python along with the Factory Method and Facade design patterns.

The Significance of Decoupling

Decoupling is the process of separating the code that handles one task from the code that handles another. It plays a vital role in software development by making code more maintainable, reusable, and easier to test. Decoupling reduces the chances of introducing errors when updating or replacing a part of the system. At first glance, decoupling may seem straightforward, but as applications grow in size and complexity, manual implementation becomes impractical. This is where design patterns come into play.

How to Achieve Decoupling

Decoupling can be achieved by adopting various techniques and practices:

  • Dependency Injection Container: Utilize a dependency injection container to manage and inject dependencies into your code, allowing for better separation of concerns.
  • Inversion of Control (IoC): Implement IoC principles to invert the control over component lifecycles, making your code more modular and testable.
  • Service Locator: Use a service locator to centralize the retrieval of services, enabling flexible and decoupled service usage.
  • Factory Method: Implement the Factory Method pattern to separate object creation logic from the rest of your code, promoting a cleaner and more modular design.
  • Adapter Pattern: Employ the Adapter pattern to provide a consistent interface to third-party libraries, making them more accessible to the rest of your codebase and allowing for future changes, such as switching the data source library.
  • Proxy Pattern: Use the Proxy pattern to provide a uniform interface to third-party libraries, enhancing accessibility and future flexibility.

The Importance of Simplicity in Software Engineering

Simplicity in software engineering entails writing code without unnecessary complexity. Simple code is easy to read, understand, and modify. Its significance is underscored by several key factors:

  • Better Understandability: Simple code is easier to comprehend, reducing the likelihood of misunderstandings, errors, and bugs.
  • Easier Maintenance: Maintaining simple code is more straightforward, as there is less code to navigate, and it is easier to modify.
  • Increased Productivity: Simplicity boosts productivity by making code quicker to write and understand, ultimately leading to more efficient development.
  • Better Quality: Simple code is easier to test and debug, resulting in more reliable software with fewer bugs and errors.

Achieving Simplicity in Your Code

To achieve simplicity in software development, consider the following best practices:

  • Keep it Simple: Strive to keep your code as straightforward as possible, avoiding unnecessary complexity. When in doubt, choose the simpler option.
  • Break it Down: Divide complex problems into smaller, manageable pieces. By breaking down problems, developers can focus on writing simple, understandable code for each component, with the Facade design pattern playing a pivotal role in facilitating this process.

Decoupling with the Factory Method

The Factory Method is a creational design pattern that separates object creation logic from the rest of your code, promoting a cleaner and more modular design. It provides an interface for creating objects in a superclass while allowing subclasses to determine the types of objects to create. This pattern enhances decoupling and modularity in your code.

You can read more about the Factory Method pattern here.

Simplifying with the Facade

The Facade is a structural design pattern that simplifies the interface to a complex set of classes, libraries, or frameworks. It offers a unified and simplified interface to a subsystem, reducing complexity and making client code more concise. The Facade pattern enhances the simplicity of your codebase.

You can read more about the Facade pattern here.

Combining Factory Method and Facade

The decision to combine the Factory Method and Facade design patterns in your Python implementation offers several benefits, ultimately facilitating decoupling and simplicity.

  • Separation of Concerns: The Factory Method separates object creation logic from the rest of your code, promoting a clean and modular design, making it easier to maintain and extend your API.
  • Abstraction: The Factory Method abstracts the actual implementation of object creation, which is helpful for hiding details of object instantiation from client code.
  • Simplified Client Code: The Facade simplifies the client’s interaction with a complex subsystem by providing a unified and simplified interface. This results in cleaner, more concise client code.
  • Flexibility: Using the Factory Method allows you to create different types of objects dynamically, which is valuable when the exact object type is determined at runtime.
  • Improved Maintainability: Separating object creation and simplifying the client code enhances the maintainability of your API. When you need to make changes or updates, you can do so more easily without affecting the client code.
  • Ease of Testing: By abstracting object creation and subsystem interactions, you can more easily test different parts of your API in isolation. This promotes better unit testing practices.
  • Reusability: Both patterns encourage the creation of reusable components. The Factory Method, in particular, can lead to the creation of reusable factories that can be used across different parts of your API.
  • Reduced Complexity: The Facade pattern reduces the complexity of the API by providing a high-level, simplified interface. This can make it easier for developers to understand and work with your API.

Implementation Example

In my previous article, I described a practical use case for combining these patterns. In my application, I have a routing system that acts as the user interface for my APIs, forwarding requests after authentication and validation to a controller. The controller acts as a facade class, initiating multiple services and injecting data access as needed.

The Factory Method is employed to select the suitable service, considering settings or parameters. These services share a common purpose but exhibit diverse implementations, catering to various environmental and request-specific factors such as source location, user specifications, or even A/B testing criteria.

From my source code

Sample Use Case

Consider a crucial feature in my application: storing files in an S3 bucket for synchronization. To optimize development speed, I created a service that stores files in the local file system for the development environment. Both services inherit from the same abstraction class, providing the same file storage purpose.

While this approach adds some complexity to the codebase, it offers flexibility, enhances testability, and allows for seamless development and testing in different environments.

# controller.py

class MediaController:

def __init__(self):
self.data_access = DataAccessFactory.build_object()
.....
self.storage_service = StorageServiceFactory.build_object(self.data_access)
.....

def create_file(self, user_id, media_id, binary_data):
try:
....
payload = self.storage_service.store_file(user_id, media_id, binary_data)
....
except (UserNotFound, MediaNotFound, Exception) as e:
self.exception_handler.abort(e)
# settings.py

class DevelopmentSettings:
.....
STORAGE_SERVICE = StorageServiceOptions.AWS_S3
# options.py
class StorageServiceOptions:
AWS_S3 = 'AWS_S3'
FILE_SYSTEM = 'FILE_SYSTEM'
# storage_service/factoy.py

class StorageServiceFactory:

@staticmethod
def build_object(data_access, service=Settings.STORAGE_SERVICE):
if service == StorageServiceOptions.AWS_S3:
return AWSS3StorageService(data_access)
if service == StorageServiceOptions.FILE_SYSTEM:
return FileSystemStorageService(data_access)

raise NotImplementedError()
# storage_service/aws_s3.py

class AWSS3StorageService(AbstractStorageService):

def __init__(self, data_access):
self.data_access = data_access
self.s3_helper = S3Helper(Config.AWS_ACCESS_KEY_ID, Config.AWS_SECRET_ACCESS_KEY, Config.AWS_BUCKET)

def store_data(self, media_id, binary_data):
# Upload to S3
# store changes into the database
pass
# storage_service/file_system.py

class FileSystemStorageService(AbstractStorageService):
def __init__(self, data_access):
self.data_access = data_access

def store_data(self, media_id, binary_data):
# create the file on the file system
# store changes into the database
pass

Conclusion

Combining the Factory Method and Facade design patterns in your Python implementation can help you achieve both decoupling and simplicity in your code. These patterns provide clear separation of concerns, abstraction, simplified client code, flexibility, improved maintainability, and reduced complexity. By understanding the principles and applying them to your projects, you can build more maintainable, scalable, and adaptable software.

--

--

Ali Moussawi

Experienced software engineer specializing in Python-based backend solutions. Leading top developers in fintech innovation.