Saturday, October 24, 2020

3.4 Architecture and Design Patterns

 As you have learned so far, modularity and reusability of the code that makes your program running is a priority of good software design and architecture. It is difficult to discuss software design without talking about OOP, a concept of binding and defining the behavior of programming objects.

You already saw that classes define the blueprint of how an object looks and behaves and that you can create class hierarchies in which the child class inherits the parent default behavior and can also enhance its role with its own implementations.

The concepts that define what OOP enables:

  • Abstraction

  • Encapsulation

  • Inheritance

  • Polymorphism

The ambition of an abstraction is to hide the logic implementation behind an interface. Abstractions offer a higher level of semantic contract between clients and the class implementations. When defining an abstraction, your objective is to expose a way of accessing the data of the object without knowing the details of the implementation. Abstract classes are usually not instantiated; they require subclasses that provide the behavior of the abstract methods. A subclass cannot be instantiated until it provides some implementation of the methods that are defined in the abstract class from which it is derived. It can also be said that the methods need to be overridden.

In statically typed languages, abstract classes and interfaces provide the explicit means of defining an abstraction. But in dynamically typed languages there are usually no special language constructs for abstractions. The usage of duck typing gives you the ability to achieve almost the same effect, except that the contract between the client and a class is not too formal. While duck typing has the advantage of being very flexible, it sometimes misses the conventional rules that statically typed languages introduce. Python ABC, a Python library, brings your code a step closer to the discipline of statically typed languages and their definitions of abstract classes and interfaces. Abstract methods need to be defined by the classes that inherit them. Observe the following example of using Python ABC.

In OOP, different objects interact with each other in their runtime. One object can access the data and methods of another object, no matter if the type of the object is the same or not. Many times, you want some of the data and methods to stay private to the object so that the object can use them internally, but other objects cannot access them. Encapsulation in OOP conceals the internal state and the implementation of an object from other objects. It can be used for restricting what can be accessed on an object. You can define that part of data that can be accessed only through designated methods and not directly; this is known as data hiding. In Java, C#, and similar statically typed, compiled languages, you will find that there is an option to explicitly define variables and methods that other objects cannot access, using the private keyword or protected keyword, which also restricts the child classes from accessing it.

In Python, encapsulation with hiding data from others is not so explicitly defined and can be interpreted rather as a convention. It does not have an option to strictly define data being private or protected. However, in Python, you would use the notation of prefixing the name with an underscore (or double underscore) to mark something as nonpublic data. When using the double underscore, name mangling occurs; this means that a variable name, prefixed with two underscores, is in a runtime that is concatenated with the class name. If you have a __auditLog() method in a device class, which is prefixed with a double underscore, the name of the method becomes _Device__auditLog(). This is helpful to prevent accidents, where subclasses override methods, and break the internal method calls on a parent class. Still, nothing prevents you from accessing the variable or method, even though, by convention, it is considered private.

The following code calls the private auditLog() method for every call of the action() method.

During these lessons, you have already encountered examples of class inheritance and the ability of building new classes on top of existing ones. Polymorphism, in OOP, goes hand in hand with class hierarchy. When a parent class defines a method that needs to be implemented by the child class, this method can be considered as polymorphic, because the implementation would have its own way of presenting a solution that the higher-level class proposed. Polymorphism can be found in any setup of an object that can have multiple forms. When a variable or a method accepts more than one type of value or parameter, it is considered to be polymorphic as well.

Designing object-oriented applications is not easy and requires a solid understanding of code constructs and experience in developing reusable and readable code. Your design should be definite for the problem you are trying to solve with the application, but at the same time generic enough so that it can be extended in the future without major problems.

It is important to understand more on how to define class interfaces, hierarchies, and relationships between different modules and classes, and also decide which programming languages to use—whether they support libraries that you will need, which database to use, how to use it, and how should all pieces of this puzzle communicate together cohesively. These are questions that fall into the architecture and design patterns paradigm, and you should ask them before you start writing any code.

Unified Modeling Language

