NET 7 Design Patterns In-Depth 6. Domain Logic Design Patterns

Chapter 6
Domain Logic Design Patterns
Introduction
To organize the domain logic, the patterns can be divided into the following four main sections:

Transaction script: This is the simplest way to store domain logic. It is a procedure that receives input from the display layer, processes it, and returns the response to the display layer. Processing in this model can include validation and calculations, storing a series of data in a database, and so on.
Domain model: We create a domain model and put the logic related to the domain in these models. The difference between this pattern and the transaction script design pattern is that a procedure is responsible for handling an action. Still, in the domain model, each model only processes the part of the request that is related to it.
Table module: This is very similar to the domain model, and the main difference is that in the domain model, each object represents a record in the database, but in the table module, there is one object in general, which has tasks related to all objects. It does the relevant things. This design pattern works with the record set pattern. Also, the table module can be considered as an intermediate layer between the transaction script and domain model.
Service layer: Tries to determine the boundaries of the system and the capabilities available to the user layer. In this way, business logic, transaction management, and so on are hidden from the user's view.
The choice between these four design patterns depends on the level of logical complexity that we want to implement.

Structure
In this chapter, we will cover the following topics:

Domain logic design patterns
Transaction script
Domain model
Table module
Service layer
Objectives
In this chapter, you will learn domain logic design patterns and how to improve code maintainability while increasing complexity using different design patterns. These design patterns will teach you to manage complex business logic appropriately and optimally. In this chapter, you will get to know the domain and learn how to manage business logic in different domains.

Domain logic design patterns
The domain model design pattern can be very useful for complex logic, and it is important to understand when logic is complex and when it is not. Understanding this point is challenging, but using domain experts, or more experienced people, can obtain a better approximation.

Thanks to the various features available in .NET, using the table module design pattern can be useful. However, maintaining this design pattern becomes difficult with increasing complexity, and the codes will become very complex over time.

There are very few good use cases for the transaction script design pattern where it can be argued that the pattern is appropriate. Still, it is possible to start the implementation with this design pattern. Over time, the design can be migrated to the domain by acquiring the appropriate knowledge about the domain model:

Figure%206.1.png
Figure 6.1: The Relation between Effort to Enhance and Complexity of Domain Logic

According to Figure 6.1, with the increase in complexity, the use of table modules and transaction script design patterns will lead to a great increase in maintenance complexity and improvement capability. As it is clear in Figure 6.1, the x and y axis in this diagram are two qualitative attributes. As long as these qualitative attributes are not converted into quantitative attributes and numbers, it is impossible to obtain the correct border from these design patterns easily.

Now that we have a general understanding of the preceding three design patterns, we need to know the fourth design pattern of this category. When the domain model or table module design pattern is used, the handling of a request is distributed among several classes. In this case, a single interface is needed so that the presentation layer can deliver its request to that interface. That interface is responsible for sending the request to different classes, and this interface is the service layer.

Using the service layer as a covering layer on the transaction script will not be very useful because, in the transaction script, we will not face the complexity of communication in the domain model or table module. The important point in using the service layer is understanding how much behavior can be included.

The simplest mode of this pattern is to use it as a forwarder that takes the request and, without doing anything, refers it to its lower layers. The most complicated case is when the business logic inside the transaction scripts is placed inside the service layer. According to the previous explanations, placing business logic inside the service layer is useless. Over time, it will lead to the complexity of maintenance and improvement.

Transaction script

Name:

Transaction script

Classification:

Domain logic design patterns

Also known as:

---

Intent:

This design pattern tries to organize the business logic with the help of a series of procedures. Each procedure handles a presentation layer request in this design pattern.

Motivation, Structure, Implementation, and Sample code:

Suppose there is a need to implement the purchase process. To implement the purchase process, we have reached the following model during the analysis:

Figure%206.2.png
Figure 6.2: Sample shipment process

According to Figure 6.2, the entire purchase process is considered a transaction. Based on the transaction script design pattern, this whole transaction can be implemented as a procedure in which the presentation layer delivers the information related to the purchase to this procedure. Then this procedure processes the transaction and returns the result to the presentation layer. According to these explanations, Figure 6.3 class diagram can be imagined for this design pattern:

Figure%206.3.png
Figure 6.3: Transaction Script design pattern UML diagram

As shown in the Figure 6.3 class diagram, ESaleTS handles purchase requests. The Sale method in this class will implement the desired procedure for purchase. To understand this pattern more precisely, consider the following pseudo code:

