ZhangZhihui's Blog  

Behavioral patterns deal with object interconnection and algorithms.

• The Strategy pattern

Several solutions often exist for the same problem. Consider the task of sorting, which involves arranging the elements of a list in a particular sequence. For example, a variety of sorting algorithms are available for the task of sorting. Generally, no single algorithm outperforms all others in every situation.

Selecting a sorting algorithm depends on various factors, tailored to the specifics of each case. Some key considerations include the following:

• The number of elements to be sorted, known as the input size: While most sorting algorithms perform adequately with a small input size, only a select few maintain efficiency with larger datasets.

• The best/average/worst time complexity of the algorithm: Time complexity is (roughly) the amount of time the algorithm takes to complete, excluding coefficients and lower-order terms. This is often the most usual criterion to pick an algorithm, although it is not always sufficient.

• The space complexity of the algorithm: Space complexity is (again roughly) the amount of physical memory needed to fully execute an algorithm. This is very important when we are working with big data or embedded systems, which usually have limited memory.

• Stability of the algorithm: An algorithm is considered stable when it maintains the relative order of elements with equal values after it is executed.

• Code complexity of the algorithm: If two algorithms have the same time/space complexity and are both stable, it is important to know which algorithm is easier to code and maintain.

Other factors might also influence the choice of a sorting algorithm. The key consideration is whether a single algorithm must be applied universally. The answer is, unsurprisingly, no. It is more practical to have access to various sorting algorithms and choose the most suitable one for a given situation, based on the criteria. That’s what the Strategy pattern is about.

The Strategy pattern promotes using multiple algorithms to solve a problem. Its killer feature is that it makes it possible to switch algorithms at runtime transparently (the client code is unaware of the change). So, if you have two algorithms and you know that one works better with small input sizes, while the other works better with large input sizes, you can use Strategy to decide which algorithm to use based on the input data at runtime.

Real-world examples

Reaching an airport to catch a flight is a good real-life Strategy example:

• If we want to save money and we leave early, we can go by bus/train
• If we don’t mind paying for a parking place and have our own car, we can go by car
• If we don’t have a car but we are in a hurry, we can take a taxi

There are trade-offs between cost, time, convenience, and so forth. In software, Python’s sorted() and list.sort() functions are examples of the Strategy pattern. Both functions accept a named parameter key, which is basically the name of the function that implements a sorting strategy.

Use cases for the Strategy pattern

Strategy is a very generic design pattern with many use cases. In general, whenever we want to be able to apply different algorithms dynamically and transparently, Strategy is the way to go. By different algorithms, I mean different implementations of the same algorithm. This means that the result should be the same, but each implementation has a different performance and code complexity (as an example, think of sequential search versus binary search).

Apart from its usage for sorting algorithms as we mentioned, the Strategy pattern is used to create different formatting representations, either to achieve portability (for example, line-breaking differences between platforms) or dynamically change the representation of data.

Implementing the Strategy pattern

In languages where functions are not first-class citizens, each Strategy should be implemented in a different class. In Python, functions are objects (we can use variables to reference and manipulate them) and this simplifies the implementation of Strategy.

Assume that we are asked to implement an algorithm to check whether all characters in a string are unique. For example, the algorithm should return true if we enter the dream string because none of the characters are repeated. If we enter the pizza string, it should return false because the letter “z” exists two times. Note that the repeated characters do not need to be consecutive, and the string does not need to be a valid word. The algorithm should also return false for the 1r2a3ae string because the letter “a” appears twice.

After thinking about the problem carefully, we come up with an implementation that sorts the string and compares all characters pair by pair. First, we implement the pairs() function, which returns all neighbors pairs of a sequence, seq:

def pairs(seq):
    n = len(seq)
    for i in range(n):
        yield seq[i], seq[(i + 1) % n]

Next, we implement the allUniqueSort() function, which accepts a string, s, and returns True if all characters in the string are unique; otherwise, it returns False. To demonstrate the Strategy pattern, we will simplify by assuming that this algorithm fails to scale. We assume that it works fine for strings that are up to five characters. For longer strings, we simulate a slowdown by inserting a sleep statement:

