-
Design Patterns. Part 4
This is the final part of the article series on design patterns. If you haven't been following our previous publications, you can catch up and go over the first three parts:
Part 1 - Abstract Factory и Strategy;
Part 2 - Observer and Mediator;
Part 3 - Memento and Bridge.
In Part 4 we are going to review the State and Interpreter patterns.
State
State is a behavioral design pattern that allows objects to change behavior depending on their state. From the outside, it seems like the object's class has changed.
The State pattern can't be considered in isolation from the state machine concept, also known as FSM (finite state machine).
The basic idea is that the program can be in one of several states that all the time replace each other. The collection of these states, as well as the transitions between them, is predetermined and finite. Being in different states, the program can react differently to the same events that happen to it.
This approach can be applied to individual objects. For example, a Document object can have three states: Draft, Moderation, or Published. In each of these states, the publish method will work in different ways:
- from the draft it will send the document for moderation;
- from the moderation - to publication, but on the condition that the administrator requests it;
- in the published state the method won't do anything.
The state machine is most often implemented by using a set of conditional statements, if/elif/else, that check the current state of the object and perform the appropriate behavior. You've probably already implemented at least one state machine in your life, even without knowing it.
The main problem of such a state machine will become apparent if you add a dozen more states to the Document. Each method will consist of a weighty conditional statement, which will be going through the available states. This code is extremely difficult to maintain. The slightest change in the transition logic will force you to recheck the operation of all methods that contain the conditional statements of the state machine.
The confusion and stockpiling of conditions are especially evident in the old projects. A set of possible states can be difficult to predetermine, so they are constantly added during the evolution of the program. Because of that, the solution that seemed simple and effective at the very beginning of development, can later become a projection of a large spaghetti monster.
The State pattern offers to create separate classes for each state that a context object can take, and then transfer the behaviors corresponding to these states there. Instead of storing the code for all states, the input object called the context will contain a reference to one of the state objects and delegate the state-dependent work to it.
Due to the fact that state objects will have a common interface, the context will be able to delegate work to the state, without being tied to its class. The context behavior can be changed at any time by connecting another object-state to it.
A very important nuance that distinguishes this pattern from the Strategy one is that both the context and the states themselves can be aware of each other and initiate the transitions from one state to another.
Situations in which this pattern should be used:
1) When you have an object whose behavior drastically changes depending on the internal state, and there are many types of states, and their code often changes.
The pattern proposes to allocate all the fields and methods associated with certain states to their own classes. The input object will constantly refer to one of the state objects, delegating a part of its work to it. To change the state it'll be enough to substitute another state-state to the context.
2) When the class code contains a set of large alike conditional statements that select behavior depending on the current values of the class fields.
The pattern suggests moving each branch of such a conditional statement to its own class. You can also place all the fields associated with those states there.
3) When you deliberately use a state machine table built on conditional statements, but you have to put up with duplicate code for similar states and transitions.
The State pattern allows you to implement a hierarchical state machine based on inheritance. You can inherit similar states from one parent class and move all the duplicate code there.
Pros:
- eliminates a lot of large conditional statements of the state machine;
- accumulate the code associated with a certain state in one place;
- simplifies the context code.
Cons:
- can unreasonably complicate the code if there are few states and they rarely change.
Let's look at a practical implementation of the pattern using the walrus example, where the same methods (eat, find_food, move, dream) will have different behaviors depending on the state in which the walrus is at the moment.
class State: def eat(self): pass def find_food(self): pass def move(self): pass def dream(self): pass class SleepState(State): def eat(self): return "can’t eat while sleeping" def find_food(self): return "looking for food, but only in its dreams" def move(self): return "can’t move while sleeping" def dream(self): return "sleeps and sees a wonderful dream" class OnGroundState(State): def eat(self): return "pours out the extracted shellfish on its belly and starts slowly eating it" def find_food(self): return "finds not fresh, but quite edible whale carcass that has been thrown to the shore" def move(self): return "awkwardly crawls along the shoreline" def dream(self): return "stops for a moment dreaming of one familiar female" class InWaterState(State): def eat(self): return "can’t eat in the water" def find_food(self): return "plows the seabed with tusks, catching shellfish" def move(self): return "gracefully breaks the waves of the world's oceans" def dream(self): return "doesn’t sleep or dream in the water - it's too difficult" class Walrus: def __init__(self, state: State): self._state = state def change_state(self, state: State): self._state = state def eat(self): self._execute('eat') def find_food(self): self._execute('find_food') def move(self): self._execute('move') def dream(self): self._execute('dream') def _execute(self, operation): try: func = getattr(self._state, operation) print("Walrus {}.".format(func())) except AttributeError: print("Walrus doesn’t know how to do this.")
Now, if we experiment with different walrus states:
sleep = SleepState() on_ground = OnGroundState() in_water = InWaterState() walrus = Walrus(on_ground) walrus.change_state(in_water) walrus.move() walrus.find_food() walrus.change_state(on_ground) walrus.eat() walrus.move() walrus.dream() walrus.change_state(sleep) walrus.dream()
...we'll get the following result:
Walrus gracefully breaks the waves of the world's oceans.
Walrus plows the seabed with tusks, catching shellfish.
Walrus pours out the extracted shellfish on its belly and starts slowly eating it.
Walrus awkwardly crawls along the shoreline.
Walrus stops for a moment dreaming of one familiar female.
Walrus sleeps and sees a wonderful dream.
As you can see, depending on the state in which the Walrus is, the same methods (in the example - move and dream) behave differently.
You can practice your newly obtained knowledge by solving the "Multicolored Lamp" task.
Interpreter
Interpreter is a behavioral design pattern that solves a frequently encountered but subject to change task. Also known as Little (Small) Language.
This pattern can be used to define the given language grammar representation, as well as to interpret the sentences of that language.
You can compare this pattern to how you put the frequently used actions into a shortened set of words, so that the "interpreter" could later turn this set into the more complex meaningful actions. In fact, every person is constantly an "interpreter". Do you want to conduct a life experiment? If someone from your family (a husband, wife, child) leaves the house, tell him a simple set of words: "a liter of milk, half white, 200 grams of cottage cheese". Basically, you didn't say anything special, just listed a set of products. But there is a great chance that the "interpreter" translates this into the command: "on your way back stop by the grocery store and buy the following ... and bring it home." The "Interpreter" pattern is designed to shorten often performed actions in a more concise description.
This pattern can be used in situations where:
1) Grammar is quite simple.
For complex grammars, the class hierarchy becomes too massive and unmanageable. In such cases, it's better to use the parser generators, since they can interpret expressions without constructing abstract syntax trees, which saves memory and perhaps time.
2) Efficiency is not the main criterion.
The most effective interpreters usually don't work directly with trees, but first translate them into another form. For example, a regular expression is often converted to a state machine. But even in this case, the translator itself can be implemented with the help of this Interpreter pattern.
Pros:
- Grammar becomes easily expanded and modified, the class implementations describing the nodes of the abstract syntax tree are similar (easily encoded);
- You can easily change the way expressions are evaluated.
Cons:
- Accompanying the grammar with a large number of rules is difficult.
A simple example is the interpreter for translating Roman numerals into familiar decimals:
class RomanNumeralInterpreter(object): def __init__(self): self.grammar = { 'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000 } def interpret(self, text): numbers = map(self.grammar.get, text) if None in numbers: raise ValueError('Error value: %s' % text) result = 0 temp = None while numbers: num = numbers.pop(0) if temp is None or temp >= num: result += num else: result += (num - temp * 2) temp = num return result
We can check this code with a couple of tests:
interp = RomanNumeralInterpreter() interp.interpret('MMMCMXCIX') == 3999 interp.interpret('MCMLXXXVIII') == 1988
You can try to solve the "Hacker Language" mission by using this pattern and the example above.
Conclusion
This was the final part about design patterns. We've reviewed 8 patterns out of 23 classic ones. In addition to the tasks specified in this series of articles, you can also try solving the following tasks, where the not covered patterns can help:
- "Capital City" (Singleton pattern);
- "Voice TV Control" (Iterator pattern).
The following resources were used in this article:
Welcome to CheckiO - games for coders where you can improve your codings skills.
The main idea behind these games is to give you the opportunity to learn by exchanging experience with the rest of the community. Every day we are trying to find interesting solutions for you to help you become a better coder.
Join the Game