본문 바로가기
Mastering Python Design Patterns

Other Structural Patterns

by 자동매매 2023. 3. 22.

8 Other Structural Patterns

Besides the patterns covered in previous chapters, there are other structural patterns we can cover: flyweight, model-view-controller* (MVC), and proxy.

What is the flyweight pattern? Object-oriented systems can face performance issues due to the overhead of object creation. Performance issues usually appear in embedded systems with limited resources, such as smartphones and tablets. They can also appear in large and complex systems where we need to create a very large number of objects (and possibly users) that need to coexist at the same time. The flyweight pattern teaches programmers how to minimize memory usage by sharing resources with similar objects as much as possible.

The MVC pattern is useful mainly in application development and helps developers improve the maintainability of their applications by avoiding mixing the business logic with the user interface.

In some applications, we want to execute one or more important actions before accessing an object, and this is where the proxy pattern comes in. An example is the accessing of sensitive information. Before allowing any user to access sensitive information, we want to make sure that the user has sufficient privileges. The important action is not necessarily related to security issues. Lazy initialization (j.mp/wikilazy) is another case; we want to

delay the creation of a computationally expensive object until the first time the user actually needs to use it. The idea of the proxy pattern is to help with performing such an action before accessing the actual object.

In this chapter, we will discuss:

  • The flyweight pattern
    • The MVC pattern
      • The proxy pattern

Other Structural Patterns Chapter 98*

The flyweight pattern

Whenever we create a new object, extra memory needs to be allocated. Although virtual memory provides us, theoretically, with unlimited memory, the reality is different. If all the physical memory of a system gets exhausted, it will start swapping pages with the secondary storage, usually a hard disk drive (HDD), which, in most cases, is unacceptable due to the performance differences between the main memory and HDD. Solid-state drives (SSDs) generally have better performance than HDDs, but not everybody is expected to use SSDs. So, SSDs are not going to totally replace HDDs anytime soon.

Apart from memory usage, performance is also a consideration. Graphics software, including computer games, should be able to render 3-D information (for example, a forest with thousands of trees, a village full of soldiers, or an urban area with a lot of cars) extremely quickly. If each object in a 3-D terrain is created individually and no data sharing is used, the performance will be prohibitive.

As software engineers, we should solve software problems by writing better software, instead of forcing the customer to buy extra or better hardware. The flyweight design pattern is a technique used to minimize memory usage and improve performance by introducing data sharing between similar objects (j.mp/wflyw). A flyweight is a shared

object that contains state-independent, immutable (also known as intrinsic) data. The state- dependent, mutable (also known as extrinsic) data should not be part of flyweight because this is information that cannot be shared, since it differs per object. If flyweight needs extrinsic data, it should be provided explicitly by the client code.

An example might help to clarify how the flyweight pattern can be practically used. Let's assume that we are creating a performance-critical game, for example, a first-person shooter (FPS). In FPS games, the players (soldiers) share some states, such as representation and behavior. In Counter-Strike, for instance, all soldiers on the same team (counter- terrorists versus terrorists) look the same (representation). In the same game, all soldiers (on both teams) have some common actions, such as jump, duck, and so forth (behavior). This means that we can create a flyweight that will contain all of the common data. Of course, the soldiers also have a lot of data that is different per soldier and will not be a part of the flyweight, such as weapons, health, location, and so on.

Real-world examples

Flyweight is an optimization design pattern, therefore, it is not easy to find a good noncomputing example of it. We can think of flyweight as caching in real life. For example, many bookstores have dedicated shelves with the newest and most popular publications. This is a cache. First, you can take a look at the dedicated shelves for the book you are looking for, and if you cannot find it, you can ask the bookseller to assist you.

The Exaile music player uses flyweight to reuse objects (in this case, music tracks) that are identified by the same URL. There's no point in creating a new object if it has the same URL as an existing object, so the same object is reused to save resources.

