Architectural Patterns
Architectural Patterns
1.1 Introduction
Design patterns are typical solutions to commonly occurring problems in software design. They
are like pre-made blueprints that you can customize to solve a recurring design problem in your
code.
The pattern is not a specific piece of code, but a general concept for solving a particular problem.
You can follow the pattern details and implement a solution that suits the realities of your own
program.
Patterns are often confused with algorithms, because both concepts describe typical solutions to
some known problems. While an algorithm always defines a clear set of actions that can achieve
some goal, a pattern is a more high-level description of a solution. The code of the same pattern
applied to two different programs may be different.
An analogy to an algorithm is a cooking recipe: both have clear steps to achieve a goal. On the
other hand, a pattern is more like a blueprint: you can see what the result and its features are, but
the exact order of implementation is up to you.
`
Design patterns differ by their complexity, level of detail and scale of applicability to the entire
system being designed. I like the analogy to road construction: you can make an intersection
safer by either installing some traffic lights or building an entire multi-level interchange with
underground passages for pedestrians.
The most basic and low-level patterns are often called idioms. They usually apply only to a single
programming language.
The most universal and high-level patterns are architectural patterns. Developers can implement
these patterns in virtually any language. Unlike other patterns, they can be used to design the
architecture of an entire application.
Creational patterns provide object creation mechanisms that increase flexibility and
reuse of existing code.
Structural patterns explain how to assemble objects and classes into larger structures,
while keeping these structures flexible and efficient.
Problem:
Imagine that you’re creating a logistics management application. The first version of
your app can only handle transportation by trucks, so the bulk of your code lives
inside the Truck class.
After a while, your app becomes pretty popular. Each day you receive dozens of
requests from sea transportation companies to incorporate sea logistics into the app.
Great news, right? But how about the code? At present, most of your code is coupled
to the Truck class. Adding Ships into the app would require making changes to the
`
entire codebase. Moreover, if later you decide to add another type of transportation to
the app, you will probably need to make all of these changes again.
As a result, you will end up with pretty nasty code, riddled with conditionals that switch
the app’s behavior depending on the class of transportation objects.
Solution:
The Factory Method pattern suggests that you replace direct object construction calls
(using the new operator) with calls to a special factory method. Don’t worry: the
objects are still created via the new operator, but it’s being called from within the
factory method. Objects returned by a factory method are often referred to
as products.
Subclasses can alter the class of objects being returned by the factory method.
At first glance, this change may look pointless: we just moved the constructor call from
one part of the program to another. However, consider this: now you can override the
factory method in a subclass and change the class of products being created by the
method.
There’s a slight limitation though: subclasses may return different types of products
only if these products have a common base class or interface. Also, the factory
method in the base class should have its return type declared as this interface.
As long as all product classes implement a common interface, you can pass their
objects to the client code without breaking it.
The code that uses the factory method (often called the client code) doesn’t see a
difference between the actual products returned by various subclasses. The client
treats all the products as abstract Transport. The client knows that all transport
objects are supposed to have the deliver method, but exactly how it works isn’t
important to the client.
Singleton Explanation
Problem:
The Singleton pattern solves two problems at the same time, violating the Single
Responsibility Principle:
1. Ensure that a class has just a single instance. Why would anyone want to control
how many instances a class has? The most common reason for this is to control
access to some shared resource—for example, a database or a file.
Here’s how it works: imagine that you created an object, but after a while decided to
create a new one. Instead of receiving a fresh object, you’ll get the one you already
created.
Note that this behavior is impossible to implement with a regular constructor since a
constructor call must always return a new object by design.
`
Clients may not even realize that they’re working with the same object all the time.
2. Provide a global access point to that instance. Remember those global variables
that you (all right, me) used to store some essential objects? While they’re very
handy, they’re also very unsafe since any code can potentially overwrite the contents
of those variables and crash the app.
Just like a global variable, the Singleton pattern lets you access some object from
anywhere in the program. However, it also protects that instance from being
overwritten by other code.
There’s another side to this problem: you don’t want the code that solves problem #1
to be scattered all over your program. It’s much better to have it within one class,
especially if the rest of your code already depends on it.
Nowadays, the Singleton pattern has become so popular that people may call
something a singleton even if it solves just one of the listed problems.
Solution:
All implementations of the Singleton have these two steps in common:
Make the default constructor private, to prevent other objects from using
the new operator with the Singleton class.
Create a static creation method that acts as a constructor. Under the hood, this
method calls the private constructor to create an object and saves it in a static field.
All following calls to this method return the cached object.
If your code has access to the Singleton class, then it’s able to call the Singleton’s
static method. So, whenever that method is called, the same object is always
returned.
`
Adapter Explanation
Problem:
Imagine that you’re creating a stock market monitoring app. The app downloads the
stock data from multiple sources in XML format and then displays nice-looking charts
and diagrams for the user.
At some point, you decide to improve the app by integrating a smart 3rd-party
analytics library. But there’s a catch: the analytics library only works with data in JSON
format.
`
You can’t use the analytics library “as is” because it expects the data in a format that’s
incompatible with your app.
You could change the library to work with XML. However, this might break some
existing code that relies on the library. And worse, you might not have access to the
library’s source code in the first place, making this approach impossible.
Solution
You can create an adapter. This is a special object that converts the interface of one
object so that another object can understand it.
An adapter wraps one of the objects to hide the complexity of conversion happening
behind the scenes. The wrapped object isn’t even aware of the adapter. For example,
you can wrap an object that operates in meters and kilometers with an adapter that
converts all of the data to imperial units such as feet and miles.
Adapters can not only convert data into various formats but can also help objects with
different interfaces collaborate. Here’s how it works:
1. The adapter gets an interface, compatible with one of the existing objects.
2. Using this interface, the existing object can safely call the adapter’s methods.
3. Upon receiving a call, the adapter passes the request to the second object, but in a format
and order that the second object expects.
Sometimes it’s even possible to create a two-way adapter that can convert the calls in
both directions.
`
Let’s get back to our stock market app. To solve the dilemma of incompatible formats,
you can create XML-to-JSON adapters for every class of the analytics library that your
code works with directly. Then you adjust your code to communicate with the library
only via these adapters. When an adapter receives a call, it translates the incoming
XML data into a JSON structure and passes the call to the appropriate methods of a
wrapped analytics object.
Real-World Analogy
Observer Explanation
Problem:
Imagine that you have two types of objects: a Customer and a Store. The customer is
very interested in a particular brand of product (say, it’s a new model of the iPhone)
which should become available in the store very soon.
The customer could visit the store every day and check product availability. But while
the product is still en route, most of these trips would be pointless.
`
On the other hand, the store could send tons of emails (which might be considered
spam) to all customers each time a new product becomes available. This would save
some customers from endless trips to the store. At the same time, it’d upset other
customers who aren’t interested in new products.
It looks like we’ve got a conflict. Either the customer wastes time checking product
availability or the store wastes resources notifying the wrong customers.
Solution:
The object that has some interesting state is often called subject, but since it’s also
going to notify other objects about the changes to its state, we’ll call it publisher. All
other objects that want to track changes to the publisher’s state are
called subscribers.
The Observer pattern suggests that you add a subscription mechanism to the
publisher class so individual objects can subscribe to or unsubscribe from a stream of
events coming from that publisher. Fear not! Everything isn’t as complicated as it
sounds. In reality, this mechanism consists of 1) an array field for storing a list of
references to subscriber objects and 2) several public methods which allow adding
subscribers to and removing them from that list.
Real apps might have dozens of different subscriber classes that are interested in
tracking events of the same publisher class. You wouldn’t want to couple the publisher
to all of those classes. Besides, you might not even know about some of them
beforehand if your publisher class is supposed to be used by other people.
That’s why it’s crucial that all subscribers implement the same interface and that the
publisher communicates with them only via that interface. This interface should
declare the notification method along with a set of parameters that the publisher can
use to pass some contextual data along with the notification.
If your app has several different types of publishers and you want to make your
subscribers compatible with all of them, you can go even further and make all
publishers follow the same interface. This interface would only need to describe a few
subscription methods. The interface would allow subscribers to observe publishers’
states without coupling to their concrete classes.
Real-World Analogy:
`
The publisher maintains a list of subscribers and knows which magazines they’re
interested in. Subscribers can leave the list at any time when they wish to stop the
publisher sending new magazine issues to them.
1.7 References
https://refactoring.guru/design-patterns