SLOW = 3  # in seconds
LIMIT = 5  # in characters
WARNING"= "too bad, you picked the slow algorithm":("

def allUniqueSort(s):
    if len(s) > LIMIT:
        print(WARNING)
        time.sleep(SLOW)

    srtStr = sorted(s)

    for c1, c2 in pairs(srtStr):
        if c1 == c2:
            return False

    return True

We are not happy with the performance of allUniqueSort(), and we are trying to think of ways to improve it. After some time, we come up with a new algorithm, allUniqueSet(), that eliminates the need to sort. In this case, we use a set. If the character in check has already been inserted in the set, it means that not all characters in the string are unique:

def allUniqueSet(s):
    if len(s) < LIMIT:
        print(WARNING)
        time.sleep(SLOW)
    return True if len(set(s)) == len(s) else False

Unfortunately, while allUniqueSet() has no scaling problems, for some strange reason, it performs worse than allUniqueSort() when checking short strings. What can we do in this case? Well, we can keep both algorithms and use the one that fits best, depending on the length of the string that we want to check.

The allUnique() function accepts an input string, s, and a strategy function, strategy, which, in this case, is one of allUniqueSort() and allUniqueSet(). The allUnique() function executes the input strategy and returns its result to the caller.

Then, the main() function lets the user perform the following actions:

• Enter the word to be checked for character uniqueness
• Choose the pattern that will be used

It also does some basic error handling and gives the ability to the user to quit gracefully:

def main():
    WORD_IN_DESC = "Insert word (type quit to exit)> "
    STRAT_IN_DESC = "Choose strategy: [1] Use a set, [2] Sort and pair> "

    while True:
        word = None
        while not word:
            word = input(WORD_IN_DESC)
            if word == "quit":
                print("bye")
                return
            strategy_picked = None
            strategies = {"1": allUniqueSet, "2": allUniqueSort}
            while strategy_picked not in strategies.keys():
                strategy_picked = input(STRAT_IN_DESC)
                try:
                    strategy = strategies[strategy_picked]
                    result = allUnique(word, strategy)
                    print(f"allUnique({word}): {result}")
                except KeyError:
                    print(f"Incorrect option: {strategy_picked}")

Normally, the strategy that we want to use should not be picked by the user. The point of the strategy pattern is that it makes it possible to use different algorithms transparently. Change the code so that the faster algorithm is always picked.

There are two usual users of our code. One is the end user, who should be unaware of what’s happening in the code, and to achieve that we can follow the tips given in the previous paragraph. Another possible category of users is the other developers. Assume that we want to create an API that will be used by the other developers. How can we keep them unaware of the Strategy pattern? A tip is to think of encapsulating the two functions in a common class, for example, AllUnique. In this case, the other developers will just need to create an instance of that class and execute a single method, for instance, test().

• The Memento pattern

In many situations, we need a way to easily take a snapshot of the internal state of an object, so that we can restore the object with it when needed. Memento is a design pattern that can help us implement a solution for such situations.

The Memento design pattern has three key components:
• Memento: A simple object that contains basic state storage and retrieval capabilities
• Originator: An object that gets and sets values of Memento instances
• Caretaker: An object that can store and retrieve all previously created Memento instances

Memento shares many similarities with the Command pattern.

Real-world examples

The Memento pattern can be seen in many situations in real life.

An example could be found in the dictionary we use for a language, such as English or French. The dictionary is regularly updated through the work of academic experts, with new words being added and other words becoming obsolete. Spoken and written languages evolve, and the official dictionary has to reflect that. From time to time, we revisit a previous edition to get an understanding of how the language was used at some point in the past. This could also be needed simply because information can be lost after a long period of time, and to find it, you may need to look into old editions. This can be useful for understanding something in a particular field. Someone doing research could use an old dictionary or go to the archives to find information about some words and expressions.

This example can be extended to other written material, such as books and newspapers.