public bool Sale(int productId, int productCount, int userId)

{

/*

* Inventory check.

var product = tblProduct.Find(productId);

if(product.Stockx.UserId==userId)

var totalPrice = productCount*product.UnitPrice;

if(userWallet.Balance < totalPrice){ throw new Exception("The balance of the wallet is not enough."); } * Deduct the purchase amount from the wallet balance. userWallet.Balance = userWallet.Balance - totalPrice; * Subtract the number of requested goods from the total number of available goods. product.Stock = product.Stock - productCount; * Registration of shipment request. DeliveryRequest request = new DeliveryRequest( item: product, count: productCount, user: userId); tblDeliveryRequest.Add(request); Save(); */ return true; } As can be seen in the preceding code, the entire purchase transaction is implemented in the form of a procedure in which the business rules are checked first. Then the balance of the wallet and the goods inventory are updated, and a request to send the shipment is finally registered. Notes: In implementing this design pattern, procedures are directly connected to the database. If there is a need for a wrapper between the procedure and the database, this wrapper should be brief enough and away from complexity. To implement each procedure we may want to implement each procedure in the form of a series of sub-procedures. It is better not to have a reference to the presentation layer in implementing the procedures. In this case, changes can be made easily. To implement procedures, several procedures can be placed inside a class, or each can be placed in its class using the command design pattern. Usually, using the domain model design pattern will be more appropriate in more complex businesses than the transaction script design pattern. Consequences: Advantages Since each request is executed separately, different procedures do not need to know how other procedures work. Consequences: Disadvantages As the business logic becomes more complex, the maintenance work will become very difficult, and we may face a significant amount of duplicate code. As business logic becomes more complex, writing unit tests will become difficult. Applicability: This design pattern can be useful for implementing simple applications. When the team's general knowledge of object-oriented concepts could be higher, this design pattern can reduce production time in small applications. Related patterns: Some of the following design patterns are not related to transaction script design patterns, but to implement this design pattern, checking the following design patterns will be useful: Command Domain model Domain model Name: Domain model Classification: Domain logic design patterns Also known as: --- Intent: Using this design pattern, it tries to model the elements involved in the domain in the form of a series of objects that include data and behavior. Motivation, Structure, Implementation, and Sample code: As seen in the transaction script design pattern, when the complexity of the business increases, the transaction script pattern will not be suitable, and its use will threaten maintainability and development. Now, when faced with the complexity of business logic, we can divide business logic into different domains and give each domain the task of defining and managing tasks related to that domain. In a way, we are now doing domain modeling. The result of this modeling will be classed with a series of features that can include data and a series of behaviors related to the domain. Since each domain has its own data and data structure, it is easy to map each domain to a table in the database. With the preceding explanations, the domain model design pattern tries to create a connected network of objects where each object manages a part of the business logic. The noteworthy point in this model is that usually, the structure of the domain model will be very similar to the structure of the database model. However, it will also have differences with the database model, among which the most important differences can be the existence of more complex relationships, multiple features, value, and use of inheritance mentioned. To design the domain model, the first step is to know the domain. The domain is the scope of business that we are trying to solve related issues. Suppose we face an internet sales system; we usually face domains such as customers, carts, or products. As mentioned, each of these domains has behaviors specific to that domain, and along with these behaviors, they also have a series of data structures similar to the database model. To clarify the issue, consider the following code: public class Customer { public int CustomerId { get; private set; } public string Name { get; private set; } public string MobileNumber { get; private set; } public Customer(int customerId, string name) { if (customerId <= 0) throw new ArgumentException("Customer ID is invalid."); CustomerId = customerId; if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Customer name is required."); Name = name; } public Customer(int customerId, string name, string mobileNumber) : this(customerId, name) { if (string.IsNullOrWhiteSpace(mobileNumber)) throw new ArgumentException("Mobile phone number is required."); if (mobileNumber.Length > 11 && mobileNumber.Length < 10) throw new ArgumentException("Mobile number must be 10 or 11 digits."); if (int.TryParse(mobileNumber, out _)) throw new ArgumentException("Mobile number must be numeric."); MobileNumber = mobileNumber; } public string GetMobileNumber() { if (string.IsNullOrWhiteSpace(MobileNumber)) return string.Empty; string maskedMobileNumber; if (MobileNumber.Length == 10) maskedMobileNumber = "0" + MobileNumber; else maskedMobileNumber = MobileNumber; maskedMobileNumber = string.Concat( maskedMobileNumber.AsSpan()[..4], "***", maskedMobileNumber.AsSpan(7, 4)); return maskedMobileNumber; } } The preceding code shows a very simple structure for the client domain. According to the definition of this domain, the customer has a customerID, name, and mobile number. As you can see, a private set is used to define these features, and this is because the value of these properties cannot be changed from the outside. Next, with the help of the two defined constructors, you can create new objects of the customer type. In this case, the advantage of using these constructors is that we are always sure that the created objects are valid. This means every object made must have a customer ID and name or a customer ID, name, and mobile number. It is also prevented from assigning invalid values to properties according to the validation built into the constructor. The customer class also has a behavior called GetMobileNumber. In this method, before returning the value of the mobile number, we have replaced the three middle digits with a star (*). Many different things can be done in the created domains, and the preceding code was just a simple example of a domain. Notes: It should always be noted that the domain models store business logic. Therefore, testing them easily without dependence on other layers should be possible. Sometimes the domain class becomes big, and we face behaviors specific to scenarios. In this case, two decisions can be made. You can separate the specific behaviors of an application from the main class and form a new domain model, or you can keep these behaviors in the main domain model class. By separating the behavior to a specific application, the size of the domain model class is reduced. On the other hand, we may continue to face code duplication and maintenance problems. In this case, keeping the codes specific to a specific application in the domain model within the same domain model is suggested. Of course, this problem can be managed with the help of other concepts, such as aggregate roots, or by using different design patterns, such as strategy. Defining domains, they can be defined as simple or complex. For example, in a simple scenario, we may define a domain model for each table in the database. Or it is possible to define and connect the domains through a series of complex relationships with the help of different design patterns and different capabilities of object orientation, such as inheritance. Usually, for complex businesses, it is more useful to use complex domain models. Still, it should be kept in mind that, in this case, mapping between the domain and database models will be more challenging than simple domain models. Usually, to establish independence between the domain model and the database and prevent dependency, simple domain models are related to the active record pattern, and complex domain models are related to the data mapper design pattern. Consequences: Advantages All the logic related to a certain area of business is placed in one place, and in this way, repetition is prevented. Consequences: Disadvantages If we are faced with a team that does not have a proper attitude towards object-oriented or domain-oriented design, usually the use of this design pattern in this team will face difficulties. Applicability: This design pattern can be useful in implementing large and complex businesses. If we deal with a system where the business rules are undergoing fundamental changes, this design pattern can be useful. Still, if we are dealing with a small business with limited business rule changes, the transaction script design pattern will be more useful. Related patterns: Some of the following design patterns are not related to domain model design pattern, but to implement this design pattern, checking the following design patterns will be useful: Strategy Active record Data mapper Transaction Script Table module Name: Table module Classification: Domain logic design patterns Also known as: --- Intent: This design pattern uses a specific object to implement the business logic associated with all the records in a database table or view. Motivation, Structure, Implementation, and Sample code: Suppose there is a need to read various employee information from the database and provide it to the user. To implement this requirement, you can benefit from the domain model and implement the requirement. But the point of this method is that there will be one object for each of the personnel in this design pattern. This procedure is normal and coincides with the object-oriented design. Each object has an identity in this design, and the data and related behaviors are next to it. According to the above design, the question that arises is that if there was no need to use databases and benefit from their power and facilities, what would be the need to use relational databases or the presence of such a complex system? Wouldn't it be better and more appropriate to use the facilities of different parts in a useful way in order to implement our requirements? There are no precise and clear answers to these questions, and these are only questions that should be answered when designing and choosing a method from a group of methods. Another way to implement the preceding method is to use an object to manage the business logic of all the records of a table or view in the database. In the domain model method, one object is considered for each record, but in the design pattern, one is considered for all records. According to the preceding description, the following codes can be considered to implement the requirements raised using the table module design pattern: public class DbManager { protected DataTable dt; protected DbManager(DataSet ds, string tableName) => dt = ds.Tables[tableName];

}