Peppy, a XEmacs-like editor implemented in Python, uses the flyweight pattern to store the state of a major mode status bar. That's because unless modified by the user, all status bars share the same properties.

Use cases

Flyweight is all about improving performance and memory usage. All embedded systems (phones, tablets, games consoles, microcontrollers, and so forth) and performance-critical applications (games, 3-D graphics processing, real-time systems, and so forth) can benefit from it.

The Gang of Four (GoF) book lists the following requirements that need to be satisfied to effectively use the flyweight pattern:

  • The application needs to use a large number of objects.
    • There are so many objects that it's too expensive to store/render them. Once the mutable state is removed (because if it is required, it should be passed explicitly to flyweight by the client code), many groups of distinct objects can be replaced by relatively few shared objects.
      • Object identity is not important for the application. We cannot rely on object identity because object sharing causes identity comparisons to fail (objects that appear different to the client code end up having the same identity).

Implementation

Let's see how we can implement the example mentioned previously for cars in an area. We will create a small car park to illustrate the idea, making sure that the whole output is readable in a single terminal page. However, no matter how large you make the car park, the memory allocation stays the same.

Before diving into the code, let's spend a moment noting the differences between the memoization and the flyweight pattern. Memoization is an optimization technique that uses a cache to avoid recomputing results that were already computed in an earlier execution step. Memoization does not focus on a specific programming paradigm such as object-oriented programming (OOP). In Python, memoization can be applied to both methods and simple functions. Flyweight is an OOP-specific optimization design pattern that focuses on sharing object data.

First, we need an Enum parameter that describes the three different types of car that are in the car park:

CarType = Enum('CarType', 'subcompact compact suv')

Then, we will define the class at the core of our implementation: Car. The pool variable is the object pool (in other words, our cache). Notice that pool is a class attribute (a variable shared by all instances).

Using the __new__() special method, which is called before __init__(), we are converting the Car class to a metaclass that supports self-references. This means that cls references the Car class. When the client code creates an instance of Car, they pass the type of the car as car_type. The type of the car is used to check if a car of the same type has already been created. If that's the case, the previously created object is returned; otherwise, the new car type is added to the pool and returned:

class Car:

pool = dict()

def __new__(cls, car_type):

obj = cls.pool.get(car_type, None) if not obj:

obj = object.__new__(cls)

cls.pool[car_type] = obj

obj.car_type = car_type

return obj

The render() method is what will be used to render a car on the screen. Notice how all the mutable information not known by flyweight needs to be explicitly passed by the client code. In this case, a random color and the coordinates of a location (of form x, y) are used for each car.

Also, note that to make render() more useful, it is necessary to ensure that no cars are rendered on top of each other. Consider this as an exercise. If you want to make rendering more fun, you can use a graphics toolkit such as Tkinter, Pygame, or Kivy.

The render() method is defined as follows:

def render(self, color, x, y):

type = self.car_type

msg = f'render a car of type {type} and color {color} at ({x}, {y})'

print(msg)

The main() function shows how we can use the flyweight pattern. The color of a car is a random value from a predefined list of colors. The coordinates use random values between 1 and 100. Although 18 cars are rendered, memory is allocated only for three. The last line of the output proves that when using flyweight, we cannot rely on object identity. The id() function returns the memory address of an object. This is not the default behavior in Python because by default, id() returns a unique ID (actually the memory address of an object as

an integer) for each object. In our case, even if two objects appear to be different, they actually have the same identity if they belong to the same flyweight family (in this case, the family is defined by car_type). Of course, different identity comparisons can still be

used for objects of different families, but that is possible only if the client knows the implementation details.

Our example main() function's code is as follows:

def main():

rnd = random.Random()

colors = 'white black silver gray red blue brown beige yellow green'.split()

min_point, max_point = 0, 100

car_counter = 0

for _ in range(10):

c1 = Car(CarType.subcompact) c1.render(random.choice(colors),

rnd.randint(min_point, max_point), rnd.randint(min_point, max_point)) car_counter += 1