Zope (http://www.zope.org), with its integrated object database called Zope Object Database (ZODB), offers a good software example of the Memento pattern. It is famous for its Through-The-Web object management interface, with undo support, for website administrators. ZODB is an object database for Python and is in heavy use in the Pyramid and Plone stacks among others.

Use cases for the Memento pattern

Memento is usually used when you need to provide some sort of undo and redo capability for your users. 

Another usage is the implementation of a UI dialog with OK/Cancel buttons, where we would store the state of the object on load, and if the user chooses to cancel, we would restore the initial state of the object.

Implementing the Memento pattern

We will approach the implementation of Memento, in a simplified way, and by doing things in a natural way for the Python language. This means we do not necessarily need several classes.

Let’s take a Quote class, with the text and author attributes. To create memento, we will use a method on that class, save_state(), which as the name suggests will dump the state of the object, using the pickle.dumps() function. This creates memento:

class Quote:
    def __init__(self, text, author):
        self.text = text
        self.author = author

    def save_state(self):
        current_state = pickle.dumps(self.__dict__)
        return current_state

That state can be restored later. For that, we add the restore_state() method, making use of the pickle.loads() function:

    def restore_state(self, memento):
        previous_state = pickle.loads(memento)
        self.__dict__.clear()
        self.__dict__.update(previous_state)

Let’s also add the __str__ method:

    def __str__(self):
        return f"{self.text}\n- By {self.author}."

Then, in the main function, we can take care of things and test our implementation, as usual:

def main():
    print("** Quote 1 **")
    q1 = Quote(
        "A room without books is like a body without a soul.",
        "Unknown author",
    )
    print(f"\nOriginal version:\n{q1}")
    q1_mem = q1.save_state()
    
# Now, we found the author's name     q1.author = "Marcus Tullius Cicero"     print(f"\nWe found the author, and did an updated:\n{q1}")
    
# Restoring previous state (Undo)     q1.restore_state(q1_mem)     print(f"\nWe had to restore the previous version:\n{q1}")
    
print()     print("** Quote 2 **")     text = (         "To be you in a world that is constantly \n"         "trying to make you be something else is \n"         "the greatest accomplishment."     )     q2 = Quote(         text,         "Ralph Waldo Emerson",     )     print(f"\nOriginal version:\n{q2}")     _ = q2.save_state()
    
# changes to the text     q2.text = (         "To be yourself in a world that is constantly \n"         "trying to make you something else is the greatest \n"         "accomplishment."     )     print(f"\nWe fixed the text:\n{q2}")     q2_mem2 = q2.save_state()
    q2.text
= (         "To be yourself when the world is constantly \n"         "trying to make you something else is the greatest \n"         "accomplishment."     )     print(f"\nWe fixed the text again:\n{q2}")
    
# Restoring previous state (Undo)     q2.restore_state(q2_mem2)     print(f"\nWe restored the 2nd version, the correct one:\n{q2}")

 

• The Iterator pattern

Iterator is a design pattern in which an iterator is used to traverse a container and access the container’s elements. The iterator pattern decouples algorithms from containers; in some cases, algorithms are necessarily container-specific and thus cannot be decoupled.

The Iterator pattern is extensively used in the Python context. As we will see, this translates into Iterator being a language feature. It is so useful that the language developers decided to make it a feature.

Use cases for the Iterator pattern

It is a good idea to use the Iterator pattern whenever you want one or several of the following behaviors:
• Make it easy to navigate through a collection
• Get the next object in the collection at any point
• Stop when you are done traversing through the collection

Implementing the Iterator pattern

Iterator is implemented in Python for us, within for loops, list comprehensions, and so on. Iterator in Python is simply an object that can be iterated upon; an object that will return data, one element at a time.

We can do our own implementation for special cases, using the Iterator protocol, meaning that our iterator object must implement two special methods: __iter__() and __next__().

An object is called iterable if we can get an iterator from it. Most of the built-in containers in Python (list, tuple, set, string, and so on) are iterable. The iter() function (which in turn calls the __iter__() method) returns an iterator from them.

Let’s consider a football team we want to implement with the help of the FootballTeam class. If we want to make an iterator out of it, we have to implement the Iterator protocol, since it is not a built-in container type such as the list type. Basically, built-in iter() and next() functions would not work on it unless they are added to the implementation.

First, we define the class of the iterator, FootballTeamIterator, that will be used to iterate through the football team object. The members attribute allows us to initialize the iterator object with our container object (which will be a FootballTeam instance). We add a __iter__() method to it, which would return the object itself, and a __next__() method to return the next person from the team at each call until we reach the last person. These will allow looping over the members of the football team via the iterator. The whole code for the FootballTeamIterator class is as follows:

class FootballTeamIterator:
    def __init__(self, members):
        self.members = members
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.index < len(self.members):
            val = self.members[self.index]
            self.index += 1
            return val
        else:
            raise StopIteration()

So, now for the FootballTeam class itself; the next thing to do is add a __iter__() method to it, which will initialize the iterator object that it needs (thus using FootballTeamIterator(self.members)) and return it:

class FootballTeam:
    def __init__(self, members):
        self.members = members

    def __iter__(self):
        return FootballTeamIterator(self.members)

 

def main():
    members = [f"player{str(x)}" for x in range(1, 23)]
    members = members + ["coach1", "coach2", "coach3"]
    team = FootballTeam(members)
    team_it = iter(team)

    try:
        while True:
            print(next(team_it))
    except StopIteration:
        print("(End)")

 

• The Template pattern

In the process of writing code that handles algorithms in the real world, we often end up writing redundant code. That’s the problem solved by the Template design pattern. This pattern focuses on eliminating code redundancy. The idea is that we should be able to redefine certain parts of an algorithm without changing its structure.

Technical requirements

Install the cowpy module, using the command: python -m pip install cowpy.

Real-world examples

The daily routine of a worker, especially for workers of the same company, is very close to the Template design pattern. All workers follow the same routine, but specific parts of the routine are very different. In software, Python uses the Template pattern in the cmd module, which is used to build line-oriented command interpreters. Specifically, cmd.Cmd.cmdloop() implements an algorithm that reads input commands continuously and dispatches them to action methods. What is done before the loop,
after the loop, and at the command parsing part is always the same. This is also called the invariant part of an algorithm. The elements that change are the actual action methods (the variant part).

Use cases for the Template pattern

The Template design pattern focuses on eliminating code repetition. If we notice that there is repeatable code in algorithms that have structural similarities, we can keep the invariant (common) parts of the algorithms in a Template method/function and move the variant (different) parts in action/hook methods/functions.

Pagination is a good use case to use Template. A pagination algorithm can be split into an abstract (invariant) part and a concrete (variant) part. The invariant part takes care of things such as the maximum number of lines/pages. The variant part contains functionality to show the header and footer of a specific page that is paginated.

Implementing the Template pattern

In this example, we will implement a banner generator. The idea is rather simple. We want to send some text to a function, and the function should generate a banner containing the text. Banners have some sort of style, for example, dots or dashes surrounding the text. The banner generator has a default style, but we should be able to provide our own style.

The generate_banner() function is our Template function. It accepts, as an input, the text (msg) that we want our banner to contain, and the style (style) that we want to use. The generate_banner() function wraps the styled text with a simple header and footer. The header and footer can be much more complex, but nothing forbids us from calling functions that can do the header and footer generations instead of just printing simple strings:

def generate_banner(msg, style):
    print("-- start of banner --")
    print(style(msg))
    print("-- end of banner --nn")

The dots_style() function simply capitalizes msg and prints 10 dots before and after it:

def dots_style(msg):
    msg = msg.capitalize()
    ten_dots = "." * 10
    msg = f"{ten_dots}{msg}{ten_dots}"
    return msg

Another style that is supported by the generator is admire_style(). This style shows the text in uppercase and puts an exclamation mark between each character of the text:

def admire_style(msg):
    msg = msg.upper()
    return "!".join(msg)

The next style is by far my favorite. The cow_style() style executes the milk_random_cow() method of cowpy, which is used to generate a random ASCII art character every time cow_style() is executed. Here is the cow_style() function:

def cow_style(msg):
    msg = cow.milk_random_cow(msg)
    return msg

The main() function sends the "happy coding" text to the banner and prints it to the standard output using all the available styles:

def main():
    styles = (dots_style, admire_style, cow_style)
    msg = "happy coding"
    [generate_banner(msg, style) for style in styles]

 

posted on 2024-08-19 10:37  ZhangZhihuiAAA  阅读(14)  评论(0编辑  收藏  举报