public class RollCall : DbManager

{

public RollCall(DataSet ds) : base(ds, "rollcalls") { }

public double GetWorkingHoursSummary(int employeeId)

{

if (employeeId == 1) return 100;

else throw new ArgumentException("Employee not found.");

}

}

public class Employee : DbManager

{

public Employee(DataSet ds) : base(ds, "employees") { }

public DataRow this[int employeeId] => dt.Select($"Id = {employeeId}")[0];

public double CalculateWorkingHours(int employeeId)

{

var employee = this[employeeId];

var workingHours = new RollCall(dt.DataSet)

.GetWorkingHoursSummary(employeeId);

if (employee["Position"] == "CEO")

workingHours *= 1.2;

return workingHours;

}

}

To implement this design pattern, DataSet has been used. Since finding the table using DataSet is a fixed method for all tables (dataset.Table[abc]), the DbManager class is considered to implement this behavior. Other classes inherit from this class and prepare their own DataTable by providing the table name.

The Employee class is responsible for implementing the business logic related to all employee records. To find specific personnel, in this implementation, a C#.NET language Indexer is used. However, this behavior can be implemented using other methods as well. The CalculateWorkingHours method also calculates the number of staff working hours. To calculate the number of working hours of personnel, by providing employeeId, working hours can be calculated. The important point in implementing this method is to connect the method to the RollCall class. To create an object from the RollCall class, it will only be necessary to present the dataset to the constructor of this class, and in this way, the data table will be created.