Codebases that you encounter in the real world are written in a specific programming language. That said, there are not many developers that are proficient in every single programming language, so how can you find the right communication layer? When you are talking about software design, it is vital that you have a common language with all stakeholders and developers on a project. Capturing the intent of software design, no matter the implementation technology, is the goal of having a unified language that is simple enough for everybody to understand.

The Unified Modeling Language (UML) was created because programming languages, or even pseudocodes, are usually not at a high level of abstraction. UML helps developers to create a graphical notation of the programs that are being built. They are especially useful for describing, or rather sketching, code written in object-oriented style.

As an example, look at the following UML class diagram.

The UML can sketch your program before you start writing code for it. It can define many details of a class and the connection between other classes. In this example, the Router object would inherit all the fields and methods from the Device object. Class inheritance is shown with a solid line and an arrow at the end.

You can use UML as part of the documentation for a program, or use it for reverse-engineering an existing application, to get a better picture of how the system works.

Architectural Patterns

The patterns that you can put into the group of architectural patterns are discussed at a higher level than the design patterns and are used to design large-scale components and structures of a system. The architecture of an application is the overall organization of the system, and it has a broader scope.

Architecture can be referred to as an abstraction of the entire system, with a focus on certain details of the implementation. Different parts of an application should communicate over their public interfaces, with their implementation being hidden behind them. Architecture is concerned with the API or public side of the system that carries the communication between components and is not concerned with implementation details.

Every system has some sort of components and relationships between them. Even a system with a single component can be treated as an example of an architecture, although it might be too simple for anyone to consider it as an architecture to go with. Architectures are composed to solve a specific problem. Some compositions happen to be more useful than others, so they become documented as architecture patterns that people can refer to.

Architecture is composed from multiple structures that include software components and relations between them. A software architecture that is documented can complement communication and understanding between stakeholders, because the abstraction of some parts allows nontechnical people to understand the requirements and express their concerns. It enables prediction of how well the system can perform and helps with onboarding new engineers on the project.

The desired attributes of a system need to be recognized and considered when designing architecture of a system. If your system needs to be highly secure, then you will have to decide which elements of the system are critical and how will you limit the communication toward them. If higher traffic is expected in peaks, then you need to take care of the performance of different elements and how to allocate resources. When you need your application to be alive constantly, then you will think in terms of high availability and how to respond to a system fault. These kinds of attributes lead the decision on the type of software architecture to use.

A decision on software architecture can be made while studying these characteristics of a system:

  • Performance

  • Availability

  • Modifiability

  • Testability

  • Usability

  • Security

What a system can do for its customers and owners is defined by the functionality requirements, but they do not necessarily determine which architecture is suitable for a specific use case. As an example, say that a user wants to list data that concerns them, with enough urgency that the performance requirements could mean designing a highly responsive application for when this kind of event occurs. As another example, if availability concerns your users, your architecture needs to consider how a failover can be performed when data cannot be accessed. After an application is released, changes usually soon follow.

Developers constantly strive to enhance the system, because of new technology, requirements on the market, or security threats. To enable developers to push out changes, you should first make sure that the changes will not break existing functionalities, so testability of a system is a top concern for often-changing code. Quality of a system from the end user perspective involves ease of use, efficiency, how fast one can learn the features it is offering, and how much the system delights them to increase return usage. The tasks within usability help to define such requirements.

Note

Failover is a tactic of switching to a redundant computer server or system when a failure of the previously active application occurs.

It is easier to poorly design a system than it is to do it right. Many good design decisions that are proven to work in practice are already discovered and made available to you for reuse.

Some of the commonly known software architecture patterns:

  • Layered or multitier architecture pattern

  • Event-driven architecture pattern

  • Microservices architecture pattern

  • Model-View-Controller (MVC) architecture pattern

  • Space-based architecture

It is time to investigate one of the more common architecture patterns.

Layered Architecture Pattern

