본문 바로가기
Mastering Python Design Patterns

The Facade Pattern

by 자동매매 2023. 3. 22.

7 The Facade Pattern

In the previous chapter, we covered a third structural pattern, the bridge pattern, which helps to define an abstraction and its implementation in a decoupled way, so that both can vary independently.

As systems evolve, they can get very complex. It is not unusual to end up with a very large (and sometimes confusing) collection of classes and interactions. In many cases, we don't want to expose this complexity to the client. This is where our next structural pattern comes to the rescue: façade.

The façade design pattern helps us to hide the internal complexity of our systems and expose only what is necessary to the client through a simplified interface. In essence, façade is an abstraction layer implemented over an existing complex system.

Let's take the example of the computer to illustrate things. A computer is a complex machine that depends on several parts to be fully functional. To keep things simple, the word "computer", in this case, refers to an IBM derivative that uses a von Neumann architecture. Booting a computer is a particularly complex procedure. The CPU, main memory, and hard disk need to be up and running, the boot loader must be loaded from the hard disk to the main memory, the CPU must boot the operating system kernel, and so forth. Instead of exposing all this complexity to the client, we create a façade that encapsulates the whole procedure, making sure that all steps are executed in the right order.

In terms of object design and programming, we should have several classes, but only the Computer class needs to be exposed to the client code. The client will only have to execute the start() method of the Computer class, for example, and all the other complex parts

are taken care of by the façade Computer class.

The Facade Pattern Chapter 7

In this chapter, we will discuss:

  • Real-world examples
    • Use cases
      • Implementation

Real-world examples

The façade pattern is quite common in life. When you call a bank or a company, you are usually first connected to the customer service department. The customer service employee acts as a façade between you and the actual department (billing, technical support, general assistance, and so on), and the employee that will help you with your specific problem.

As another example, a key used to turn on a car or motorcycle can also be considered a façade. It is a simple way of activating a system that is very complex internally. And, of course, the same is true for other complex electronic devices that we can activate with a single button, such as computers.