Notes:

The object produced using this design pattern is very similar to conventional objects, with the difference that the object produced using this method has no identity. This means that if we want to get more details of a database record, we will need to search for that record from the mass of records using a parameter. Often, the primary key in the database can be a suitable parameter to find a record among the mass of records. For example, the following code shows how we can fetch employee detail by using employeeId:
Employee GetDetail(int employeeId)

To choose between the power of the domain model in implementing complex business logic and the ease of integration of the table module with the tabular structure, a cost-benefit analysis will be required.
There is often the need to communicate with several table modules to implement business logic.
To implement a table module, it can be defined as static or instance. In this case, you should consider the differences between class definitions as instance and static. One of the most important differences between these two methods of class definition is the possibility or impossibility of using inheritance.
In implementing table module queries, you can use the factory method design pattern with a table data gateway. The advantage of using a table data gateway is that you can connect to several data sources using the corresponding gateway using a table module. Each table module does its work on the record set. To access the record set, it will be necessary first to make the set of records available with the help of a table data gateway or factory method.
Consequences: Advantages

By using this design pattern, while collecting data and related behaviors in a class, you can benefit from the capabilities and advantages of the database.
Consequences: Disadvantages

If we face a more complex business logic since the relationships between objects are not seen in this design pattern and polymorphism features cannot be used. The domain model design pattern will be a better choice.
Applicability:

This design pattern can be useful when faced with a tabular data structure and looking for a convenient way to access the data.
Related patterns:

Some of the following design patterns are not related to the table module design pattern, but to implement this design pattern, checking the following design patterns will be useful:

Domain model
Factory method
Table data gateway
Record set
Service layer
Name:

Service layer

Classification:

Domain logic design patterns

Also known as:

---

Intent:

This design pattern tries to determine the boundaries of the system and the capabilities available to the client layer. In this way, business logic, transaction management, and so on are hidden from the client's view.

Motivation, Structure, Implementation, and Sample code:

Suppose that several repositories are prepared using the repository design pattern. The client, as a controller, needs to communicate with one of these repositories and insert a new user. Before adding a new user, a series of business validations need to happen. Suppose that we want to check the non-duplication of the username in the form of this validation. There are different ways to implement this validation.

One way is to do this validation inside the controller. The problem with this way is that the controller will depend on the data access layer. For this purpose, we must validate the controller using a repository-type object.

The second way is to validate the repository. This method also has an important drawback: the Separation of Concern (SoC) will be violated. Because of this, we have entered the business logic into the data access layer.

The better way is to do the desired business logic as a middle layer. This middle layer takes the request from the controller and communicates with the repository, and performs the validation work. This middle interface is the service layer:

Figure%206.4.png
Figure 6.4: Service Layer design pattern UML diagram

As shown in Figure 6.4 diagram, UserService implements the IUserService interface, and the client submits a new user insertion request. Now the business logic is checked in the UserService, and a new user is inserted, or an appropriate error is returned to the client. The following code can be considered for this class diagram:

public interface IRepository

{

void Insert(User user);

User FindByUserName(string username);

}

public class UserRepository : IRepository

{

public User FindByUserName(string username)

=> UserDbSet.Users.FirstOrDefault(x => x.Username == username);

public void Insert(User user) => UserDbSet.Users.Add(user);

}

public interface IUserService

{

void Add(User user);

}

public class UserService : IUserService

{

private readonly IRepository _repository;

public UserService() => _repository = new UserRepository();

protected bool IsValid(User user)

=> _repository.FindByUserName(user.Username) == null;

public void Add(User user)

{

if (IsValid(user))

_repository.Insert(user);

else

throw new Exception("User exists!");

}

}