for _ in range(3):

c2 = Car(CarType.compact) c2.render(random.choice(colors),

rnd.randint(min_point, max_point), rnd.randint(min_point, max_point)) car_counter += 1

for _ in range(5):

c3 = Car(CarType.suv) c3.render(random.choice(colors),

rnd.randint(min_point, max_point), rnd.randint(min_point, max_point)) car_counter += 1

print(f'cars rendered: {car_counter}')

print(f'cars actually created: {len(Car.pool)}')

c4 = Car(CarType.subcompact)

c5 = Car(CarType.subcompact)

c6 = Car(CarType.suv)

print(f'{id(c4)} == {id(c5)}? {id(c4) == id(c5)}') print(f'{id(c5)} == {id(c6)}? {id(c5) == id(c6)}')

Here is the full code listing (the flyweight.py file) to show you how the flyweight pattern is implemented and used:

  1. We need a couple of imports:

import random

from enum import Enum

  1. The Enum for the types of cars is shown here:

CarType = Enum('CarType', 'subcompact compact suv')

  1. Then we have the Car class, with its pool attribute and the __new__() and render() methods:

class Car:

pool = dict()

def __new__(cls, car_type):

obj = cls.pool.get(car_type, None) if not obj:

obj = object.__new__(cls)

cls.pool[car_type] = obj

obj.car_type = car_type

return obj

def render(self, color, x, y):

type = self.car_type

msg = f'render a car of type {type} and color {color} at ({x}, {y})'

print(msg)

  1. In the first part of the main function, we define some variables and render a set of cars of type subcompact:

def main():

rnd = random.Random()

colors = 'white black silver gray red blue brown beige yellow green'.split()

min_point, max_point = 0, 100

car_counter = 0

for _ in range(10):

c1 = Car(CarType.subcompact) c1.render(random.choice(colors),

rnd.randint(min_point, max_point), rnd.randint(min_point, max_point)) car_counter += 1

  1. The second part of the main function is as follows:

for _ in range(3):

c2 = Car(CarType.compact) c2.render(random.choice(colors),

rnd.randint(min_point, max_point), rnd.randint(min_point, max_point)) car_counter += 1

  1. The third part of the main function is as follows:

for _ in range(5): c3 = Car(CarType.suv) c3.render(random.choice(colors),

rnd.randint(min_point, max_point), rnd.randint(min_point, max_point)) car_counter += 1

print(f'cars rendered: {car_counter}')

print(f'cars actually created: {len(Car.pool)}')

  1. Finally, here is the fourth part of the main function:

c4 = Car(CarType.subcompact)

c5 = Car(CarType.subcompact)

c6 = Car(CarType.suv)

print(f'{id(c4)} == {id(c5)}? {id(c4) == id(c5)}') print(f'{id(c5)} == {id(c6)}? {id(c5) == id(c6)}')

  1. We do not forget our usual __name__ == '__main__' trick and good practice, as follows:

if __name__ == '__main__': main()

The execution of the python flyweight command shows the type, random color, and coordinates of the rendered objects, as well as the identity comparison results between flyweight objects of the same/different families:

Do not expect to see the same output since the colors and coordinates are random, and the object identities depend on the memory map.

The model-view-controller pattern

One of the design principles related to software engineering is the separation of concerns (SoC) principle. The idea behind the SoC principle is to split an application into distinct sections, where each section addresses a separate concern. Examples of such concerns are the layers used in a layered design (data access layer, business logic layer, presentation layer, and so forth). Using the SoC principle simplifies the development and maintenance of software applications.

The MVC pattern is nothing more than the SoC principle applied to OOP. The name of the pattern comes from the three main components used to split a software application: the model, the view, and the controller. MVC is considered an architectural pattern rather than a design pattern. The difference between an architectural and a design pattern is that the former has a broader scope than the latter. Nevertheless, MVC is too important to skip just for this reason. Even if we will never have to implement it from scratch, we need to be familiar with it because all common frameworks use MVC or a slightly different version of it (more on this later).