In software, the django-oscar-datacash module is a Django third-party module that integrates with the DataCash payment gateway. The module has a gateway class that provides fine-grained access to the various DataCash APIs. On top of that, it also offers a façade class that provides a less granular API (for those who don't want to mess with the details), and the ability to save transactions for auditing purposes.

Use cases

The most usual reason to use the façade pattern is for providing a single, simple entry point to a complex system. By introducing façade, the client code can use a system by simply calling a single method/function. At the same time, the internal system does not lose any functionality, it just encapsulates it.

Not exposing the internal functionality of a system to the client code gives us an extra benefit: we can introduce changes to the system, but the client code remains unaware of and unaffected by the changes. No modifications are required to the client code.

Façade is also useful if you have more than one layer in your system. You can introduce one façade entry point per layer, and let all layers communicate with each other through their façades. That promotes loose coupling and keeps the layers as independent as possible.

Implementation

Assume that we want to create an operating system using a multi-server approach, similar to how it is done in MINIX 3 (j.mp/minix3) or GNU Hurd (j.mp/gnuhurd). A multiserver operating system has a minimal kernel, called the microkernel, which runs in privileged mode. All the other services of the system are following a server architecture (driver server, process server, file server, and so forth). Each server belongs to a different memory address space and runs on top of the microkernel in user mode. The pros of this approach are that the operating system can become more fault-tolerant, reliable, and secure. For example, since all drivers are running in user mode on a driver server, a bug in a driver cannot crash the whole system, nor can it affect the other servers. The cons of this approach are the performance overhead and the complexity of system programming, because the communication between a server and the microkernel, as well as between the independent servers, happens using message passing. Message passing is more complex than the shared memory model used in monolithic kernels such as Linux (j.mp/helenosm).

We begin with a Server interface. An Enum parameter describes the different possible

states of a server. We use the ABC module to forbid direct instantiation of the Server interface and make the fundamental boot() and kill() methods mandatory, assuming

that different actions are needed to be taken for booting, killing, and restarting each server. If you have not used the ABC module before, note the following important things:

  • We need to subclass ABCMeta using the metaclass keyword.
    • We use the @abstractmethod decorator for stating which methods should be implemented (mandatory) by all subclasses of server.

Try removing the boot() or kill() method of a subclass and see what happens. Do the same after removing the @abstractmethod decorator also. Do things work as you expected?

Let's consider the following code:

State = Enum('State', 'new running sleeping restart zombie') class Server(metaclass=ABCMeta):

@abstractmethod

def __init__(self):

pass

def __str__(self):

return self.name

@abstractmethod

def boot(self):

pass

@abstractmethod

def kill(self, restart=True): pass

A modular operating system can have a great number of interesting servers: a file server, a process server, an authentication server, a network server, a graphical/window server, and so forth. The following example includes two stub servers: the FileServer and the ProcessServer. Apart from the methods required to be implemented by the Server

interface, each server can have its own specific methods. For instance, the FileServer has

a create_file() method for creating files, and the ProcessServer has a create_process() method for creating processes.

The FileServer class is as follows:

class FileServer(Server):

def __init__(self):

'''actions required for initializing the file server''' self.name = 'FileServer'

self.state = State.new

def boot(self):

print(f'booting the {self}')

'''actions required for booting the file server''' self.state = State.running

def kill(self, restart=True):

print(f'Killing {self}')

'''actions required for killing the file server'''

self.state = State.restart if restart else State.zombie

def create_file(self, user, name, permissions):

'''check validity of permissions, user rights, etc.'''

print(f"trying to create the file '{name}' for user '{user}' with permissions {permissions}")

The ProcessServer class is as follows:

class ProcessServer(Server):

def __init__(self):

'''actions required for initializing the process server''' self.name = 'ProcessServer'

self.state = State.new

def boot(self):

print(f'booting the {self}')

'''actions required for booting the process server''' self.state = State.running

def kill(self, restart=True):

print(f'Killing {self}')

'''actions required for killing the process server'''

self.state = State.restart if restart else State.zombie

def create_process(self, user, name):

'''check user rights, generate PID, etc.'''

print(f"trying to create the process '{name}' for user '{user}'")

The OperatingSystem class is a façade. In its __init__(), all the necessary server instances are created. The start() method, used by the client code, is the entry point to the system. More wrapper methods can be added, if necessary, as access points to the services of the servers, such as the wrappers, create_file() and create_process().

From the client's point of view, all those services are provided by the OperatingSystem class. The client should not be confused by unnecessary details such as the existence of servers and the responsibility of each server.

The code for the OperatingSystem class is as follows:

class OperatingSystem:

'''The Facade'''

def __init__(self):

self.fs = FileServer()

self.ps = ProcessServer()

def start(self):

[i.boot() for i in (self.fs, self.ps)]

def create_file(self, user, name, permissions):

return self.fs.create_file(user, name, permissions)

def create_process(self, user, name):

return self.ps.create_process(user, name)

As you are going to see in a minute, when we present a summary of the example, there are many dummy classes and servers. They are there to give you an idea about the required abstractions (User, Process, File, and so forth) and servers (WindowServer,

NetworkServer, and so forth) for making the system functional. A recommended exercise

is to implement at least one service of the system (for example, file creation). Feel free to change the interface and the signature of the methods to fit your needs. Make sure that after your changes, the client code does not need to know anything other than the façade OperatingSystem class.

We are going to recapitulate the details of our implementation example; the full code is in the facade.py file:

  1. We start with the imports we need:

from enum import Enum

from abc import ABCMeta, abstractmethod

  1. We define the State constant using Enum, as shown earlier.
  2. We then add the User, Process, and File classes, which do nothing in this minimal but functional example:

class User: pass

class Process: pass

class File: pass

  1. We define the base Server class, as shown earlier.
  2. We then define the FileServer class and the ProcessServer class, which are both subclasses of Server.
  3. We add two other dummy classes, WindowServer and NetworkServer:

class WindowServer: pass

class NetworkServer: pass

  1. Then we define our façade class, OperatingSystem, as shown earlier.
  2. Finally, here is the main part of the code, where we use the façade we have defined:

def main():

os = OperatingSystem()

os.start()

os.create_file('foo', 'hello', '-rw-r-r') os.create_process('bar', 'ls /tmp')

if __name__ == '__main__': main()

As you can see, executing the python facade.py command shows the starting message of our two stub servers:

booting the FileServer

booting the ProcessServer

trying to create the file 'hello' for user 'foo' with permissions -rw-r-r trying to create the process 'ls /tmp' for user 'bar'

The façade OperatingSystem class does a good job. The client code can create files and processes without needing to know internal details about the operating system, such as the existence of multiple servers. To be precise, the client code can call the methods for creating files and processes, but they are currently dummy. As an interesting exercise, you can implement one of the two methods, or even both.

Summary

In this chapter, we have learned how to use the façade pattern. This pattern is ideal for providing a simple interface to client code that wants to use a complex system but does not need to be aware of the system's complexity. A computer is a façade since all we need to do to use it is press a single button for turning it on. All the rest of the hardware complexity is handled transparently by the BIOS, the boot loader, and the other components of the system software. There are more real-life examples of façade, such as when we are connected to the customer service department of a bank or a company, and the keys that we use to turn a vehicle on.

We discussed a Django third-party module that uses Façade: django-oscar-datacash. It uses the façade pattern to provide a simple DataCash API and the ability to save transactions.

We covered the basic use cases of façade and ended the chapter with an implementation of the interface used by a multiserver operating system. A façade is an elegant way of hiding the complexity of a system because, in most cases, the client code should not be aware of such details.

In the next chapter, we will cover other structural design patterns.

[ 75 ] )

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

The Chain of Responsibility  (0) 2023.03.22
Other Structural Patterns  (0) 2023.03.22
The Bridge Pattern  (0) 2023.03.22
The Decorator Pattern  (0) 2023.03.22
The Adapter Pattern  (0) 2023.03.22

댓글