Chapter 4
Behavioral Design Patterns – Part I
Introduction
Behavioral design patterns deal with the behavior of objects and classes. Different methods and algorithms for doing the same work between different objects are the main goals and focal points of this category of design patterns. In this category of design patterns, not only objects and classes are discussed, but the relationship between them is also discussed.
Structure
In this chapter, we will cover the following topics:
Chain of responsibility
Command
Mediator
State
Strategy
Template method
Objectives
It is expected that by the end of this chapter, you will be familiar with the six most famous behavioral design patterns and be able to understand their differences. Using the points presented in this chapter, it is still expected that you will be able to design the correct infrastructure for the classes to fulfil their expected behaviors.
Behavioral design patterns
In this category of design patterns, there are a total of eleven different design patterns, and in this chapter, the six most famous design patterns of this category are introduced, which are:
Chain of responsibility: It allows sending requests along a chain of objects.
Command: Encapsulate a request or task in the form of an object.
Mediator: Facilitate the communication between several objects by directing toward a central object.
State: Change the behavior of the object by changing its internal state.
Strategy: Separate different methods and algorithms for the execution of a task by forming an algorithmic family.
Template method: While defining the main body of the execution of a task, the steps of its execution are assigned to the child classes.
Behavioral design patterns are divided into two types: class and object. Class categories try to distribute a behavior between classes using inheritance. Template Method and Interpreter (Which will discuss in Chapter 5, Behavioral Design Patterns, Part II) are included in this category. On the other hand, the object category aims to show how objects can work together to do a task by using composition. Other behavior patterns are also included in this category.
Apart from the eleven GoF behavioral design patterns, other design patterns deal with the behavior between objects and classes. Including:
Blackboard pattern: Provides the possibility of integrating specialized and diverse modules for which there is no definite strategy for their development.
Null object pattern: Provide the default object value.
Protocol stack: Provide communication by different layers, like the different layers of the network.
Scheduled task pattern: Provide the possibility of executing a task in certain time frames.
Single-serving visitor pattern: It is an improved version of the Visitor design pattern.
Specification pattern: By using Boolean logic, it tries to combine different business logic.
Chain of responsibility
Name:
Chain of responsibility
Classification:
Behavioral design patterns
Also known as:
---
Intent:
This design pattern tries to make the incoming request move along a chain of processors, and at each step, each member of the chain checks the request and decides to deliver the request to the next member of the chain or terminate the request.
Motivation, Structure, Implementation, and Sample code:
Suppose that a requirement is raised and it is requested to design and implement an infrastructure through which users can create a web service. In general, the mechanism is the request that is created by the user and sent to the server, and the corresponding web service is found and executed on the server side.
After some time, it is announced that requests must be authenticated before running the web service. After a while, it is announced that access must be controlled for the requests after authentication. These continuous changes in the requirements gradually cause the quality of the code to drop.
To avoid this, the whole process can be seen as a chain, where each member of this chain performs a task. For example, the first node performs authentication, and the second node performs access control. If we want to add the request log mechanism later, we can easily define a node in the chain for it and then put it in place. Refer to Figure 4.1:
Figure%204.1.png
Figure 4.1: Calling web service request processing chain
With these explanations, it can be concluded that we are finally facing a series of handlers, and each handler is doing its work. Each handler has several tasks, including executing its logic and pointing to the next node. With these explanations, we are faced with the following class diagram:
Figure%204.2.png
Figure 4.2: Chain of Responsibility design pattern UML diagram
In Figure 4.2, the Handle method is responsible for implementing the logic of each node. This method should call the Next method to maintain the execution sequence. The ContinueWith method specifies the next node in the chain and stores its value in _successor, and the Next method executes the next nodes in the chain. For the diagram in Figure 4.2, we have the following code:
public class Request
{
public string IP { get; set; }
public string Url { get; set; }
public string Username { get; set; }
}
public abstract class RequestHandler
{
RequestHandler _successor;
public abstract void Handle(Request request);
public void ContinueWith(RequestHandler handler) { _successor = handler; }
protected void Next(Request request)
{
if (_successor != null)
_successor.Handle(request);
}
}
public class AuthenticationHandler : RequestHandler
{
public override void Handle(Request request)
{
if (!string.IsNullOrWhiteSpace(request.Username) &&
UserHasAccess(request.Username, request.Url))
Next(request);
else
throw new Exception("User not authenticated");
}
}
public class AuthorizationHandler : RequestHandler
{
public override void Handle(Request request)
{
if (request.IP.StartsWith("10."))
Next(request);
else
throw new Exception("Access Denied");
}
}
public class LoggingHandler : RequestHandler
{
public override void Handle(Request request)
{
//log the request here
Next(request);
}
}
To use the preceding structure, you can use the following code:
RequestHandler authenticationHandler = new AuthenticationHandler();
RequestHandler authorizationHandler = new AuthorizationHandler();
RequestHandler loggingHandler = new LoggingHandler();
authenticationHandler.ContinueWith(authorizationHandler); //Line 4
authorizationHandler.ContinueWith(loggingHandler); //Line 5
authenticationHandler.Handle(new Request //Line 6
{
IP = "192.168.1.1",
Username = "vahid",
Url = "http://abc.com/get"
});
As it is clear in lines 4 and 5 of the preceding code, each handler, using the ContinueWith method, has specified what handler should be the next node of the chain, and then in line 6, the execution of the chain is started, and then with the help of the Next method, this execution is carried out from node to node.
Participants:
Handler: In the preceding scenario, the RequestHandler is responsible for defining the format for processing requests.
Concrete handler: In the preceding scenario, it is the same as AuthenticationHandler, AuthorizationHandler, and LoggingHandler and is responsible for processing the relevant request. The behavior in this section is that if the handler is responsible for processing the request, it performs the processing; otherwise, it transfers the request to the next node.
Client: The same user is responsible for starting the request processing in the chain through the concrete handler.
Notes:
One of the tasks of the ConcreteIterator is to maintain the position of the current object.
Care should be taken not to form a loop in the design of the chain
Another example of implementing this design pattern can be Middleware in ASP.NET Core.
Handlers can be implemented using composite.
Consequences: Advantages
The request processing order can be controlled. For example, check IP access first, then authenticate the user, then control the access.
The Single Responsibility Principle (SRP) is upheld because the classes that are tasked with carrying out the operation are kept apart from the classes that are tasked with implementing it.
The open/close principle is met. It is possible to add new handlers to the chain or remove existing handlers without changing the user code. This increases flexibility.
Dependency is reduced. By using this design pattern, both the sender and receiver of the request do not have detailed information about each other and are not dependent on each other. Also, the input request has no information about the chain structure.
Consequences: Disadvantages
Some requests may not be processed due to not finding a suitable handler, and therefore there is no guarantee that the request will be processed.
Applicability:
When it is necessary to perform different processes on a request in order.
When you are faced with variable handlers while processing a request, the order of the handlers is not predetermined.
Related patterns:
Some of the following design patterns are not related to the chain of responsibility design pattern, but to implement this design pattern, checking the following design patterns will be useful:
Composite
Decorator
Observer
Command
Command
Name:
Command
Classification:
Behavioral design patterns
Also known as:
Action/Transaction
Intent:
This design pattern tries to encapsulate a request or work in the form of an object. Further, this object can be queued and executed to process and perform assigned work. This feature makes it possible to undo a request before it is processed or interrupt the execution of the task.
Motivation, Structure, Implementation, and Sample code:
Suppose we are designing a system for the human resources unit of the organization. One of the stated requirements is that after hiring a new employee, it should be possible to define an email account, identification card, and business card for him. This requirement has been introduced in such a way that the human resource unit expert must register his request to perform all three tasks in the system. We also need to design the implementation of these three tasks in the form of a transaction so that if any one of the tasks is not done, the rest of the tasks are not done either.
With these explanations, it is clear that we are dealing with a series of commands, such as the command to create an email account, the command to create an ID card, and the command to create a business card. Each of these commands should be able to do two different things according to the requirement description. For example, the email account creation command must be able to create the email account and undo the creation of the email account. We are also faced with a set of commands. Each of these commands is a member of this set and starts to be executed in a way.
With these words, Figure 4.3 class diagram can be imagined:
Figure%204.3.png
Figure 4.3: Command design pattern UML diagram
As shown in the Figure 4.3 diagram, the expert of the human resources unit (Client) submits his request to the Recruitment class, and this class executes the commands through the commands that have been registered, and whenever a problem occurs, it undoes the operation he/she does. In this design, it is clear that the Client is not directly connected with the EmployeeManager, because the Client may not know the logic of executing tasks or canceling tasks. The Client only presents their request, and this request is queued and executed in the form of a series of commands. For Figure 4.3 design, the following codes can be imagined:
public interface IRecruitmentCommand
{
void Execute();
void Undo();
}
public class CreateEmailCommand : IRecruitmentCommand
{
EmployeeManager _manager;
public CreateEmailCommand(EmployeeManager manager) => _manager = manager;
public void Execute() => _ manager.CreateEmailAccount();
public void Undo() => _ manager.UndoCreateEmailAccount();
}
public class DesignIdentityCardCommand : IRecruitmentCommand
{
EmployeeManager _ manager;
public DesignIdentityCardCommand(EmployeeManager manager)
=> _manager = manager;
public void Execute() => _ manager.DesignIdentityCard();
public void Undo() => _ manager.UndoDesignIdentityCard();
}
public class DesignVisitingCardCommand : IRecruitmentCommand
{
EmployeeManager _ manager;
public DesignVisitingCardCommand(EmployeeManager manager)
=> _manager = manager;
public void Execute() => _ manager.DesignVisitingCard();
public void Undo() => _ manager.UndoDesignVisitingCard();
}
/*
For simplicity, the implementation for the methods of this class is not provided
*/
public class EmployeeManager
{
public EmployeeManager(int employeeId) { }
public void CreateEmailAccount() { }
public void UndoCreateEmailAccount() { }
public void DesignIdentityCard() { }
public void UndoDesignIdentityCard() { }
public void DesignVisitingCard() { }
public void UndoDesignVisitingCard() { }
}
public class Recruitment
{
public List
= new List
public void Invoke()
{
try
{
foreach (var command in Commands)
command.Execute();
}
catch //If an error occurs, all tasks must be canceled from the beginning
{
foreach (var command in Commands)
command.Undo();
}
}
}
To use this structure, you can do the following:
EmployeeManager employeeManager = new(1); // New employee with ID 1 is being recruited
Recruitment recruitment = new();
recruitment.Commands.Add(new CreateEmailCommand(employeeManager));
recruitment.Commands.Add(new DesignIdentityCardCommand(employeeManager));
recruitment.Commands.Add(new DesignVisitingCardCommand(employeeManager));
recruitment.Invoke();
As it is clear in the preceding code, the Client first creates an object of the type EmployeeManager. Then, instead of using the methods inside this class, it sets commands through Recruitment and then assigns the task of executing the request to Recruitment.
Participants:
Command: In the preceding scenario, it is the IRecruitmentCommand, which is responsible for defining the template for executing requests. The command usually declares methods such as Execute or Undo.
ConcreteCommand: In the preceding scenario, it is the same as CreateEmailCommand, DesignIdentityCardCommand, and DesignVisitingCardCommand, which is responsible for implementing the structure provided by the command. This implementation happens in the form of calling the desired work (Action) through the receiver object.
Invoker: In the preceding scenario, it is Recruitment that has the task of asking a command to execute the request.
Receiver: In the preceding scenario, it is the same as EmployeeManager and is responsible for implementing the logic of each task.
Client: It is the same user who is responsible for creating ConcreteCommand objects and sending them to the receiver.
Notes:
By using serialization, commands can be converted into strings and stored in the data source. They can even be sent over the network to a remote user.
To better implement the undo process, a class can be used as history. All the operations that are executed are stored in the form of a stack data structure. Next, you can perform the undo operation by scrolling the stack. The implementation of the undo operation with this design pattern does not seem simple because, during an operation, some private variables may have changed. To solve this problem, you can use the Memento design pattern.
Consequences: Advantages
The single responsibility principle is upheld because the classes that implement the operation are distinct from the classes that invoke the operation.
New commands can be defined and used without changing the existing codes. For this reason, the Open/Close principle is observed.
Undo and redo processes can be implemented.
It is possible to implement the operation with a delay.
Consequences: Disadvantages
Since a new layer is added between the sender and receiver, the complexity of the design increases.
Applicability:
When we need to take a break between tasks, or we have tasks that need to be executed according to a schedule.
When we need to have an operation with undo capability.
The handlers in the chain of responsibility design pattern can be implemented with the help of this design pattern.
To save a copy of the command, you can use the prototype design pattern.
When it is necessary to implement the call-back mechanism, commands are an object-oriented alternative to callbacks. This is achieved with the help of sending actions through parameters to objects.
Related patterns:
Some of the following design patterns are not related to the command design pattern, but to implement this design pattern, checking the following design patterns will be useful:
Prototype
Composite
Memento
Observer
Chain of responsibility
Mediator
Name:
Mediator
Classification:
Behavioral design patterns
Also known as:
---
Intent:
This design pattern tries to communicate between different objects through a coordination center. The coordination center has the task of maintaining communication, and in this way, the applicant and the respondent can communicate and do not need information.
Motivation, Structure, Implementation, and Sample code:
Suppose a requirement has been raised in which it is requested to provide the possibility of conversation between users. Users will need to chat, specify the text of the message and the recipient of the message, send the message, and the other party will receive the message. For the design of this requirement, it is clear that if the users are in direct communication with each other, the complexity will be very high.
On the other hand, users can send and receive messages through an interface. In this case, users will not need to communicate with each other directly, and all their transactions will be done through the interface. So far, the scenario can be concluded that on one side of the design, we have the users who need to talk to each other, which is called colleagues, and on the other side, we have the communication interface, which is called the mediator. With the preceding explanations, the following class diagram can be imagined:
Figure%204.4.png
Figure 4.4: Mediator design Pattern UML diagram
As it is clear from the Figure 4.4 class diagram, different users can talk to each other, all of them are defined in this scenario through the Participant class and have implemented the IParticipant interface, and in this way, they provide their implementation for the Send and Receive methods. The responsibility of these two methods is to send and receive messages. On the other hand, a communication intermediary called Chatroom has been defined, which implements the IChatroom interface and provides its implementation of Login, Send, and Logout methods. The user sends a message through this method. For the Figure 4.4 class diagram, you can have the following code:
public interface IChatroom
{
void Login(IParticipant participant);
void Send(string from, string to, string message);
void Logout(IParticipant participant);
IParticipant GetParticipant(string name);
}
public class Chatroom : IChatroom
{
Dictionary
public void Login(IParticipant participant)
{
if (!participants.ContainsKey(participant.Name))
participants.Add(participant.Name, participant);
}
public void Logout(IParticipant participant)
{
if (participants.ContainsKey(participant.Name))
participants.Remove(participant.Name);
}
public void Send(string from, string to, string message)
{
IParticipant receiver = participants[to];
if (receiver != null)
receiver.Receive(from, message);
else
throw new Exception("Invalid participant");
}
public IParticipant GetParticipant(string name)
=> participants.ContainsKey(name) ? participants[name] : null;
}
public interface IParticipant
{
public string Name { get; set; }
void Send(string to, string message);
void Receive(string from, string message);
}
public class Participant : IParticipant
{
private readonly IChatroom room;
public string Name { get; set; }
public Participant(string name, IChatroom room)
{
this.Name = name;
this.room = room;
}
public void Receive(string from, string message)
=> Console.WriteLine($"Sender: {from}, To: {Name}, Message: {message}");
public void Send(string to, string message)
=> room.Send(Name, to, message);
}
As can be seen in the preceding code, when the Send method is called in Participant, instead of directly delivering the request to the target, this method delivers the request to the room object. This object plays the role of mediator. Then, upon receiving the Send request, the room object calls the Receive method and delivers the message to the recipient. Also, the IChatroom interface in this example has an auxiliary method called GetParticipant, which is not related to the design and is only used to find other users.
Participants:
Mediator: In the preceding scenario, it is IChatroom and is responsible for defining the format for communication with colleagues.
ConceretMediator: In the preceding scenario, it is the Chatroom and is responsible for implementing the format provided by the mediator, and in this way, it communicates between colleagues. This class needs to know the colleagues in order to communicate.
Colleague: In the preceding scenario, it is the IParticipant and Participant who is always in contact with the Mediator, and whenever any of the colleagues needs to communicate with another colleague, they communicate through the mediator.
Client: It is the same user who executes the code through the mediator and ConcreteMediator.
Notes:
Mediator must implement collaborative communication between colleagues.
The purpose of this design pattern is to eliminate interdependencies.
When there is only one mediator, there is no need to define mediator and ConcreteMediator, and colleagues can directly communicate with ConcreteMediator. This principle also applies to colleagues.
Consequences: Advantages
By using this design pattern, communication between objects is transferred to a single place, and in this way, while complying with the single responsibility principle, maintaining the code is also easier. You can also focus on the communication logic of objects through Mediator and on the business of objects through colleagues.
Without the need to change the codes, a new mediator can be defined, and in this way, the Open/Close principle has been observed.
Stickiness between classes decreases, and colleagues communicate through the mediator.
Reusability is improved, and colleagues can be used by defining the new mediator.
By using this design pattern, many-to-many relationships become one-to-many relationships. One-to-many relationships are much simpler and easier to maintain and understand.
Consequences: Disadvantages
Over time, mediator objects themselves become objects that contain many complexities, which are called God objects, which can cause problems.
Applicability:
When it is not possible to change classes due to dependence on other classes, in this case, all communication is taken out of the class and established through a mediator.
When we need to use an object in another program, it is not possible due to many dependencies on other objects. In this case, with the presence of a mediator, communications are taken to this class, and therefore the class can be used in other programs as well. A sufficient condition to use the class in another application is to create a new mediator.
Related patterns:
Some of the following design patterns are not related to the mediator design pattern, but to implement this design pattern, checking the following design patterns will be useful:
Facade
Observer
State
Name:
Adapter
Classification:
Behavioral design patterns
Also known as:
Objects for states
Intent:
This design pattern tries to change the behavior of the object by changing its internal state. In this case, different situations are presented in the form of different objects.
Motivation, Structure, Implementation, and Sample code:
Suppose there is a requirement in which a workflow engine is requested to be produced for the support process of a company. The support process in this company is that the manager of the support unit sends a ticket to one of the experts from the list of registered tickets; the expert checks the ticket and takes the necessary action. Then it waits for the confirmation of the user who registered the ticket. After the end user's approval, the support manager makes a final check, the ticket is closed, and the next ticket is sent to the expert. This description of the state diagram of this process can be considered as follows:
Figure%204.5.png
Figure 4.5: Company's support process
To implement this entire structure, you can use only one class and control different states and movement between these states through if and switch blocks. In this way of implementation, by adding a new state to the process or applying changes to any of the existing states, the class codes will be changed, which will make the code testing process be done from the beginning. Therefore, this type of design will require code maintenance and development.
However, to design the preceding process, each situation can be defined as a class, and the business related to each situation can be encapsulated in that class. In this case, the problem raised about adding a new status, or changing the business of the existing status, will not affect the codes of other statuses. After each state is converted to a class, an interface is needed to process tickets. This interface class maintains the status of each ticket and allows the processing of each ticket.
According to the preceding explanations, the following class diagram can be imagined:
Figure%204.6.png
Figure 4.6: State design pattern UML diagram
As can be seen in the preceding class diagram, each of the states has become a series of classes that implement the ITicketState interface, and for this reason, they must provide their implementation for the Process method. Through this method, the work that needs to be done in any situation is carried out. Also, the TicketContext class contains the State variable through which it stores the state of each ticket. The Process method in this class is also responsible for using State to call the appropriate Process method in one of the AssignState, DoingState, ApprovalState, or ClosingState classes. According to these explanations, the following code can be imagined for Figure 4.6:
public interface ITikcetState
{
void Process(TicketContext context);
}
public class AssignState : ITikcetState
{
public void Process(TicketContext context)
{
Console.WriteLine("Ticket Assigned");
context.State = new DoingState();
}
}
public class DoingState : ITikcetState
{
public void Process(TicketContext context)
{
Console.WriteLine("Ticket Done");
context.State = new ApprovalState();
}
}
public class ApprovalState : ITikcetState
{
public void Process(TicketContext context)
{
Console.WriteLine("Ticket Approved");
context.State = new ClosingState();
}
}
public class ClosingState : ITikcetState
{
public void Process(TicketContext context)
{
Console.WriteLine("Ticket Closed");
context.State = new AssignState();
}
}
public class TicketContext
{
public ITikcetState State { get; set; }
public TicketContext(ITikcetState state) => this.State = state;
public void Process() => State.Process(this);
}
As can be seen in the preceding code, every time the Process method is called in the TicketContext class, the state of the ticket will change and go to the next state. To use the preceding code, you can do the following:
TicketContext context = new(new AssignState());
context.Process();
In the first line, the initial state is AssignState, so by calling the Process method in the second line, the Process method in AssignState will be executed. This method will also change the state of the ticket to DoingState after the work is done. Therefore, by calling the Process method in the TicketContext class again, this time, the Process method in the DoingState class will be executed, and this cycle will proceed in the same way.
Participants:
Context: In the preceding scenario, it is the TicketContext and is responsible for maintaining the current status. Also, this class is responsible for defining the appropriate framework for use by the client.
State: In the preceding scenario, ITicketState is responsible for defining the format for processing states.
ConcreteState: In the preceding scenario, it is AssignState, DoingState, ApprovalState, and ClosingState, which is responsible for implementing the business related to each state according to the format provided by State.
Client: It is the same user who executes the codes through context.
Notes:
Both the context class and the ConcreteState class can change the state of the object.
If the classes related to different states need context, according to the preceding scenario, you can send a context object to ConcreteState.
Both context and ConcreteState can determine the next state related to the current state based on the conditions.
The client sets the initial state through context. After the initial setting, the client no longer needs to interact directly with the state objects.
There are two modes regarding the creation and destruction of state objects. In the first case, the state object is created whenever necessary and destroyed after the work is done. The second state of the state object is never destroyed after it is created. The choice between these two modes depends on different modes. For example, if the status changes do not occur continuously or the statuses are unclear at the time of execution, the first mode is a suitable option. This mode prevents the creation of objects that are not used. But if the situation changes continuously, you can use the second mode.
State objects can often be defined using the singleton pattern.
Consequences: Advantages
Considering that the codes related to each situation are transferred to the corresponding molasses, the single responsibility principle has been observed.
Since by creating a new state, there is no change in the classes of the previous situations and the context class, the open/close principle has been observed.
By removing if blocks and similar blocks from the context class, existing codes become simpler, and maintenance and development are easier.
Consequences: Disadvantages
If the number of states is small, using this design pattern causes complexity.
Applicability:
It can be useful when an object should have different behaviors based on different states.
When an object has a large number of states and the codes for each state are constantly changing.
When there is a class in which there are a large number of if blocks or similar blocks that change the behavior provided by the methods of the class.
When there are duplicate codes for each situation.
Related patterns:
Some of the following design patterns are not related to the state design pattern, but to implement this design pattern, checking the following design patterns will be useful:
Singleton
Flyweight
Strategy
Strategy
Name:
Strategy
Classification:
Behavioral design patterns
Also known as:
Policy
Intent:
This design pattern tries to separate different methods and algorithms of executing a task by forming an algorithm family. The formation of this family will make it possible to replace the algorithms with the least changes when using them.
Motivation, Structure, Implementation, and Sample code:
Suppose a requirement is raised during which it is requested to provide the possibility of outputting the data of a table. Table data can be exported to CSV, TXT, and XML formats. To implement this requirement, different output algorithms can be mentioned in one class. This method, like the problem discussed in the state design pattern, will cause changes in an algorithm or the addition of a new algorithm, leading to changes in class codes. This makes the code more difficult to maintain.
But it is possible to implement this method, similar to the approach that existed in the state design pattern, to implement each algorithm in a separate class. On the other hand, each algorithm forms a family by implementing and following an interface. The user can also communicate with this family by using the context class. In this way, applying a change in an algorithm or adding a new algorithm will affect only the same algorithm, and this will increase the maintainability and extensibility of the code.
According to the preceding explanations, the following class diagram can be imagined:
Figure%204.7.png
Figure 4.7: Strategy design pattern UML diagram
As can be seen in the Figure 4.7 class diagram, different algorithms include XMLExporter, CSVExporter, and TXTExporter classes, which all implement the IExporter interface and, in this way, provide their implementation for the Export method. The ExportContext class also takes the type of algorithm required by the user and calls the corresponding Export method using the Process method. This type of algorithm or work execution method is called strategy. For Figure 4.7 class diagram, the following code can be considered:
public interface IExporter
{
void Export(object data);
}
public class XMLExporter : IExporter
{
public void Export(object data)
=> Console.WriteLine("data exported to xml");
}
public class CSVExporter : IExporter
{
public void Export(object data)
=> Console.WriteLine("data exported to csv");
}
public class TXTExporter : IExporter
{
public void Export(object data)
=> Console.WriteLine("data exported to txt");
}
Public class ExportContext
{
private readonly IExporter _strategy;
public ExportContext(IExporter strategy) => this._strategy = strategy;
public void Process(object data) => _strategy.Export(data);
}
To use this code, you can do the following:
ExportContext export = new ExportContext(new XMLExporter());
export.Process(new { Name = "Vahid", LastName = "Farahmandian" });
Participants:
Strategy: In the preceding scenario, it is the same as IExporter and is responsible for defining the format for defining algorithms.
ConcreteStrategy: In the preceding scenario, it is XMLExporter, CSVExporter, and TXTExporter, and it is responsible for implementing the format provided by the strategy.
Context: Allows the client to use different algorithms. ConcreteStrategies can access the data of this class.
Client: The same user executes the code by presenting the strategy to the Context.
Notes:
Using inheritance, common codes between algorithms can be transferred to another class
Generics can also be used in context implementation. When the strategies are clear at the time of compilation, and there is no need to change them at the time of execution, then this method can be useful.
Sending strategy to context can be optional. In this case, if the user does not introduce the desired strategy, the context class will use the default strategy to advance the work.
There are different ways to implement this design pattern. One of these methods is using delegates. For this type of implementation, we have the following code:
public delegate void Export(object data);
public class Exporter
{
public static void XMLExport(object data)
{ Console.WriteLine("data exported to xml"); }
public static void CSVExport(object data)
{ Console.WriteLine("data exported to csv"); }
public static void TXTExport(object data)
{ Console.WriteLine("data exported to txt"); }
}
public class ExportContext
{
private Export _strategy;
public ExportContext(Export strategy) => this._strategy = strategy;
public void Process(object data) => _strategy(data);
}
which can be used as follows:
ExportContext export = new(new Export(Exporter.XMLExport));
export.Process(new { Name = "Vahid", LastName = "Farahmandian" });
This design pattern and the state design pattern have many similarities in the first point, but they have differences, including:
The state design pattern has to do with the internal state of a class, while the strategy pattern is just an algorithm that is used to do something and has nothing to do with the internal state of the class.
In the state design pattern, the state object usually needs a context to access information from the context. But in the strategy design pattern, each algorithm is an independent class and does not need client information.
In general, the state design pattern tries to do different things based on different situations, but the strategy design pattern tries to do the same thing with different existing methods.
Consequences: Advantages
It is possible to change the used algorithm during the execution.
The implementation details of an algorithm can be separated from the user of the algorithm.
It is possible to define a new algorithm without changing the existing codes or change the existing algorithm without changing other algorithms. For this reason, the Open/Close principle has been observed.
Consequences: Disadvantages
If we are dealing with a small number of algorithms that rarely change, using this pattern can increase the complexity.
The client needs to know the differences between the existing algorithms to be able to use them.
Applicability:
When we are faced with different algorithms to do the same task.
When the algorithms use data that the client should not have access to, by using this design pattern, the related and specific data of each algorithm are encapsulated in the class of that algorithm.
When several classes are similar and differ only in the way of implementing behaviors.
When we need to separate the business domain of a class from the algorithm implementation details, these algorithm implementation details are usually not important in the business domain of a class. For example, in a business related to a class, the sorting operation needs to happen somewhere. From the business point of view, it does not matter which algorithm the sorting function is, so, in this case, the strategy design algorithm can be useful.
When we are faced with a class that has many if blocks and similar blocks, and during these blocks, it tries to choose and execute one of the different algorithms.
Related Patterns:
Some of the following design patterns are not related to the strategy design pattern, but to implement this design pattern, checking the following design patterns will be useful:
Decorator
Flyweight
State
Template method
Template method
Name:
Template method
Classification:
Behavioral design patterns
Also known as:
---
Intent:
This design pattern tries to define the main body by doing a task and assigning its implementation steps to the child classes.
Motivation, Structure, Implementation, and Sample code:
Suppose you are implementing the ability to enter data from different data sources into your system's database. Data can be imported from different sources and in different formats. For example, you may want to enter data from various sources in txt, XML, and CSV formats into your database.
In the preceding scenario, the overall structure of the work is fixed and will probably be similar to the following:
Figure%204.8.png
Figure 4.8: Data transformation process
The steps and the sequence are fixed for all source and destination formats, and the way of implementing these steps is different for each source type. For example, if the data source has CSV format, probably in the source data validation step, commas and columns should be checked. Or if the data is in XML format, then it is necessary to pay attention to the correctness of the XML format and the compatibility of some XSD structures.
With the preceding explanations, the following class diagram can be considered:
Figure%204.9.png
Figure 4.9: Template Method design pattern UML diagram
As it is clear in Figure 4.9 class diagram, the main body of the work is defined through the DataReader class. In this class, all methods except the Import method are abstract methods. The Import method is not abstract because child classes are not allowed to change it, and this method defines the sequence of work execution. This method is called the template method. The child classes inherit from the DataReader class and provide their implementation for each step. For the preceding class diagram, the following codes can be considered:
public abstract class DataReader
{
public void Import()
{
Connect();
Validate();
Read();
Convert();
}
public abstract void Connect();
public abstract void Validate();
public abstract void Read();
public abstract void Convert();
}
public class TXTReader : DataReader
{
public override void Connect()
=> Console.WriteLine("Connected to txt file");
public override void Validate()
=> Console.WriteLine("Data has valid txt format");
public override void Read()
=> Console.WriteLine("Data from txt file read done");
public override void Convert()
=> Console.WriteLine("Data converted from txt to destination type");
}
public class XMLReader: DataReader
{
public override void Connect()
=> Console.WriteLine("Connected to xml file");
public override void Validate()
=> Console.WriteLine("Data has valid xml format");
public override void Read()
=> Console.WriteLine("Data from xml file read done");
public override void Convert()
=> Console.WriteLine("Data converted from xml to destination type");
}
public class CSVReader : DataReader
{
public override void Connect()
=> Console.WriteLine("Connected to csv file");
public override void Validate()
=> Console.WriteLine("Data has valid csv format");
public override void Read()
=> Console.WriteLine("Data from csv file read done");
public override void Convert()
=> Console.WriteLine("Data converted from csv to destination type");
}
As can be seen in the preceding code, all the methods, except the Import method, are abstract. To use this structure, you can do the following:
DataReader reader = new CSVReader();
reader.Import();
Participants:
AbstractClass: In the preceding scenario, it is the same DataReader that is responsible for abstractly defining the steps of work execution. Also, this class is responsible for defining and implementing the template method. The template method is responsible for calling the steps based on the expected structure.
ConcreteClass: In the preceding scenario, it is TXTReader, XMLReader, and CSVReader. It is responsible for implementing the abstract steps defined by AbstractClass.
Client: The same user executes the code in the desired way.
Notes:
This design pattern creates an inverted control structure, which is called the Hollywood principle. This principle shows how the parent class can call the methods of the child class.
It is very important to identify hook and abstract methods in the template method implementation. Hook methods are methods that have a default implementation in the parent class, and child classes can override them. While abstract methods have no implementation, child classes must override them.
One should always try to keep the number of abstract methods in the template Method small.
The use of naming conventions can help the user identify the methods that should be overridden. For example, it can be a good idea to use the Do prefix before the names of abstract methods (DoRead, DoConvert, and so on). Of course, if you use Visual Studio to develop C# codes, it is possible to override the abstract methods of the parent in the child classes through this IDE.
The factory method is often called the template method.
Consequences: Advantages:
The user can define and implement certain parts of the algorithm based on his needs.
Reducing the size of the codes by transferring the common parts of the algorithm from the child classes to the parent class.
Consequences: Disadvantages
Limiting the user to the general structure provided.
As the number of abstraction steps increases, the code becomes more difficult to maintain.
Applicability:
When implementing an algorithm, we need to leave the implementation of some steps of the algorithm to the child classes to implement them based on their needs.
When several classes almost implement the same algorithm and differ only in a series of small parts. In this case, common codes can be removed from the child classes and placed in the parent class to prevent code rewriting.
Related patterns:
Some of the following design patterns are not related to the template method design pattern, but to implement this design pattern, checking the following design patterns will be useful:
Factory method
Strategy
Observer
Conclusion
In this chapter, you got acquainted with the most famous behavioral design patterns and learned how to send requests along a chain of objects. We learned to encapsulate a request or task in the form of an object and how to facilitate communication between several objects by directing them toward a central object. We also learned to change the behavior of the object by changing its internal state and to separate different methods and algorithms for the execution of a task by forming an algorithmic family. We learned to assign the execution steps of a task to child classes while defining the main body of the execution of a task.
In the next chapter, you will learn about the rest of the behavioral design patterns.
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