The model is the core component. It represents knowledge. It contains and manages the (business) logic, data, state, and rules of an application. The view is a visual representation of the model. Examples of views are a computer GUI, the text output of a computer terminal, a smartphone's application GUI, a PDF document, a pie chart, a bar chart, and so forth. The view only displays the data; it doesn't handle it. The controller is the link/glue between the model and view. All communication between the model and the view happens through a controller.

A typical use of an application that uses MVC, after the initial screen is rendered to the user is as follows:

  1. The user triggers a view by clicking (typing, touching, and so on) a button
  2. The view informs the controller of the user's action
  3. The controller processes user input and interacts with the model
  4. The model performs all the necessary validation and state changes and informs the controller about what should be done
  5. The controller instructs the view to update and display the output appropriately, following the instructions that are given by the model

You might be wondering, why the controller part is necessary? Can't we just skip it? We could, but then we would lose a big benefit that MVC provides: the ability to use more than one view (even at the same time, if that's what we want) without modifying the model. To achieve decoupling between the model and its representation, every view typically needs its own controller. If the model communicated directly with a specific view, we wouldn't be able to use multiple views (or at least, not in a clean and modular way).

Real-world examples

MVC is the SoC principle applied to OOP. The SoC principle is used a lot in real life. For example, if you build a new house, you usually assign different professionals to: 1) install the plumbing and electricity; and, 2) paint the house.

Another example is a restaurant. In a restaurant, the waiters receive orders and serve dishes to the customers, but the meals are cooked by the chefs.

In web development, several frameworks use the MVC idea:

  • The Web2py Framework (j.mp/webtopy) is a lightweight Python Framework that embraces the MVC pattern. If you have never tried Web2py, I encourage you to do it since it is extremely simple to install. There are many examples that demonstrate how MVC can be used in Web2py on the project's web page.
    • Django is also an MVC Framework, although it uses different naming conventions. The controller is called view, and the view is called template. Django uses the name Model-Template-View (MTV). According to the designers of Django, the view describes what data is seen by the user, and therefore, it uses the name view as the Python callback function for a particular URL. The term template in Django is used to separate content from representation. It describes how the data is seen by the user, not which data is seen.

Use cases

MVC is a very generic and useful design pattern. In fact, all popular web frameworks (Django, Rails, and Symfony or Yii) and application frameworks (iPhone SDK, Android, and QT) make use of MVC or a variation of it—model-view-adapter (MVA), model-view- presenter (MVP), and so forth. However, even if we don't use any of these frameworks, it makes sense to implement the pattern on our own because of the benefits it provides, which are as follows:

  • The separation between the view and model allows graphics designers to focus on the UI part and programmers to focus on development, without interfering with each other.
    • Because of the loose coupling between the view and model, each part can be modified/extended without affecting the other. For example, adding a new view is trivial. Just implement a new controller for it.
      • Maintaining each part is easier because the responsibilities are clear.

When implementing MVC from scratch, be sure that you create smart models, thin controllers, and dumb views.

A model is considered smart because it does the following:

  • Contains all the validation/business rules/logic
    • Handles the state of the application
      • Has access to application data (database, cloud, and so on)
        • Does not depend on the UI

A controller is considered thin because it does the following:

  • Updates the model when the user interacts with the view
    • Updates the view when the model changes
      • Processes the data before delivering it to the model/view, if necessary
        • Does not display the data
          • Does not access the application data directly
            • Does not contain validation/business rules/logic

A view is considered dumb because it does the following:

  • Displays the data
    • Allows the user to interact with it
      • Does only minimal processing, usually provided by a template language (for example, using simple variables and loop controls)
        • Does not store any data
          • Does not access the application data directly
            • Does not contain validation/business rules/logic

