Chapter 3
Structural Design Patterns
Introduction
Structural design patterns deal with the relationships between classes in the system. This category of design patterns determines how different objects can form a more complex structure together.
Structure
In this chapter, we will cover the following topics:
Structural design pattern
Adapter
Bridge
Composite
Decorator
Façade
Flyweight
Proxy
Objectives
By the end of this chapter, you will be familiar with structural design patterns and be able to understand their differences. It is expected that by using the points presented in this chapter, you will be able to design the correct structure for the classes and use each of the structural design patterns in their proper place.
Structural design patterns
In this category of design patterns, there are seven different design patterns, which are:
Adapter: Allows objects with incompatible interfaces to cooperate and communicate. This design pattern can exist in two different ways:
Adapter pipeline: In this case, several adapters are used together.
Retrofit interface pattern: An Adapter is used as a new interface for several classes.
Bridge: Separates abstractions from implementation. This design pattern can also be implemented in another way:
Tombstone: In this type of implementation, an intermediate object plays the seeker role, and through that, the exact location of the destination object is identified.
Composite: A tree structure can be created with a single mechanism.
Decorator: New behaviors and functions can be dynamically added to the object without inheritance.
Facade: It is possible to connect to a complex system by providing a straightforward method.
Flyweight: It provides the ability to answer many requests through the sharing of objects
Proxy: By providing a substitute for the object, you can do things like controlling access to the object
In addition to these seven GoF patterns, other patterns are about the structure of objects:
Aggregate pattern: It is a Composite with methods for aggregating children.
Extensibility pattern: Hides complex code behind a simple interface.
Marker pattern: An empty interface assigns metadata to the class.
Pipes and filters: A chain of processes in which each process's output is the next input.
Opaque pointer: Pointer to an undeclared or private type, to hide implementation details.
Adapter
In this section, the adapter design pattern is introduced and analyzed, according to the structure presented in GoF Design Patterns section in Chapter 1, Introduction to Design pattern.
Name:
Adapter
Classification:
Structural Design Patterns
Also known as:
Wrapper
Intent:
This design pattern tries to link two classes that are not structurally compatible with each other and work with them.
Motivation, Structure, Implementation, and Sample code:
Suppose a company has designed an infrastructure to connect and call web services, and through this infrastructure, it connects to various web services. This infrastructure is as follows:
Figure3.1.png
Figure 3.1: Client-External Service initial relation
In the current structure, the Client calls the services through an ExternalService object and according to the type of web service (let us assume it is only Get and Post). Currently, the codes on the client side are as follows:
IExternalService service = new ExternalService();
service.Url = “http://something.com/api/user”;
service.Get(“pageIndex = 1”);
This mechanism of calling a web service is used in most places of the software. After some time, the company concluded using an external server to connect to external services, and for this purpose, it bought a product called API Gateway. This product’s problem is connecting to the services purchased using the API Gateway. The class structure of the same is provided as follows:
Figure3.2.png
Figure 3.2: API Gateway Proxy class structure
Now, to oblige the client to use APIGatewayProxy, we will need to change many parts of the program, which will be a costly task that can naturally involve risks. It is possible to act another way and make the two classes APIGatewayProxy and ExternalService compatible with each other and finally work with them.
To establish this compatibility, we will need to define another class according to the provided IexternalService format and communicate with APIGatewayProxy through this class. In this case, while there will be much fewer changes on the client side (to the extent of changing the class name), we will be able to work with another class that is completely different from ExternalService in terms of the structure instead of working with ExternalService. According to the preceding explanations, the following class diagram can be considered:
Figure3.3.png
Figure 3.3: Adapter design pattern UML diagram
As seen in the preceding class diagram, the ServiceAdapter class is defined, which implements the IexternalService interface. This is because the ServiceAdapter class matches the ExternalService class. Then inside the ServiceAdapter class, APIGatewayProxy should be connected. For the preceding class diagram, the following code can be considered:
public interface IexternalService
{
public string Url { get; set; }
public Dictionary
void Get(string queryString);
void Post(object body);
}
public class ExternalService : IexternalService
{
public string Url { get; set; }
public Dictionary
public void Get(string queryString)
=> Console.WriteLine($”Getting data from: {Url}?{queryString}”);
public void Post(object body)
=> Console.WriteLine($”Posting data to: {Url}”);
}
public class APIGatewayProxy
{
public string BaseUrl { get; set; }
public void Invoke(
string action, object parameters, object body,
string verb, Dictionary
)
=> Console.WriteLine($”Invoking {verb} {action} from {BaseUrl}”);
}
public class ServiceAdapter : IexternalService
{
public string Url { get; set; }
public Dictionary
public void Get(string queryString)
{
var proxy = new APIGatewayProxy(){ BaseUrl = Url[..Url.LastIndexOf(“/”)]};
proxy.Invoke(
Url[(Url.LastIndexOf(“/”) + 1)..],
queryString, null, “GET”, Headers);
}
public void Post(object body)
{
var proxy = new APIGatewayProxy(){ BaseUrl = Url[..Url.LastIndexOf(“/”)]};
proxy.Invoke(
Url[(Url.LastIndexOf(“/”) + 1)..],
null, body, “POST”, Headers);
}
}
As it is clear in the preceding code, ServiceAdapter communicates with APIGatewayProxy while implementing the IExternalService interface. In the preceding implementation, instead of creating an APIGatewayProxy object every time inside the methods, this can be done in the ServiceAdapter constructor, and that object can be used in the methods. Therefore, according to the preceding codes, the client can use these codes as follows:
IexternalService service = new ServiceAdapter();
service.Url = “http://something.com/api/user”;
service.Get(“pageIndex=1”);
As you can see, the preceding code is the same one presented at the beginning of the discussion, with the difference that it was made from the ExternalService object class at the beginning of the discussion. Still, in the preceding code, it is made from the ServiceAdapter object.
Participants:
Target: In the preceding scenario, it is the same as IexternalService, which is responsible for defining the client’s interface. This interface defines the communication protocol between Adaptee and the Client.
Adaptee: In the preceding scenario, it is the same as APIGatewayProxy, which means an interface must match the existing structure. Usually, the client cannot communicate with this class because it has a different structure.
Adapter: In the preceding scenario, it is the same as ServiceAdapter and is responsible for adapting the Adaptee to the Target.
Client: The same user uses objects compatible with Target.
Notes:
The vital point in this design pattern is that the client calls the desired operation in the Adapter, and this Adapter is responsible for calling the corresponding operation in the Adaptee.
The amount of work the adapter must do in this design pattern depends on the similarity or difference between the Target and the Adaptee.
To implement an Adapter, you can also use an inheritance called Class Adapter. Using this method, the Adapter inherits from the Adaptee class while implementing the interface provided by Target. If we assume that Target is a class, it would be impossible to implement this method in languages like C#, where multiple inheritances are not allowed.
The implementation presented in the previous section, in which the Adapter has an Adaptee object, is called Object Adapter.
The bridge design pattern can be very similar to the adapter design pattern, but these two have different purposes. The bridge design pattern tries to separate the structure from the implementation to enable these two parts to be developed independently. But the adapter tries to change the current interface structure.
The decorator design pattern tries to add a series of new features to the class without changing the interface structure. The proxy design pattern also tries to define a proxy for an object. Therefore, these two design patterns are different from the adapter design pattern.
In the preceding implementation, the intention was only to match Adaptee with Target. If there is a need for both the Adaptee to match the Target and the Target to match the Adaptee, then the Two-Way Adapter will need to be used. In this case, while implementing the Target, the adapter also inherits from the Adaptee. The prerequisite for implementing this method in languages like C# that does not support multiple inheritances is that the Target or Adaptee must be an interface. This method is briefly shown in the following code:
Public interface Target{
void A();
}
public class Adaptee{
Public void B(){}
}
Public class Adapter: Adaptee, Target{
Public void A(){}
}
An object can contain methods and variables. Methods are executable, and variables can be initialized and updated. With this description, if we store the method inside the variable, we can give more flexibility to the adapter design pattern. Because in this way, the method to be executed can be changed at the time of execution. In other words, in previous implementations of the adapter design pattern, the client needed to know the method’s name to use it. If it is possible to change the name of the method in question at the time of execution, it is possible to make the name of the method that the client calls and the method that exists in the Target different. In this case, the Adapter should be able to manage this name change. To implement this mechanism, you can use delegates in C# language. This way of implementing the adapter design pattern is also called Pluggable Adapter.
Public class Adaptee
{
public void Print(string message) => Console.WriteLine(message);
}
public class Target
{
public void Show(string input) => Console.WriteLine(input);
}
public class Adapter : Adaptee
{
public Action
public Adapter(Adaptee adaptee) => Request = adaptee.Print;
public Adapter(Target target) => Request = target.Show;
}
The preceding code is a Two-Way Adapter implemented in a Pluggable way. As you can see, a Request is defined as an Action type and a different method is executed in the constructor according to the input sent. To use the preceding code, you can proceed as follows:
Adapter adapter1 = new(new Adaptee());
adapter1.Request(“Hello Vahid”);
Adapter adapter2 = new(new Target());
adapter2.Request(“Hi my name is Vahid”);
In the first line, an Adaptee object(adapter1) is sent to the Adapter, and then the Request is executed. This will make the Print method that is placed in the Request to be executed. In the next instance(adapter2), a Target object has been sent to the Adapter, which causes the Show method to be executed when the Request is executed. Using a two-way adapter to implement a pluggable adapter is not necessary. Apart from this method, there are other ways to implement pluggable adapter, including using abstract operations or a parametrized adapter.
Consequences: Advantages
Since the structure is separated from the business logic, SRP is followed.
You can add an adapter without changing the client codes. In this case, OCP has been observed.
Consequences: Disadvantages
It is necessary to define and add new classes and interfaces. It can sometimes increase the complexity of the code.
Applicability:
When we need to use an existing class, but the structure of this class does not match the existing codes.
When we have several classes inherited from a parent class and all lack a certain behavior. Assuming that this behavior cannot be placed in the parent class, Adapter can be used to cover this need.
Related Patterns:
Some of the following design patterns are not related to Adapter design pattern, but to implement this design pattern, checking the following design patterns will be useful:
Bridge
Proxy
Decorator
Bridge
In this section, the bridge design pattern is introduced and analyzed, according to the structure presented in GoF design patterns section in Chapter 1, Introduction to Design Pattern.
Name:
Bridge
Classification:
Structural design patterns
Also known as:
Handle/Body
Intent:
This design pattern tries to separate the abstraction from the implementation and develop abstraction and implementation independently and separately. In other words, this design pattern tries to divide larger classes into two independent structures called abstraction and implementation. With this, each of these two structures can be developed independently. Consider two independent islands. Each of these islands can develop and change independently and a bridge has been created to connect these two islands.
Motivation, Structure, Implementation, and Sample code:
Suppose a requirement has been raised, and it is requested to implement the necessary infrastructure for a service bus. This service bus should be able to call the desired service and log the received result. To implement, the team concludes that they will first develop a service bus with minimum features and complete it in the following versions. Therefore, for the first version, the team only wants the service bus to save the received response’s log in a text file while running the service. For the second version, the team will try to provide the service bus with the ability of access control and then log in to the elastic database.
Considering the preceding scenario, one design option is to create a class for the first version, define the features in this class, and do the same for the second version through a separate class. To integrate different versions, these classes can inherit from a parent class. This design is based on inheritance and problem with this design, is that if we want to develop the connecting to services or develop the logs requirements, we will probably need to define a new class. Suppose in the third version, we want to add the log to the SQL Server database. We will need to define a new class and support this requirement.
A good design in this scenario is to separate the work of invoking the service and logging. Allowing the logging or connecting to services needs to be developed separately. For this relationship to be formed, the type of relationship changes from Inheritance to Aggregation, and this aggregation relationship plays the role of a bridge between these two separate islands. For each island, an interface is defined so that different implementations can happen according to these interfaces.
The following class diagram can be considered with the explanations provided in the preceding sections. The following class diagram shows that connecting to the service is separated from logging. The ILogger interface is defined to develop the log, and the IServiceBus interface to develop the connection to the services. The BasicBus and AdvandeBus classes implement the IServiceBus interface, and the TextLogger and ElasticLogger classes implement the ILogger interface. Also, IServiceBus is connected to ILogger through Logger. Therefore, the client can use his appropriate log class and gateway. Also, if we want to add the ability to log in to SQL Server, we can easily define this class, and there will be no need to change the bus section:
Figure3.4.png
Figure 3.4: Bridge design pattern UML diagram
For the preceding class diagram, the following codes can be considered:
public class Log
{
public DateTime LogDate { get; set; }
public string Result { get; set; }
public string Message { get; set; }
}
public interface ILogger
{
void Log(Log data);
}
public class TextLogger : ILogger
{
public void Log(Log data)
=> Console.WriteLine(
$"Log to text: {data.LogDate}-{data.Result}-{data.Message}");
}
public class ElasticLogger : ILogger
{
public void Log(Log data)
=> Console.WriteLine(
$"Log to elastic: {data.LogDate}-{data.Result}-{data.Message}");
}
public interface IServiceBus
{
public ILogger Logger { get; set; }
void Call(string url);
}
public class BasicBus : IServiceBus
{
public ILogger Logger { get; set; }
public void Call(string url)
{
Console.WriteLine($"Request sent to {url}");
Logger.Log(new Log {
LogDate = DateTime.Now,
Result = "OK",
Message = "Response received."
});
}
}
public class AdvanceBus : IServiceBus
{
public ILogger Logger { get; set; }
public void Call(string url)
{
if (new Uri(url).Scheme == "http")
Logger.Log(new Log {
LogDate = DateTime.Now,
Result = "Failed",
Message = "HTTP not supported!"
});
else
{
Console.WriteLine($"Request sent to {url}");
Logger.Log(new Log {
LogDate = DateTime.Now,
Result = "OK",
Message = "Response received."
});
}
}
}
As it is clear in the preceding code, the IServiceBus interface has a feature called Logger of ILogger type, which is connected to the logging mechanism. This connection is visible inside the Call method. Also, the two classes BasicBus and AdvanceBus have implemented the IServiceBus interface, the BasicBus class is the same as the first version and the AdvanceBus is the same as the second version. To use this structure, you can do the following:
IServiceBus bus = new BasicBus()
{
Logger = new TextLogger()
};
bus.Call("https://google.com");
According to the preceding code, if we want to save the log in Elasticsearch instead of text file, we can create object from ElasticLogger instead of creating object from TextLogger. Also, if we want to use the second version or AdvandeBus instead of the first version or BasicBus, we need to create an object from AdvanceBus instead of using the object from BasicBus.
Participants:
Abstraction: which is the same as IServiceBus in the preceding scenario. It is responsible for defining the format for abstractions. This interface should also provide a reference to the implementor.
RefinedAbstraction: the same as BasicBus and AdvanceBus in the preceding scenario. It is responsible for implementing the format provided by abstraction.
Implementor: which in the preceding scenario is ILogger. It is responsible for defining the format for implementations. This interface does not need to have a reference to abstraction.
ConcreteImplementor: which in the preceding scenario is the same as TextLogger and ElasticLogger, is responsible for implementing the template provided by implementor.
Client: It is the user and executes the code through abstraction.
Notes:
In this design pattern, abstraction is responsible for sending the request to the implementor.
Abstraction usually defines and implements a series of complex behaviors dependent on a series of basic behaviors defined through the implementor.
If we have more than one implementation, we cannot use implementor.
The important point in design pattern is when and from which implementor should the object be prepared? There are different ways to answer this question. One method is to prepare the appropriate object from implementor using the Abstract Factory design pattern, according to other features. In this case, the client will not be involved in the complexity of providing the object from the implementor.
Consequences: Advantages
It is possible to define a new RefinedAbstraction without changing the implementor. It is also possible to add new ConcreteImplementors without changing the abstraction. Hence, the OCP has been met.
Due to the separation of abstraction and implementor from each other, SRP has been observed.
Consequences: Disadvantages
Applying this design pattern to a code structure with many internal dependencies will increase complexity.
Applicability:
This design pattern can be used when faced with a class where different types of behavior have been implemented. Suppose there was a class that included the log operation in the text file and elastic together. Then, using this design pattern, we could separate this one-piece structure. The smaller the classes are, the easier it will be to debug and develop them.
When developing a class in different and independent dimensions, this design pattern can be used. For this purpose, each dimension can be defined and implemented in a separate structure.
If we need to move different implementations together during execution, we can benefit from this pattern.
Related patterns:
Some of the following design patterns are not related to bridge design pattern, but to implement this design pattern, the following design patterns will be useful:
Abstract Factory
Builder
Adapter
Composite
In this section, the composite design pattern is introduced and analyzed, according to the structure presented in GoF design patterns section in Chapter 1, Introduction to design pattern.
Name:
Composite
Classification:
Structural design patterns
Also known as:
---
Intent:
This design pattern tries to treat objects of the same type in a single form and turn them into a tree structure.
Motivation, Structure, Implementation, and Sample code:
Suppose there is a requirement in which we need to give the user the possibility to define the program’s menus. Each menu can have sub-menus and menu members. Each member of the menu has text and address of the desired page, which should be directed to the desired page by clicking on it. In this scenario, we are facing a hierarchical or tree structure. For example, the following figure shows an example of this menu:
Figure3.5.png
Figure 3.5: Sample menu structure
In the preceding structure, the main menu has a member called Overview. This member has four other members: Intro, Architecture Components, Class Libraries and Tutorials. The Tutorials member has two other members: Template Changes and Use Visual Studio, of which Use Visual Studio again has three members: New Project, Debug and Publish. In other words, each member in the menu, according to the Figure 3.5, has three features: text, address and subcategory members. The leaves of the tree, in the preceding structure, do not have subgroup members and only have text and address.
With these explanations, we are faced with two entities. One entity for tree leaves and another for tree nodes. With these explanations, the following figure of class diagram can be imagined:
Figure3.6.png
Figure 3.6: Composite design pattern UML diagram
As shown in the preceding class diagram, there are two types of menu members. The first type: tree leaves and the second type: tree nodes. These different types, regardless of whether they are leaves or nodes, have text and address and can be printed. Menu class has the role of tree nodes and allows to add sub-set nodes or leaves to the nodes. For this reason, the Menu class, regardless of whether it implements the IMenuComponent interface, also has an aggregation relationship with this interface. The MenuItem class also has the role of a leaf in the preceding design. With the preceding explanations, the following code can be considered for the Figure 3.6:
public interface ImenuComponent
{
public string Text { get; set; }
public string Url { get; set; }
string Print();
}
public class Menu : ImenuComponent
{
public string Text { get; set; }
public string Url { get; set; }
public List
public string Print()
{
StringBuilder sb = new();
sb.Append($”Root: {Text}”);
sb.Append(Environment.NewLine);
foreach (var child in Children)
{
sb.Append($”Parent: {Text}, Child: {child.Print()}”);
sb.Append(Environment.NewLine);
}
return sb.ToString();
}
}
public class MenuItem : ImenuComponent
{
public string Text { get; set; }
public string Url { get; set; }
public string Print() => $”{Text}”;
}
To create a menu, with the structure presented at the beginning of the topic, you can proceed as follows:
ImenuComponent menu = new Menu()
{
Text = “Overview”,
Url = “/overview.html”,
Children = new List
{
new MenuItem{Text =”Intro”,Url=”/intro.html”},
new MenuItem{Text =”Architecture Component”,Url=”/arch.html”},
new MenuItem{Text =”Class Libraries”,Url=”/class.html”},
new Menu{
Text =”Tutorials”,
Url=”/tutorials.html”,
Children=new List
{
new MenuItem{Text =”Template Changes”,Url=”/tpl.html”},
new Menu{
Text =”Use Visual Studio”,
Url=”/vs.html”,
Children=new List
{
new MenuItem{Text =”New Project”,Url=”/new-project.html”},
new MenuItem{Text =”Debug”,Url=”/debug.html”},
new MenuItem{Text =”Publish”,Url=”/publish.html”}
}
}
}
}
}
};
Console.WriteLine(menu.Print());
When the Print method is called in the last line, according to the nature of the menu object, which is of the Menu type, the Print method in this class is called. Within this method, according to the nature of each member (node or leaf), the corresponding Print method is called in the corresponding class.
Participants:
Component: In the preceding scenario, it is the same as ImenuComponent and is responsible for defining the common format for Leaf and Composite. If needed, component can be defined as an abstract class and default implementations can be placed in this class. Also, component should provide a template for member navigation. Being able to access the parent node is another task that the Component can provide, however this feature is optional for it.
Leaf: In the preceding scenario, it is the same as MenuItem and is responsible for implementing the leaf.
Composite: In the preceding scenario, it is the same as Menu and is responsible for the task of the node. Each node can have a subset and composite should provide appropriate capabilities for this purpose.
Client: It is the user and creates a tree structure through component.
Notes:
Using the composite design pattern, we have a primitive type (leaf) and a composite type (node). Both of these types implement a common interface. According to these points, a tree structure can be implemented using this design pattern.
Typically, the navigation method is such that the request is executed if it reaches the leaf. Otherwise, if it reaches the node, the request is sent to the subset nodes or leaves. This process continues until the leaf is reached. In this case, in the node, you can perform series of tasks before or after sending the request to the subset nodes or leaves.
By placing a reference to the parent node, it is possible to obtain both its subset and parent nodes through one node. This feature helps to implement the Chain of Responsibility design pattern.
Using the Flyweight design pattern, the parent can be shared between nodes or leaves.
To design the component as much as possible, common tasks should be placed in this class or interface. But sometimes, we face meaningless behaviors while composite and meaningful for others. For example, in the preceding scenario, children are only placed in composite, whereas we could have placed it in component and then provided it in the default implementation. What decision to make in these circumstances is a cost-benefit analysis. For example, in the preceding scenario, if we put the children attribute in the component, we would make the method of dealing with Leaf and Composite the same from the client's point of view. This creates transparency in the design. But on the other hand, it causes us to endanger the safety of the code or to allow the client to do meaningless things that cannot be detected at the time of compilation. This will also waste space(of course, this waste of space is very small).
On the other hand, by not placing children in the component, we have maintained the safety of the code, and the client cannot do meaningless tasks, and these tasks can be identified at the time of compilation. But in this case, we have lost the transparency. Therefore, the choice of the implementation method is very dependent on the cost-benefit analysis.
When you need to operate such as search and this operation is repeated many times, you can increase the overall efficiency by using caching.
The structure can be navigated using the Iterator design pattern.
By using the Visitor template, you can perform a task on all members.
Consequences: Advantages
The method of dealing with primitive and compound types is fixed from the client’s point of view, making it easier for the client.
You can add new members without changing the existing codes. This addition of new types occurs in a situation where the client is not involved in these events and in this way the OCP has been observed.
You can easily work with complex tree structures.
Consequences: Disadvantages
Although the possibility of adding new members can be considered an advantage, but from another point of view, this can also be a disadvantage. Because it is appreciated that sometimes we want to define restrictions on different members and specific members can only be added. Using this design pattern, solving this possibility requires runtime controls.
It may be not easy to define the component and put common behaviors; we have to define the component very general.
Applicability:
When we are faced with a tree structure of objects.
When we need, from the client’s point of view, the method of dealing with leaves and nodes seems fixed. This feature is achieved by implementing a single and common interface between nodes and leaves.
Related patterns:
Some of the following design patterns are not related to Composite design pattern, but to implement this design pattern, checking the following design patterns will be useful:
Chain of Responsibility
Flyweight
Iterator
Visitor
Command
Builder
Decorator
Interpreter
Decorator
In this section, the decorator design pattern is introduced and analyzed, according to the structure presented in GoF design patterns section in Chapter 1, Introduction to Design patterns.
Name:
Decorator
Classification:
Structural design patterns
Also known as:
Wrapper1
Intent:
This design pattern tries to add some new functionality to the class at runtime or dynamically.
Motivation, Structure, Implementation, and Sample code:
Suppose we are designing a photo editing application and there is a requirement to convert the image to black-white or to add text on the image. There are different ways to solve requirement. One way is to go into the main class and add the necessary methods to black-white and add text. But this will make the main class complicated to maintain by adding more requirements.
Another way is to define a series of classes for black-white and adding text that inherit from the main class. In fact, in such scenarios where the capabilities of a class need to be developed, inheritance is one of the first ideas that comes to mind. But inheritance, in turn, has a series of points that must be considered before applying it to the design. For example, in many programming languages, a class can inherit
from only one class. Also, due to its nature, inheritance has a static nature, that is, by using inheritance, one behavior can be replaced by the behavior of the parent class, and the behavior of the parent class cannot be changed. Even the desired main class may be defined as Sealed and there is practically no possibility of inheriting from the class.
The third solution is to use aggregation or composition relationships to solve this problem. By using this type of a relationship, an object has a reference to the main object and in this way, it can do a series of additional tasks compared to the main object. Also, by using this solution, we will no longer face the limitation we had in inheritance. The only important point in this solution is that from the user’s point of view, the main and supplementary objects should look the same. To comply with this point, both the main class and the supplementary class implement a single interface. And finally, to be able to add a new feature to the main object, it is enough to send the main object to the supplementary class.
Therefore, with these explanations, the following class diagram can be considered:
Figure3.7.png
Figure 3.7: Decorator design pattern UML diagram
As shown in the diagram Figure 3.7, the Photo class is the main class that implements the IPhoto interface. If the client wants the original photo, he uses this class and receives the original image. On the other side is the PhotoDecoratorBase class. This class is considered abstract in the preceding diagram. This class implements the Iphoto interface like the Photo class and has an aggregation relationship with Iphoto. This means that by taking a Photo object, it can add a series of new features to it. Next, WatermarkDecorator and BlackWhiteDecorator are defined, which inherit from PhotoDecoratorBase. The important point here is that the GetPhoto method in the PhotoDecoratorBase class is defined as a virtual method so that each child class can change that behavior. With all these explanations, the client can deliver the main image to the decorators and, in this way, do the desired work on the photo, or it can do nothing with the decorators and work directly with the Photo class.
According to the preceding diagram, the following codes can be considered:
public interface Iphoto
{
Bitmap GetPhoto();
}
public class Photo : Iphoto
{
private readonly string filename;
public Photo(string filename) => this.filename = filename;
public Bitmap GetPhoto() => new(filename);
}
public abstract class PhotoDecoratorBase : Iphoto
{
private readonly Iphoto _photo;
public PhotoDecoratorBase(Iphoto photo) => _photo = photo;
public virtual Bitmap GetPhoto() => _photo.GetPhoto();
}
public class WatermarkDecorator : PhotoDecoratorBase
{
private readonly string text;
public WatermarkDecorator(Iphoto photo, string text)
: base(photo) => this.text = text;
public override Bitmap GetPhoto()
{
var photo = base.GetPhoto();
Graphics g = Graphics.FromImage(photo);
g.DrawString(
text,
new Font(“B Nazanin”, 18),
Brushes.Black,
photo.Width / 2,
photo.Height / 2);
g.Save();
return photo;
}
}
public class BlackWhiteDecorator : PhotoDecoratorBase
{
public BlackWhiteDecorator(Iphoto photo) : base(photo) { }
public override Bitmap GetPhoto()
{
var photo = base.GetPhoto();
//Convert photo to black and white here
return photo;
}
}
As can be seen in the preceding code, the Photo class has implemented the Iphoto interface. The PhotoDecoratorBase class also receives an Iphoto object while implementing this interface. Then the decorators inherit from this class and each one tries to provide its own functionality. Each decorator, while rewriting GetPhoto, first calls GetPhoto related to the Photo object and then adds a new feature to it. In order to use this code, you can do the following:
Iphoto photo = new Photo(“C:\\sample.png”);
photo.GetPhoto();//returns the original photo
WatermarkDecorator watermarkDecorator = new WatermarkDecorator(photo, “Sample Watermark”);
watermarkDecorator.GetPhoto();
BlackWhiteDecorator blackWhiteDecorator = new BlackWhiteDecorator(photo);
blackWhiteDecorator.GetPhoto();
In the first line, a Photo object is created and the GetPhoto method of the Photo class is called through this object. In the third and fourth lines, we give the object created in the first line to the WatermarkDecorator class and add text to the photo through that. In the fifth and sixth lines, we give the object created in the first line to the BlackWhiteDecorator and convert the image to black and white.
Participants:
Component: In the preceding scenario, it is the same as Iphoto and has the task of defining the template for the classes to which we want to add new features.
ConcreteComponent: In the preceding scenario, it is Photo that implements the template provided by component. In fact, this class is the same class to which we want to add a new feature.
Decorator: In the preceding scenario, the PhotoDecoratorBase has a reference to the component and tries to provide a template matching the template provided by the component.
ConcreteDecorator: In the preceding scenario, it is the same as WatermarkDecorator and BlackWhiteDecorator, which adds functionality to the component.
Client: It is the same user who uses the code through the component or decorator.
Notes:
In this design pattern, the decorator sends the incoming request to the component. In this process, it may perform some additional tasks before or after sending the request to the component.
Using this design pattern, there is more flexibility in adding or removing a feature than inheritance.
By using this design pattern and decorator, you can add features based on your requirements, and you don’t need to add all useless features to the class.
The decorator object must have an interface that matches the component interface.
If only one feature needs to be added, then there is no need to define the decorator abstract class.
The component class should be light and simple as much as possible, otherwise the decorator will have to implement features that it does not need, and this will make the decorator heavy and increase its complexity.
Using the Strategy design pattern, the internal structure and communications are changed and transformed, while by using the Decorator, only the procedure of the object is changed. For example, suppose we are dealing with a heavy and complex component. In this case, instead of defining a decorator for it, you can use Strategy to send a series of component capabilities to other objects. In this case, there is no need to transfer the weight of the component to the decorator.
Using this design pattern, only the procedure of the object is changed, so there is no need for the component to know about the decorators. This point is also one of the differences between Strategy and Decorator.
Adapter, Proxy and Decorator design patterns have similarities, but at the same time they have an important difference. The Adapter design pattern basically provides a different interface, the Proxy pattern provides exactly the same interface, and the Decorator pattern reinforces and provides the existing interface.
The Chain of Responsibility design pattern also has similarities with the Decorator design pattern. The difference between these two design patterns is that by using Chain of Responsibility, the chain of execution can be interrupted somewhere, but this is not possible in Decorator. Also, by using Chain of Responsibility, the execution of a task in one node of the chain is independent of its execution in another node. But in Decorator, the execution of a task depends on the main interface.
If there is a need to define different decorators, then these decorators can be independent from each other.
Consequences: Advantages
You can extend the behavior of an object without adding a child class.
It is possible to add or disable some features to the class at runtime.
Several behaviors can be combined with each other using decorator.
A large class can be divided into several small classes, each class has a single task, and in this way, SRP is observed.
Consequences: Disadvantages
Removing a Decorator has some complications.
Code debugging process becomes more complicated.
Applicability:
When we need to add a feature dynamically and subtly to a class and at the same time, we do not want to affect the previous objects.
When it is not possible to add new functionality to the class through inheritance.
Related patterns:
Some of the following design patterns are not related to Decorator design pattern, but in order to implement this design pattern, checking the following design patterns will be useful:
Strategy
Composite
Chain of responsibility
Adapter
Proxy
Facade
In this section, the facade design pattern is introduced and analyzed, according to the structure presented in GoF design patterns section in Chapter 1, Introduction to Design Patterns.
Name:
Facade
Classification:
Structural design patterns
Also known as:
---
Intent
This design pattern tries to facilitate communication with different components of a system by providing a simple interface.
Motivation, Structure, Implementation, and Sample code:
In Figure 3.8 to use different components of the system, users are directly connected with these components, which increases the complexity. The Facade design pattern tries to manage this complexity by providing a simple interface, so that the user does not get involved in communication complications:
Figure3.8.png
Figure 3.8: Interactions in system without the presence of Facade
Therefore, in the presence of the Facade design pattern, the Figure 3.8 becomes as follows:
Figure3.9.png
Figure 3.9: Interactions in system with presence of Façade
In order to clarify the issue, pay attention to the following scenario:
Suppose there is a need in which the necessary infrastructure is needed to inquire about plane tickets from different airlines. Let us assume that we are dealing with three airlines, Iran Air, Mahan and Ata, and we need to inquire about the price of plane tickets from these airlines by providing the date, origin and destination, and then show the results of the inquiry to the user in order of cheap to expensive.
One view is that the user directly connects to each airline and performs the inquiry process. In this case, as in the Figure 3.8, there will be communication complexity and the user will be involved in how to connect and communicate with the airlines, which will increase the complexity and make the code more difficult to maintain.
According to the Figure 3.9, by providing a simple interface to the user, the way of communicating with the airline can be hidden from the user's view, and the user can only work with the provided interface and make ticket inquiries. With these explanations, the following class diagram can be imagined:
Figure3.10.png
Figure 3.10: Facade design pattern UML diagram
As shown in preceding figure diagram, the user submits the inquiry request to the TicketInquiry class, and this class communicates with each of the servers, collects the results, and returns them to the user. For the preceding design, the following code can be imagined:
public class Ticket
{
public DateTime FlightTime { get; set; }
public string FlightNumber { get; set; }
public string From { get; set; }
public string To { get; set; }
public int Price { get; set; }
public override string ToString()
=> $"{From}-{To}, {FlightTime}, Number:{FlightNumber},Price:{Price}";
}
public class TicketInquiry
{
public List
{
var iranAirFlights = new IranAir().SearchFlights(date, from, to);
var mahanFlights = new Mahan().Search(date, from, to);
var ataFlights = new ATA().Find(date, from, to);
List
result.AddRange(iranAirFlights);
result.AddRange(mahanFlights);
result.AddRange(ataFlights);
return result.OrderBy(x => x.Price).ToList();
}
}
public class IranAir
{
readonly Ticket[] iranairTickets = new[]
{
new Ticket() { FlightNumber = "IA1000", FlightTime = new DateTime(2021,01,02,11,20,00), Price = 800000, From="Tehran",To="Urmia" },
new Ticket() { FlightNumber = "IA2000", FlightTime = new DateTime(2021,01,02,12,45,00), Price = 750000, From="Tehran",To="Rasht" },
new Ticket() { FlightNumber = "IA3000", FlightTime = new DateTime(2021,01,03,09,10,00), Price = 700000, From="Tehran",To="Urmia" },
new Ticket() { FlightNumber = "IA4000", FlightTime = new DateTime(2021,01,02,18,45,00), Price = 775000, From="Tehran",To="Tabriz" },
new Ticket() { FlightNumber = "IA5000", FlightTime = new DateTime(2021,01,02,22,00,00), Price = 780000, From="Tehran",To="Ahvaz" },
};
public Ticket[] SearchFlights(DateTime date, string from, string to)
=> iranairTickets.Where(
x => x.FlightTime.Date == date.Date &&
x.From == from && x.To == to).ToArray();
}
public class Mahan
{
readonly Ticket[] mahanTickets = new[]
{
new Ticket() { FlightNumber = "M999", FlightTime = new DateTime(2021,01,03,13,30,00), Price = 1500000, From="Tehran",To="Zahedan" },
new Ticket() { FlightNumber = "M888", FlightTime = new DateTime(2021,01,04,15,00,00), Price = 810000, From="Tehran",To="Urmia" },
new Ticket() { FlightNumber = "M777", FlightTime = new DateTime(2021,01,02,06,10,00), Price = 745000, From="Tehran",To="Rasht" }
};
public Ticket[] Search(DateTime date, string from, string to)
=> mahanTickets.Where(
x => x.FlightTime.Date == date.Date &&
x.From == from && x.To == to).ToArray();
}
public class ATA
{
readonly Ticket[] ataTickets = new[]
{
new Ticket() { FlightNumber = "A123", FlightTime = new DateTime(2021,01,02,07,10,00), Price = 805000, From="Tehran",To="Urmia" },
new Ticket() { FlightNumber = "A456", FlightTime = new DateTime(2021,01,03,09,20,00), Price = 750000, From="Tehran",To="Sari" },
new Ticket() { FlightNumber = "A789", FlightTime = new DateTime(2021,01,02,16,50,00), Price = 700000, From="Tehran",To="Tabriz" },
new Ticket() { FlightNumber = "A159", FlightTime = new DateTime(2021,01,03,23,10,00), Price = 775000, From="Tehran",To="Sanandaj" },
new Ticket() { FlightNumber = "A357", FlightTime = new DateTime(2021,01,02,05,00,00), Price = 780000, From="Tehran",To="Urmia" },
};
public Ticket[] Find(DateTime date, string from, string to)
=> ataTickets.Where(
x => x.FlightTime.Date == date.Date &&
x.From == from && x.To == to).ToArray();
}
As can be seen in the preceding code, the user submits the request to TicketInquiry, and TicketInquiry is in charge of managing the relationship with the airlines and inquiries about the ticket and delivers the result to the user. In order to use this structure, you can proceed as follows:
TicketInquiry ticketInquiry = new TicketInquiry();
foreach (var item in ticketInquiry.Inquiry(new DateTime(2021,01,02),"Tehran","Urmia"))
{
Console.WriteLine(item.ToString());
}
In the preceding code, the user makes an inquiry through the TicketInquiry class and displays the received answer in the output.
Participants:
Facade: In the preceding scenario, it is TicketInquiry and is responsible for handling the client's request. In this responsibility, facade can connect to subsystems and track the request.
Subsystem classes: In the preceding scenario, it is IranAir, Mahan and ATA, which is responsible for implementing the business logic related to each subsystem. These classes should not have any knowledge of facade.
Client: It is the same user who delivers the request to facade and receives the response from facade.
Notes:
In order to prevent Facade from becoming complicated, it may be necessary to use several facades and each facade will implement the task of managing related requests.
If we want to hide the way of instantiating subsystems from the client's view, then we can use abstract factory instead of this design pattern.
Flyweight design pattern tries to create small and light objects, while facade design pattern tries to cover the whole system through one object.
Facade design pattern can be similar to mediator design pattern. Because both of these design patterns try to manage communication between classes. However, these two design patterns also have some differences:
The facade pattern only introduces a simple interface and does not provide any new functionality. Also, the subsystems themselves are unaware of the facade and have the possibility to communicate directly with another subsystems.
The mediator design pattern, on the other hand, depends on communication to a central core, and subsystems are not allowed to communicate directly with each other and can only communicate with each other through this central core.
In most cases, the facade class can be implemented as a singleton.
The facade design pattern has similarities with the proxy pattern. Because both of these patterns maintain a complex entity and create objects from it when necessary. However, unlike facade, the proxy design pattern has an interface that implements different services of this interface and therefore these services can be replaced with each other.
In order to reduce the dependency between client and subsystem, facade can be defined as an abstract class. In this case, different implementations can be provided for each facade and allow the Client to choose and use the most appropriate implementation among these different implementations.
If it is necessary to cut off the client's access to the subsystem, the subsystems can be defined as private classes in the Facade.
Consequences: Advantages
Due to the fact that the subsystem logic is isolated, it reduces the complexity of the current system. In fact, by using this design pattern, the client deals with fewer objects, making it possible to use subsystems easily.
There is a loose coupling between the client and subsystem.
Consequences: Disadvantages
Facade class can become God Object over time.
Applicability:
When there is a need to communicate with a specific and complex subsystem.
Related patterns:
Some of the following design patterns are not related to Decorator design pattern, but in order to implement this design pattern, checking the following design patterns will be useful:
Singleton
Mediator
Abstract factory
Flyweight
Proxy
Flyweight
In this section, the flyweight design pattern is introduced and analyzed, according to the structure presented in GoF design patterns section in Chapter 1, Introduction to Design Pattern.
Name:
Flyweight
Classification:
Structural design patterns
Also known as:
---
Intent:
This design pattern tries to make optimal use of resources by sharing objects. Resources can be computing resources, memory or others.
Motivation, Structure, Implementation, and Sample code:
Suppose we are designing a computer game. In one sequence of this game, we need to design a forest and create and display two million trees in it. Suppose there are only pine trees in this forest. A pine tree has characteristics such as color, size (short, medium and tall), name (in this scenario we only have a pine tree) and coordinates.
To implement this scenario, one type of design is to define a class for the tree and then create the required number of objects from it. Suppose the following table is assumed for tree object properties:
Property
Data Type
Size
Name
String (20 chars)
40 Bytes
Color
String (20 chars)
40 Bytes
Size
String (5 chars)
10 Bytes
Coord_X
Integer
4 Bytes
Coord_Y
Integer
4 Bytes
Sum for only 1 tree
98 Bytes
Sum for all the 2 million trees
2,000,000x98=196,000,000 Bytes
Table 3.1: Detailed required memory
NOTE: For simplicity, Coord_X and Coord_Y are defined as integer. In a real-world example, you might need to use other data types for these attributes
According to the preceding table, to build two million trees, about 196 million bytes of memory will be needed.
But when we look at the preceding properties, we realize that some of these properties are common to all trees. For example, all the trees in the forest have three different sizes and have a fixed color for each size, and only the X and Y coordinates are different for each tree. Now, if there is a way to share this common data between trees, then there will be a significant reduction in memory consumption. For example, we can separate the fixed properties from the variable properties. After receiving the object related to the fixed properties, through the definition of a series of methods, we can set the variable properties. In this case, only one object of fixed properties is always created. So, the amount of memory consumption for fixed properties is as follows: (assume that all trees are made with a fixed size):
Attribute
Data Type
Size
Name
String (20 chars)
40 Bytes
Color
String (20 chars)
40 Bytes
Size
String (5 chars)
10 Bytes
Sum
90 Bytes
Table 3.2: Required memory for fixed properties
And for variable properties we also have:
Attribute
Data Type
Size
Coord_X
Integer
4 Bytes
Coord_Y
Integer
4 Bytes
Sum for only 1 tree
8 Bytes
Sum for all the 2 million trees
2,000,000x8=16,000,000 Bytes
Table 3.3: Required memory for variable properties
Finally, the total memory required will be around 16,000,090 Bytes. This number means about 180 million bytes of reduction in memory consumption. With the preceding explanations, the following class diagram can be considered:
Figure3.11.png
Figure 3.11: Flyweight design pattern UML diagram
As shown in the Figure 3.11 class diagram, the Tree class is used to create a tree and implements the ITree interface. This class has two sets of features. Fixed properties, such as Name, Color and Size, along with variable properties such as Coord_X and Coord_Y. The TreeFactory class is responsible for providing the tree object. In fact, this class is responsible for providing the tree object along with fixed properties. The Client class is responsible for setting the variable properties by receiving the object from the TreeFactory through the SetCoord method.
For Figure 3.11 class diagram, the following codes can be imagined:
public interface Itree
{
void SetCoord(int x, int y);
}
public class Tree : Itree
{
public string Name { get; private set; }
public string Color { get; private set; }
public string Size { get; private set; }
public int Coord_X { get; private set; }
public int Coord_Y { get; private set; }
public Tree(string name, string color, string size)
{
Name = name;
Color = color;
Size = size;
}
public void SetCoord(int x, int y)
{
this.Coord_X = x;
this.Coord_Y = y;
}
}
public class TreeFactory
{
readonly Dictionary
public Itree this[string name, string color, string size]
{
get
{
Itree tree;
string key = $”{name}_{color}_{size}”;
if (_cache.ContainsKey(key))
tree = _cache[key];
else{
tree = new Tree(name, color, size);
_cache.Add(key, tree);
}
return tree;
}
}
}
As can be seen in the preceding code, Name, Size and Color properties are fixed properties and Coord_X and Coord_Y properties are variable properties. An indexer is defined in the TreeFactory class, the job of this indexer is to provide or create a new object from Tree. In this indexer, if a tree with the given name, size and color has already been created, the same object is returned, otherwise, a new object is created and added to the object repository. When the user needs a Tree object, instead of directly creating the object, it is expected that the user will create the object through the TreeFactory class. To use this structure, you can proceed as follows:
TreeFactory treeFactory = new();
Itree tree = treeFactory[“pine”, “green”, “short”];
tree.SetCoord(1, 1);
Itree tree2 = treeFactory[“pine”, “green”, “short”];
tree2.SetCoord(2, 2);
Itree tree3 = treeFactory[“pine”, “green”, “short”];
tree3.SetCoord(3, 3);
As shown in the preceding code, in the second line, the object request for the tree named Pine, in green color and in short size, is registered. Since this object does not exist, TreeFactory creates a new object. In third line, the constructed tree is placed in coordinates x=1 and y=1. In fourth line, the tree with the same properties as before is requested again. The TreeFactory class, instead of creating a new object, provides the same object as the previous one, and this tree is also placed in the coordinates x=2 and y=2.
Participants:
Flyweight: In the preceding scenario, it is the same Itree that is responsible for defining the template for working on variable properties.
ConcreteFlyweight: In the preceding scenario, it is the same as Tree (Name, Size and Color properties), and it implements the interface provided by Flyweight. This class is responsible for maintaining static properties.
UnsharedConcreteFlyweight: In the preceding scenario, it is the same Tree (Coord_X and Coord_Y properties) and is responsible for implementing the interface provided by flyweight. In another design of this structure, you can separate the UnsharedConcreteFlyweight and ConcreteFlyweight classes and put a reference of ConcreteFlyweight in UnsharedConcreteFlyweight. This class is also called Context.
FlyweightFactory: In the preceding scenario, it is the same as TreeFactory and is in charge of maintaining, creating and presenting flyweight objects.
Client: It is the same user and adjusts variable properties through flyweight received from FlyweightFactory.
Notes:
There are two categories of properties related to this design pattern: intrinsic properties and extrinsic properties. Intrinsic properties, which we referred to as fixed properties, are shareable properties and are stored in ConcreteFlyweight, while extrinsic properties, which we refer to as variable properties, are non-shareable properties and are stored in UnsharedConcreteFlyweight. These properties can also be calculated on the client side and sent to ConcreteFlyweight.
Client should not directly create object from ConcreteFlyweight and should only obtain this object through FlyweightFactory. In this case, the object sharing mechanism is always observed and applied.
Although the preceding structure has implemented the flyweight design pattern, it still needs some help. The first problem is that the user has access to the Tree class and can create objects directly from this class. To solve this problem, you can put the Tree class as a private class inside the TreeFactory class. Refer to the following code:
public class TreeFactory
{
private class Tree : Itree
{
public string Name { get; private set; }
…
Now, with this change, the user will not have direct access to create objects from the Tree class.
The next problem with the preceding structure is that the objects provided by TreeFactory for the same key, are always same. This means that the change of an attribute in an object is reflected in other objects as well.
TreeFactory treeFactory = new();
Itree tree = treeFactory[“pine”, “green”, “short”];
tree.SetCoord(1, 1);
Itree tree2 = treeFactory[“pine”, “green”, “short”];
tree2.SetCoord(2, 2);
Itree tree3 = treeFactory[“pine”, “green”, “short”];
tree3.SetCoord(3, 3);
In the preceding code, in the third line, the SetCoord method sets the values of Coord_X and Coord_Y with the value 1. But in the fifth line, these values are shifted by 2, and this makes tree1 and tree2 objects see both Coord_X and Coord_Y attribute values equal to 2. To solve the problem, it will be necessary to separate the storage location of fixed properties and variable properties. Pay attention to the following code:
public class TreeType : Itree
{
public string Name { get; private set; }
public string Color { get; private set; }
public string Size { get; private set; }
public TreeType(string name, string color, string size)
{
Name = name;
Color = color;
Size = size;
}
public void Draw(Itree tree)
{
var obj = (Tree)tree;
Console.WriteLine($”
TreeType:{GetHashCode()},{Name},
Tree:{obj.GetHashCode()}({obj.Coord_X},{obj.Coord_Y})”);
}
}
…
In the preceding code, the fixed properties are completely separated from the variable properties. Fixed properties are moved into the TreeType class and variable properties are placed into the Tree class. TreeFactory class creates or provides TreeType object through indexer. To use this structure, you can do the following:
List
TreeType type = new TreeFactory()[“pine”, “green”, “short”];
Tree tree1 = new(type, 1, 1);
trees.Add(tree1);
Tree tree2 = new(type, 2, 2);
trees.Add(tree2);
Tree tree3 = new(type, 3, 3);
trees.Add(tree3);
foreach (var item in trees)
{
item.Draw(item);
}
In second line, the TreeFactory object with the provided properties is requested. Then this object has been sent to the Tree class in lines 3, 5 and 7 to create the desired trees in the desired coordinates. Now, the type variable, which is of TreeType type, is shared between all three objects, tree1, tree2, and tree3. The output of the preceding code will be as follows:
TreeType:58225482,pine,Tree:54267293(1,1)
TreeType:58225482,pine,Tree:18643596(2,2)
TreeType:58225482,pine,Tree:33574638(3,3)
As it is clear in the output, only one object of TreeType type is created and it is shared between three objects tree1, tree2 and tree3. This sharing will greatly reduce the consumption of resources such as memory.
This design pattern is very similar to singleton design pattern. However, there are some differences between these two design patterns. Including:
In singleton, there is only one object, but in flyweight, there is one object per fixed property class.
The object created through singleton is mutable while the object created by flyweight is immutable.
Same as the singleton design pattern, concurrency topics should be considered in this pattern.
Consequences: Advantages
Memory consumption is significantly reduced.
Consequences: Disadvantages
Code complexity increases.
Applicability:
When there is a need to create a large number of objects and memory is not available for this amount of created objects.
Related patterns:
Some of the following design patterns are not related to Flyweight design pattern, but in order to implement this design pattern, checking the following design patterns will be useful:
Singleton
State
Strategy
Interpreter
Composite
Facade
Proxy
In this section, the proxy design pattern is introduced and analyzed, according to the structure presented in GoF design patterns section in Chapter 1, Introduction to Design Pattern.
Name:
Proxy
Classification:
Structural design patterns
Also known as:
Surrogate
Intent:
This design pattern tries to control the use of an object by providing a substitute for it.
Motivation, Structure, Implementation, and Sample code:
Suppose we are designing an infrastructure to communicate with a GIS service. For the sake of simplicity. Let us assume that the requirement is such that we need to provide the name of a geographic region (country, city, street, and so on.), and receive information about the latitude and longitude of that region.
In order to implement this mechanism, one option is to connect the user directly to the GIS service. But by doing this, we notice the slow response, because for every request sent to the GIS service, this service tries to find the latitude and longitude from the beginning.
Another way to implement this requirement is to send the requests to an intermediary instead of directly from the user to the GIS service, and then send the request to the GIS service through that intermediary. This is called a proxy. By doing this, we will get several advantages, including being able to cache requests and responses for some popular requests from the proxy side before sending them to GIS. With this, we will no longer need to submit any request to the GIS service. Also, in the presence of the proxy, you can record the request and response logs. In the presence of proxy, requests can be controlled.
According to the preceding explanations, the following class diagram can be imagined:
Figure3.12.png
Figure 3.12: Proxy design pattern UML diagram
As shown in the Figure 3.12 class diagram, the GISService class has a method called GetLatLng, which takes the name of the geographic region and returns the longitude and latitude of that region. We need to connect the user through a proxy instead of directly connecting to the GISService. For this, in order for the user to avoid being involved in changes and design details, it is necessary to design the GISServiceProxy class in such a way that its methods have a proper mapping with GISService. For this, the IGISService interface is defined and both GISService and GISServiceProxy classes have implemented this interface. In this case, the user can easily connect to the proxy, and perform the necessary control and monitoring tasks in the proxy, and finally, the request is sent to GISService if needed, and the response is received by the proxy and delivered to the user. According to these explanations, the following codes can be considered:
public interface IGISService
{
string GetLatLng(string name);
}
public class GISService : IGISService
{
static readonly Dictionary
{
{ "Tehran", "35.44°N,51.30°E" },
{ "Urmia", "37.54°N,45.07°E" },
{ "Khorramabad", "33.46°N,48.33°E" },
{ "Shahrekord", "32.32°N,50.87°E" },
{ "Zahedan", "29.45°N,60.88°E" },
{ "Ilam", "33.63°N,46.41°E" },
{ "Yasuj", "30.66°N,51.58°E" },
{ "Ahvaz", "31.31°N,48.67°E" },
{ "Rasht", "37.26°N,49.58°E" },
{ "Sari", "36.56°N,53.05°E" },
{ "Sanandaj", "35.32°N,46.98°E" },
{ "Ardabil", "37.54°N,45.07°E" }
};
public string GetLatLng(string name)
{
Thread.Sleep(5000);
return map.FirstOrDefault(x => x.Key == name).Value;
}
}
public class GISServiceProxy : IGISService
{
static readonly Dictionary
static readonly GISService _gisService = new();
public string GetLatLng(string name)
{
var requestOn = DateTime.Now.TimeOfDay;
if (!mapCache.ContainsKey(name))
{
string latlng = _gisService.GetLatLng(name);
if (!string.IsNullOrWhiteSpace(latlng))
mapCache.TryAdd(name, latlng);
else
throw new Exception("Given Geo not found!");
}
var responseOn = DateTime.Now.TimeOfDay;
return
$"Geo:{name},
Sent:{requestOn},
Received:{responseOn},
Response:{mapCache[name]}";
}
}
In the preceding code, for the purpose of simulation, within the GetLatLng method in the GISService class, a five-second delay is considered for each incoming request. However, if we pay attention to the implementation of this method in the GISServiceProxy class, we will notice that the responses received from the GISService are somehow cached, and subsequent requests for a fixed geographic region are answered without referring to the GISService, so we face an increase in the response speed. In order to execute the preceding code, you can do as follows:
IGISService gisService = new GISServiceProxy();
Console.WriteLine(gisService.GetLatLng(“Urmia”));
Console.WriteLine(gisService.GetLatLng(“Tehran”));
Console.WriteLine(gisService.GetLatLng(“Urmia”));
In the preceding code, when the Urmia query is sent for the first time, the response to this query is received with a delay of five seconds. This is while Urmia's re-query is no longer delayed and the direct response from the cache is returned by the proxy. Therefore, for the preceding code, we will have the following output:
Geo:Urmia,Sent:16:56:47.5390793,Received:16:56:52.5762895,
Response:37.54°N,45.07°E
Geo:Tehran,Sent:16:56:52.5932198,Received:16:56:57.6020481,
Response:35.44°N,51.30°E
Geo:Urmia,Sent:16:56:57.6052643,Received:16:56:57.6053073,
Response:37.54°N,45.07°E
In the preceding output, it can also be seen that, the Urmia query was sent at 16:56:47 and its response was received at 16:56:52, that is, about five seconds later, while the second query of the same city, without a five second delay, sent at 16:56:57 and received a response within a few milliseconds.
Participants:
Subject: which is the same as IGISService in the preceding scenario, is responsible for defining the format based on RealSubject. Using the provided template, wherever RealSubject is needed, it can be replaced with proxy.
Proxy: which is the GISServiceProxy in the preceding scenario, is in charge of communicating with RealSubject based on the format provided by Subject. This communication can be done for various purposes such as access control, logging, and so on. Proxy, methods are defined like RealSubject methods, so that the client does not notice possible changes.
RealSubject: In the preceding scenario, it is the same as GISService, it is responsible for defining the real object based on the format provided by subject. The proxy object is supposed to be replaced with this object.
Client: It is the same user who executes the code through proxy.
Notes:
Different types of proxy can be defined, including:
Remote proxy: In this type of proxy, subject and proxy are located in two different places, and in fact, we are trying to define a local proxy for the subject that is in another place. This type of proxy is very similar to the concept of using a web service. For example, suppose the client intends to use two web services provided at an external address. In this case, the task of communicating with the external service and related settings can be defined in a remote proxy, and the client communicates with the proxy instead of needing to communicate with the web service while repeating the code. Therefore, the following code can be considered for it:
public class RemoteProxy : IRemoteService
{
readonly HttpClient _client;
public RemoteProxy() => _client = new HttpClient
{
BaseAddress = new Uri("https://jinget.com/api")
};
public async Task
=>await (await
_client.GetAsync($"/users/{id}")).Content.ReadAsStringAsync();
public async Task
=>await (await
_client.GetAsync("/users")).Content.ReadAsStringAsync();
}
In the preceding code, the Subject is located at a remote address at https://jinget.com.
Virtual proxy: This type of proxy can be used when the process of creating an object from a subject is time-consuming or expensive. In this case, the object is created from the subject only when it is really needed. This type of proxy is also called Lazy initialization proxy.
Protection proxy: This type of proxy can be used when we intend to control access to the requested resource. For example, by using this type of proxy, it is possible to check whether the client has the right to access this resource before connecting to the subject.
Smart proxy: This type of proxy is usually used when we need to do some specific tasks before accessing the subject object. For example, we may need to prevent the creation of multiple objects from subject through singleton implementation.
Logging proxy: There are times when we need to log all references to subject.
Caching proxy: There are times when we need to save the results of references to subject. In the scenario mentioned preceding, this type of proxy has been used.
When using this design pattern, not necessarily all requests may be sent to subject.
By using the copy on write technique, you can prevent the creation of additional objects. Using this technique, a new object is created when there is a need to change the object, otherwise the existing object is shared among all.
Using this design pattern, proxy can only sometimes know the type of subject. When the proxy needs to create an object from the subject, it needs to know its type, otherwise it does not need to know the subject type.
Consequences: Advantages
The object related to RealSubject can be controlled without the client noticing. This control can even include the control of the life span of the object.
Without the need to touch RealSubject codes and change them, a new Proxy can be created, and therefore OCP has been observed.
By using a remote proxy, the fact that the subject is located in another place is hidden from the client's view.
By using a virtual proxy, you can create the subject object if needed or reuse the existing objects. This can improve performance.
Consequences: Disadvantages
Due to the definition of many classes and methods, it can lead to an increase in code complexity.
This template can cause a delay in receiving the final response.
Client may use RealSubject in some places and proxy in some other places, which causes confusion in the structure and code.
Applicability:
When it is necessary to perform a series of tasks such as: access control, caching, logging, and so on before connecting to the subject, then this design pattern can be used.
When we need to use a series of services located at an external address (such as web service, connection with Windows services, and so on), we can use this design pattern.
Related patterns:
Some of the following design patterns are not related to Proxy design pattern, but in order to implement this design pattern, checking the following design patterns will be useful:
Adapter
Decorator
façade
Conclusion
In this chapter, you learned about structural design patterns and learned how to manage the connections between classes and objects for different scenarios. In the next chapter, you will learn about behavioral design patterns and learn how to deal with behavior of objects and classes.
1 Adapter design pattern is also known as Wrapper too.
Join our book's Discord space
Join the book's Discord Workspace for Latest updates, Offers, Tech happenings around the world, New Release and Sessions with the Authors:
https://discord.bpbonline.com