The layered architecture pattern, also known as the multitier or n-tier architecture pattern, is one of the most common general purpose software architecture patterns. This design pattern closely relates to the organizational structures of most companies (Conway's law), so it is an instinctive choice for most application developments that concern enterprise businesses.

Software components within this pattern are formed into horizontal layers, where each layer performs a specific role in the application. There is no specification on the number of layers that you should use, but the combination of four layers is the most frequently used approach. The four typical layers that are used in the architecture patterns are the presentation, business, persistence, and database layers. For larger and more complex applications, more layers can be used.

Note

The business and persistence layers are sometimes combined, so you end up with three layers, or so called three-tier architecture.

The responsibility of the presentation layer is handling the logic for user interface communication. It processes the input of the user, which is then passed down to other layers that handle the request and return the results that are then formatted and presented to the user interface with the help of the presentation layer. The business layer is used to perform specific business rules based on the events that happen in the system or requests that originate from the user. Handling customer data, processing orders in a web store, and any kind of calculation or action that is considered as a part of business functionality evolves in the business layer.

The persistence layer is handling the requests for data in the database layer. When the business layer needs to retrieve data or save data to the database, it passes the request to the persistence layer that performs the required action, using the query language supported by the database layer that stores all the data—for example, Structured Query Language (SQL).

Each of the layers handles its own domain of actions. The presentation layer is not concerned about how to store data in the database, the same as the persistence layer is not concerned with how to format data on the user interface. Business logic (when using a persistence layer) will not talk to the database layer directly. Instead, it will retrieve data from the persistence layer, perform some business-related actions on the data, and then pass it up to the presentation layer or back down to persistence.

One of the most prominent features that this architecture promotes is the separation of concern among different layers. Different pieces inside the layers deal only with the logic that relates to that specific layer. With this kind of separation between the layers, it is easier to develop and maintain applications. When requests move from one layer to another, it is mandatory that they go through all the layers below it. If the request originates in the presentation layer and finishes at the database layer, it first must go through the business and persistence layers.

You should not communicate directly with the database layer from the presentation layer; otherwise, you violate the layers of isolation principle. This principle again refers to the idea that development changes in one layer of the architecture should not affect changes in other layers. If you allow direct communication of the presentation layer with the persistence or database layer, then these two become tightly coupled. If you change the technology of the database layer, it would be required to change the presentation logic, which, as a side effect, might affect other parts of the presentation components.

Sometimes, you need components that are only accessible from some layers and hidden from the others. In this case, it makes sense to introduce a new layer that will be used only in some scenarios. For example, if you want to create a special actions layer that is needed by the business layer, it will reside below the business layer. This new layer should be an open type layer, which means that the business layer does not have to go through it every time a request comes from above. A layer that is mandatory in the request traversal is referred to as a closed type layer. The open and closed layer concept helps with identifying the relationship between different layers in the architecture and provides you with the information about request flow and restrictions.

Each layer should be developed independently with changes that are done in isolation of other layers. The layers in the layered architecture should have well-defined APIs or interfaces over which they communicate. This way, your system will be more loosely coupled and easier to maintain and test. As an example, consider the following scenario for an application.

In the presentation layer, the overall behavior and the user experience of ordering items is developed. When a user executes an action on the orders page, this event gets delegated to the orders agent that is listening to such events. The request proceeds in the next layer, where the business logic defines how to process order requests. If needed, the orders module will communicate with the persistence layer, either for data retrieval or for storing information persistently in the database. When the request reaches the last layer, the response is generated and processed in the opposite direction and is finally presented on the user order page.

If a request that comes from the presentation layer needs to reach the database layer, but the business layer (and possibly some other closed type layers) do not perform any processing of the request—instead, they send the request to the next layer—then this is considered by some developers as the architecture sinkhole antipattern. It is almost impossible to avoid this situation, but the goal is to minimize such request scenarios, which might result in changing some layers to be more open.

The layered or n-tier architecture makes a solid ground for most of the applications. It is easy to test, with the possibility to mock some layers in your tests, and it is simple to develop, because the skills of the developers can be split across different layers. You can have developers who are more comfortable with the front-end development work on the presentation layer. Some people may be more skilled with databases, so they would work on the persistence and database layer. One disadvantage of this architecture is that it is not the easiest to scale compared with a microservices architecture. Most layered development tends to be monolithic, which complicates the deployments and makes it harder to create a deployment pipeline. The deployments must be planned because of the downtime.

Software Design Patterns

A good software architecture is important, but it is not enough for establishing good quality of a system. To ensure the best experience for all parties involved, the attributes, besides being well designed, need also to be well implemented. The architecture patterns will give you a bigger picture of how components should be assembled. Software design patterns will dive into separate components and ensure that the optimal coding techniques and patterns are used in order to avoid highly coupled and tangled code.

As with architectural patterns, the software design patterns provide solutions to commonly occurring obstacles in software design. They are concepts for solving problems in your code and not libraries that you would import into your project. Also, when compared with algorithms, design patterns do not define a clear set of actions but rather a high-level definition of a solution, so the same pattern, applied to different applications, can have different code.

The simplest design patterns are called idioms and are usually tied to a single programming language to solve a specific deficiency in that language. Most software design patterns are language agnostic and can be distinguished by their complexity and applicability to the system in observation.

If you were developing software before, you might have already used some of the patterns without knowing it. When you gain experience, you should be able to use design patterns, not coincidentally, but because you know that it will solve a problem.

Software design patterns can reduce the time of development because they promote reusability. Loosely coupled code is easier to reuse than tangled code that was not written with extensibility and flexibility in mind.

Note

One of the most influential books on object-oriented software design is Design Patterns: Elements of Reusable Object-Oriented Software, which set industry standards for writing better code by relying on patterns. Many of the concepts that you will learn here are coming out of the ideas of the book authors— Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides—who are commonly known as the Gang of Four (GoF).

When you are reading about software design patterns, you will typically be introduced to a pattern using a name—for example, adapter pattern. Each pattern will describe the context and problem that it is solving, then a solution that acts as a template applicable to different programming languages, and finally, the good characteristics that a pattern brings, as well as the trade-offs of using patterns.

The sections that are usually discussed with the design patterns are:

  • Intent

  • Motivation

  • Applicability

  • Structure in a modeling language

  • Implementation and sample code

There are many software design patterns. They vary in their applicability and level of abstraction but can still be classified based on their purpose. The groups in which patterns are divided are creational, which are the patterns concerned with the class or object creation mechanisms; structural, which deals with the class or object compositions for maintaining flexibility in larger projects; and behavioral, which describes ways of interaction between classes or objects.

A pattern will describe the context and problems that it solves, together with a solution in a modeling language and code examples.

Creational Patterns

Structural Patterns

Behavioral Patterns

Singleton

Adapter

Observer

Abstract Factory

Decorator

Interpreter

Prototype

Facade

Mediator

Factory Method

Proxy

State

The patterns follow many design principles. The ideas of encapsulation for minimizing side effects when changing parts of code, SOLID principles, and depending on abstraction, not implementations, are intertwined into design patterns that can be adopted for your problem solving.

As an example, observe the singleton pattern. The intent of this pattern is to ensure that a class has only one instance while providing a global access point to it. The motivation for it are classes, where you expect to get that object instead of a new fresh one once an object exists. This way, you can control access to a resource shared among other objects—for example, a database object shared by other client code.

Using this pattern, you can provide global access to an object without having to store it to a global variable. Global variables might seem like a clever way of providing access to any other object, but they also pose a threat, because a global variable can easily be overwritten with other content. The singleton pattern enables the access to an object from anywhere, and it also protects that object from being overwritten. This protection is done by making the class constructor private and creating a static method that, if accessible by your other code, returns the original instance to the caller.

Here is how you would implement the singleton pattern in the Python language.

Class DataAccess() can be instantiated only once. The __init__() constructor first checks if an object instance exists, and if it does, it raises an error. If your code needs to access the object, it should retrieve the instance using the get_instance() method.

Content Review Question

What does encapsulation in OOP mean?

No comments:

Post a Comment