ZhangZhihui's Blog  

Creational design patterns deal with different aspects of object creation. Their goal is to provide better alternatives for situations where direct object creation, which in Python happens within the __init__() function, is not convenient.

• The factory pattern

The factory method

The factory method is based on a single function written to handle our object creation task. We execute it, passing a parameter that provides information about what we want, and, as a result, the wanted object is created.

Interestingly, when using the factory method, we are not required to know any details about how the resulting object is implemented and where it is coming from.

Real-world examples

We can find the factory method pattern used in real life in the context of a plastic toy construction kit. The molding material used to construct plastic toys is the same, but different toys (different figures or shapes) can be produced using the right plastic molds. This is like having a factory method in which the input is the name of the toy that we want (for example, a duck or car) and the output (after the molding) is the plastic toy that was requested.

Use cases for the factory method pattern

If you realize that you cannot track the objects created by your application because the code that creates them is in many different places instead of in a single function/method, you should consider using the factory method pattern. The factory method centralizes object creation and tracking your objects becomes much easier. Note that it is fine to create more than one factory method, and this is how it is typically done in practice. Each factory method logically groups the creation of objects that have similarities. For example, one factory method might be responsible for connecting you to different databases (MySQL, SQLite); another factory method might be responsible for creating the geometrical object that you request (circle, triangle); and so on.

The factory method is also useful when you want to decouple object creation from object usage. We are not coupled to a specific class when creating an object; we just provide partial information about what we want by calling a function. This means that introducing changes to the function is easy and does not require any changes to the code that uses it.

Should you use the factory method pattern?

The main critique that veteran Python developers often express toward the factory method pattern is that it can be considered over-engineered or unnecessarily complex for many use cases. Python’s dynamic typing and first-class functions often allow for simpler, more straightforward solutions to problems that the factory method aims to solve. In Python, you can often use simple functions or class methods to create objects directly without needing to create separate factory classes or functions. This keeps the code more readable and Pythonic, adhering to the language’s philosophy of Simple is better than complex.

Also, Python’s support for default arguments, keyword arguments, and other language features often makes it easier to extend constructors in a backward-compatible way, reducing the need for separate factory methods. So, while the factory method pattern is a well-established design pattern in statically typed languages such as Java or C++, it is often seen as too cumbersome or verbose for Python’s more flexible and dynamic nature.

• The builder pattern

Imagine that we want to create an object that is composed of multiple parts, and the composition needs to be done step by step. The object is not complete unless all its parts are fully created. That’s where the builder design pattern can help us. The builder pattern separates the construction of a complex object from its representation. By keeping the construction separate from the representation, the same construction can be used to create several different representations.

Real-world examples

In our everyday life, the builder design pattern is used in fast-food restaurants. The same procedure is always used to prepare a burger and the packaging (box and paper bag), even if there are many kinds of burgers (classic, cheeseburger, and more) and different packages (small-sized box, medium-sized box, and so forth). The difference between a classic burger and a cheeseburger is in the representation and not in the construction procedure. In this case, the director is the cashier who gives instructions about what needs to be prepared to the crew, and the builder is the person from the crew who takes care of the specific order.

Comparison with the factory pattern

At this point, the distinction between the builder pattern and the factory pattern might not be very clear. The main difference is that a factory pattern creates an object in a single step, whereas a builder pattern creates an object in multiple steps and almost always uses a director. Another difference is that while the factory pattern returns a created object immediately, in the builder pattern, the client code explicitly asks the director to return the final object when it needs it.

Use cases for the builder pattern

The builder pattern is particularly useful when an object needs to be constructed with numerous possible configurations. A typical case is a situation where a class has multiple constructors with a varying number of parameters, often leading to confusion or error-prone code. 

The pattern is also beneficial when the object’s construction process is more complex than simply setting initial values. For example, if an object’s full creation involves multiple steps, such as parameter validation, setting up data structures, or even making calls to external services, the builder pattern can encapsulate this complexity.

• The prototype pattern 

The prototype pattern allows you to create new objects by copying existing ones, rather than creating them from scratch. This pattern is particularly useful when the cost of initializing an object is more expensive or complex than copying an existing one. In essence, the prototype pattern enables you to create a new instance of a class by duplicating an existing instance, thereby avoiding the overhead of initializing a new object.

In its simplest version, this pattern is just a clone() function that accepts an object as an input parameter and returns a clone of it. In Python, this can be done using the copy.deepcopy() function.

Use cases for the prototype pattern

The prototype pattern is useful when we have an existing object that needs to stay untouched and we want to create an exact copy of it, allowing changes in some parts of the copy.

There is also the frequent need for duplicating an object that is populated from a database and has references to other database-based objects. It is costly (multiple queries to a database) to clone such a complex object, so a prototype is a convenient way to solve the problem.

• The singleton pattern

import urllib.request


class SingletonType(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            obj = super(SingletonType, cls).__call__(*args, **kwargs)
            cls._instances[cls] = obj
        return cls._instances[cls]


class URLFetcher(metaclass=SingletonType):
    def __init__(self):
        self.urls = []

    def fetch(self, url):
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req) as response:
            if response.code == 200:
                page_content = response.read()
                with open("content.html", "a") as f:
                    f.write(str(page_content))
                self.urls.append(url)

Should you use the singleton pattern?

While the singleton pattern has its merits, it may not always be the most Pythonic approach to managing global states or resources. Our implementation example worked, but if we stop a minute to analyze the code again, we notice the following:
• The techniques used for the implementation are rather advanced and not easy to explain to a beginner
• By reading the SingletonType class definition, it is not easy to immediately see that it provides a metaclass for a singleton if the name does not suggest it

In Python, developers often prefer a simpler alternative to singleton: using a module-level global object.

By adopting the global object technique, as explained by Brandon Rhodes in what he calls the Global Object Pattern (https://python-patterns.guide/python/module-globals/), you can achieve the same result as the singleton pattern without the need for complex instantiation processes or forcing a class to only have one instance.

• The object pool pattern

Use cases for the object pool pattern

The object pool pattern is especially useful in scenarios where resource initialization is costly or time-consuming. This could be in terms of CPU cycles, memory usage, or even network bandwidth. For example, in a shooting video game, you might use this pattern to manage bullet objects. Creating a new bullet every time a gun is fired could be resource-intensive. Instead, you could have a pool of bullet objects that are reused.

class Car:
    def __init__(self, make: str, model: str):
        self.make = make
        self.model = model
        self.in_use = False


class CarPool:
    def __init__(self):
        self._available = []
        self._in_use = []

    def acquire_car(self) -> Car:
        if len(self._available) == 0:
            new_car = Car("BMW", "M3")
            self._available.append(new_car)
        car = self._available.pop()
        self._in_use.append(car)
        car.in_use = True
        return car

    def release_car(self, car: Car) -> None:
        car.in_use = False
        self._in_use.remove(car)
        self._available.append(car)

 

posted on 2024-08-16 09:26  ZhangZhihuiAAA  阅读(3)  评论(0编辑  收藏  举报