If you are implementing MVC from scratch and want to find out if you did it right, you can try answering some key questions:

  • If your application has a GUI, is it skinnable? How easily can you change the skin/look and feel of it? Can you give the user the ability to change the skin of your application during runtime? If this is not simple, it means that something is going wrong with your MVC implementation.
    • If your application has no GUI (for instance, if it's a terminal application), how hard is it to add GUI support? Or, if adding a GUI is irrelevant, is it easy to add views to display the results in a chart (pie chart, bar chart, and so on) or a document (PDF, spreadsheet, and so on)? If these changes are not trivial (a matter of creating a new controller with a view attached to it, without modifying the model), MVC is not implemented properly.

If you make sure that these conditions are satisfied, your application will be more flexible and maintainable compared to an application that does not use MVC.

Implementation

I could use any of the common frameworks to demonstrate how to use MVC, but I feel that the picture will be incomplete. So, I decided to show you how to implement MVC from scratch, using a very simple example: a quote printer. The idea is extremely simple. The user enters a number and sees the quote related to that number. The quotes are stored in a quotes tuple. This is the data that normally exists in a database, file, and so on, and only the model has direct access to it.

Let's consider the example in the following code:

quotes =

(

'A man is not complete until he is married. Then he is finished.',

'As I said before, I never repeat myself.',

'Behind a successful man is an exhausted woman.', 'Black holes really suck...',

'Facts are stubborn things.'

)

The model is minimalistic; it only has a get_quote() method that returns the quote (string) of the quotes tuple based on its index n. Note that n can be less than or equal to zero, due to the way indexing works in Python. Improving this behavior is given as an exercise for you at the end of this section:

class QuoteModel:

def get_quote(self, n): try:

value = quotes[n]

except IndexError as err: value = 'Not found!' return value

The view has three methods: show(), which is used to print a quote (or the message Not found!) on the screen, error(), which is used to print an error message on the screen, and select_quote(), which reads the user's selection. This can be seen in the following code:

class QuoteTerminalView:

def show(self, quote):

print(f'And the quote is: "{quote}"')

def error(self, msg):

print(f'Error: {msg}')

def select_quote(self):

return input('Which quote number would you like to see? ')

The controller does the coordination. The __init__() method initializes the model and view. The run() method validates the quoted index given by the user, gets the quote from the model, and passes it back to the view to be displayed as shown in the following code:

class QuoteTerminalController:

def __init__(self):

self.model = QuoteModel()

self.view = QuoteTerminalView()

def run(self):

valid_input = False

while not valid_input: try:

n = self.view.select_quote() n = int(n)

valid_input = True

except ValueError as err:

self.view.error(f"Incorrect index '{n}'")

quote = self.model.get_quote(n)

self.view.show(quote)

Last but not least, the main() function initializes and fires the controller as shown in the following code:

def main():

controller = QuoteTerminalController() while True:

controller.run()

The following is the full code of the example (the mvc.py file):

We start by defining a variable for the list of quotes as shown in the following code snippet:

quotes =

(

'A man is not complete until he is married. Then he is finished.',

'As I said before, I never repeat myself.',

'Behind a successful man is an exhausted woman.',

'Black holes really suck...',

'Facts are stubborn things.'

)

Here is the code for the model class, QuoteModel:

class QuoteModel:

def get_quote(self, n): try:

value = quotes[n]

except IndexError as err: value = 'Not found!' return value

  • Here is the code for the view class, QuoteTerminalView:

class QuoteTerminalView:

def show(self, quote):

print(f'And the quote is: "{quote}"')

def error(self, msg):

print(f'Error: {msg}')

def select_quote(self):

return input('Which quote number would you like to see? ')

Here is the code for the controller class, QuoteTerminalController:

class QuoteTerminalController:

def __init__(self):

self.model = QuoteModel()

self.view = QuoteTerminalView()

def run(self):

valid_input = False

while not valid_input:

try:

n = self.view.select_quote()

n = int(n)

valid_input = True

except ValueError as err:

self.view.error(f"Incorrect index '{n}'") quote = self.model.get_quote(n) self.view.show(quote)

  • Here is the end of our example code with the main() function:

def main():

controller = QuoteTerminalController() while True:

controller.run()

if __name__ == '__main__': main()

A sample execution of the python mvc.py command shows how the program prints quotes to the user:

The proxy pattern

The proxy design pattern gets its name from the proxy (also known as surrogate) object used to perform an important action before accessing the actual object. There are four different well-known proxy types (j.mp/proxypat). They are as follows:

  • A remote proxy, which acts as the local representation of an object that really exists in a different address space (for example, a network server).
    • A virtual proxy, which uses lazy initialization to defer the creation of a computationally expensive object until the moment it is actually needed.
      • A protection/protective proxy, which controls access to a sensitive object.
        • A smart (reference) proxy, which performs extra actions when an object is accessed. Examples of such actions are reference counting and thread-safety checks.

I find virtual proxies very useful so let's see an example of how we can implement them in Python right now. In the Implementation section, you will learn how to create protective proxies.

There are many ways to create a virtual proxy in Python, but I always like focusing on the idiomatic/Pythonic implementations. The code shown here is based on the great answer by Cyclone, a user of the site stackoverflow.com (j.mp/solazyinit). To avoid confusion, I

should clarify that in this section, the terms property, variable, and attribute are used interchangeably. First, we create a LazyProperty class that can be used as a decorator.

When it decorates a property, LazyProperty loads the property lazily (on the first use), instead of instantly. The __init__() method creates two variables that are used as aliases

to the method that initializes a property. The method variable is an alias to the actual method, and the method_name variable is an alias to the method's name. To get a better understanding of how the two aliases are used, print their value to the output (uncomment the two commented lines in the following code):

class LazyProperty:

def __init__(self, method):

self.method = method

self.method_name = method.__name__

  • print(f"function overriden: {self.fget}")
  • print(f"function's name: {self.func_name}")

The LazyProperty class is actually a descriptor (j.mp/pydesc). Descriptors are the recommended mechanisms to use in Python to override the default behavior of its attribute access methods: __get__(), __set__(), and __delete__(). The LazyProperty class

overrides only __set__() because that is the only access method it needs to override. In other words, we don't have to override all access methods. The __get__() method

accesses the value of the property the underlying method wants to assign, and uses

setattr() to do the assignment manually. What __get()__ actually does is very neat: it replaces the method with the value! This means that not only is the property lazily loaded, it can also be set only once. We will see what this means in a moment. Again, uncomment the commented line in the following code to get some extra information:

def __get__(self, obj, cls):

if not obj:

return None

value = self.method(obj)

  • print(f'value {value}')

setattr(obj, self.method_name, value) return value

The Test class shows how we can use the LazyProperty class. There are three attributes: x, y, and _resource. We want the _resource variable to be loaded lazily; thus, we initialize it to None as shown in the following code:

class Test:

def __init__(self):

self.x = 'foo'

self.y = 'bar'

self._resource = None

The resource() method is decorated with the LazyProperty class. For demonstration purposes, the LazyProperty class initializes the _resource attribute as a tuple, as shown in the following code. Normally, this would be a slow/expensive initialization (database, graphics, and so on):

@LazyProperty

def resource(self):

print(f'initializing self._resource which is: {self._resource}') self._resource = tuple(range(5)) # expensive

return self._resource

The main() function, as follows, shows how lazy initialization behaves:

def main():

t = Test() print(t.x) print(t.y)

  • do more work...

print(t.resource) print(t.resource)

Notice how overriding the __get()__ access method makes it possible to treat the resource() method as a simple attribute (we can use t.resource instead of t.resource()).

In the execution output of this example (the lazy.py file), we can see that:

  • The _resource variable is indeed initialized not by the time the t instance is created, but the first time that we use t.resource.
    • The second time t.resource is used, the variable is not initialized again. That's why the initialization string initializing self._resource is shown only once.

Here is the output we get when executing the python lazy.py command:

There are two basic, different kinds of lazy initialization in OOP. They are as follows:

  • At the instance level: This means that an object's property is initialized lazily, but the property has an object scope. Each instance (object) of the same class has its own (different) copy of the property.
    • At the class or module level: In this case, we do not want a different copy per instance, but all the instances share the same property, which is lazily initialized. This case is not covered in this chapter. If you find it interesting, consider it as an exercise.

Real-world examples

Chip (also known as Chip and PIN) cards (j.mp/wichpin) offer a good example of how a protective proxy is used in real life. The debit/credit card contains a chip that first needs to be read by the ATM or card reader. After the chip is verified, a password (PIN) is required to complete the transaction. This means that you cannot make any transactions without physically presenting the card and knowing the PIN.

A bank check that is used instead of cash to make purchases and deals is an example of a remote proxy. The check gives access to a bank account.

In software, the weakref module of Python contains a proxy() method that accepts an

input object and returns a smart proxy to it. Weak references are the recommended way to add reference-counting support to an object.

Use cases

Since there are at least four common proxy types, the proxy design pattern has many use cases, as follows:

  • It is used when creating a distributed system using either a private network or the cloud. In a distributed system, some objects exist in the local memory and some objects exist in the memory of remote computers. If we don't want the client code to be aware of such differences, we can create a remote proxy that hides/encapsulates them, making the distributed nature of the application transparent.
    • It is used when our application is suffering from performance issues due to the early creation of expensive objects. Introducing lazy initialization using a virtual proxy to create the objects only at the moment they are actually required can give us significant performance improvements.
  • It is used to check if a user has sufficient privileges to access a piece of information. If our application handles sensitive information (for example, medical data), we want to make sure that the user trying to access/modify it is allowed to do so. A protection/protective proxy can handle all security-related actions.
    • It is used when our application (or library, toolkit, framework, and so forth) uses multiple threads and we want to move the burden of thread safety from the client code to the application. In this case, we can create a smart proxy to hide the thread-safety complexities from the client.
      • An object-relational mapping (ORM) API is also an example of how to use a remote proxy. Many popular web frameworks, including Django, use an ORM to provide OOP-like access to a relational database. An ORM acts as a proxy to a relational database that can be actually located anywhere, either at a local or remote server.

Implementation

To demonstrate the proxy pattern, we will implement a simple protection proxy to view and add users. The service provides two options:

  • Viewing the list of users: This operation does not require special privileges
    • Adding a new user: This operation requires the client to provide a special secret message

The SensitiveInfo class contains the information that we want to protect. The users variable is the list of existing users. The read() method prints the list of the users. The add() method adds a new user to the list.

Let's consider the following code:

class SensitiveInfo:

def __init__(self):

self.users = ['nick', 'tom', 'ben', 'mike']

def read(self):

nb = len(self.users)

print(f"There are {nb} users: {' '.join(self.users)}") def add(self, user):

self.users.append(user)

print(f'Added user {user}')

The Info class is a protection proxy of SensitiveInfo. The secret variable is the message required to be known/provided by the client code to add a new user. Note that this is just an example. In reality, you should never do the following:

  • Store passwords in the source code
    • Store passwords in a clear-text form
      • Use a weak (for example, MD5) or custom form of encryption

In the Info class, as we can see next, the read() method is a wrapper to SensitiveInfo.read() and the add() method ensures that a new user can be added only if the client code knows the secret message:

class Info:

'''protection proxy to SensitiveInfo'''

def __init__(self):

self.protected = SensitiveInfo()

self.secret = '0xdeadbeef'

def read(self):

self.protected.read()

def add(self, user):

sec = input('what is the secret? ')

self.protected.add(user) if sec == self.secret else print("That's wrong!")

The main() function shows how the proxy pattern can be used by the client code. The client code creates an instance of the Info class and uses the displayed menu to read the list, add a new user, or exit the application. Let's consider the following code:

def main():

info = Info()

while True:

print('1. read list |==| 2. add user |==| 3. quit') key = input('choose option: ')

if key == '1':

info.read()

elif key == '2':

name = input('choose username: ') info.add(name)

elif key == '3':

exit()

else:

print(f'unknown option: {key}')

Let's recapitulate the full code of the proxy.py file:

  1. First, we define the LazyProperty class:

class LazyProperty:

def __init__(self, method):

self.method = method

self.method_name = method.__name__

  • print(f"function overriden: {self.fget}")
  • print(f"function's name: {self.func_name}")

def __get__(self, obj, cls):

if not obj:

return None

value = self.method(obj)

  • print(f'value {value}')

setattr(obj, self.method_name, value)

return value

  1. Then, we have the code for the Test class, as follows:

class Test:

def __init__(self):

self.x = 'foo'

self.y = 'bar'

self._resource = None

@LazyProperty

def resource(self):

print(f'initializing self._resource which is: {self._resource}')

self._resource = tuple(range(5)) # expensive return self._resource

  1. Finally, here is the main() function and the end of the code:

def main():

t = Test() print(t.x) print(t.y)

  • do more work...

print(t.resource) print(t.resource)

if __name__ == '__main__': main()

  1. We can see here a sample output of the program when executing the python proxy.py command:

Have you already spotted flaws or missing features that can improve our proxy example? I have a few suggestions. They are as follows:

This example has a very big security flaw. Nothing prevents the client code from bypassing the security of the application by creating an instance of

SensitiveInfo directly. Improve the example to prevent this situation. One

way is to use the abc module to forbid direct instantiation of SensitiveInfo.

What are other code changes required in this case?

A basic security rule is that we should never store clear-text passwords. Storing a password safely is not very hard as long as we know which libraries to use (j.mp/hashsec). If you have an interest in security, try to implement a secure way to store the secret message externally (for example, in a file or database).

The application only supports adding new users, but what about removing an existing user? Add a remove() method.

[ 98 ] )
Other Structural Patterns Chapter 99*