The preceding code shows that the business logic required to insert a new user happens before the insert operation is performed inside the UserService. In this way, the client does not need to communicate with the repository, and the UserService provides a new interface for the client to send requests through. In this provided interface, the client submits the insertion request, the UserService, performs the necessary checks, and if the request is valid and can be performed, it handles the request. To use the preceding code, you can do the following:

IUserService userService = new UserService();

userService.Add(new User { Username = "user5", Password = "123456" });

In the preceding implementation, one important point is that the client is accessing an object of type User. Usually, the repository's model is different from the model the client works with. This is because the repository usually works with a model that matches the data source, but the client works with a model that matches its needs. Therefore, it may be necessary to map between the model sent by the client and the model delivered to the repository in the implementation of the service layer.

Notes:

In examining the service layer design pattern, it is very important to pay attention to different types of business logic. In general, there are two types of business logic:
Business logic is related to the domain, called domain logic, in which we implement a solution for a problem within the domain. For example, we will implement the withdrawal of funds or how to increase the balance.
The business logic of the user program, which is called application logic, and in which the withdrawal process is implemented. For example, to withdraw money, first, the user's request is received, then the balance should be checked, then the current balance should be updated, and finally, a response should be returned to the user. How the account balance is checked or how the current balance is updated is addressed in domain logic format.
One of the most important applications of the service layer is domain-driven design. There is a need to establish a connection between several domains, and a series of checks take place. This way of implementing a service layer is also called a domain facade.
What methods or behaviors should be presented to the user through the service layer? The answer to this question is simple. The service layer should provide the user with the capabilities that the user needs. In other words, the capabilities provided by the service layer are based on user requirements. The user and their needs may directly relate to the User Interface (UI). This dependency is because the UI is usually designed and implemented based on the user's needs. In the following, it should be noted that most requirements in implementing a user program are limited to Create, Read, Update, and Delete (CRUD) operations. Implementing each part of CRUD usually includes a series of different tasks. For example, creation often requires a series of validations and then insertion. After the completion of creation, it is usually necessary to inform others or other parts of the program about the result of this operation so that those parts can do other necessary work. This part of the work, which includes informing other departments and coordinating with them, is one of the duties of the service layer.
To implement the service layer, the domain facade, and the operation script methods can be used:
In the domain facade implementation, the service layer is defined as a series of classes or thin interfaces, and the classes that implement these interfaces do not implement any business logic (the reason for using the word thin is the same), and business logic is implemented only through the domain model. These interfaces define the boundary and range the user layer can communicate with.
In implementing the operation script method, unlike the domain facade method, the service layer is defined as a series of fatter classes containing business logic (the reason for using the word fat is the same). Each class that implements business logic is usually called an application service in this type of implementation.
To implement the business logic of the domain, both the domain model and the transaction script design pattern can be used. It is recommended to use a domain model because the business logic is not repeated in different departments, and we will not face duplication.
To implement the service layer, it is better to implement it locally first, and then according to the need, if necessary, add the possibility of remote to it. To add remote functionality, you may need to use a remote facade.
Consequences: Advantages

Using the service layer, it is possible to get the business logic of domain-related business inside the domain, and the business logic of the user program will be implemented in another part. This separation will make the different business logic into different parts depending on their type (Domain logic or application logic).
Consequences: Disadvantages

If we face a single user and do not need to communicate with different transaction resources to meet their needs, this design pattern will increase the complexity.
One of the points mentioned in the implementation of this service is its remote implementation. Suppose there is no good cost-benefit analysis between the remote and local implementation of this design pattern. In that case, implementation will lead to an increase in complexity without achieving a specific advantage.
Applicability:

When faced with several different users and the need to communicate with multiple transaction sources for each user, then using this design pattern can be useful.
Related patterns:

Some of the following design patterns are not related to the service layer design pattern, but to implement this design pattern, checking the following design patterns will be useful:

Repository
Remote facade
Domain model
Transaction script
Conclusion
In this chapter, you got acquainted with the design patterns related to domain business and learned how to manage different businesses suitably using different design patterns. Always try to establish a good relationship between the complexity of the business and the upcoming requirements and choose the more appropriate design pattern. Otherwise, you cannot always use a fixed design pattern for different business management blindly and without careful examination. This will reduce the quality of the code, and over time, code maintenance will become a very serious challenge. In the next chapter, you will learn about design patterns related to data source architecture.

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

Leave a Reply

Your email address will not be published. Required fields are marked *