Summary

In this chapter, we covered three other structural design patterns: flyweight, MVC, and proxy.

We can use flyweight when we want to improve the memory usage and possibly the performance of our application. This is quite important in all systems with limited resources (think of embedded systems), and systems that focus on performance, such as graphics software and electronic games.

In general, we use flyweight when an application needs to create a large number of computationally expensive objects that share many properties. The important point is to separate the immutable (shared) properties from the mutable. We implemented a tree renderer that supports three different tree families. By providing the mutable age and x, y properties explicitly to the render() method, we managed to create only three different objects instead of eighteen. Although that might not seem like a big win, imagine if the trees were 2,000 instead of 18.

MVC is a very important design pattern used to structure an application in three parts: the model, the view, and the controller. Each part has clear roles and responsibilities. The model has access to the data and manages the state of the application. The view is a representation of the model. The view does not need to be graphical; textual output is also considered a totally fine view. The controller is the link between the model and the view. Proper use of MVC guarantees that we end up with an application that is easy to maintain and extend.

We discussed several use cases of the proxy pattern, including performance, security, and how to offer simple APIs to users. In the first code example, we created a virtual proxy (using decorators and descriptors), allowing us to initialize object properties in a lazy manner. In the second code example, we implemented a protection proxy to handle users. This example can be improved in many ways, especially regarding its security flaws and the fact that the list of users is not persistent.

In the next chapter, we will start exploring behavioral design patterns. Behavioral patterns cope with object interconnection and algorithms. The first behavioral pattern that will be covered is a chain of responsibility.

[ 99 ])

'Mastering Python Design Patterns' 카테고리의 다른 글

The Command Pattern  (0) 2023.03.22
The Chain of Responsibility  (0) 2023.03.22
The Facade Pattern  (0) 2023.03.22
The Bridge Pattern  (0) 2023.03.22
The Decorator Pattern  (0) 2023.03.22

댓글