Author Archives: user

NET 7 Design Patterns In-Depth 9. Object-Relational Structures Design Patterns

Chapter 9
Object-Relational Structures Design Patterns
Introduction
To organize the object-relational structures, the design patterns of this category can be divided into the following ten main sections:

Identity field: Tries to maintain the data identity between the object and the database record by storing the value of the database identity column(s) inside the object.
Foreign key mapping: Tries to map the foreign key relationship between tables in the database as a relationship between objects.
Association table mapping: Tries to map the many-to-many relationship between tables in the database as relationships between objects.
Dependent mapping: Attempts to make a class responsible for managing child class communication with the database.
Embedded value: It can map an object to several table columns related to another object.
Serialized LOB: It can convert a graph of objects into a LOB (Large Object) and store it in a database field.
Single table inheritance: It mapped the classes that participated in the inheritance structure to a table.
Class table inheritance: Tries to map each class to one table in the database in an inheritance structure.
Concrete table inheritance: Tries to map each non-abstract class to one table in the database.
Inheritance mappers: Tries to organize the various Mappers to manage the inheritance structure.
Structure
In this chapter, we will cover the following topics:

Object-relational structures design patterns
Identity field
Foreign key mapping
Association table mapping
Dependent mapping
Embedded value
Serialized LOB
Single table inheritance
Class table inheritance
Concrete table inheritance
Inheritance mappers
Objectives
In this chapter, you will learn how to map different relationships between database tables (such as one-to-many and many-to-many relationships) to classes. In this chapter, you will learn how to map an inheritance structure to tables in a database. Also, you will learn how to work with dependent data and map them with the data in the database tables.

Object-relational structures design patterns
There are important points to pay attention to when mapping relationships. First, to associate an object with a record in a table, it is often necessary to insert a primary key value into the object. For this, using the identity field design pattern will be useful. Now, one table may be related to another through a foreign key relationship, and the foreign key mapping design pattern can be used to implement this relationship between objects.

The connection between objects may be very long or circular. Therefore, when loading one of the objects, it is necessary to fetch the data related to the dependent object and repeat this process continuously. As mentioned earlier, a lazy load design pattern can be useful in this section. But there is a situation where there is a one-to-many relationship between A and B, and B's data can only be used through A, and we have nothing to do with them outside of A. In this case, A can be considered the owner of B and benefit from the dependent mapping design pattern. This type of communication becomes complicated when the one-to-many relationship becomes a many-to-many relationship. In this case, you can use the association table mapping design pattern.

As mentioned earlier, identity field design patterns can be used for communication between objects. But some data should not be mapped to specific tables, and value objects are among these data. The embedded value design pattern is a better way to store value objects. Sometimes the amount of data that must be stored in this method can be very large. To solve this problem, you can use the serialized LOB design pattern.

The preceding points were related to composition-type relationships. Sometimes there may be an inheritance relationship between classes. This type of relationship does not exist in relational databases, so there are three options for implementing this type of relationship: one table for the entire inheritance structure (Single table inheritance design pattern), one table for each non-abstract class (Concrete table inheritance design pattern), and finally one table for each class (Class table inheritance design pattern).

Identity field
Name:

Identity field

Classification:

Object-relational structures design patterns

Also known as:

---

Intent:

This design pattern tries to maintain the data identity between the object and the database record by storing the value of the database identity column(s) inside the object.

Motivation, Structure, Implementation, and Sample code:

Suppose a requirement is raised and it is necessary to retrieve the data in the books table or to define a new book. Suppose the following structure exists for the books table in the database:

BookId

Title

PublishDate

1

Design Patterns in .NET

2021-10-01

2

Programming in C#.NET

2021-11-01

Table 9.1: Book table in the database

When using the identity field design pattern, we store the identity value of the books table in the object of the Book class. This identifier value must have an important feature, and that is that its value must be unique. The reason for this requirement is that we must be able to uniquely identify each record and each object with the help of this value. The best candidate for an identifier will often be the primary key value since the primary key always has a unique value. With this assumption, the code for the Book class will be as follows:

public class Book

{

public int BookId { get; set; }

public string Title { get; set; }

public DateOnly PublishDate { get; set; }

}

As it is clear from the code related to the Book class, the BookId property will be used to store the value of the BookId column of the database. Based on this structure, the following objects can be considered:

Book book1 = new()

{

BookId = 1,

Title = "Design Patterns in .NET",

PublishDate = new DateOnly(2021, 10, 01)

};

In the preceding code, the value of the BookId property of the book1 object is equal to 1, so this object refers to the record with the value of the bookId=1 column in the Books table.

To insert a new book, you can do the following:

public bool Create(Book book)

{

book.BookId = GetNewBookId();

return Save(book);

}

private int GetNewBookId()

{

/*

* In this section, the desired method to generate the

* new value of the key must be implemented.

*/

}

According to the type of key (simple or composite) and the selected method to generate the new value of the key, the GetNewBookId method should be implemented.

Notes:

Key selection is very important in this design pattern. There are two main options for choosing a key. Using a meaningful key and using a meaningless key:
Meaningful key: It is also called natural key; its value is always unique and has a meaning for the user. For example, the ISBN is a unique value for a book and has a meaning for the end user.
Meaningless key: It is also called a generated key. It is a key whose value is unique but has no meaning for the user. For example, the primary key column, which is automatically and sometimes randomly set, is often a meaningless key for the user.
Choosing between these two types of keys is important. Although the meaningful key has a unique value, it has an important shortcoming. It is expected that the key value is immutable and unique. Immutability is not necessarily true for a meaningful key because a mistake may occur when entering its value, and the user wants to edit it. In this case, the immutability condition for the key will be violated. The value of the key can be changed until the changes are sent to the database, and the database has not checked the uniqueness of the value; the object can be used. This can affect reliability. For small scenarios or special situations, a meaningful key can be useful.

The keys can be defined as simple or composite according to their nature. For example, in the database design, the bookId column may be available and has the role of the primary key. In this case, the key is simple. In another case, for example, for the sales information table, storeId and bookId columns together maybe keys. In this case, it is a composite key. Managing uniqueness and immutability constraints for composite keys will be important and sensitive. To implement the composite key, using the key class can be helpful. In any case, regarding the key, two types of operations should be considered. The first operation compares the values, and the second is related to generating the next value. In the following code, you can see how to implement the key class:
public override bool Equals(object? obj)

{

if (obj is not CompoundKey)

return false;

CompoundKey other = (CompoundKey)obj;

if (keys.Length != other.keys.Length)

return false;

for (int i = 0; i < keys.Length; i++) if (!keys[i].Equals(other.keys[i])) return false; return true; } The preceding method shows the value comparison process for the composite key. As it is clear in the code, inside the for loop, the value of all the members of the composite key is compared one by one. According to the requirement, the preceding implementation may be varied and different to compare the values of the keys. Also, to make a composite key, you can proceed as follows: public class CompoundKey { private object[] keys; public CompoundKey(object[] keys) => this.keys = keys;

}

Different constructors with different input types can be defined to create composite keys with specified members or values, and the preceding code can be improved.

Keys can be on two levels in terms of uniqueness. The first level may require that the key value in a table is always unique, and the second level may require that the key value be unique in the entire database. It is usually very common to use the first level, while the advantage of the second level will be that an identity map will be needed.
Depending on the data type selected for the key, we may reach the maximum value when using it. In this case, to generate the next value, the value of the key used for the deleted records can be used. This method will be provided that the deletion has occurred physically. 64-bit data types or combination keys can reduce the possibility of reaching the maximum allowed value. Usually, the mistake is using data types such as GUID. Although the value of these data types is always unique, they occupy a lot of space. Therefore, before using them, the necessary checks must be done.
When creating a new object and storing its value in the database, generating a new value for the key will be necessary. Different ways to generate a new value for the key include setting the key column in the database to auto-generate, using the GUID for the key, or implementing your mechanism:
Using the auto-generate method is simpler. However, it may cause problems in the record and object mapping process. Because until the record is inserted in the database and then the same data is not retrieved from the database, the value assigned to the key will be unknown. An alternative method for auto generate in Microsoft SQL server database is to use sequence, which plays the role of database level counter.
Using the GUID method, as mentioned earlier, may cause performance problems.
To implement a custom mechanism, methods such as using the MAX function to find the last value of the key may also be used. This method may be useful for small tables with a lower simultaneous change rate. It will not be a good method for larger tables or tables with a high simultaneous change rate because, while having a negative effect on performance, it may be due to not having the appropriate isolation level for the transaction. Multiple objects get a common value for the key. Another way to implement this method is to use a key table. Using this table, you can store the next value for the key in this table and read the next value when you need to create a new object. It is better to access the key table through a separate transaction so that the rest of the transactions that need the key do not wait for the lock to be released. The problem with this method is that if the insert transaction is canceled for any reason, the generated key value will not be usable.
An alternative method for using the identity field design pattern is the identity map design pattern.
Consequences: Advantages

Using this design pattern, a common feature can be placed between the database records and the object created in the memory. The object and its corresponding record in the database can be identified through that feature. Without using this design pattern, in databases, the primary key value is usually used to identify the uniqueness of a record. In .NET, objects have their unique identifier, but there is no relationship between these two values.
Consequences: Disadvantages

If the data type of the key is a string, generating a new key or comparing the values of the keys can be time-consuming or heavy operations.
If the new value of the key is duplicated for any reason, if this value is not sent to the database, it cannot be noticed that it is duplicated. Obviously, in this case, performance will suffer.
Applicability:

When there is a need to map records in the database to objects, using this design pattern can be useful. This mapping usually uses domain models or row data gateway design patterns.
Related patterns:

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

Identity map
Domain model
Row data gateway
Foreign key mapping
Name:

Foreign key mapping

Classification:

Object-relational structures design patterns

Also known as:

---

Intent:

Using this design pattern, the foreign key relationship between tables in the database is mapped as a relationship between objects.

Motivation, Structure, Implementation, and Sample code:

Continuing the example presented in the identity field design pattern, we assume we need additional information for each book, located in a separate table next to the book object. The relationship between the books and the bookDetail is one-to-one. One-to-one relationship means that each book has only one record in the bookDetail table, and each record in the bookDetail table corresponds to one book. In other words, the following relationship can be imagined between the books and bookDetail tables:

Figure%209.1.png
Figure 9.1: Relationship between books and bookDetail tables

As it is clear from Figure 9.1, ER diagram, the books table has a primary key column called bookId, and the bookDetail table also has a foreign key column called bookId. In the bookDetail table, the bookId column also plays the role of the primary key. To implement this relationship, you can do as follows:

public class Book

{

public int BookId { get; set; }

public string Title { get; set; }

public DateOnly PublishDate { get; set; }

private BookDetail detail;

public BookDetail Detail

{

get => detail;

set

{

detail = value;

if (detail.Book == null)

detail.Book = this;//’this’ refers to the current instance of the class }

}

}

public class BookDetail

{

public int BookId { get; set; }

public byte[] File { get; set; }

public Book Book { get; set; }

}

As it is clear from the preceding code, each Book and BookDetail class has designed the identity field design pattern. The BookId property in these two classes is mapped to the bookId column in the books and bookDetail tables. Also, the Book class has a property called Detail related to the BookDetail class. The BookDetail class also has a property called Book and is related to the Book class. This relationship maps the database's foreign key to the relationship between objects. In the preceding code, a one-to-one relationship is implemented in a bidirectional way, which means that the corresponding BookDetail object can be accessed through a Book object and vice versa. Removing one of the Book or Detail properties is enough to implement this relationship unidirectionally.

In this one-to-one implementation, inserting, editing, and deleting will be simple operations. To insert or edit a record in the supplementary information table of the book, create the BookDetail object and send it to the database. The only important point in constructing this object is to comply with the principles related to the foreign key constraint.

Next, suppose that we need to implement the comments that the readers have given to the book. For this purpose, the comments table is designed in the database. There is a one-to-many relationship between the books and comments tables, which means that there can be several comments for one book, and on the other hand, each comment is only specific to one book. With these explanations, the ER diagram can be imagined as follows:

Figure%209.2.png
Figure 9.2: Relationship between books and comments tables

As shown in the Figure 9.2 ER diagram, the comments table has a column called bookId, which plays the role of a foreign key and is connected to the bookId column in the books table. Also, as determined by the relationship type, several records may be in the comments table for each bookId. With these explanations, the following code can be considered to implement this relationship:

public class Book

{

public int BookId { get; set; }

public string Title { get; set; }

public DateOnly PublishDate { get; set; }

private ICollection comments = new List();

public ICollection Comments

{

get => comments;

set

{

comments = value;

foreach (var item in comments.Where(x => x.Book == null))

item.Book = this; //’this’ refers to the current instance of the class

}

}

private BookDetail detail;

public BookDetail Detail

{

get => detail;

set

{

detail = value;

if (detail.Book == null)

detail.Book = this;

}

}

}

public class Comment

{

public int CommentId { get; set; }

public string Text { get; set; }

private Book book;

public Book Book

{

get => book;

set

{

book = value;

if (!book.Comments.Contains(this))

{

book.Comments.Add(this);

}

}

}

}

As it is clear in the preceding code, the Book class has an ICollection property called Comments, through which you can see all the comments related to that book. Also, the Comment class has a property called Book, which can connect the comment to its corresponding book. This method will also represent the implementation of one-to-many relationships. Remember that the preceding implementation is bidirectional, and it is possible to implement this relationship unidirectionally, as discussed in BookDetail.

In implementing the one-to-many relationship. There are three different ways to implement the editing process:

Delete and insert: This method is the easiest way to implement the editing operation. In this method, all the comments related to the book are removed from the comments table, and all the comments in the Book object are inserted into the comments table. This method has various problems, including that the comments related to the book may not have changed. In this method, we will have to delete and insert them again. Also, the bigger problem with this method is that this method can only be used if the comments have not been used elsewhere.
Reconciliation: Can be done in two ways:
Reconciliation with the data in the database: In this method, the comments related to the book should be read from the database and compared with the current comments. Any comment in the database but not in the list of current comments should be deleted. Any comment in the current list and not among the comments read from the database should be inserted.
Reconciliation with previously fetched data: In this method, storing the book comments fetched in the beginning will be necessary. This method will work better than the previous method because there is no need to refer to the database, but the data reliability will be low. The comparison method in this method will be the same as the previous method.
Using a bidirectional relationship instead of a unidirectional relationship: This method, also called the Back Pointer, allows us to access the related book through each comment instead of accessing the list of comments through the book. In this case, we will have a one-to-one relationship with the book for each comment, and as the one-to-one relationship explained earlier, the editing process can be implemented.
For the delete operation, the process will be almost similar. Deleting a comment often means that the comment is completely deleted, which naturally should also be deleted from the table. Or, it means that the comment has been transferred from one book to another, in which case, the comment record will also be edited when processing that book. In some cases, the comment may not be related to a specific book. In such cases, the value of the bookId column in the comments table will be equal to null (This method is only possible if the corresponding foreign key constraint allows storing null in the foreign key column table).

Notes:

If the relation is of immutable type, then the editing operation will be meaningless. For example, comments sent for a book cannot be edited or transferred to another book.
Using the UnitOfWork design pattern to implement editing, inserting, or deleting scenarios can be very helpful.
In implementing this design pattern, the occurrence of a loop is very likely. You can benefit from the lazy load or identity map design pattern to prevent stack overflow and properly manage the loop.
In the one-to-many relationship, you can use join to write a query to receive the list of book comments instead of sending several queries to the database. By sending one query to the database, all the information is required to be received. In this method, the number of remote calls or references to the database will be reduced, and the efficiency will be significantly improved.
Consequences: Advantages

Using this design pattern, the first principle of normalization can be observed.
Consequences: Disadvantages

Implementing a many-to-many relationship using this design pattern is impossible because, according to the first principle of normalization, the value of several foreign keys cannot be stored in one field.
Applicability:

To implement relationships between tables, this design pattern can be used. The association table mapping design pattern should be used to implement many-to-many relationships.
Related patterns:

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

Identity field
Unit of work
Lazy load
Identity map
Association table mapping
Association table mapping
Name:

Association table mapping

Classification:

Object-relational structures design patterns

Also known as:

---

Intent:

Using this design pattern, the many-to-many relationship between tables in the database is mapped as relationships between objects.

Motivation, Structure, Implementation, and Sample code:

In the continuation of the example presented for the foreign key mapping design pattern, let us assume that we need to have the information of the authors of the book along with the book object. The book information is in the tblBooks table, and the author information is in the authors table. Each author may have written several books and may have several authors. With these explanations, we can conclude that the relationship between the book and the author is many-to-many, and the foreign key mapping design model cannot implement this model. The foreign key mapping model is useful when one side of the relationship is a single value (such as one-to-one or one-to-many relationships). Now we are facing a relationship where both sides are multi-valued.

In the database world, there is a famous solution to solve this problem, and that is to use an intermediate table. The task of this intermediate table is to convert the many-to-many relationship into two one-to-many relationships. This intermediate table is sometimes called a link table. The name of this intermediate table in the given example is bookAuthors. The relationship between the tblBooks and the authors can be imagined as follows:

Figure%209.3.png
Figure 9.3: Relationship between tblBooks and authors tables

As shown in Figure 9.3, ER diagram, the tblBooks table has a one-to-many relationship with the bookAuthors table. Also, the authors table has the same one-to-many relationship as the bookAuthors table. The primary key in the preceding link table is a composite key, and the primary key is defined from the combination of the bookId and authorId columns. Now that the many-to-many relationship problem has been solved in the database, you can use the association table mapping design pattern to solve this problem at the model level and also fix the .NET codes:

public class Book

{

public int BookId { get; set; }

public string Title { get; set; }

public DateOnly PublishDate { get; set; }

public ICollection Authors { get; set; } = new List();

}

public class Author

{

public int AuthorId { get; set; }

public string FirstName { get; set; }

public string LastName { get; set; }

public ICollection Books { get; set; } = new List();

}

As it is clear in the preceding code, the Book class is connected to the Author class through the Authors property, and the Author class is also connected to the Book class through the Books property. The important thing about this design pattern is that, as seen in the preceding code, the link table designed in the database has no related objects.

Suppose we need to get the list of authors of a specific book. There are two ways to read the information in this structure:

Refer to the link table and get different authorIds for the desired book and then refer to the authors table and get the information of each author. If the data is not in the memory and it is necessary to refer to the database for each step, we will face a performance problem, and otherwise, if the data is in the memory, this process can be simple.
Using Join in the database and receiving information at once. In this case, going back and forth to the database will be reduced, which will reduce the number of remote calls and improve efficiency; on the other hand, complexity will be created to map the results on the models.
To perform the editing operation, considering that the records of the link table are often not related to another table, therefore, the editing operation can be easily implemented in the form of deletion and insertion operations (Contrary to what we saw in the foreign key mapping design pattern). If the records of the link table are related to other tables, then the same tasks as mentioned in the foreign key mapping design pattern should be performed to perform editing operations.

Notes:

Sometimes the link table, apart from maintaining the values of the foreign keys, also carries additional information. In this case, the link table will have its class. For example, suppose we need to know how much each author contributed to writing a book, and the contribution value is the data that should be stored in the bookAuthors table.
Consequences: Advantages

Using this design pattern, you can connect two different tables without adding a new column to the table and simply by creating an intermediate table.
Consequences: Disadvantages

This design pattern can be used to implement other types of communication, but it is not recommended due to the complexity of this task.
Applicability:

To implement many-to-many relationships, this design pattern can be used.
Related patterns:

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

Foreign key mapping
Dependent mapping
Name:

Dependent mapping

Classification:

Object-relational structures design patterns

Also known as:

---

Intent:

Using this design pattern, a class becomes responsible for managing the communication of the child class with the database.

Motivation, Structure, Implementation, and Sample code:

Following the examples presented in the previous sections, suppose we need to have the contact information and address of the authors. The relationship between the contact information table or contactInfo and the authors table is one-to-many. In this way, each author can have several contact information. For example, one author can provide a phone number, mobile phone number, and work address in the form of contact information, and another author can provide a home address and work address.

To implement this scenario, it is very important to pay attention to one point: the identity and nature of the contact information, as the author is meaningful. In other words, the contact information cannot be accessed and worked by itself, and all transactions must be done through the author. This structure is exactly what the dependent mapping design pattern seeks to provide.

In this design pattern, we face a series of dependent elements and an owner for that dependent element. All transactions necessary for fetching, inserting, editing, and deleting the dependent element will happen only through its owner. Now, with the explanations provided, consider the following codes to implement the requirements:

public class Author

{

public int AuthorId { get; set; }

public string FirstName { get; set; }

public string LastName { get; set; }

public ICollection ContactInfo { get; set; }

public void UpdateAuthor(Author author)

{

this.FirstName = author.FirstName;

this.LastName = author.LastName;

UpdateContactInfo(author);

}

// Editing contact information includes deleting and re-inserting it

public void UpdateContactInfo(Author author)

{

RemoveAllContactInfo();

foreach (var item in author.ContactInfo)

{

AddContactInfo(item);

}

}

/*

All communication of contact information with the database

happens through the owner

*/

public void AddContactInfo(ContactInfo contactInfo)

=> ContactInfo.Add(contactInfo);

public void RemoveContactInfo(ContactInfo contactInfo)

=> ContactInfo.Remove(contactInfo);

public void RemoveAllContactInfo() => ContactInfo.Clear();

}

public class ContactInfo

{

public ContactType ContactType { get; set; }

public string Value { get; set; }

}

public enum ContactType : short

{

HomePhone,

MobilePhone,

HomeAddress,

WorkAddress

}

The preceding code shows that the Author class has a property called ContactInfo. This property maintains a set of contact information for the author. This class provides the necessary methods to insert and delete the author's contact information (according to the requirement, different implementations may be provided for these methods, and the code sample is provided here for simplicity). On the other hand, the ContactInfo class only has ContactType for the type of contact information (mobile phone number, home number, residential address, and work address) and Value to store the amount of contact information. As it is clear in the preceding code, acting through the Author class is necessary to communicate with the data related to ContactInfo in the database. Therefore, the Author class has the same role as the owner, and the ContactInfo class has the role of the dependent element. In implementing the UpdateContactInfo method in the Author class, the editing operation can be deleted and reinserted by using this design pattern and assuming the dependent element to be immutable.

Notes:

Each dependent element must have only one owner.
When using active record or row data gateway design patterns, the class related to the dependent element will not contain the necessary code to communicate with the database. As mentioned before, these communications should be established only through the owner, and these codes will be placed in the owner's mapper class when using the data mapper design pattern. Finally, there will be no dependent element when using the table data gateway design pattern.
Usually, when reading the owner's information from the database, related elements are also retrieved. If reading the information on dependent elements is costly, or the information related to dependent elements is of little use, the lazy load design pattern can be used to improve efficiency.
The dependent element does not have an identity field, so it is not possible to place it in the identity map; so, like other models, the method that returns a specific record by receiving the key value is meaningless for this element. All these works will be in the scope of the owner's duties.
A dependent element can be the owner of another dependent element. In this case, the main owner has the same duty towards the second dependent element as the first. Therefore, all the communications of the second dependent element with the database must happen through the owner of the first dependent element.
The foreign key of any table should not be inside the dependent element table unless the owner of both tables is the same.
Using UML, the relationship between the owner and the dependent element can be displayed through the Composition relationship.
The use of this design pattern requires two necessary conditions:
A dependent element must have only one owner.
No reference should be from any object other than the owner to the dependent element.
As much as possible, the communication graph using this design pattern should not be too long. The longer and larger this graph is, the more complicated the operation of managing the dependent element through the owner will be.
If the UnitofWork design pattern is used, it is not recommended to use this design pattern. Because UnitofWork does not recognize dependent elements, which can damage the principles of the dependent mapper design pattern.
Consequences: Advantages

If we consider the dependent element immutable, then the change process in the dependent element and how to manage it will be very simple. Editing can be simulated by removing all dependent elements and inserting them again.
Consequences: Disadvantages

Using this design pattern, it will be difficult to track changes. For example, whenever the dependent element changes, this change should be notified to the owner so that the owner can prepare and send the changes to be applied to the database.
Applicability:

This design pattern can be used when an object is available through another object. Often, the type of this relationship is one-to-many.
Using this design pattern will be useful when there is a one-to-many relationship between two models, but their connection is implemented unidirectionally.
Related patterns:

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

Active record
Row data gateway
Data mapper
Table data gateway
Lazy load
Identity map
UnitofWork
Embedded value
Name:

Embedded value

Classification:

Object-relational structures design patterns

Also known as:

Aggregate mapping or composer

Intent:

Using this design pattern, one object can be mapped to several table columns related to another object.

Motivation, Structure, Implementation, and Sample code:

Following the previous examples, suppose we need to store the information of the publisher of the book. The book publisher has information such as name and address. The address of the book publisher has been designed and implemented as a value object in which the address consists of country, province, city, and street sections.

To implement this structure, one way is to consider two different classes for the publisher and their address, which have a one-to-one relationship with each other, and then map each of these classes to two different tables in the database and connect through the foreign key. If we do the design in this way, then the following structure can be considered for them:

Figure%209.4.png
Figure 9.4: Mapping Publisher and Address to separate tables

In Figure 9.4, two classes, Publisher and Address, are on the left side, each of which is mapped separately to the Publisher and PublisherAddress tables. The problem with this design is that we must use Join whenever we need to fetch and display the publisher's address next to their name. PublisherId also needs to be repeated in the Publisher and PublisherAddress classes, which leads to unnecessary duplicate data. The following code shows the sample query required to retrieve the name and address of the publisher with publisherId=1 using SQL:

SELECT p.Name, a.Country, a.Province, a.City, a.Street

FROM Publisher as p

INNER JOIN PublisherAddress as a

ON p.publisherId = a.publisherId

Where a.publisherId = 1

The preceding design and implementation have no terms of functionality, but the problem in the preceding design and implementation is performance. The embedded value design pattern helps to solve this performance problem by mapping one object to several columns of a table related to another object. In fact, by using this design pattern, the preceding model changes as follows:

Figure%209.5.png
Figure 9.5: Mapping Publisher and Address to one table

In Figure 9.5, two classes, Publisher and Address, are mapped to one table. Now, if we want to rewrite the previously presented query for this model, we will reach the following SQL code:

SELECT Name, Country, Province, City, Street

FROM Publisher WHERE PublisherId = 1

The preceding query will perform better than the previous query due to no need to use JOIN.

With these explanations, the proposed requirements can be implemented as follows:

public class Publisher

{

public int PublisherId { get; set; }

public string Name { get; set; }

public Address AddressInfo { get; set; }

public async Task FindAsync(int publisherId)

{

var reader = await new SqlCommand($"" +

$"SELECT * " +

$"FROM Publisher " +

$"WHERE ID = {publisherId}", DB.Connection).ExecuteReaderAsync();

reader.Read();

Publisher result = new()

{

PublisherId = (int)reader["Id"],

Name = (string)reader["Name"],

AddressInfo = new Address

{

Country = (string)reader["Country"],

Province = (string)reader["Province"],

City = (string)reader["City"],

Street = (string)reader["Street"]

}

};

return result;

}

}

public class Address

{

public string Country { get; set; }

public string Province { get; set; }

public string City { get; set; }

public string Street { get; set; }

}

As it is clear in the preceding code, the Address class does not have any method or code to communicate with the database, and all the communication of this class with the database happens through its owner, that is, the Publisher class. In other words, AddressInfo in the Publisher class is a value object. As it is evident in the Address class, this value object has no identity, and its identity finds its meaning through the owner. This is precisely why the Address class is not mapped to a separate table but only to a series of special columns of its owner object.

Notes:

Understanding when two objects can be stored in the embedded value format is a point that should be considered. The important point in making this decision is to pay attention to data retrieval and storage processes. The question must be answered whether the dependent element is useful outside the owner's domain. If yes, then should we probably store both in the same table (note, it is said probably because this decision is subject to other conditions as well), or should the dependent element data also be fetched whenever the owner data is fetched?
This design pattern is usually used for one-to-one relationships.
This design pattern is a special mode of the dependent mapping design pattern.
Consequences: Advantages

Using this design pattern, you can easily work with value objects.
Consequences: Disadvantages

This design pattern is only suitable for simple dependent elements. If the dependent elements are complex, this design pattern will not be suitable, and the serialized LOB design pattern should be used.
Applicability:

When we are faced with a table that has many columns, but in terms of modeling and object formation, we need to divide this table into different models and put methods specific to each class; in this case, this design pattern can be useful.
Related patterns:

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

Dependent mapping
Serialized LOB
Serialized LOB
Name:

Serialized LOB

Classification:

Object-relational structures design patterns

Also known as:

---

Intent:

Using this design pattern, a graph of objects can be converted into a Large Object (LOB) and stored in a database field.

Motivation, Structure, Implementation, and Sample code:

In the continuation of the previous example for the embedded value design pattern, suppose a requirement is raised and, in its format, it is necessary to select and save the publisher's field of activity from thematic categories. The subject category has a tree structure as follows:

General
Novel
Science fiction
Motivational
Academic
Computer field
Software
Hardware
Industries
Law
International law
English language
Children
Teenagers
Adults
For example, for publisher X, we should be able to create and store the following tree:

Academic
Computer field
Software
English language
Children
Teenagers
Adults
There are different ways to implement this scenario, but one of the simplest ways is to convert each publication tree into a LOB and store the LOB result in a table field. In other words, the following code can be imagined to implement the above requirement:

public class Publisher

{

public int PublisherId { get; set; }

public string Name { get; set; }

public ICollection Categories { get; set; }

}

public class Category

{

public string Title { get; set; }

public ICollection SubSet { get; set; }

}

As you can see, the Publisher class has a property called Categories, where the LOB result is supposed to be stored. The structure of the Publisher table in the database is as follows:

CREATE TABLE Publisher(

PublisherId INT PRIMARY KEY IDENTITY(1,1),

Name NVARCHAR(200) NOT NULL,

Categories NVARCHAR(MAX)

)

As it is clear from the table structure, the Categories column in the Publisher table has a string data type, so the Categories property in the Publisher class must be somehow converted into a string. Although there was no requirement for this column to be a string, it could be binary. With this explanation, there are different methods to generate LOB, which are:

Binary LOB (BLOB): The object is converted into binary content and stored. The advantage of this method is the simplicity of production and storage and its small volume. The drawback of this method is that the result is not human-readable (of course, this may be an advantage in terms of security).
Character LOB (CLOB): The object is converted into a string and stored. The advantage of this method is that the result is human-readable, and it is also possible to run queries on them in the database without the need to convert them into objects again. The drawback of this method is that the content is bulky compared to the BLOB method.
After choosing a method to convert the object to LOB, the mechanisms for converting an object to LOB and LOB to an object must be implemented. We assume we want to use the CLOB method in the above example. Therefore, the following methods can be considered to insert or convert LOB into the expected object:

public async Task AddAsync(Publisher publisher)

{

return (await new SqlCommand($"" +

$"INSERT INTO Publisher " +

$"(Name, Categories) VALUES " +

$"N'{publisher.Name}',

N'{JsonConvert.SerializeObject(publisher.Categories)}'",

DB.Connection).ExecuteNonQueryAsync()) > 0;

}

public ICollection GetCategories(string serializedCategories)

=> JsonConvert.DeserializeObject>(serializedCategories);

As shown in the AddAsync method, when inserting publisher information, the information related to Categories is converted into a string with a JSON structure and then saved. Also, using the GetCategories method, the CLOB stored in the table is converted into the expected object. Newtonsoft Json.NET1 tool was used for these conversions.

Notes:

Sometimes it may be necessary to place the LOB in another table instead of the main table. This dependent table usually has two columns, one for ID and another for LOB.
In this design pattern, you should always be sure that the LOB content will be available only through its owner.
Suppose there is a separate database or data warehouse for reporting. In that case, the content of the LOB can be converted into a suitable table in the destination data source so that we do not face performance problems in reporting.
By using compression methods, it is possible to reduce the space consumption of CLOB. In this case, the effect of the most important drawback of this method is slightly reduced.
Consequences: Advantages

Converting the less used part of the object to LOB makes it possible to save the used space, improving efficiency.
Consequences: Disadvantages

When using this design pattern, if data outside the LOB has a reference inside the LOB, it will cause this design pattern to cause problems, including performance, for the program.
Some database management systems do not allow queries in XML or JSON formats; therefore, it is difficult to work with this data type on the database side. (Microsoft SQL Server allows writing queries on both XML and JSON).
Applicability:

When we are faced with an object where some of its data are not used much, then by using this design pattern, we can convert that part of the object into a LOB and store it in a field.
Related patterns:

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

Embedded Value
Single table inheritance
Name:

Single table Inheritance

Classification:

Object-relational structures design patterns

Also known as:

Table per hierarchy or Discriminated mapping

Intent:

Using this design pattern, you can map the classes that participated in the inheritance structure to one table.

Motivation, Structure, Implementation, and Sample code:

Following the example in the previous sections, suppose that authors who work with publishers collaborate in two formats. Some authors work hourly, and others work with the publisher on a fixed monthly salary. Now a requirement has been raised, and we want to have the financial information of the authors as well. Specifically, the design that can be considered for this requirement can be seen in the form of the following class diagram:

Figure%209.6.png
Figure 9.6: Relation between the Author and his/her collaboration format

According to Figure 9.6 class diagram, authors are of two types. Authors who are paid monthly, whose monthly receipt information is in the MonthlyPaidAuthor class, and authors who are paid hourly and their hourly working information is in the HourlyPaidAuthor class. The Figure 9.6 class diagram can be implemented as follows:

public abstract class Author

{

public int AuthorId { get; set; }

public string FirstName { get; set; }

public string LastName { get; set; }

}

public class HourlyPaidAuthor : Author

{

public int HourlyPaid { get; set; }

public int HoursWorked { get; set; }

}

public class MonthlyPaidAuthor : Author

{

public int Salary { get; set; }

}

As the preceding code shows, the HourlyPaidAuthor and MonthlyPaidAuthor classes inherit from the Author class. Now, this inheritance structure can be mapped to a table in the database so that the authors' data can be retrieved and worked with without using JOIN. Therefore, for the whole preceding structure, the following table can be considered:

CREATE TABLE Author(

AuthorId INT PRIMARY KEY IDENTITY(1,1),

FirstName NVARCHAR(200) NOT NULL,

LastName NVARCHAR(200) NOT NULL,

HourlyPaid INT,

HoursWorked INT,

Salary INT

)

As shown in the preceding T-SQL code, all the properties in the inheritance structure are presented as a table in the database. The important point is that there should be a way to distinguish between the records related to HourlyPaidAuthor and MonthlyPaidAuthor classes. To create this feature, we need to use a separate column. This column contains information about the corresponding class, such as the class name or a unique code for each class. This column is called Discriminator. Therefore, with these explanations, the structure of the table changes as follows:

CREATE TABLE Author(

AuthorId INT PRIMARY KEY IDENTITY(1,1),

FirstName NVARCHAR(200) NOT NULL,

LastName NVARCHAR(200) NOT NULL,

HourlyPaid INT,

HoursWorked INT,

Salary INT,

Type VARCHAR(100) NOT NULL

)

In the preceding table, the Type column plays the role of Discriminator. When the information of authors with monthly income is placed in this table, then the values of HourlyPaid and HoursWorked columns will be empty. The same rule is also true for records related to hourly paid authors, and for them, the value of the Salary column will be empty.

The following code shows how to retrieve records for each class:

public abstract class Author{

private readonly string discriminator;

protected Author(string discriminator) => this.discriminator = discriminator;

public int AuthorId { get; set; }

public string FirstName { get; set; }

public string LastName { get; set; }

protected async Task GetAllAsync()

=> await new SqlCommand($"" +

$"SELECT * " +

$"FROM Authors " +

$"WHERE Discriminator=N'{discriminator}'",

DB.Connection).ExecuteReaderAsync();

}

public class HourlyPaidAuthor : Author{

public HourlyPaidAuthor() : base(nameof(HourlyPaidAuthor)) { }

public int HourlyPaid { get; set; }

public int HoursWorked { get; set; }

public async Task> ReadAllAsync(){

var result = new List();

var reader = await base.GetAllAsync();

while (reader.Read())

result.Add(new HourlyPaidAuthor

{

AuthorId = (int)reader["AuthorId"],

FirstName = (string)reader["FirstName"],

LastName = (string)reader["LastName"],

HourlyPaid = (int)reader["HourlyPaid"],

HoursWorked = (int)reader["HoursWorked"]

});

return result;

}

}

public class MonthlyPaidAuthor : Author{

public MonthlyPaidAuthor() : base(nameof(MonthlyPaidAuthor)) { }

public int Salary { get; set; }

public async Task> ReadAllAsync(){

var result = new List();

var reader = await base.GetAllAsync();

while (reader.Read())

result.Add(new MonthlyPaidAuthor

{

AuthorId = (int)reader["AuthorId"],

FirstName = (string)reader["FirstName"],

LastName = (string)reader["LastName"],

Salary = (int)reader["Salary"]

});

return result;

}

}

As specified in the GetAllAsync method in the Author class, the discriminator filter is applied when sending a request to retrieve records. To implement this section, you may take help from other design patterns, but here we have tried to display the code in the simplest possible way. Also, in the preceding code, the Author class is defined abstractly.

Notes:

Discriminator content can be either a class name or a unique code. If the class name is used, its advantage is the ease of use, and its drawback is the large space that the string content occupies in the database. Also, if the code is used, the advantage is that it takes up less space, and its drawback is the need for a converter to find out which class should be instantiated for each code.
When inserting or retrieving records, records related to a specific class can be accessed by providing the Discriminator value.
To simplify the implementation, the inheritance mapper design pattern can be used.
Consequences: Advantages

Using this design pattern, we only deal with one table in the database, simplifying the work in the database.
Since different data are in the same table, there is no need to use Join to retrieve and read records.
It is possible to refactor the code without changing the structure of the database. You can move various features in the inheritance tree without changing the database structure.
Consequences: Disadvantages

All the fields in this type of structure are not necessarily related to each other, and this can increase the complexity when using the table.
Using this design pattern, some columns in the table will have empty content, which can negatively impact performance and used space. Of course, many database management systems today provide different methods for compressing empty spaces.
In this case, we will face a very large and bulky table. Increasing the volume and size of the table means the presence of different indexes and the placement of multiple locks on the table, which can have a negative effect on performance.
Applicability:

When we face an inheritance structure and want to store the data related to the entire structure in a table, we can use this design pattern.
Related patterns:

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

Inheritance mapper
Class table inheritance
Concrete table inheritance
Class table inheritance
Name:

Class table inheritance

Classification:

Object-relational structures design patterns

Also known as:

Root-leaf mapping

Intent:

Using this design pattern, in an inheritance structure, each class is mapped to a separate table in the database.

Motivation, Structure, Implementation, and Sample code:

Suppose that in the example provided in the single table inheritance section, we want to put the information of each class in its table. Suppose we need to store the author's general information in the authors table, financial information related to authors with hourly payments in the hourlyPaidAuthor table, and monthly payments in the monthlyPaidAuthor table.

With these explanations, the codes presented in the single table inheritance design pattern can be rewritten as follows:

public class Author

{

public int AuthorId { get; set; }

public string FirstName { get; set; }

public string LastName { get; set; }

public async Task GetAllAsync() => await new SqlCommand($"" +

$"SELECT * " +

$"FROM authors", DB.Connection).ExecuteReaderAsync();

}

public class HourlyPaidAuthor : Author

{

public int HourlyPaid { get; set; }

public int HoursWorked { get; set; }

public async Task GetAllAsync() => await new SqlCommand($"" +

$"SELECT * " +

$"FROM authors AS a " +

$"INNER JOIN hourlyPaidAuthor as h" +

$"ON a.AuthorId = h.AuthorId", DB.Connection).ExecuteReaderAsync();

}

public class MonthlyPaidAuthor : Author

{

public int Salary { get; set; }

public async Task GetAllAsync()

=> await new SqlCommand($"" +

$"SELECT * " +

$"FROM authors AS a " +

$"INNER JOIN monthlyPaidAuthor as m" +

$"ON a.AuthorId = m.AuthorId", DB.Connection).ExecuteReaderAsync();

}

As it is clear in the preceding code, the Author class has the GetAllAsync method. By using this method, the author's general information is fetched from the authors table. If we need to retrieve the information of hourly authors, we can use the GetAllAsync method available in the HourlyPaidAuthor class. In this class, Join is required and used to retrieve the author's information.

Notes:

There is no requirement to map the entire inheritance structure with one method, and you can benefit from different mapping patterns when dealing with an inheritance structure.
The important point for this design pattern will be how to read data from the database. For this, you can send a specific query for each table, which will cause performance problems. Another method would be to use Join to read the data. In using Join, the Outer Join method may also be used depending on the conditions.
Another point that should be paid attention to is how different records in different tables are related. There are different ways for this relationship. One of these ways is to use the common value in the primary key field. Another method is that each class has its primary key value and has the foreign key of the parent table.
To simplify the implementation, the Inheritance Mapper design pattern can be used.
Consequences: Advantages

By using this design pattern, there will be no waste of space in the database.
The connection between the domain model and the tables is clear.
Consequences: Disadvantages

To retrieve the data, it is necessary to read it using Join from several tables or send several queries.
Any refactoring that changes the location of properties in the inheritance structure will change the table structure.
Tables that are close to the root of the inheritance tree can cause problems due to frequent references.
Applicability:

When we face an inheritance structure, and we want to store the data related to each class in the inheritance structure in its table, we can use this design pattern.
Related patterns:

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

Single table inheritance
Inheritance mapper
Concrete table inheritance
Concrete table inheritance
Name:

Concrete table inheritance

Classification:

Object-relational structures design patterns

Also known as:

Leaf level inheritance

Intent:

Using this design pattern, each non-abstract class is mapped to a table in the database.

Motivation, Structure, Implementation, and Sample code:

In the example provided for the class table inheritance design pattern, suppose a requirement is raised so that we have two types of authors, the first type is paid hourly, and the second type is paid monthly. And we want to put the data of each type in its tables. Regardless of the type of author, each author has a series of common characteristics with other authors. Including first and last names, and so on. Because of this feature, usually, the following structure can be considered:

Figure%209.7.png
Figure 9.7: Relation between the Author and his/her collaboration format

As it is clear in the Figure 9.7 class diagram, the Author class is of an abstract type, and the two inherent classes MonthlyPaidAuthor and HourlyPaidAuthor are inherited from it. Using the concrete table inheritance design pattern, each intrinsic class is mapped to a table in the database. This means that in the database, we will encounter two tables monthlyPaidAuthor and hourlyPaidAuthor tables, each of which will contain columns related to themselves and their parent. Therefore, we will have the following tables in the database:

CREATE TABLE monthlyPaidAuthor(

AuthorId INT PRIMARY KEY IDENTITY(1,1),

FirstName NVARCHAR(200) NOT NULL,

LastName NVARCHAR(200) NOT NULL,

Salary INT

)

CREATE TABLE hourlyPaidAuthor(

AuthorId INT PRIMARY KEY IDENTITY(1,1),

FirstName NVARCHAR(200) NOT NULL,

LastName NVARCHAR(200) NOT NULL,

HourlyPaid INT,

HoursWorked INT

)

As can be seen, all the properties related to the parent class are repeated in each of the tables. With the preceding explanation, the following code can be imagined:

public abstract class Author

{

public int AuthorId { get; set; }

public string FirstName { get; set; }

public string LastName { get; set; }

public abstract Task GetAllAsync();

}

public class HourlyPaidAuthor : Author

{

public int HourlyPaid { get; set; }

public int HoursWorked { get; set; }

public override async Task GetAllAsync()

=> await new SqlCommand($"" +

$"SELECT * " +

$"FROM hourlyPaidAuthor", DB.Connection).ExecuteReaderAsync();

}

public class MonthlyPaidAuthor : Author

{

public int Salary { get; set; }

public override async Task GetAllAsync()

=> await new SqlCommand($"" +

$"SELECT * " +

$"FROM monthlyPaidAuthor", DB.Connection).ExecuteReaderAsync();

}

As it is clear in the preceding code, the Author class is defined abstractly and will not be mapped to any table in the database. The HourlyPaidAuthor class is inherited from the Author class to be mapped to the hourlyPaidAuthor table in the database. Also, this class has the implementation of the GetAllAsync method, and in this way, it reads the records related to its table. The implementation of insert, edit and delete methods will be the same.

Notes:

There is no requirement to map the entire inheritance structure with one method, and you can benefit from different mapping patterns when dealing with an inheritance structure.
To simplify the implementation, you can use the inheritance mapper design pattern.
Using this design pattern, you should consider the primary key's value. Usually, the primary key column is in an abstract class. If the code for generating the primary key is also in that class, duplicate values may be generated for the primary key.
Consequences: Advantages

Each table contains all its related data, and there is no need for additional columns.
There is no need to use Join to read the table data; all the parent class features are repeated in the child classes, resulting in their tables.
Consequences: Disadvantages

Primary key value management can be difficult.
In the case of refactoring the codes and changing the location of the properties, it will be necessary to change the structure of the tables as well. This change is less than required in the class table inheritance design pattern and more than in the single table inheritance design pattern.
If the parent class is changed, all tables related to child classes must be changed.
Applicability:

When faced with an inheritance structure and we want to store the data related to each class inherent in the inheritance structure in our table, we can use this design pattern.
Related patterns:

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

Inheritance mapper
Class table inheritance
Single table inheritance
Inheritance mappers
Name:

Inheritance mappers

Classification:

Object-relational structures design patterns

Also known as:

---

Intent:

Organizing different existing mappers to manage the inheritance structure using this design pattern is possible.

Motivation, Structure, Implementation, and Sample code:

In the examples presented for different methods of mapping the inheritance structure to tables, it is often necessary to use a structure to prevent the generation of duplicate codes. There is also a need for a class to be responsible for loading and storing records related to a domain class. All these things can be done with the help of inheritance mappers' design patterns. Following the example provided for authors, suppose we want to implement fetch, insert, delete, and edit operations for each type of author. Consider the example of the concrete table inheritance design pattern. According to that example, we have an abstract class called Author and two intrinsic classes called HourlyPaidAuthor and MonthlyPaidAuthor.

Usually, we have a mapper for each model. The following class diagram can be considered for mappers:

Figure%209.8.png
Figure 9.8: Relation between AuthorMapper and different collaboration format mappers

As seen in the Figure 9.8 class diagram, the AuthorMapper class is abstract. We must use this model to implement fetching, inserting, editing, and deleting operations.

For retrieving records, since retrieving records from the database will result in the corresponding method returning one or a set of intrinsic objects. Therefore, according to the nature of this operation, the fetching operation must be placed in each of the inherent Mappers and cannot be placed in the parent’s abstract Mapper. Therefore, for fetching, the following codes for HourlyPaidAuthorMapper can be imagined:

public abstract class AuthorMapper { }

public class HourlyPaidAuthorMapper : AuthorMapper

{

protected HourlyPaidAuthor Load(IDataReader reader)

{

return new HourlyPaidAuthor()

{

AuthorId = (int)reader["AuthorId"],

FirstName = (string)reader["FirstName"],

LastName = (string)reader["LastName"],

HourlyPaid = (int)reader["HourlyPaid"],

HoursWorked = (int)reader["HoursWorked"]

};

}

public async Task> GetAllAsync()

{

var result = new List();

var reader = await new SqlCommand($"" +

$"SELECT * " +

$"FROM hourlyPaidAuthor", DB.Connection).ExecuteReaderAsync();

while (reader.Read())

result.Add(Load(reader));

return result;

}

}

The implementation of these methods for the monthlyPaidAuthorMapper class is also the same. The mechanism can be different from inserting or editing, and you can get help from the AuthorMapper class. For example, as follows:

public abstract class AuthorMapper

{

private readonly string tableName;

public AuthorMapper(string tableName) => this.tableName = tableName;

protected string GetUpdateStatementById(Author author)

{

return $"UPDATE {tableName} " +

$"SET FirstName =N'{author.FirstName}',

LastName=N'{author.LastName}' " +

$"#UpdateToken# " +

$"WHERE AuthorId = {author.AuthorId}";

}

protected async Task SaveAsync(string query)

=> await new SqlCommand(query, DB.Connection).ExecuteNonQueryAsync() > 0;

}

public class HourlyPaidAuthorMapper : AuthorMapper

{

public HourlyPaidAuthorMapper() : base("hourlyPaidAuthor") { }

public async Task UpdateAsync(HourlyPaidAuthor obj)

=> await SaveAsync(base.GetUpdateStatementById(obj)

.Replace("#UpdateToken#", $"HoursWorked = {obj.HoursWorked}"));

}

For the sake of simplicity, the codes related to data retrieval have been removed. As can be seen in the preceding code, the UpdateAsync method in the HourlyPaidAuthorMapper class performs editing operations with the help of the GetUpdateStatementById and SaveAsync methods defined in the parent mapper. The insertion mechanism is also similar to this mechanism. The deletion process is also like this mechanism, which can be seen in the following code:

public abstract class AuthorMapper

{

private readonly string tableName;

public AuthorMapper(string tableName) => this.tableName = tableName;

public string GetDeleteStatementById(int authorId)

=> $"DELETE FROM {tableName} WHERE AuthorId = {authorId}";

protected async Task SaveAsync(string query)

=> await new SqlCommand(query, DB.Connection).ExecuteNonQueryAsync() > 0;

public virtual async Task DeleteAsync(int authorId)

=> await SaveAsync(GetDeleteStatementById(authorId));

}

public class HourlyPaidAuthorMapper : AuthorMapper

{

public HourlyPaidAuthorMapper() : base("hourlyPaidAuthor") { }

}

The codes related to editing and data retrieval have been removed for simplicity. As seen in the preceding code, the AuthorMapper class has implemented data deletion logic, and child mappers can use these methods.

Notes:

This design pattern is usually used alongside single, class, or concrete table inheritance design patterns.
Consequences: Advantages

It removes duplicate codes and thus increases the maintainability of the code.
Consequences: Disadvantages

For small scenarios, it can increase the complexity.
Applicability:

This design pattern can be useful when a database mapping based on inheritance exists.
Related patterns:

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

Single table inheritance
Class table inheritance
Concrete table inheritance
Conclusion
In this chapter, you got acquainted with various methods for data mapping and mapping the structure of classes and tables in databases. Also, in this chapter, you learned how to store and retrieve Value Objects with a proper table structure.

In the next chapter, you will learn about Object-relational metadata mapping patterns.

1 Newtonsoft Json.NET is a popular high-performance JSON framework for .NET

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

NET 7 Design Patterns In-Depth 8. Object-Relational Behaviors Design Patterns

Chapter 8
Object-Relational Behaviors Design Patterns
Introduction
To organize the object-relational behaviors, the design patterns of this category can be divided into the following three main sections:

Identity map: Tries to provide a method to fetch each record from the database only once.
Lazy load: Tries to load an object's data when needed.
Unit of work: Tries to send business transactions to the database as a single transaction.
The choice between these three design patterns depends on the level of logical complexity that we want to implement.

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

Object-relational behaviors design patterns
Unit of work
Identity map
Lazy load
Objectives
In this chapter, you will be introduced to object-relational behaviors design patterns and learn how to manage business transactions properly. You will also learn how to improve performance and efficiency by reducing references to the data source or when necessary.

Object-relational behaviors design patterns
Chapter 7, Data Source Architectural Patterns, tried to explain how different objects can be linked to tables in the database. In this chapter, we face another challenge: paying attention to behaviors. Behaviors mean how the data should be fetched from the database or how it should be stored in it. For example, suppose a lot of data is fetched from the database, and some have changed. It will be very important to answer the question of which of the data has changed or how to store the changes again in the database, provided that the data consistency is not disturbed.

Ensuring that the data is not read and changed by someone else is an issue related to concurrency management and design patterns. How to manage and apply changes is something that the unit of work design pattern can be useful for. When using the unit of work to improve efficiency, we need to ensure that the data that has been read will not be re-read. This problem can be solved with the identity map design pattern.

When the domain model is used, most models connect with other models. Reading a model will lead to retrieving all its relationships, again jeopardizing efficiency. To solve this problem, you can use the lazy load design pattern.

Unit of work
Name:

Unit of work

Classification:

Object-relational behaviors design patterns

Also known as:

---

Intent:

This design pattern tries to send business transactions to the database as a single transaction.

Motivation, Structure, Implementation, and Sample code:

Suppose we are implementing an infrastructure in which we want to manage the projects and people involved in an organization's project. The project and team member information of each project will be stored in separate tables in the database. There are different ways to implement this scenario. A simple way is to send each business request directly to the database and manage the transaction by placing and defining the transaction at the database level. This method has a problem, and sending a series of small requests to the database is necessary. In a system with high requests and user volume, these small requests will become the root of a big problem.

Another way is to manage these business requests at the application level and, after completing the business transaction process, send the entire business transaction to the database in the form of a single transaction. The unit of work design pattern can be useful in achieving this design.

Suppose we assume that we are facing only one model named project. The events that happen in this model are the events that lead to the creation of a new project, the deletion or editing of the existing project, and the reading of the project. With this hypothesis, to track changes at the application level, you can simply define several collections or arrays to track the changes and send them to the database at the right time.

There are different ways to implement the unit of work pattern. One of the most common methods is to combine the implementation of this design pattern with the repository design pattern. For example, suppose we have two repositories named UserRepository and BankAccountRepository. These two repositories have their own DbContext object and are connected to a common database. Figure 8.1 shows this relationship:

Figure%208.1.png
Figure 8.1: User and Bank account repositories with separated DbContexts

In this case, because UserRepository and BankAccountRepository have different objects from DbContext, their work is sent to the database in the form of two transactions (two units of work). These two repositories must use a shared DbContext object to solve this problem. Figure 8.2 shows this arrangement:

Figure%208.2.png
Figure 8.2: User and Bank account repositories with shared DbContext

As shown in Figure 8.2, the two repositories UserRepository and BankAccount Repository use a shared DbContext object so that the work of these two repositories can be sent to the database in the form of one transaction (one unit of work). As stated in the repository design pattern, the generic repository can also be used to implement this design pattern. Next, we will implement the unit of work design pattern with a generic repository.

According to the preceding explanations, the class diagram of this model in the presence of the generic repository model can be considered as follows:

Figure%208.3.png
Figure 8.3: UnitofWork with generic repository UML diagram

As can be seen in Figure 8.3 class diagram, the GenericRepository class is defined as generic and implements the IRepository interface. The UserRepository class can also inherit from the GenericRepository class and change some of its implementations (we have not implemented this part and assumed that the behaviors of the UserRepository are consistent with the GenericRepository). The UnitOfWork class implemented the IUnitOfWork interface and used the IRepository interface inside. This is because all access to repositories happens through the UnitOfWork object. Refer to the following code for this structure:

public interface IUnitOfWork

{

void Commit();

}

public class UnitOfWork : IUnitOfWork

{

private SampleDbContext _context = new();

private IRepository _userRepository;

private IRepository _bankAccountRepository;

public IRepository UserRepository

{

get

{

if (_userRepository == null)

_userRepository = new GenericRepository(_context);

return _userRepository;

}

}

public IRepository BankAccountRepository

{

get

{

if (_bankAccountRepository == null)

_bankAccountRepository =

new GenericRepository(_context);

return _bankAccountRepository;

}

}

public void Commit() => _context.SaveChanges();

}

As you can see in the preceding code, when creating a repository object, a DbContext object is sent to it, and this makes both repositories have the same DbContext object. Also, for the preceding code, the generic repository design pattern is implemented as follows:

public interface IRepository

{

TEntity Find(TKey id);

List GetAll();

void Add(TEntity user);

void Update(TEntity user);

void Delete(TKey id);

}

public class GenericRepository : IRepository

where TEntity : class

{

internal SampleDbContext _context;

internal DbSet _dbSet;

public GenericRepository(SampleDbContext context)

{

_context = context;

_dbSet = context.Set();

}

public virtual List GetAll() => _dbSet.ToList();

public virtual TEntity Find(TKey id) => _dbSet.Find(id);

public virtual void Add(TEntity entity) => _dbSet.Add(entity);

public virtual void Delete(TKey id) => _dbSet.Remove(_dbSet.Find(id));

public virtual void Update(TEntity entityToUpdate)

{

_context.Entry(entry).State = EntityState.Modified;

}

}

Remember that this implementation can be improved from the repository and unit of work model. It is presented in the simplest implementation mode just to facilitate content transfer. Now, to use this structure, you can proceed as follows:

IUnitOfWork unitOfWork = new UnitOfWork();

unitOfWork.UserRepository.Add(new User { Id = 1, Name = "Ahmad" });

unitOfWork.BankAccountRepository.Add(

new BankAccount { Id = 101, AccountNumber = 12345});

unitOfWork.Commit();

In the preceding code, a UnitOfWork object is created. A new user is added through the UserRepository, and a bank account is defined through the BankAccountRepository. Finally, the changes are sent to the database as one transaction.

Notes:

To track changes using this design pattern, it will be necessary to record the changes somewhere. There are two ways to do this:
Caller registration: The user must register the changes in the unit of work object to be applied in the database. The advantage of this method is its flexibility in sending changes to apply to the database. The problem with this method is that the user may forget to register the changes for any reason.
Object registration: In this method, the registration process is done through the methods in the target object. Usually, when performing the retrieval operation, the desired object is registered as a clean version. Then, as soon as a change occurs on this object, the Dirty version is formed. Despite having overhead, two versions of the object can significantly help detect changes.
This design pattern is usually used next to the repository design pattern.
The significant point is that the entity framework has implemented this change tracking process and unit of work design pattern in the SaveChanges method. Here the question arises whether this feature in the entity framework still needs to implement this design pattern in the systems or not. As with the repository design pattern, the answer to this question is also challenging, but implementing a unit of work in systems can be very useful.
Consequences: Advantages

Provides better control for transaction management.
Due to the reduction in the number of visits to the database, it provides better efficiency for batch operations.
The ability to maintain flexibility and testability of codes will be improved, and unit tests can be compiled and executed easily using mock mechanisms.
With the existence of a unit of work, there will be no need to use classes like DbContext and so on in the business layers. It will enable a loose coupling between the business layer and the framework used in the data access layer to easily make changes in the data access layer without changing the business layer (for example, entity framework can be replaced with another ORM).
Consequences: Disadvantages

For business logic and simple data access, often the use of this design pattern increases the complexity because today, the majority of ORMs offer the features presented in this design pattern. Using this design pattern will be useful when a new feature is added to them apart from the features provided by ORM.
Applicability:

This design pattern can be useful when there is a need to separate the communication of the business layer from the data layer and optimize the number of communication times with the database.
Using this design pattern in Domain-Driven Design (DDD) is very practical.
This design pattern will be useful when we face a series of requests and want to process them in one transaction.
Related patterns:

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

Repository
Identity map
Identity map
Name:

Identity map

Classification:

Object-relational behaviors design patterns

Also known as:

---

Intent:

This design pattern tries to provide a method to fetch each record from the database only once. For this purpose, the records fetched for the first time are kept in a mapping set to be retrieved from this set whenever necessary.

Motivation, Structure, Implementation, and Sample code:

For example, consider the following code:

User user1 = userService.GetByName("Vahid");

User user2 = userService.GetByName("Vahid");

In the preceding code, data is read twice from the database and placed in two objects. In this situation, making changes to user1 has nothing to do with user2. Even if we want to save both objects in the database, one of the objects will be written on the other object. With this method, there is a risk that will result in concurrency problems. The identity map design pattern tries to solve this problem.

The identity map design pattern stores the records that have been read once from the database in a set so that when that record is needed later, instead of retrieving it from the database, the record is returned from the set that formed the identity map. In this way, the overall efficiency of the application will be improved. To implement an identity map, you can have a mapping set for each table in the database. Also, when implementing an identity map, issues related to concurrency should be considered.

Among the applications of this design pattern, we can mention the ability to cache data. The following class diagram shows the identity map design pattern:

Figure%208.4.png
Figure 8.4: Identity Map design pattern UML diagram

The Figure 8.4 class diagram shows that the UserMap class has a set of mappings named _mappings. This class has methods to add, delete and read records from the mapping set. Suppose we assume that the UserMap class is available from somewhere like the repository before sending the request to read the record to the database. In that case, that record is first searched by the Get method in the UserMap mapping set. If the desired record is not found in this set, the request to the database is sent, and the received response is first recorded in the mapping set using the Add method, and then the response is returned to the requester. With these explanations, the Figure 8.4 class diagram can be implemented as follows:

public class UserDbSet

{

public static List Users = new()

{

new User() { Id = 1, Username = “Vahid”, Email = “vahid@gmail.com” },

new User() { Id = 2, Username = “Ali”, Email = “ali@yahoo.com” },

new User() { Id = 3, Username = “Reza”, Email = “reza@gmail.com” },

new User() { Id = 4, Username = “Maral”, Email = “maral@gmail.com” },

new User() { Id = 5, Username = “Hassan”, Email = “hassan@yahoo.com” }

};

}

public class User

{

public int Id { get; set; }

public string Username { get; set; }

public string Email { get; set; }

}

public class UserMap

{

private readonly Dictionary _mappings = new();

public void Add(User user)

{

if (!_mappings.ContainsKey(user.Id))

_mappings.Add(user.Id, user);

}

public User? Get(int id)

{

if (_mappings.ContainsKey(id))

return _mappings[id];

return null;

}

public void Remove(int key)

{

if (_mappings.ContainsKey(key))

_mappings.Remove(key);

}

}

public interface IUserRepository

{

User Get(int id);

}

public class UserRepository : IuserRepository

{

private readonly UserMap _usermap;

public UserRepository() => _usermap = new UserMap();

public User Get(int id)

{

var cachedUser = _usermap.Get(id);

if (cachedUser == null)

{

var user = UserDbSet.Users.FirstOrDefault(x => x.Id == id);

_usermap.Add(user);

return user;

}

return cachedUser;

}

}

As seen in the preceding code, when the Get method is executed in the UserRepository class, the mapping set in the UserMap is first checked through the Get method. If this record already exists in this mapping set, then the record is returned without referring to the database; otherwise, the record is read from the database and registered in the mapping set, and returned.

Notes:

Retrieving data from the database multiple times and placing it in several objects can damage its accuracy. The same multiple retrieving from an external source can affect performance.
There will be one mapping for each database table in a similar structure (database structure and model structure are the same).
Consider the following code:
var query1 = ctx.Users.Where(x => x.Name == “Vahid”);

var user1 = query1.FirstOrDefault();

var user2 = query1.FirstOrDefault();

var query2 = ctx.Users.Where(x => x.Id == 1);

var user3 = query2.FirstOrDefault();

Console.WriteLine(user1.GetHashCode());

Console.WriteLine(user2.GetHashCode());

Console.WriteLine(user3.GetHashCode());

By executing the preceding code, we realize that all three objects, user1, user2, and user3, are one object. This means that the entity framework does the same thing as the identity map behind the scenes and does not allow loading an object more than once.
A key will be needed to design the mapping set, so the record can be found later based on that key. The best option for the key is to choose the table’s primary key, although other combinations can also be used as keys.
This design pattern can also be implemented generically. In this case, the whole process can be implemented using one class. The key selection is important in implementing the generic method. Because, in this case, all sets of mappings must have a fixed formula for the key.
How and where to store the mapping set is also important in this design pattern. This collection should be stored in a way that is different for each session. For some read-only data that never change, the storage method and location will not matter, and those data can be shared between sessions.
Since concurrency management is important when using this design pattern, the optimistic offline lock and design patterns are widely used.
Usually, using this design pattern in the unit of work is a better design. Because the unit of work is the data entry and exit point, if there is no unit of work, the presence of this design pattern next to the registry design pattern can be useful.
There is no need to use this design pattern for immutable objects. The reason for this is also clear. When an object is immutable, its value will not change; when the value does not change, there is no need to worry about the anomalies caused by the change. Among the most important immutable objects are value objects.
Consequences: Advantages

This design pattern can be used to implement the cache mechanism and thus reduce the number of references to the database.
Consequences: Disadvantages

This design pattern can manage the collision event in one session but cannot do anything for the collision event between several sessions. To solve this problem, you must use optimistic offline and pessimistic offline locks.
Applicability:

This design pattern can be useful when data needs to be read only once from the source.
Related patterns:

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

Unit of work
Optimistic offline lock
Pessimistic offline lock
Registry
Lazy load
Name:

Lazy load

Classification:

Object-relational behaviors design patterns

Also known as:

---

Intent:

This design pattern tries to load an object’s data when needed. In this case, while object initialization, no data is loaded, which can positively affect performance.

Motivation, Structure, Implementation, and Sample code:

Suppose there is a requirement in which the customer’s information is returned along with his orders. But in some places of the program, only customer information is needed, and in other places, orders are needed. In this case, receiving customer orders for each request will not be pleasant and can harm efficiency. Another way is to fetch the customer’s information from the database in every request but not fetch his orders. Wherever orders are needed, then the orders are picked up. After retrieving orders, if new orders are needed, the same list of previous orders can be returned.

The preceding method implements lazy load using the lazy initialization method. There are other methods to implement this design pattern, such as virtual proxy, value holder, and ghost, which we will learn about later.

Lazy initialization method
In Figure 8.5, you can see the class diagram of the Lazy Initialization method for the lazy load design pattern:

Figure%208.5.png
Figure 8.5: Lazy Initialization UML diagram

As shown in Figure 8.5 diagram, CustomerService receives order information from ExternalSource only once and only when needed. According to Figure 8.5 class diagram, the following codes can be considered:

public class Order

{

public int Id { get; set; }

public double Price { get; set; }

public int CustId { get; set; }

}

public class Customer

{

private List _orders;

public int Id { get; set; }

public string Name { get; set; }

public List Orders

{

get

{

if (_orders == null)

{

Console.WriteLine($"Loading orders for customer: {this.Name}");

_orders = OrderDbSet.Orders

.Where(x => x.CustId == this.Id).ToList();

}

return _orders;

}

}

}

As it is clear in the preceding code, inside the Orders property, it is checked whether the list of orders has already been loaded. If it is not loaded, the list of orders will be loaded. If you prefer to Orders again, the list of orders will not be loaded from the beginning, and the same list as before will be referred to. It can also be seen in the preceding code that Orders have no value when the Customer information is fetched, and when it is needed, the values inside will be filled. To use this code, you can proceed as follows:

1. Customer customer = CustomerDbSet.Customers.FirstOrDefault(x => x.Id == 1);

2. List orders = customer.Orders;

3. List orders2 = customer.Orders;

In line 1, customer information is fetched. This information was not fetched in this line because Orders information was unnecessary.
In line 2, Orders are needed for the first time, so in this line, the list of customer orders will be fetched from the database.
In line 3, where Orders are needed again, fetching has not happened, and the same list of previous orders is returned.
NOTE: .NET has a class called Lazy, which you can easily delay object initialization until it is used. For example, consider the following code:

Lazy lazyCustomer = new Lazy();

According to the preceding code, no instance of Customer is created. To create n instance, the instance should be received through lazyCustomer.Value. Referencing lazyCustomer.Value again will return the same object as before. The Lazy class allows you to provide the initialization process in the form of a Lambda expression to the constructor of the Lazy class:

Lazy lazyCustomer = new Lazy(()=>

{

Customer obj = new Customer();

//do something more with obj

return obj;

});

Now, using the Lazy class, you can rewrite the Customer class as follows:

public class Customer

{

private Lazy> _orders;

public int Id { get; set; }

public string Name { get; set; }

public Customer()

{

_orders = new Lazy>(() =>

{

return OrderDbSet.Orders.Where(x => x.CustId == this.Id).ToList();

});

}

public List Orders => _orders.Value;

}

Virtual proxy method

In this method, the virtual proxy object has the same structure as the main object. The main object is created the first time the virtual proxy object is requested. Then, the originally created object is returned whenever a reference is made to the virtual proxy. This implementation method combines proxy design patterns and lazy initialization method:

public interface IService

{

public List Orders { get; }

public int Id { get; set; }

public string Name { get; set; }

}

public class Customer : IService

{

private List _orders;

public int Id { get; set; }

public string Name { get; set; }

public Customer() =>_orders = OrderDbSet.Orders

.Where(x => x.CustId == this.Id).ToList();

public List Orders => _orders;

}

public class CustomerProxy : IService

{

private IService _service;

private void InitIfNeeded()

{

if (_service == null)

_service = new Customer();

}

public List Orders

{

get

{

InitIfNeeded();

return _service.Orders;

}

}

public int Id { get; set; }

public string Name { get; set; }

}

Ghost method
In this method, the desired object is incompletely loaded, and only information related to the primary key or main information is loaded. Other information that has not been loaded will be loaded on the first reference to them. The first reference to data that has not been loaded will cause all data to be loaded.

public class Customer

{

private string _name;

private List _orders;

private bool _isOrdersLoaded;

private bool _isloaded;

public int Id { get; set; }

public string Name

{

get

{

if (!_isloaded)

Load();

return _name;

}

}

public List Orders

{

get

{

if (!_isOrdersLoaded)

LoadOrders();

return _orders;

}

}

private void Load()

{

var customer = CustomerDbSet.Customers

.FirstOrDefault(x => x.Id == this.Id);

this._name = customer.Name;

_isloaded = true;

}

private void LoadOrders()

{

_orders = OrderDbSet.Orders.Where(x => x.CustId == this.Id).ToList();

_isOrdersLoaded = true;

}

}

Value holder method
In this method, another object called the value holder is responsible for managing lazy loading, and this object is used in the main object. The problem with this method is that the user must be aware of the presence of the value holder:

public class Customer

{

private ValueHolder _valueHolder;

public int Id { get; set; }

public string Name { get; set; }

public Customer() => _valueHolder = new ValueHolder(this.Id);

public List Orders => _valueHolder.GetOrders();

class ValueHolder

{

private List _orders;

private readonly int _id;

public ValueHolder(int id) => _id = id;

public List GetOrders()

{

if (_orders == null)

_orders = OrderDbSet.Orders.Where(x => x.CustId == _id).ToList();

return _orders;

}

}

}

Notes:

Using this design pattern when producing web applications or websites is very important. For example, loading an image or iframe only when the page is scrolled enough can have a significant impact on the initial loading speed of the web page.
The opposite of lazy loading is the eager loading method. The eager method will load the required resources once the code is executed.
Consequences: Advantages

Because it is not necessary to set all the objects, it increases the efficiency.
Using this design pattern in web-based applications will reduce overall bandwidth consumption.
Consequences: Disadvantages

Using this design pattern can increase the complexity because we always need to check whether the desired object has been loaded or not, or basically whether the desired object needs to be loaded lazily or not.
Applicability:

This design pattern can be very effective in implementing the singleton design pattern.
IQueryable or IEnumerable types support a type of lazy load called deferred execution.
The lazy load can be used in entity Framework when we need to get data from the Join of several tables.
When the complete loading of the object is not needed or is expensive, using this design pattern can be useful.
Related patterns:

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

Eager loading
Singleton
Proxy
Conclusion
In this chapter, you learned how to properly design and manage business transactions using the unit of work design pattern. You also learned how to prevent redundant references to data sources using an identity map design pattern. Finally, you learned how to use a lazy load design pattern to retrieve only the required data from the data source. Now that you are familiar with these design patterns in this chapter, you will be familiarized with object-relational structures design patterns in the next chapter.

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

NET 7 Design Patterns In-Depth 7. Data Source Architecture Design Patterns

Chapter 7
Data Source Architecture Design Patterns
Introduction
To organize the data source architecture, the patterns of this category can be divided into the following four main sections:

Active record: An object has the task of a gateway to a record from the database, and along with this task, it also implements the business logic related to the domain of that record.
Data mapper: A layer of mappers is placed between the domain and the database, and in this way, they eliminate the dependency on the database and the domain.
Row data gateway: An object acts as a gateway to a record in the data source, and there is a different object for each record in the data source.
Table data gateway: An object acts as a gateway to a database table or view, and communication with all the records of that table or view happens through this object.
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 discuss the following topics:

Data source architecture design patterns
Table data gateway
Row data gateway
Active record
Data mapper
Objectives
In this chapter, you will learn to know data source architecture design patterns and how to simulate the data source structure in your programs using different design patterns. You will also learn how to facilitate the construction and management of various SQL queries. In this chapter, you will learn how to link the data source structure to the domain model using mapping mechanisms.

Data source architecture design patterns
The most important task of the data access layer is facilitating communication with different data sources so the program can do its work. Most of today's data sources are relational databases, and with the help of the SQL language, you can easily communicate with all types of data sources and exchange data. Along with all the advantages of SQL, there are still some problems related to data sources. One of these problems is sometimes the lack of sufficient knowledge of SQL among programmers, which leads to writing wrong or ineffective queries.

With the preceding explanation, putting the SQL-related codes in a separate layer from the domain seems useful. The best way to implement this type of data infrastructure is to implement it according to the structure of database tables. In this case, there will be one class for each table in the database, and these classes will form gateways.

There are also different ways to have a gateway. One of these ways is to have a gateway object for each table record. This method is what the row data gateway is trying to express. Another way is to have one gateway object for each table. This method is also what the table data gateway is trying to express. Usually, in the table data gateway method, the result of the query sent to the database is returned as a record set. The connection and coordination between the table data gateway and record set also help to use this design pattern in the table module.

When using the domain model, due to the existing complexities, we may want each domain model to have the task of loading and changing its related data. This is where active records can come in handy.

With the increase of complexity within the domain, the logic of the domain is broken and divided between several classes. This causes a mismatch between the domain model and the database. Because there is no feature like inheritance in the database, on the other hand, with increasing complexity, it is necessary to test the domain's logic without the need to connect with the database. All these points and problems make us go for an indirect communication method between the domain model and the data source. In this case, completely separating the domain model from the database would be a better method. However, an interface will be needed to map domain objects to database tables. The data mapper design pattern will be useful in this section.

Choosing between a data mapper and an active record is a choice that depends on the level of complexity. For simpler scenarios, the active record will be useful, and as the complexity increases, using a data mapper will be a better option.

Table data gateway
Name:

Table data gateway

Classification:

Data source architecture design patterns

Also known as:

---

Intent:

Using this design pattern, an object plays the role of a gateway to a database table or view, and communication with all the records of that table or view happens through this object.

Motivation, Structure, Implementation, and Sample code:

Suppose a requirement is raised, and we need to connect to the database and perform Create, Read, Update, Delete (CRUD) operations on the user table. For this purpose, functions such as adding a new user, changing the password, deleting a user, finding a specific user, and receiving a list of users should be implemented.

There are different ways to implement this requirement. One of the ways is to connect to the database using raw queries and do the desired work. Using this method, the placement of the queries is important. If these codes are placed among the business logic code of the application, they will create a significant problem. The problem is that programmers will need to write queries. Most programmers either do not have enough knowledge to write queries or if they do have enough knowledge, database administrators will have difficulty finding and improving the performance of these queries.

However, a better option is placing the queries outside the business logic location. Table data gateway design patterns can be important in achieving this goal. In this way, all the required queries are collected and defined in a separate business logic section:

public class UserTableDataGateway

{

private SqlConnection connection;

public UserTableDataGateway()

{

connection = new SqlConnection("...");

}

public async Task GetAllAsync()

{

return await new SqlCommand("" +

"SELECT * " +

"FROM Users", connection).ExecuteReaderAsync();

}

public async Task FindByUserNameAsync(string username)

{

return await new SqlCommand($"" +

$"SELECT * " +

$"FROM Users " +

$"WHERE UserName = N'{username}'", connection).ExecuteReaderAsync();

}

public async Task ChangePasswordAsync(

string username, string newPassword)

{

return (await new SqlCommand($"" +

$"UPDATE Users " +

$"SET [Password] = N'{newPassword}' " +

$"WHERE UserName = N'{username}'"

, connection).ExecuteNonQueryAsync()) > 0;

}

}

As seen in the preceding code, the GetAllAsync method is used to get the list of all users. To find a specific user based on username, the FindByUsernameAsync method is used, and the ChangePasswordAsync method is used to change the password. Other operations, such as inserting a new user and deleting a user, can be defined similarly. Another noteworthy point in the preceding implementation is that for the sake of simplicity, in the definition of queries, things like SQL Injection and opening and closing the connection to the database have not been paid attention to. Still, in the real environment, it will be necessary to do such things. In the business logic layer, you can use this gateway if you need to work with the Users table. Also, database administrators must only check the gateways to monitor and improve queries.

Notes:

By using this design pattern, database queries are encapsulated in the form of a series of methods.
To implement this design pattern, you can also benefit from a Data set. While using this method, most of the operations will be similar, and you can also benefit from a public gateway and send things like the table name in the form of input parameters.
In implementing this design pattern, there is no requirement to communicate with the database tables. According to the requirement, it may be necessary to communicate with the views. If a public gateway is used for communication, it should be noted that it is often impossible to change the table or sub-tables through views1. Therefore, defining a public gateway for tables and a public gateway for views will probably be necessary. Also, to hide the structure of the tables, it is possible to benefit from the stored procedures in the database and create a gateway to communicate with these stored procedures.
The important point in using this design pattern is how to return the results to the user. For this purpose, Data Transfer Object (DTO) or methods such as mapping can be useful. Using the mapping method is often not a good option, as we will not be able to notice the problem at the time of compilation, which will cause a bug to appear. In using the DTO method, it should be noted that a new object must be created, and this method can be considered only if the created DTO can be reused. The results can be returned in the domain object format if this design pattern is used along with the domain model pattern.
This design pattern, table module, and transaction script patterns can be very useful. For example, this design pattern returns the record set required for the table module, so it can perform its processing on the results.
Consequences: Advantages

All queries are placed in the database separately from the business logic, making it easier to make changes to the queries.
The implementation of this design pattern is very simple.
Mapping between the gateway and database tables or views will be simple. Each table or view in the database is mapped to a gateway.
Consequences: Disadvantages

Using this design pattern next to the domain model will not be appropriate, and it is usually better to use the domain model design pattern and the data mapper pattern.
As the complexity in the domain increases, this design pattern will become more complex and does not have good scalability.
Applicability:

It can be useful for implementing small applications. If the business logic of the domain is simple, this design pattern can be very useful.
This design pattern and the table module design pattern can be useful.
Related patterns:

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

Table module
Transaction script
Domain model
Data mapper
Data transfer object
Row data gateway

Row data gateway

Name:

Row data gateway

Classification:

Data source architecture design patterns

Also known as:

---

Intent:

Using this design pattern, an object acts as a gateway to a record in the data source, and there is a different object for each record.

Motivation, Structure, Implementation, and Sample code:

Suppose we want to rewrite the example presented in the table data gateway design pattern with a row data gateway. In the table data gateway design pattern, we put all the queries related to the user's table in one class and connect to the database through that class. This section shows the difference between the row data gateway design pattern and the table data gateway pattern. In the row data gateway model, instead of a class having all the queries related to a table, we work through an object with a database record.

It is obvious that to find a record from the database and assign it to an object, and we will need a separate class. We name this class Finder. Finder is tasked to find and return a record from the table based on the provided condition. In other words, the finder tries to return a gateway for each record. The returned gateway has two main parts:

Properties: This section is the one-to-one mapping between the properties of the class and the columns of the table or data source. For example, ID, username, password, and so on.
Behaviors: This section is the queries that need to be executed on the data source. For example: insert, edit, delete, and so on.
In the following code, we have rewritten the scenario related to users with row data gateway:

public static class UserFinder

{

public static async Task FindAsync(string username)

{

var reader = await new SqlCommand($"" +

$"SELECT * " +

$"FROM Users " +

$"WHERE UserName = N'{username}'",DB.Connection).ExecuteReaderAsync();

reader.Read();

return UserRowDataGateway.Load(reader);

}

public static async Task> GetAllAsync()

{

var gatewayList = new List();

var reader = await new SqlCommand(

"SELECT * FROM Users",DB.Connection).ExecuteReaderAsync();

while (reader.Read())

{

gatewayList.Add(UserRowDataGateway.Load(reader));

}

return gatewayList;

}

}

public class UserRowDataGateway

{

public string UserName { get; set; }

public string Password { get; set; }

public static UserRowDataGateway Load(IDataReader reader)

{

return new UserRowDataGateway()

{

UserName = reader["Username"].ToString(),

Password = reader["Password"].ToString()

};

}

public async Task ChangePasswordAsync()

{

return (await new SqlCommand($"" +

$"UPDATE Users " +

$"SET [Password] = N'{this.Password}' " +

$"WHERE UserName = N'{this.UserName}'",

DB.Connection).ExecuteNonQueryAsync()) > 0;

}

}

In the preceding code, issues related to SQL injection or connection opening and closing are not mentioned, but in the real project, you must pay attention to these issues.

UserFinder is responsible for mapping a record to a gateway in the preceding code. It first retrieves the record from the database and then assigns the result to a specific object through the Load method.

Notes:

In using this design pattern, a finder class is often considered for each data source (table, view, and so on).
This design pattern is very similar to the active record design pattern. If the row data gateway contains domain business logic, this design pattern essentially becomes an active record and contains the business logic of the database.
Like the table data gateway design pattern, this design pattern can also communicate with a table, view, or stored procedure.
Consequences: Advantages

When mapping the columns of a record to class properties, type conversion may be required. Using this design pattern, this type of conversion happens only in one place (Load method).
Mapping between database objects and classes is simple, as each table has a gateway class. Depending on the type of implementation, a finder class will probably be needed for each table.
Consequences: Disadvantages

Using this design pattern, the maintenance cost sometimes increases because a series of clear codes must be written constantly.
Dependence on the database in using this design pattern is high because the object's properties are directly dependent on the table's columns.
The compatibility of this design pattern with the domain model design pattern is low, and the use of this design pattern along with the domain model design pattern will cause us to face three different data view methods. The first view is in the form of a domain model, the second is in the form of a gateway, and the third is in the form of a database. This is even though two different data views are needed, so using this design pattern alongside the domain model is not recommended. It is better to use other design patterns, such as active records.
Applicability:

Using this design pattern, the transaction scripts design pattern is a good combination.
This design pattern can be useful when the business logic of the domain is simple, and the probability of making changes to the data source is low. Type Safety is important when designing and coding.
Related patterns:

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

Table data gateway
Active record
Transaction scripts
Domain model
Active record

Name:

Active record

Classification:

Data source architecture design patterns

Also known as:

---

Intent:

In this design pattern, an object has the task of a gateway to a database record. In addition to this task, it also implements the business logic related to the domain of that record.

Motivation, Structure, Implementation, and Sample code:

An object has a series of data and behaviors, and data often needs to be persistent, stored, and retrieved in a database. This operation is presented and implemented in the form of data access logic. Using an active record design pattern, object-related behaviors, which are the business logic of the domain, are placed next to the data access business logic.

Suppose we want to rewrite the example presented in the row data gateway design pattern section with an active record design pattern. As seen in the row data gateway section, we used the finder class to find the desired record and shape the desired object, and through the gateway class, we implemented requirements such as changing the password. Now, if we add business logic related to the domain to that structure, the designed structure will become an active record:

public class UserActiveRecord

{

public string UserName { get; set; }

public string Password { get; set; }

public static UserActiveRecord Load(IDataReader reader)

{

return new UserActiveRecord()

{

UserName = reader["Username"].ToString(),

Password = reader["Password"].ToString()

};

}

public bool IsPasswordValid()

=> Password.Length > 6 &&

(Password.Contains('@') || Password.Contains('#'));

public async Task ChangePasswordAsync()

{

if (IsPasswordValid())

{

return (await new SqlCommand($"" +

$"UPDATE Users " +

$"SET [Password] = N'{this.Password}' " +

$"WHERE UserName=N'{this.UserName}'",

DB.Connection).ExecuteNonQueryAsync())> 0;

}

else

throw new Exception("The password is not strong enough.");

}

public async Task IncreaseFailedLoginAttemptAsync()

{

return (await new SqlCommand($"" +

$"UPDATE Users " +

$"SET [FailedLoginAttempt] = [FailedLoginAttempt] + 1 " +

$"WHERE UserName = N'{this.UserName}'",

DB.Connection).ExecuteNonQueryAsync()) > 0;

}

public async Task ResetFailedLoginAttemptAsync()

{

return (await new SqlCommand($"" +

$"UPDATE Users " +

$"SET [FailedLoginAttempt] = 0 " +

$"WHERE UserName = N'{this.UserName}'",

DB.Connection).ExecuteNonQueryAsync()) > 0;

}

public async Task GetFailedLoginAttemptAsync()

{

return (int)await new SqlCommand($"" +

$"SELECT [FailedLoginAttempt] FROM Users " +

$"WHERE UserName = N'{this.UserName}'",

DB.Connection).ExecuteScalarAsync();

}

public async Task IsUserExistsAsync()

{

return ((int)await new SqlCommand($"" +

$"SELECT COUNT(*) FROM Users " +

$"WHERE [Password] = N'{this.Password}' AND " +

$"UserName = N'{this.UserName}'",

DB.Connection).ExecuteScalarAsync()) > 0;

}

public async Task LoginAsync()

{

var loginResult = await IsUserExistsAsync();

if (loginResult == false)

{

await IncreaseFailedLoginAttemptAsync();

if (await GetFailedLoginAttemptAsync() > 3)

throw new Exception("Your account has been locked.");

}

else

await ResetFailedLoginAttemptAsync();

return loginResult;

}

}

In the preceding implementation, for the sake of simplicity, things like SQL injection and opening and closing the connection to the database have not been considered in the definition of the queries. However, in real scenarios and production environments, it will be necessary to consider such things.

In the preceding code, as you can see, in addition to the data access logic such as ChangePasswordAsync, IncreaseFailedLoginAttemptAsync, ResetFailedLogin AttemptAsync, GetFailedLoginAttemptAsync or IsUserExistsAsync methods related to domain business logic such as IsPasswordValid and LoginAsync are also present. As it is clear in the preceding code, the design is completely similar to the design of the row data gateway design pattern, with the difference that the business logic of the domain is also included in this design.

Another point in this design is that the finder class is not included, and the design of this class will be the same as the design done in the row data gateway design pattern.

Notes:

Part of the domain logic may be written using the transaction script pattern in this design pattern, and data access logic or data-related codes may be implemented with Active Record.
The data structure presented in the active record must be the same as the data structure of the database.
In using this design pattern, it is possible to communicate with the view or stored procedures instead of communicating with the table.
This design pattern is very similar to the row data gateway.
Consequences: Advantages

Implementation and understanding of active records are very simple.
It is very compatible with transaction script design patterns.
Since the domain business logic related to a record is next to the data access logic, the design is more coherent.
Consequences: Disadvantages

Using this design pattern can be useful only when the active record object is the same as the structure of the tables in the database. This design pattern can be useful in structures of the same shape. Still, if the business logic becomes more complex and there is a need to use capabilities such as inheritance and so on in the code, this design pattern will not be useful.
Due to the high dependence of the database structure on the object structure, it will be difficult to perform refactor operations at the database or object level, and hence the scalability will decrease.
The presence of domain business logic, data access logic, and increasing design and code coherence reduces reusability. Data access logic can be placed in parent classes through inheritance to solve this problem.
Due to the high dependence of this design pattern on the database, it is difficult to write unit tests.
Due to data access logic next to domain logic, the Single Responsibility Principle (SRP) and Separation of Concern (SoC) are violated. For this reason, this design pattern is suitable for simple applications.
Applicability:

This design pattern can be useful for implementing simple domain business logic such as CRUD operations or almost complex logic in the case that the possibility of changing the data source is low.
Related patterns:

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

Transaction script
Row data gateway
Data mapper
Name:

Data mapper

Classification:

Data source architecture design patterns

Also known as:

---

Intent:

Using this design pattern, a layer of mappers is placed between the domain and the database, and in this way, the dependency on the database and the domain is eliminated. The task of these mappers is to transfer data from the domain to the database and vice versa.

Motivation, Structure, Implementation, and Sample code:

Suppose we want to rewrite the example of users mentioned in active record design patterns. As we saw in the active record design pattern, the principle of SRP and SoC was violated due to the high dependency on the database and the domain layer. Using this design pattern will be very simple. It is enough that the object related to the domain is not involved in issues related to the database or data source, and the data source or database is also not involved in the business logic of the domain. This makes these two areas not have mutual effects. For example, a domain-related object may have collections, inheritance, and so on.

The same features in the domain object will help you later in implementing the complex business logic of the domain. This is while these capabilities do not exist in the database, so this difference in different capabilities will cause two completely different data structures to exist. One structure conforms to the domain implementation, and the other to the database design. The part in charge of harmonizing and translating these two completely different structures is the same as the mapper. Consider the following code:

public class UserModel

{

public string UserName { get; set; }

public string Password { get; set; }

}

public class UserMapper

{

public static async Task Create(UserModel newUser)

{

return (await new SqlCommand($"" +

$"INSERT INTO Users " +

$"(Username, [Password]) VALUES " +

$"N'{newUser.UserName}', N'{newUser.Password}'",

DB.Connection).ExecuteNonQueryAsync())>0;

}

}

public class UserDomain

{

public static bool IsPasswordValid(string password)

=> password.Length > 6 &&

(password.Contains('@') || password.Contains('#'));

public static async Task Register(string username, string password)

{

if (IsPasswordValid(password))

{

return await UserMapper.Create(

new UserModel {UserName=username,Password=password });

}

else

{

throw new Exception("The password is not strong enough.");

}

}

}

In the preceding implementation, for the sake of simplicity, things like SQL injection and opening and closing the connection to the database have not been considered in the definition of the queries. Still, it will be necessary to consider such things in the real environment.

As it is clear in the preceding code, the UserDomain class is responsible for implementing the business logic of the domain. This class has no information about the structure of the data source and the database. Even this class has no information about the existence of data sources. The UserDomain class prepares a UserModel object according to its needs and delivers it to the UserMapper. UserMapper is responsible for communicating between the domain and the data source. Therefore, upon receiving a UserModel object, it prepares and sends it to the database.

Notes:

According to this design pattern, the one-to-one relationship between the database structure and the domain model has been lost, and this relationship may not necessarily be one-to-one.
The preceding code example is a simple data mapper design pattern implementation. In more complex design and implementation, to perform insertion, editing, or even deletion operations, the mapper will need to know the previous and new state of the object so that it can choose the appropriate operation based on that. Also, in order to be able to make these changes in the form of a transaction, the UnitOfWork design pattern can be used.
Considering the internal connections of the domain models with each other, it is necessary to know to what level the data should be fetched and placed in the model. If we do not have this information, we may have to retrieve the entire data, which of course, will not be the right thing to do. You can use the lazy load design pattern to solve this problem and improve the amount of fetched data.
There will be two ways to create domain model objects inside the mapper. The first way is to use the parametric builder for the domain model. In this way, to create a domain model object, it will be necessary to send the required information to the class constructor. The second way is to use the constructor without parameters. In this way, first, the object is created, and then its various characteristics are set. The first way is better in terms of object quality guarantee. The problem with using the first method is the possibility of a cyclic dependency problem. In circular dependency, object A needs object B to be created, and object B needs object A to be created.
Suppose the domain model is simple, and the task of designing and developing the database is in the hands of the domain developer team. In that case, the development process can be improved with direct access to the database. For this purpose, an active record design pattern can be used instead of this design pattern.
In the first step of software development, there is no need to produce a complete mapper with all possible features. Sometimes using a pre-produced and ready-made mapper is better than implementing a new mapper.
Consequences: Advantages

When developing the domain, the data source or database can be completely ignored during design, development, and testing.
There is a loose coupling between the data source and the domain.
It has very high compatibility with the domain model design pattern, and along with this design pattern, it helps the reusability of the code.
Consequences: Disadvantages

For simple business logic, using this design pattern will not be cost-effective because it increases the complexity by adding a new layer (mapper).
Applicability:

Using this design pattern can be useful if we want to design and develop the data source model independently of the domain model.
This design pattern is often used together with the domain model design pattern.
This design pattern can be useful when the business logic of the domain is complex.
When the probability of change in the data source or domain layer is high, using this design pattern will be useful.
Related patterns:

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

Unit of work
Domain model
Active record
Lazy load
Conclusion
In this chapter, you got acquainted with different design patterns related to data source architecture. In this chapter, you also learned how to simulate the data source structure and map them to different objects.

In the next chapter, you will learn about object-relational behavior design patterns.

1 If the view contains a series of conditions or a series of triggers are used, data can also be changed through the view.

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

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

NET 7 Design Patterns In-Depth 5. Behavioral Design Patterns – Part II

Chapter 5
Behavioral Design Patterns – Part II
Introduction
Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects. Behavioral patterns describe patterns of objects or classes and communication patterns between them. These patterns characterize complex control flow that is difficult to follow at run-time. They shift your focus away from the control flow to let you concentrate on how objects are interconnected.

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

Behavioral design patterns
Interpreter
Iterator
Memento
Observer
Visitor
Objectives
In Chapter 4, Behavioral Design Patterns: Part I, you got acquainted with the most famous behavioral design patterns. In this chapter, you will get to know the rest of the design patterns of this category. These design patterns may be less famous, but in terms of importance and usage, they are very important and widely used and can easily solve complex problems.

Behavioral design patterns
In this category of design patterns, there is a total of eleven different design patterns. In this chapter, the rest of the design patterns of this category are introduced, which are:

Interpreter: Tries to interpret the given language with the help of defined rules.
Iterator: Tries to provide the possibility of navigation and sequential access to the collection members.
Memento: Tries to capture and save an object's state by keeping the encapsulation rules.
Observer: When an event occurs, it can notify other objects.
Visitor: It tries to perform various operations on an object without changing its structure.
Interpreter
Name:

Interpreter

Classification:

Behavioral design patterns

Also known as:

---

Intent:

This design pattern tries to interpret the given language with the help of defined rules.

Motivation, Structure, Implementation, and Sample code:

Suppose we want to define rules using SQL language so that we can use them to execute the existing methods inside a class. For example, if we assume the following class:

public class Sample{

public void SomeMehtod(){}

}

We want to execute SomeMethod using SQL language rules. For example:

Select SomeMethod From Sample

If we consider the rules of the SQL language very limited and assume that the commands used are only SELECT and FROM commands, then we can say that the expressions raised in this language will be in the following form:

Now consider that we want to leave the calendar placement on the page and the initial settings to the HR expert. Since this user needs to gain more technical knowledge in the field of HTML, it will be difficult for him to use the preceding code. Therefore, by defining a series of language rules, it is possible to give this feature so that this user can provide the following code:

[form postTo="process"]

[datePicker minDate="some_val" maxDate="some_val" allowTime="false"]

[end]

Then by receiving this expression and interpreting it, we will reach the desired code.

Related patterns:

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

Composite
Iterator
Visitor
Flyweight
Iterator
Name:

Iterator

Classification:

Behavioral design patterns

Also known as:

Cursor

Intent:

This design pattern tries to navigate its members without paying attention to the internal structure of the collections.

Motivation, Structure, Implementation, and Sample code:

We often come across different data structures in the programs we produce. Data structures include trees, graphs, linked lists, and so on. Each of these data structures has different methods for traversing. Even a well-defined and fixed data structure, such as a tree, has different ways of traversing it.

Suppose we are facing a collection and must traverse it based on the stack data structure. Implementing this will be a simple task. But the problem comes when we need to provide the queue method in addition to the stack method. Gradually, however, the client will face complexity. For the client, as a user, it may not be very important what method we use to traverse a collection. What is important for them is that we can cover a series of requirements for him. Requirements such as finding the next or previous member in the collection, finding the current member in the collection or identifying the first or last member in the collection, and so on.

On the other hand, regardless of the traversing method, we face different collections during software production. For example, the list of registered people, the list of cities between the origin and destination, the list of ticket purchase requests, and so on. These collections need a method to traverse to be traversed. For example, to traverse the list of cities between origin and destination, we may use a graph and queue for the list of people registered to buy tickets.

With the preceding scenario, we are faced with the following questions:

What do we want to traverse?
How do we want to traverse?
Actually, by using the Iterator design pattern, we want to answer these two questions. It is important to note that we may have multiple traversing methods for the same collection or use one traversing method to traverse several different collections.

Imagine we are asked to provide an infrastructure to send messages to our connections on social networks. In this requirement, we face various social networks such as LinkedIn, Instagram, and so on. Our communications are also two groups. The first group is close friends, and the second group is colleagues. The way to traverse connections on LinkedIn and Instagram can be completely different. So far in the scenario, we are facing an abstraction called a social network, which introduces browsing the list of friends and colleagues. LinkedIn and Instagram are a type of social network and offer their implementation to navigate friends and colleagues. Also, to be able to have the list of connections, we need to be able to traverse through the profile. The profile browser is itself an abstraction, and the LinkedIn and Instagram browsers use it during the traversing requirements. With these explanations, we are faced with the Figure 5.3 class diagram:

Figure%205.3.png
Figure 5.3: Iterator design pattern UML diagram

As it is clear from Figure 5.3, we are facing two different types of collections called InstagramCollection and LinkedInCollection. Each of these classes is connected to its appropriate iterator using the factory method pattern. For each of these two sets, we have an iterator, and each iterator has implemented the operation necessary to traverse the set:

public class Profile

{

public string Name { get; set; }

public string Email { get; set; }

public short Gender { get; set; }

public override string ToString()

=> $"Name: {Name},

Email: {Email},

Gender: {(Gender == 1 ? "Male" : "Female")}";

}

public abstract class ProfileIterator

{

protected int _currentPosition;

protected Profile[] _profiles;

public virtual Profile First()

{

_currentPosition = 0;

return _profiles[_currentPosition];

}

public virtual Profile Last()

{

_currentPosition = _profiles.Length - 1;

return _profiles[_currentPosition];

}

public virtual Profile Next() => _profiles[_currentPosition++];

public virtual Profile Prev() => _profiles[--_currentPosition];

public virtual bool HasNext() => _currentPosition < _profiles.Length; public virtual bool IsFirst() => _currentPosition == 0;

}

public class LinkedInIterator : ProfileIterator

{

public LinkedInIterator(LinkedInCollection linkedIn, short gender)

=> _profiles = linkedIn.Profiles.Where(x => x.Gender == gender).ToArray();

}

public class InstagramIterator : ProfileIterator

{

public InstagramIterator(InstagramCollection instagram, short gender)

=> _profiles = instagram.Profiles

.Where(x => x.Gender == gender).ToArray();

}

public interface ISocialNetworkCollection

{

IProfileIterator CreateMaleIterator();

IProfileIterator CreateFemaleIterator();

}

public class LinkedInCollection : ISocialNetworkCollection

{

public Profile[] Profiles

=> new Profile[] {

new() { Name="John", Email="john@linkedin.com", Gender = 1},

new() { Name="Paul", Email="paul@linkedin.com", Gender = 1},

new() { Name="Sara", Email="sara@linkedin.com", Gender = 0},

new() { Name="Antonio", Email="anton@linkedin.com", Gender = 1},

new() { Name="Liza", Email="liza@linkedin.com", Gender = 0}

};

public IProfileIterator CreateMaleIterator()

=> new LinkedInIterator(this, 1);

public IProfileIterator CreateFemaleIterator()

=> new LinkedInIterator(this, 0);

}

public class InstagramCollection : ISocialNetworkCollection

{

public Profile[] Profiles

=> new Profile[] {

new() { Name = "Vahid", Email = "john@instagram.com", Gender = 1 },

new() { Name = "Narges", Email = "paul@instagram.com", Gender = 0 },

new() { Name = "Reza", Email = "sara@instagram.com", Gender = 1 },

new() { Name = "Hasan", Email = "anton@instagram.com", Gender = 1 },

new() { Name = "Maryam", Email = "liza@instagram.com", Gender = 0 }

};

public IProfileIterator CreateMaleIterator()

=> new InstagramIterator(this, 1);

public IProfileIterator CreateFemaleIterator()

=> new InstagramIterator(this, 0);

}

As it is clear in the preceding code, each of the iterators can use its own method for traversing and implement the First, Last, Next, Prev, HasNext, and IsFirst methods. Now, to use the preceding structure, we can use the following code:

ISocialNetworkCollection social = new LinkedInCollection();

var profiles = social.CreateMaleIterator();

while (profiles.HasNext())

{

Console.WriteLine(profiles.Next());

}

Console.WriteLine("###############");

while (!profiles.IsFirst())

{

Console.WriteLine(profiles.Prev());

}

In the preceding code, we first specify that we intend to connect with LinkedIn, and then continue to traverse the collection using CreateMaleIterator and HasNext methods, and so on.

Participants:

Iterator: In the preceding scenario, it is ProfileIterator, which is responsible for defining the format for accessing and browsing the collection.
Concrete iterator: In the preceding scenario, it is the InstagramIterator and LinkedInIterator, which is responsible for implementing the format defined by the iterator.
Aggregate: In the preceding scenario, it is the same as ISocialNetwork Collection and is responsible for defining the template for defining iterator.
Concrete aggregate: In the preceding scenario, it is the same as InstagramCollection and LinkedInCollection and is responsible for implementing the template provided by aggregate.
Client: It is the same user who navigates aggregate through the iterator.

Notes:

One of the tasks of the ConcreteIterator is to maintain the position of the current object.
In the implementation of the preceding scenario, instead of the ProfileIteratror abstract class, the interface can also be used.
In order to implement this design pattern in .NET, there are concepts such as enumerator and enumerable. Understanding these two can help in implementing this pattern. The enumerator is actually responsible for finding objects in a collection, which is called enumerable. In order to implement this design pattern with the mentioned concepts, IEnumerable and IEnumerator interfaces can be used:
public class InstagramIterator : IEnumerator

{

int _currentPosition = -1;

Profile[] _profiles;

public InstagramIterator(Profile[] profiles) => _profiles = profiles;

public object Current => _profiles[_currentPosition];

public bool MoveNext() => (++_currentPosition) < _profiles.Length; public void Reset() => _currentPosition = -1;

}

public class InstagramCollection : IEnumerable

{

private readonly short gender;

Profile[] Profiles => new Profile[] {

new() { Name = "Vahid", Email = "john@instagram.com", Gender = 1 },

};

public InstagramCollection(short gender) => this.gender = gender;

public IEnumerator GetEnumerator()

=> new InstagramIterator(

Profiles.Where(x => x.Gender == gender).ToArray());

}

We can use the preceding code as follows:

InstagramCollection social = new InstagramCollection(0);

var socialEnum = social.GetEnumerator();

while (socialEnum.MoveNext())

Console.WriteLine(socialEnum.Current);

In the preceding implementation, instead of implementing the IEnumerator interface, we could also use yield. In this case, the code would change as follows:

public class InstagramCollection : IEnumerable

{

private readonly short gender;

Profile[] Profiles => new Profile[] {

new() { Name = "Vahid", Email = "john@instagram.com", Gender = 1 },

};

public InstagramCollection(short gender) => this.gender = gender;

public IEnumerator GetEnumerator()

{

yield return

from item in Profiles.Where(x => x.Gender == gender)

select item;

}

}

Pay attention to the use of yield in the preceding code. In fact, when using yield, we specify that we are dealing with an iterator, and there is no need to use the IEnumerator interface anymore. The preceding code can be used as follows:

InstagramCollection social = new InstagramCollection(0);

foreach (var item in social)

Console.WriteLine(item);

Factory method design patterns can be used to build different Iterators.
Consequences: Advantages

Separating collection traversing logic from the main business of code can improve code readability. This advantage is in line with the single responsibility principle.
When we intend to add a new traversing method or a new data structure is added, the infrastructure can be expanded without changing the previous codes. (Open/Close principle)
It is possible to traverse a collection in parallel.
Consequences: Disadvantages

In the whole program, we only face simple collections; using this pattern can increase the complexity.
In the case of a series of special collections, it can be more optimal to refer directly to the elements of the collection instead of using this design pattern.
Applicability:

When you are faced with a collection that has complications in traversing this collection, and you do not intend to convey this complexity to the user.
When you need to traverse a set in different ways, or the implementation method of the sets itself is not clear.
Related Patterns:

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

Factory method
Composite
Interpreter
Memento
Visitor
Memento
Name:

Memento

Classification:

Behavioral design patterns

Also known as:

Token

Intent:

This design pattern tries to capture and store the state of an object by maintaining the encapsulation rules. This makes it possible to restore the state of the object later. This restoration of the previous state of the object makes it possible to implement mechanisms such as undo and checkpoint mechanisms.

Motivation, Structure, Implementation, and Sample Code:

Suppose a requirement has been raised and a questionnaire has been asked to be designed for users. Users will be allowed to answer the questions of the questionnaire incompletely and save the answers as a draft before sending the final questionnaire. The users can come back later and complete the questionnaire, for which the draft must be uploaded, and the user must answer the rest of the questions or edit the previous answers and finally register the final form of the questionnaire and send the answers.

According to the scenario, it is clear that we need to allow the user to save the desired object before the final registration. Saving of the object should be done in such a way that the encapsulation is not damaged, and the implementation process and the internal information of the object are not leaked. One way to store an object state is to refer to individual properties and fields of the object and store their values in a repository. This way is often not practical because objects usually hide much of their data using different access levels, such as private.

public class Survey { public class Survey_Copy {

public string Prop1{get; set;} Š public string Prop1_Copy {get; set;}

private string Prop2; Š ?//It is private and we do not have access to it!

} }

If we assume that the desired object does not hide any details and that all its fields and properties have public access, then the proposed method will not be practical. Because later, if we want to add a new property or field to the class, we will need to change the class that has the task of copying the object, which can cause problems and disturb the maintenance and development of the code.

Even if we assume that we have ignored the preceding problems, we are facing a more serious problem. The problem is that when we need to copy the state of a class in order to be able to store values, properties, and fields, we need a class where these properties and fields have public access in that class so that we can read and write operations. To do this will make all the details of the class that were previously hidden in line with encapsulation fully accessible.

With the conditions stated in the preceding sections, it seems that there are no more than two ways:

Violating encapsulation and making available all the hidden details of the class.
Maintain encapsulation in which state saving is impossible.
There is another way, and that is to do this inside the class instead of trying to get an image of the state of the object and copy it from outside the class. Then we save the prepared image in another object called a memento. The content of this object is not available except through the class that created it. Next, you can save the generated memento in another object called caretaker. In this storage, the stack data structure can be used. Since the caretaker has limited access to the memento, it does not have access to change the state. However, the class that created the memento has full access to the state and can change it and restore the state.

With the preceding explanations, it is clear that in this design, we need an element to be able to take a photo and retrieve it. This element is called a memento. On the other hand, we need a German to be able to save the photo thereafter it is taken, and final implements, a German that plays the role of the target class, and we take pictures from it. According to the preceding explanations, the Figure 5.4 class diagram can be considered:

Figure%205.4.png
Figure 5.4: Memento design pattern UML diagram

As can be seen in the Figure 5.4 class diagram, the Survey class has the same role as a questionnaire. The questionnaire has a series of questions and answers to these questions, which are available through _state (SurveyState class is not included for simplicity). Users can submit the questionnaire in which the questionnaire information will be registered permanently. Also, users can use SaveDraft to save the draft of the questionnaire to return later and complete the questionnaire. The RestoreDraft method is also defined to restore the draft of the questionnaire.

In order to be able to save the questionnaire draft, the Survey class needs to save the current state of the questionnaire by creating a new object from SurveySnapshot. Next, this draft is added to the list of drafts through the AddMemento method in the SurveyCaretaker class. In this case, the user can have several copies of the draft and restore them in order (by doing this, the user will be able to undo it). You can also browse the history of changes through the SurveyCaretaker class.

For the Figure 5.4 class diagram, the following code can be considered:

public class SurveyState

{

public Dictionary Answers { get; set; }

public override string ToString()

{

StringBuilder sb = new StringBuilder();

foreach (var item in Answers)

sb.Append($"Question:{item.Key},

Answer: {item.Value}{Environment.NewLine}");

return sb.ToString();

}

}

public class Survey

{

private SurveyState _state = new SurveyState()

{ Answers = new Dictionary() };

public void Submit() => Console.WriteLine(_state);

public SurveySnapshot SaveDraft()

=> new(new SurveyState { Answers = new(_state.Answers) });

public void RestoreDraft(SurveySnapshot snapshot)

=> _state = snapshot.Restore();

public void SetAnswer(int question, string answer)

=> _state.Answers.Add(question, answer);

}

public class SurveySnapshot{

private readonly SurveyState _state;

public SurveySnapshot(SurveyState state) => _state = state;

public SurveyState Restore() => _state;

}

public class SurveyCaretaker

{

readonly Stack _mementos = new();

public void AddMemento(SurveySnapshot snapshot) => _mementos.Push(snapshot);

public SurveySnapshot GetMemento() => _mementos.Pop();

}

In the preceding code and in the Survey class, when executing the SaveDraft method, it should be noted that the copy of the object sent to SurveySnapshot is of deep type. To execute the preceding code, you can proceed as follows:

1. SurveyCaretaker caretaker = new SurveyCaretaker();

2. Survey s = new Survey();

3. s.SetAnswer(1, "A");

4. s.SetAnswer(2, "B");

5. caretaker.AddMemento(s.SaveDraft());

6. s.SetAnswer(3, "C");

7. caretaker.AddMemento(s.SaveDraft());

8. s.SetAnswer(4, "D");

9. s.RestoreDraft(caretaker.GetMemento());

10. s.Submit();

In the preceding code, new answers are recorded in lines 3 and 4, and drafts are saved in line 5. A new answer is added in line 6, and a new draft is saved again in line 7. In line 8, another answer is recorded, and in line 9, the last draft is retrieved. By doing this, the last answer that was given (line 8) is lost. Therefore, by executing the preceding code, the available answers will be as follows:

1-A

2-B

3-C

Participants:

Memento: In the preceding scenario, it is the same as SurveySnapshot and is responsible for saving the internal state of the originator object. Another task of this class is to determine what information should be stored in the originator.
Originator: In the preceding scenario, it is the Survey that is responsible for preparing the image of the current situation and restoring the situation based on the existing image.
Caretaker: In the preceding scenario, he is the SurveyCartaker and is responsible for maintaining the images. This class should not manipulate the content of the image.
Client: It is the same user who executes the code through the mediator and ConcreteMediator.
Notes:

There are different ways to implement this design pattern. Apart from the preceding method, which is called the nested class method, this design pattern can also be implemented using interfaces. For example, the originator was sent to the memento through the constructor parameter, in which case each memento is connected to its originator. In this way, the memento itself can perform the task of restoring the status, and the implementation of this work can be removed from the caretaker. Another way to implement this design pattern is to use the narrow interface method. In this implementation method, the memento class is defined as an internal class in the originator, and in this way, the memento object is encapsulated in the originator. However, the caretaker communicates with the IMemento interface instead of the memento class. In this way, we have the following code:
public class Survey

{

private class SurveySnapshot : ISurveySnapshot

{

private readonly SurveyState _state;

public SurveySnapshot(SurveyState state) => _state = state;

public SurveyState Restore() => _state;

}

private SurveyState _state = new() { Answers = new() };

public void Submit() => Console.WriteLine(_state);

public ISurveySnapshot SaveDraft()

=> new SurveySnapshot(new SurveyState {

Answers = new(_state.Answers) });

public void RestoreDraft(ISurveySnapshot snapshot)

=> _state = snapshot.Restore();

public void SetAnswer(int question, string answer)

=> _state.Answers.Add(question, answer);

}

public interface ISurveySnapshot

{

SurveyState Restore();

}

public class SurveyCaretaker

{

readonly Stack _mementos = new();

public void AddMemento(ISurveySnapshot snapshot)

=> _mementos.Push(snapshot);

public ISurveySnapshot GetMemento() => _mementos.Pop();

}

In this method, the originator has access to all aspects of the memento, while the caretaker and other objects only have access to the information provided in the interface framework (in the preceding example, the ISurveySnapshot interface). This means that the caretaker is explicitly prohibited from making changes to the originator's status.
This design pattern is often associated with the use case of undo in minds, but another use case is in the design of transactions. By using this design pattern in the design of transactions, you can support rollback and commit operations.
This design pattern can be used alongside the command design pattern. In this case, different operations can be performed on the destination object with the help of the command design pattern, and through memento, before executing the command, you can save the status of the object.
Consequences: Advantages

It is possible to save the internal state of the object without violating encapsulation.
Using the caretaker, a part of the originator codes related to the history of changes is removed and transferred to the caretaker, which simplifies and improves code maintenance.
Consequences: Disadvantages

If mementos are produced too much, memory consumption will increase.
In order for the caretaker to remove unused mementos, they must monitor the originator's life cycle.
Applicability:

When there is a need to implement undo and redo mechanisms.
When there is a need to save checkpoints, for example, in the design of computer games, with the help of a checkpoint, we save the state of the object, and later we can restore it.
Related patterns:

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

Prototype
Command
Iterator
Observer
Name:

Observer

Classification:

Behavioral design patterns

Also known as:

Dependents or publish-subscribe

Intent:

This design pattern tries to define a mechanism for 1-N dependencies so that when there is a change in the state of an object, all the dependents of that object are automatically informed of the change.

Motivation, Structure, Implementation, and Sample code:

Suppose you are visiting an online shopping website. There are various products on this website. When you visit this website, you intend to buy shoes, but currently, the shoes you want are not available. On the other hand, you need to be informed as soon as the shoes you want are available so that you can buy them. Also, we need to register a login to the database as soon as the desired shoe is available so that we know on what date and time the shoe was available.

To implement this requirement, a simple way is for the user to visit the website every day and make a purchase when the shoes are available. It is clear that in this method, the user has to visit the website many times, which eventually causes the user to stop buying shoes from that website and buy shoes from another collection. Therefore, over time, the online store will lose its customers and face a decrease in profits. For this reason, it can be said that this method is not a suitable method.

Another way to implement this mechanism is to send a notification (via SMS or email) to all users every time the shoes are available in the store. This method also has its own problem. In this method, a large number of users receive messages that are not of their interest. This causes the sent notification to become spam gradually and has a negative impact on the customer's shopping experience.

Observer design pattern tries to solve this kind of problem. The approach is such that only the intended user can become a member of the list of subscribers of the desired shoe announcement. Therefore, whenever the desired shoe becomes available, a notification will be sent to the desired user. In fact, what happens is very similar to the magazine subscription mechanism, where people buy a subscription to a magazine, and from then on, whenever the magazine is published, a copy of the magazine is sent to its subscribers. In the proposed scenario, the mechanism is the same procedure.

As in the example of the magazine, we are facing publishers and subscribers, and we have these roles in the mentioned scenario. In terminology, the publisher is called the publisher, and the subscriber is called the subscriber. Now, every time the shoes are available, we are faced with two different customers. The first subscriber is the customer, and the second subscriber is the log recording classubscription, ability of shoes is communicated to these two classes, and each class, based on its task, implements and executes the appropriate business.

As subscribers can subscribe to the publication, they should be able to cancel their subscriptions in a similar way. In this case, the subscriber is no longer interested in receiving notifications from publications.

With the preceding explanations, Figure 5.5 class diagram can be considered for the proposed scenario:

Figure%205.5.png
Figure 5.5: Observer design pattern UML diagram

As can be seen in the Figure 5.5 class diagram, there is a publisher named ShowNotifier and two subscribers named CustomerObserver and LoggingObserver. Publishing has three methods: Subscribe to register a subscription, Unsubscribe to delete a subscription, and Notify to send notifications. For Figure 5.5 class diagram, the following codes can be imagined:

public class Shoe

{

public int Size { get; set; }

public string Color { get; set; }

}

public interface IShoeObserver

{

void Update(Shoe param);

}

public class CustomerObserver : IShoeObserver

{

public void Update(Shoe param)

=> Console.WriteLine("Sending email to customer...");

}

public class LoggingObserver : IShoeObserver

{

public void Update(Shoe param) => Console.WriteLine("Saving log...");

}

public interface IShoeNotifier

{

void Subscribe(IShoeObserver param);

void Unsubscribe(IShoeObserver param);

void Notify(Shoe param);

}

public class ShoeNotifier : IShoeNotifier

{

private static List _subscribers;

static ShoeNotifier() => _subscribers = new List();

public void Subscribe(IShoeObserver param) => _subscribers.Add(param);

public void Unsubscribe(IShoeObserver param) => _subscribers.Remove(param);

public void Notify(Shoe param)

{

foreach (var item in _subscribers)

item.Update(param);

}

}

As it can be seen in the preceding code, for simplicity, the list of subscribers is kept in a static field called _observers. The reason this field is static is to preserve its value between requests. In order to initialize this field, a static constructor is also used. In more realistic implementations, other repositories may be used to store the list of subscribers. To use the preceding codes, you can proceed as follows:

1. IShoeObserver customerObserver = new CustomerObserver();

2. IShoeObserver loggingObserver = new LoggingObserver();

3. IShoeNotifier notifier = new ShoeNotifier();

4. notifier.Subscribe(customerObserver);

5. notifier.Subscribe(loggingObserver);

6. notifier.Notify(new Shoe { Size = 45, Color = "Red" });

In the preceding code, as soon as line 6 is executed, the codes in the Update method in the CustomerObserver and LoggingObserver classes will start to be executed. The priority and delay of the implementation of each of these classes are directly related to the order of their entry into the _subscribers list.

Participants:

Subject: In the preceding scenario, it is the same as IShoeNotifier, and the task is to provide a template for registering and canceling the subscription.
Observer: In the preceding scenario, it is the IShoeObserver and is responsible for providing the updated format for subscribers.
ConcreteSubject: In the preceding scenario, it is the ShoeNotifier and is responsible for implementing the template provided by the subject. Also, this class is responsible for sending notifications to observers.
ConcreteObserver: In the preceding scenario, it is the same as CustomerObserver and LoggingObserver and is responsible for implementing the template provided by the observer. This class can store a reference to ConcreteSubject.
Client: It is the same user who executes the code by defining the subject and observer and registering observers in the list of subscribers.
Notes:

This design pattern has similarities with a chain of responsibility, command, and mediator design patterns:
In the chain of responsibility design pattern, a request starts moving along a dynamic chain until one of the members of this chain processes the request.
Command design pattern establishes two-way communication between sender and receiver.
The mediator design pattern removes the direct communication between the sender and the receiver and forces the objects to communicate with each other through the mediator.
The observer design pattern makes it possible to subscribe or cancel a registration object dynamically at runtime.
Regarding the mediator design pattern, it can be said that in this design pattern, the mediator can have the role of publisher. And other components can have the role of Subscriber. If the mediator design pattern is designed and considered this way, then it will be very similar to the observer pattern.
When the ConcreteObserver becomes aware of the changes that have occurred in the ConcreteSubject, it can communicate with the ConcreteSubject again and request additional information from it. Actually, the relationship between the observer and the subject can be pulled or pushed. Each of these methods has its advantages and disadvantages. For example, in the push model, it is assumed that the subject knows the observer's needs and always sends complete information to the observer. On the opposite point in the Pull model, the observer object must be able to determine what has changed without getting help from the subject:
In push communication, the subject sends all the details of the changes to the observer and does not care whether this information will be useful for the observer or not.
In the Pull communication, the Subject object informs the observer of the changes with the minimum possible information, and then the observers who are interested can get the details of the changes by reconnecting with the Subject.
In this design pattern, it is possible for an observer to monitor several subjects. In this case, the important point is which of the subjects is called the update method. A solution for this problem is that the update method itself receives the subject-object as an input parameter so that it can be recognized which subject is called an update on the observer side.
In using this design pattern, it is important to specify how the notify method should be called. There are two ways for this:
After changing any status, the notify method is called automatically: in this case, the notify method may be called several times per work unit, which can be expensive for the observer side. On the other hand, the advantage of this method is that we always know that the notify method will be called after changing the subject status.
The user should have the task of manually calling the notify method: the disadvantage of this method is that the user will find a new task, and that is calling the notify method. Its advantage is that the notify method can be called only once per work unit.
By using the template method design template, it is possible to prevent the sending of change notifications when the subject itself is in an unstable state. In this way, the last method in the template method can be called the notify method. Therefore, this makes us sure that the notify method will always be called at the end of the work and when the subject is in a stable state.
In a complex scenario, if we are dealing with multiple Subjects and want to notify Observers after making changes to all Subjects, then a ChangeManager will be needed. ChangeManager removes the direct connection between the subject and the observer and establishes the connection through an interface. The role of ChangeManager in this pattern is as a mediator, and a singleton pattern can be used in its implementation. In terms of application, ChangeManager also works very similarly to a service bus. To implement ChangeManager, the following code can be considered:
public class ChangeManager

{

private ChangeManager() { }

private static readonly object _lock = new();

private static ChangeManager _instance;

public static ChangeManager GetInstance()

{

if (_instance == null)

{

lock (_lock)

{

if (_instance == null)

_instance = new ChangeManager();

}

}

return _instance;

}

static List mappings = new();

public void Register(IShoeNotifier notifier, IShoeObserver observer)

{

if (!mappings.Any(

x => x.Notifier == notifier &&

x.Observer == observer))

mappings.Add(

new Mapping { Notifier = notifier, Observer = observer });

}

public void Unregister(IShoeNotifier notifier, IShoeObserver observer)

{

var map = mappings.FirstOrDefault(

x => x.Notifier == notifier && x.Observer == observer);

if (map != null)

mappings.Remove(map);

}

public void Notify(Shoe param)

{

foreach (var item in mappings)

{

item.Observer.Update(param);

}

}

}

Also, the subject codes for the proposed scenario are changed as follows:

public class ShoeNotifier : IShoeNotifier

{

private ChangeManager _changeManager;

public ShoeNotifier() => _changeManager = ChangeManager.GetInstance();

public void Subscribe(IShoeObserver param)

=> _changeManager.Register(this, param);

public void Unsubscribe(IShoeObserver param)

=> _changeManager.Unregister(this, param);

public void Notify(Shoe param)

{

_changeManager.Notify(param);

}

}

As can be seen in the preceding code, the connection between the subject and observer is broken, and communication happens only through ChangeManager. There can be two approaches to implementing ChangeManager. The simple approach is to notify all observers of all subjects while notifying them. In this method, an observer may be mapped several times with different subjects, and then the update method will be called several times for that observer. The second method is only to call the unique mappings while calling the update method.

Consequences: Advantages

New subscribers can be defined without changing the existing codes. For this reason, the open/close principle has been observed.
It is possible to establish a relationship between objects during execution.
All the information that the subject has is what observers shared with them, and it has no information about ConcreteObservers. In this way, there is a loose coupling between the subject and the observer.
Consequences: Disadvantages

If it is not clear what has changed on the subject side, this can cause the update method of all observers to be activated and executed, which can be very costly.
Applicability:

When there is a need to change the state of an object, a set of other objects can also be changed, and this design pattern can be used. In this case, the use of this design pattern is more evident when the list of target objects (subscribers) is unclear.
This design pattern can be used when there is a need to have a set of objects, one object, for a short period of time. With the help of subscribe and unsubscribe methods, objects can start and finish the monitoring process.
Related patterns:

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

Chain of responsibility
Command
Mediator
Singleton
Template method
Visitor
Name:

Visitor

Classification:

Behavioral design patterns

Also known as:

---

Intent:

This design pattern tries to perform various operations on an object without changing its structure.

Motivation, Structure, Implementation, and Sample code:

Suppose there is a requirement in which different discounts can be applied on different items of the invoice, according to the type of item and the type of customer loyalty (gold customer and silver customer). Let us assume that this requirement has been raised by a large chain store that sells various products such as food, cosmetics, and household appliances.

To implement this requirement, business related to discounts and customer loyalty types can be done within the invoice class. The problem with this method is that over time and with the identification of other requirements, the invoice class will contain many unrelated codes, which will make it difficult to maintain the code. But there is another solution to this need. In the preceding scenario, we need to navigate the invoice items with the help of a mechanism and apply discounts related to the level of customer loyalty based on the type of item.

In fact, in the preceding scenario, we are facing two different elements. The first element is the various invoice items (food, cosmetics, and household appliances), and the second element is the various operations that must be performed on the invoice items (applying discounts on food items for gold and silver users). We are facing a class called invoice, which consists of a list of various items. Each type of item has its own calculation method. We are also faced with an abstraction called visitor, which has implemented different types of visits, such as visits to apply discounts for gold customers and visits to apply discounts for silver customers, and all types of invoice items benefit from these types of visits.

With the preceding explanations, the following class diagram can be imagined:

Figure%205.6.png
Figure 5.6: Visitor design pattern UML diagram

As can be seen in the Figure 5.6 class diagram, the invoice class consists of a series of items (IInvoiceItems). InvoiceItems include Food, Cosmetics, and HomeAppliances. Each IInvoiceItem provides its implementation of the Accept method and connects to the appropriate visitor. Visitors are under the format of ILoyalityVisitor and include GoldenUsersVisitor to apply discounts for golden customers and SilverUsersVisitor to apply discounts for silver customers. For Figure 5.6 class diagram, the following code can be considered:

public class Invoice

{

private readonly ILoyalityVisitor visitor;

public Invoice(ILoyalityVisitor visitor) => this.visitor = visitor;

public List Items { get; set; } = new List();

public double Calculate() => Items.Sum(x => x.Accept(visitor));

}

public interface IInvoiceItems

{

public double Cost { get; set; }

double Accept(ILoyalityVisitor visitor);

}

public class Food : IInvoiceItems

{

public double Cost { get; set; }

public double Accept(ILoyalityVisitor visitor) => visitor.VisitFoods(this);

}

public class Cosmetics : IInvoiceItems

{

public double Cost { get; set; }

public double Accept(ILoyalityVisitor visitor)

=> visitor.VisitCosmetics(this);

}

public class HomeAppliances : IInvoiceItems

{

public double Cost { get; set; }

public double Accept(ILoyalityVisitor visitor)

=> visitor.VisitHomeAppliances(this);

}

public interface ILoyalityVisitor

{

double VisitFoods(Food item);

double VisitCosmetics(Cosmetics item);

double VisitHomeAppliances(HomeAppliances item);

}

public class GoldenUsersVisitor : ILoyalityVisitor

{

public double VisitCosmetics(Cosmetics item) => item.Cost * 0.9;

public double VisitFoods(Food item) => item.Cost * 0.8;

public double VisitHomeAppliances(HomeAppliances item) => item.Cost * 0.9;

}

public class SilverUsersVisitor : ILoyalityVisitor

{

public double VisitCosmetics(Cosmetics item) => item.Cost * 0.95;

public double VisitFoods(Food item) => item.Cost * 0.9;

public double VisitHomeAppliances(HomeAppliances item) => item.Cost * 0.95;

}

According to the preceding code, the following discount table is in place:

Product

SilverUsersVisitor

GoldenUsersVisitor

Food

10% discount

20% discount

Cosmetic

5% discount

10% discount

Home Appliances

5% discount

10% discount

Table 5.1: Customers discount table

As it is clear in the preceding code, each visitor (GoldenUsersVisitor and SilverUsersVisitor) has provided their implementation for the methods related to Foods, Cosmetics, and HomeAppliances. These types of items have also tried to provide an implementation for the Accept method, through which you can visit the invoice items of customers and perform the relevant calculations. In the Invoice class, if you pay attention, you can see that in the Calculate method, the invoice items are traversed, and for each item, the Accept method is called, and the visit is accepted.

To use this structure, you can use the following code:

Invoice invoice = new(new GoldenUsersVisitor());

invoice.Items.Add(new Food() { Cost = 100 });

invoice.Items.Add(new Cosmetics() { Cost = 150 });

invoice.Items.Add(new HomeAppliances() { Cost = 1000 });

Console.WriteLine($"Total: {invoice.Calculate()}");

Participants:

Visitor: In the preceding scenario, it is the ILoyalityVisitor, which is responsible for providing the template for visiting each ConcreteElement in an object structure. Usually, in the definition of this format, the signature of the method and its name indicate the sender of the visit request. For example, in the preceding scenario, the name and signature of the FoodVisitor(Food item) method indicate that the visit was requested by Food.
ConcreteVisitor: In the preceding scenario, it is GoldenUsersVisitor and SilverUsersVisitor, which is responsible for implementing the template provided by Visitor.
Element: In the preceding example, it is the same as IInvoiceItems, which is responsible for defining the format of the Accept method. This method must receive visitors as input.
ConcreteElement: In the preceding scenario, it is the same as Food, Cosmetics, and HomeAppliances and is responsible for implementing the template provided by Element.
ObjectStructure: In the preceding example, it is the same as Invoice and is responsible for defining the object structure.
Client: It is the same user who executes the code by providing ObjectStructure and Visitor.
Notes:

This design pattern can be used along with the iterator design pattern to navigate complex structures such as trees.
This design pattern can be used to interpret the interpreter design pattern.
In the implementation of this design pattern, it should be noted that the accept method, which is located in the object structure, uses the double dispatch technique. It means calling the accept method requires the exact type of visitor and element, each of which is determined at runtime.
Single Dispatch: We want to call a virtual method that is overridden in the child class. We can call this method through an object of the child or parent class. This mode is called Single Dispatch.

1. public class Base{ public virtual void X(){} }

2. public class Child: Base{ public override void X(){} }

3. //…

4. Base obj = new Child();

5. obj.X();

Method X requires a type obj in order to run. Here, obj is of Child type, so the X method in the Child class will be called.

Double Dispatch: Consider the following code:

public class Base

{

public class Class1 { }

public class Class2 { }

public virtual void Method(Class1 obj) { }

public virtual void Method(Class2 obj) { }

}

public class Derived : Base

{

override public void Method(Class1 obj) { }

override public void Method(Class2 obj) { }

}

//…

Base b = new Derived();

var objs = new object[] { new Class1(), new Class2() };

Now we need to traverse the objs array and call the Method for each of its members. To call the method, we need two things to be clear. First, we need to know the type of object b, in which the preceding code is of Derived type. We also need the type of Method input parameter. This mode is called double dispatch. That means we need two elements to call the method. C# language supports double dispatch through a feature called dynamic typing. Therefore, by using this feature, the stated requirement can be implemented as follows:

foreach (dynamic item in objs)

{

b.Method(item);

}

In the preceding code, the type of item will be specified at the time of execution, and the appropriate method will be called at each for each traversing. In fact, double dispatch is a type of multiple dispatch.

Now, according to the above explanations, in the Visitor design pattern, by using the Double Dispatching technique (Which is used in calling the Accept method), different Visitors can visit one type, and different types can be visited by one Visitor:

Public class Invoice

{

private readonly IloyalityVisitor visitor;

public Invoice(IloyalityVisitor visitor)

=>this.visitor = visitor;

public List Items { get; set; }

= new List();

public double Calculate() => Items.Sum(x => x.Accept(visitor));

}

That is, in the above code, the execution of the Accept method depends on the type of x and visitor, both of which will be specified at the runtime.

Consequences: Advantages

It is possible to define new behaviors without making changes in the existing codes, and hence Open/Close principle has been observed. For example, you can define the mechanism for bronze customers without changing the codes of gold and silver customers.
Due to the separation of different types of behavior (visiting food items, visiting cosmetics, and so on), a single responsibility principle has been observed.
The object that performs the visit can save information about that visit every time it is visited. This possibility can be very useful in visiting complex structures, such as trees.
This design pattern collects related operations in visitors and prevents these operations from spreading to different classes. Also, unrelated operations are placed in visitors specific to that operation, and in this way, for example, a specific algorithm for traversing a data structure is placed in the corresponding visitor.
Consequences: Disadvantages

When a new class is created, or a class is deleted, the visitor must be updated, and hence it is not easy to define a ConcreteElement.
Since visitors do not have access to private methods, visiting operations may face problems when these methods are needed.
This design pattern often tries to access the status and internal data of an element by providing public methods. This causes the encapsulation to be damaged.
Applicability:

Performed a series of operations on the members of a complex object when needed.
When necessary, separate the codes related to complementary businesses from the main code. In the preceding scenario, applying discounts based on customer loyalty is a complementary business to the main business. The main business in the preceding scenario was selling products.
Related patterns:

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

Iterator
Interpreter
Composite
Conclusion
In this chapter, you got acquainted with the rest of the behavioral design patterns and learned how to easily perform complex tasks such as traversing a collection, notifying changes, saving object data, and so on using behavioral design patterns. This chapter was the last chapter about GoF design patterns, and from the next chapter, you will learn about PoEAA 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

NET 7 Design Patterns In-Depth 4. Behavioral Design Patterns – Part I

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 Commands { get; set; }

= 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 participants = new();

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

NET 7 Design Patterns In-Depth 3. Structural Design Patterns

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 Headers { get; set; }

void Get(string queryString);

void Post(object body);

}

public class ExternalService : IexternalService

{

public string Url { get; set; }

public Dictionary Headers { get; set; }

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 headers

)

=> Console.WriteLine($”Invoking {verb} {action} from {BaseUrl}”);

}

public class ServiceAdapter : IexternalService

{

public string Url { get; set; }

public Dictionary Headers { get; set; }

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 Request;

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 Children { get; set; }

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 Inquiry(DateTime date, string from, string to)

{

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 = new();

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 _cache = new();

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 trees = new();

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 map = new()

{

{ "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 mapCache = new();

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 GetAsync(int id)

=>await (await

_client.GetAsync($"/users/{id}")).Content.ReadAsStringAsync();

public async Task GetAllAsync()

=>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

NET 7 Design Patterns In-Depth 2. Creational Design Patterns

Chapter 2 Creational Design Patterns
第2章 创建式设计模式

Introduction

简介

As the name suggests, creational design patterns deal with object construction and how to create instances. In C# .NET programming language, a new keyword is used to create an object. To create an object, it is necessary to provide the name of the class. In other words, wherever an object is needed, it can be created using the new keyword and the class name. However, there are instances where it is necessary to hide the way the object is made from the user's view. In this case, creational design patterns can be useful.

顾名思义,创建式设计模式处理对象构造以及如何创建实例。在 C# .NET 编程语言中,new 关键字用于创建对象。要创建对象,必须提供类的名称。换句话说,只要需要对象,就可以使用 new 关键字和类名来创建它。但是,在某些情况下,有必要从用户的视图中隐藏对象的创建方式。在这种情况下,创建式设计模式可能很有用。

Structure

结构

In this chapter, we will cover the following topics:

在本章中,我们将介绍以下主题:

  • Creational design pattern
    创建式设计模式

  • Factory method
    工厂方法

  • Abstract factory
    抽象工厂

  • Builder
    Builder

  • Prototype
    原型

  • Singleton
    单例

  • Objectives
    目标

By the end of this chapter, you will be familiar with creational design patterns and be able to understand their differences. It is also expected that by using the tips provided in this chapter, you can use each creational design pattern in its proper place.

在本章结束时,您将熟悉创建式设计模式并能够理解它们的差异。此外,通过使用本章中提供的提示,您可以在适当的位置使用每个创建性设计模式。

Creational design pattern

创建式设计模式

Using creational design patterns, you can understand which object was made by whom, how, and when. In the category of creational design patterns in GoF, five design patterns have been introduced, which are:

使用创建性设计模式,您可以了解哪个对象是由谁、如何以及何时制造的。在 GoF 的创建性设计模式类别中,引入了五种设计模式,它们是:

  • Factory method: Tries to select a class from several existing classes and create an object.
    Factory method:尝试从多个现有类中选择一个类并创建一个对象。

  • Abstract factory: Tries to create objects from classes of the same family.
    抽象工厂:尝试从同一系列的类创建对象。

  • Builder: Tries to separate the object’s construction from its presentation.
    生成器:尝试将对象的构造与其表示分开。

  • Prototype: Tries to make a copy of an existing object.
    原型:尝试创建现有对象的副本。

  • Singleton: Tries to have only one object of the class.
    Singleton:尝试只具有类的一个对象。

These five design patterns can be used interchangeably and can be complementary in some situations. For example, the prototype and abstract factory design patterns may be useful in some situations. Or the singleton pattern may be used in the prototype implementation to make the prototype implementation more complete.

这五种设计模式可以互换使用,并且在某些情况下可以互补。例如,原型和抽象工厂设计模式在某些情况下可能很有用。或者可以在 prototype 实现中使用 singleton 模式,以使 prototype 实现更加完整。

Usually, the creational design patterns can be used for the following conditions:
通常,创建性设计模式可用于以下条件:

  • When the system needs to be independent of how the objects are made
    当系统需要独立于对象的创建方式时

  • When a set of objects are to be used together
    当一组对象一起使用时

  • When there is a need to hide the class implementation from the user's view
    当需要从用户视图中隐藏类实现时

  • When there is a need for different presentations of a complex object.
    当需要对复杂对象进行不同的表示时。

  • Sampling should be clear at the time of execution.
    在执行时应明确抽样。

  • When only one object is required to be provided to the user.
    当只需要向用户提供一个对象时。

Apart from the five GoF patterns, other patterns are related to the creation of objects, such as:

除了五种 GoF 模式外,其他模式与对象的创建有关,例如:

  • Dependency injection pattern: Instead of creating an object, the class receives the required object through the Injector.
    依赖注入模式:该类不是创建对象,而是通过 Injector 接收所需的对象。

  • Object pooling pattern: It prevents the object from being destroyed and recreated and reuses the existing objects in the recovery method.
    对象池模式:它可以防止对象被销毁和重新创建,并在恢复方法中重用现有对象。

  • Lazy initialization pattern: Delays the creation of the object until it is used.
    延迟初始化模式:延迟对象的创建,直到使用它。

Factory method

工厂方法

This section introduces and analyzes the factory method design pattern according to the structure presented in the GoF design patterns section in Chapter 1, Introduction to Design Patterns.

本节根据第 1 章 设计模式简介的 GoF 设计模式部分中介绍的结构介绍和分析工厂方法设计模式。

Name:名字
Factory method工厂方法

Classification:分类
Creational design patterns创建式设计模式

Also known as:也称为
Virtual constructor虚拟构造函数

Intent:意图
This design pattern tries to delegate the creation of objects to child classes in a parent-child structure.此设计模式尝试将对象的创建委托给父子结构中的子类。

Motivation, Structure, Implementation, and Sample code:
动机、结构、实现和示例代码:

Suppose we are building a residential unit and must make a "door". To make a "door", we must refer to a "door manufacturer", but so far, we have no idea what type of door we need (wooden, aluminum, iron, and so on). Secondly, we do not know which manufacturer we should refer to (Wooden or iron door manufacturer, and so on). So, we are facing two abstractions here: "door" and "door manufacturer":

假设我们正在建造一个住宅单元,并且必须制作一扇“门”。要制作“门”,我们必须参考“门制造商”,但到目前为止,我们不知道我们需要什么类型的门(木门、铝门、铁门等)。其次,我们不知道我们应该参考哪个制造商(木门或铁门制造商,等等)。因此,我们在这里面临两个抽象:“door” 和 “door manufacturer”:

alt text
Figure 2.1: Door-Door manufacturer relation
图 2.1: 门-门制造商关系

According to Figure 2.1, the following code could be considered:
根据图 2.1,可以考虑以下代码:


public abstract class Door
{
    public abstract void Design();
    public abstract void Build();
    public abstract void Coloring();
}

public abstract class DoorManufacturer
{
    public Door CreateDoor(){}
}

According to the preceding paragraph, we finally decided to choose a "wooden door" for our unit, so we must go to a "carpenter" to make a "wooden door". It is clear that "wooden door" is a type of "door" and "carpenter" is a type of "door maker". So, we have two classes WoodenDoor for "wooden door" and Carpenter for "carpenter".

根据前一段,我们最后决定为我们的单位选择一扇“木门”,那么我们必须去找一个“木匠”来制作一扇“木门”。很明显,“木门”是“门”的一种,而“木匠”是“造门师”的一种。因此,我们有两个类 WoodenDoor 用于 “wooden door” 和 “carpenter” 的 Carpenter。

On the other hand, we know that Design, Build, and Coloring behaviors can be defined for doors. We still do not know exactly how these behaviors happen for different types of doors (wooden, iron, and so on). To solve this problem in the WoodenDoor class, we need to implement these behaviors to determine how each behavior happens for a wooden door. A similar mechanism should be implemented for other types of doors, such as iron doors.

另一方面,我们知道可以为门定义 Design、Build 和 Coloring 行为。我们仍然不知道这些行为对于不同类型的门(木制、铁制等)是如何发生的。要在 WoodenDoor 类中解决此问题,我们需要实现这些行为来确定木门的每种行为是如何发生的。其他类型的门也应采用类似的机制,例如铁门。

We also know that a door manufacturer must be able to make a door. To make a door, he must design, build, and color, but we have yet to learn of a door manufacturer. To solve this problem, we add a method called GetDoor to the DoorManufacturer class and specify that this method is responsible for creating a "door manufacturer" type object. Finally, every door manufacturer (such as a wooden door manufacturer) implements this method according to his/her requirements and processes. Therefore, through this implementation in the DoorManufacturer class, we find out which manufacturer intends to make the door:

我们也知道,门制造商必须能够制造门。要制造门,他必须设计、建造和着色,但我们还没有了解到门制造商。为了解决此问题,我们将一个名为 GetDoor 的方法添加到 DoorManufacturer 类中,并指定此方法负责创建“门制造商”类型对象。最后,每个门制造商(例如木门制造商)都根据他/她的要求和流程实施这种方法。因此,通过 DoorManufacturer 类中的这个实现,我们找出哪个制造商打算制造门:

public abstract class Door
{
    public abstract void Design();
    public abstract void Build();
    public abstract void Coloring();
}

public class WoodenDoor : Door
{
    public override void Design()
    {
        Console.WriteLine("Wooden door design done");
    }

    public override void Build()
    {
        Console.WriteLine("Wooden door build done");
    }

    public override void Coloring()
    {
        Console.WriteLine("Wooden door coloring done");
    }
}

public abstract class DoorManufacturer
{
    public void CreateDoor()
    {
    Door door = this.GetDoor();
    door.Design();
    door.Build();
    door.Coloring();
    Console.WriteLine("Your door is ready!");
    }

    public abstract Door GetDoor();
}

public class Carpenter : DoorManufacturer
{
    public override Door GetDoor()
    {
        return new WoodenDoor();
    }
}

Finally, we go to the carpenter to make the door and give the desired dimensions and features and ask them to make the door for us:
最后,我们去找木匠制作门,给出所需的尺寸和功能,并要求他们为我们制作门。

DoorManufacturer manufacturer = new Carpenter();
manufacturer.CreateDoor();

In the preceding design, if we need to add an iron door manufacturer, it is enough to consider a class for an iron door (Let us call it, IronDoor) which inherits from the Door class, and consider a class for an iron door manufacturer (Let us call it, IronDoorMaker) that inherits from DoorManufacturer. In this way, new doors and manufacturers can be defined without manipulating the previous codes. Finally, we will have the following class diagram for this design pattern:

在前面的设计中,如果我们需要添加一个铁门制造商,那么考虑一个继承自 Door 类的铁门类(我们称之为 IronDoor)就足够了,并考虑一个继承自 DoorManufacturer 的铁门制造商的类(我们称之为 IronDoorMaker)。通过这种方式,可以在不纵先前代码的情况下定义新的门和制造商。最后,我们将为这个设计模式提供以下类图:

alt text
Figure 2.2: Factory Method UML diagram
图 2.2: 工厂方法 UML 图

Participants:
参与者:

  • Product: Based on the preceding section, the Door is responsible for defining a template for the objects made by the factory method.
    Product:根据上一节,Door 负责为工厂方法生成的对象定义模板。

  • Concrete product: In the preceding scenario, WoodenDoor and IronDoor are responsible for implementing the template defined by the Product.
    具体产品:在前面的场景中,WoodenDoor 和 IronDoor 负责实现 Product 定义的模板。

  • Creator: This is also called the factory. In the preceding scenario, the DoorManufacturer is responsible for defining the Factory Method. This method is responsible for creating and returning a product-type object. In the preceding scenario, the GetDoor method is the same as the factory method. It is unnecessary to define this method as an abstract method, and the creator class can also have a default implementation of this method.
    创建者:这也称为工厂。在前面的方案中,DoorManufacturer 负责定义 Factory Method。该方法负责创建并返回 product-type 对象。在前面的方案中,GetDoor 方法与工厂方法相同。没有必要将此方法定义为抽象方法,并且 creator 类也可以具有此方法的默认实现。

  • Concrete creator: In the preceding scenario, the Carpenter and IronDoorMaker are responsible for rewriting the factory method to make the appropriate object and provide it to the creator.
    具体创建者:在前面的场景中,Carpenter 和 IronDoorMaker 负责重写工厂方法以生成适当的对象并将其提供给创建者。

Notes:
笔记:

  • In the given implementation and design (Figure 2.2), we can use the interface instead of the abstract class for the product.
    在给定的实现和设计(图 2.2)中,我们可以使用接口而不是产品的抽象类。

  • To reuse the created objects, you can use the object pool.
    This design pattern tries to manage the way objects are made, and this is because the factory method design pattern is included in the category of creational design patterns.
    要重用创建的对象,您可以使用对象池。此设计模式试图管理对象的创建方式,这是因为工厂方法设计模式包含在创建设计模式的类别中。

  • You can also use the Parametrized Factory Method to implement the Factory Method. In this implementation method, we send the type of object we want to create to the Factory Method through a parameter, and through that, we proceed to create the desired object. Concrete Creators can extend these methods by overriding them by using this implementation method. To implement this procedure, the GetDoor method in the preceding scenario is changed as follows:
    您还可以使用 Parametrized Factory Method 来实现 Factory Method。在这个实现方法中,我们通过参数将要创建的对象类型发送到 Factory Method,然后通过该参数,我们继续创建所需的对象。Concrete Creators 可以通过使用此实现方法覆盖这些方法来扩展这些方法。为了实现此过程,上述方案中的 GetDoor 方法更改如下:

    public virtual Door GetDoor(string type)
    {
    switch (type)
    {
        case "Wooden":
        return new WoodenDoor();
        case "Iron":
        return new IronDoor();
    }
    return null;
    }

In this case, if we want to add the aluminum door manufacturer, we can cover this new requirement by overriding the following method:
在这种情况下,如果我们想添加铝门制造商,我们可以通过覆盖以下方法来满足这个新要求:

public class AluminumDoorMaker: DoorManufacturer
{
    public override Door GetDoor(string type)
    {
    if (type == "Aluminum")
    return new AluminumDoor();
    else
    return base.GetDoor(type);
    }
}

C# language supports generics from version 2 onwards. This feature of the C# language, instead of creating many subclasses, you can create a product by simply providing a generic type. Using this feature, you can rewrite the concrete creator section as follows:
C# 语言支持版本 2 及更高版本的泛型。C# 语言的这一功能,无需创建许多子类,只需提供泛型类型即可创建产品。使用此功能,您可以按如下方式重写 concrete creator 部分:

public class StarndardDoorManufacturer<T>: DoorManufacturer
    where T: Door, new()
    {
        public override Door GetDoor()
        {
        return new T();
    }

}

Pay attention to the where section. With the help of this section, we can filter the generic types so that only types can be sent that are both Door type and its subset types (where T: Door) and also have a default constructor (where T: new()).

注意 where 部分。在本节的帮助下,我们可以过滤泛型类型,以便只能发送既是 Door 类型又是其子集类型(其中 T: Door)并且还具有默认构造函数(其中 T: new())的类型。

Finally, with the applied changes, it can be used as follows:
最后,通过应用的更改,可以按如下方式使用:

DoorManufacturer obj = new StarndardDoorManufacturer<WoodenDoor>();

obj.CreateDoor();

It is better to use a suitable naming template in design patterns so that the design pattern is understood by the template used. It would have been better in the preceding scenario, instead of using the title DoorManufacturer, titles such as DoorFactory or similar were used.

最好在设计模式中使用合适的命名模板,以便所使用的模板能够理解设计模式。在前面的场景中,最好不要使用标题 DoorManufacturer,而是使用 DoorFactory 或类似标题。

You can start the design with the factory method, then mature the design with the Abstract factory, prototype, or builder.

您可以从 factory 方法开始设计,然后使用 Abstract factory、prototype 或 builder 完善设计。

Consequences: Advantages
后果:优势

A loose connection between the creator and the product increases the ability to maintain and develop the code:

创建者和产品之间的松散连接增加了维护和开发代码的能力:

The principle of Single Responsibility Principal (SRP) has been observed. By moving the creation of the object outside the factory method, we made each method responsible for doing only one thing.
遵守单一责任委托人 (SRP) 的原则。通过将对象的创建移到工厂方法之外,我们使每个方法只负责做一件事。

The principle of Open/Close Principle (OCP) has been observed. As mentioned in the preceding scenario, new products and creators can be added to the program without manipulating the existing codes.
已遵守开/关原则 (OCP) 原则。如前面的场景所述,可以将新产品和创作者添加到程序中,而无需作现有代码。

Consequences: Disadvantages
后果:缺点

If the generic feature is not used, then each concrete product and creator must be defined as a subclass. This structure can become complicated to maintain with the increase in the number of classes.
如果未使用通用特征,则必须将每个具体产品和创建者定义为一个子类。随着类数量的增加,维护此结构可能会变得复杂。

Applicability:
适用性:

  • When a class does not know the exact type of objects it should create, and need to delegate this task to its child classes.
    当一个类不知道它应该创建的对象的确切类型,并且需要将此任务委托给其子类时。

  • When we are developing a software development framework and providing the ability to add new features to the framework.
    当我们开发软件开发框架并提供向框架添加新功能的能力时。

  • When we need to control the number of created objects and reuse created objects as much as possible.
    当我们需要控制已创建对象的数量并尽可能多地重用已创建的对象时。

Related patterns:
相关模式:

Some of the following design patterns are not related to the Factory Method design pattern, but to implement this design pattern, checking the following design patterns will be useful:
以下一些设计模式与 Factory Method 设计模式无关,但要实现此设计模式,检查以下设计模式将很有用:

  • Object pool 对象池
  • Abstract factory 抽象工厂
  • Template method 模板方法
  • Singleton 单例
  • Iterator 迭代器

Abstract factory

抽象工厂

The abstract factory design pattern is introduced and analyzed in this section, according to the structure presented in the GoF design patterns section in Chapter 1, Introduction to Design Patterns.
本节根据第 1 章 设计模式简介的 GoF 设计模式部分介绍的结构,介绍和分析抽象工厂设计模式。

Name:
Abstract factory
抽象工厂
Classification:
Creational design patterns
创建式设计模式
Also known as:
Kit
工具箱
Intent:
This design pattern tries to create a group of related objects through an interface without referring to their class.
此设计模式尝试通过接口创建一组相关对象,而不引用它们的类。

Motivation, Structure, Implementation, and Sample code:
动机、结构、实现和示例代码:
We have several car factories in the country that produce cars. The cars produced by these factories are divided into two categories: gasoline and diesel cars. So far, we are facing two types of abstractions in the scenario. The first abstraction is related to the car manufacturing plant, and the second is related to the type of car. So far in the scenario, we are facing the following class diagram:
我们在该国有几家生产汽车的汽车工厂。这些工厂生产的汽车分为汽油车和柴油车两大类。到目前为止,我们在场景中面临两种类型的抽象。第一个抽象与汽车制造厂有关,第二个抽象与汽车类型有关。到目前为止,在该方案中,我们面对的是以下类图:

alt text

Figure 2.3: Car-Car factory initial relation
图 2.3: 汽车-汽车工厂的初始关系

According to Figure 2.3, the following code could be considered:
根据图 2.3,可以考虑以下代码:

public abstract class CarFactory
{
    public abstract PetrolCar CreatePetrolCar();
    public abstract DieselCar CreateDieselCar();
}

public abstract class PetrolCar
{
    public abstract void AssembleSeats();
}

public abstract class DieselCar
{
    public abstract void AssembleDieselEngine();
}

According to the class (Figure 2.3 and the preceding code), we must bring classes that use these abstractions to complete the design. For example, the CreatePetrolCar method is in the CarFactory abstract class, but it is unclear how this method is implemented. Logically, it is also true because we still need to know which car manufacturer we are talking about. When it is clear which car manufacturer we are talking about, we can determine how CreatePetrolCar should be implemented. According to the presented scenario, we assume that we have two automobile factories in the country named IranKhodro and Saipa. These factories should be able to produce their own gasoline and diesel products. With this explanation, the preceding design changes as the following:

根据类(图 2.3 和前面的代码),我们必须引入使用这些抽象的类来完成设计。例如,CreatePetrolCar 方法位于 CarFactory 抽象类中,但尚不清楚此方法是如何实现的。从逻辑上讲,这也是正确的,因为我们仍然需要知道我们正在谈论的是哪个汽车制造商。当清楚我们谈论的是哪个汽车制造商时,我们可以确定应该如何实施 CreatePetrolCar。根据所呈现的情景,我们假设我们在该国有两家名为 IranKhodro 和 Saipa 的汽车工厂。这些工厂应该能够生产自己的汽油和柴油产品。通过此说明,前面的设计将更改如下:

alt text
Figure 2.4: IranKhodro-Saipa factories relations to CarFactory
图 2.4:IranKhodro-Saipa 工厂与 CarFactory 的关系

According to Figure 2.4, the following code could be considered:
根据图 2.4,可以考虑以下代码:

public abstract class CarFactory {
    public abstract PetrolCar CreatePetrolCar();
    public abstract DieselCar CreateDieselCar();
}

public class IranKhodro : CarFactory
{
    public override DieselCar CreateDieselCar()=> throw NotImplementedException();
    public override PetrolCar CreatePetrolCar() =>throw NotImplementedException();
}

public class Saipa : CarFactory
{
    public override DieselCar CreateDieselCar() =>throw NotImplementedException();
    public override PetrolCar CreatePetrolCar() =>throw NotImplementedException();
}

public abstract class PetrolCar
{
    public abstract void AssembleSeats();
}

public abstract class DieselCar
{
    public abstract void AssembleDieselEngine();
}

So far, it has been found that car factories are making gasoline and diesel cars. We accepted that Iran Khodro and Saipa are car factories, and they produce gasoline and diesel cars. Now the question remains unanswered here: which diesel car does the Iran Khodro or Saipa factory produce? To answer this question, we must first answer which car is gasoline and which is diesel. We can determine which car manufacturer produces gasoline and diesel cars when we answer this question. So, with these conditions, the class diagram changes as the following:

到目前为止,已经发现汽车工厂正在生产汽油和柴油汽车。我们接受了伊朗 Khodro 和 Saipa 是汽车工厂,他们生产汽油和柴油汽车。现在问题仍未得到解答:伊朗 Khodro 或 Saipa 工厂生产哪种柴油车?要回答这个问题,我们首先要回答哪辆车是汽油车,哪辆车是柴油车。当我们回答这个问题时,我们可以确定哪个汽车制造商生产汽油和柴油汽车。因此,在这些条件下,类图将更改如下:

alt text
Figure 2.5: Factories and cars relations

According to Figure 2.5, the following code could be considered:
根据图 2.5,可以考虑以下代码:

public abstract class CarFactory
{
    public abstract PetrolCar CreatePetrolCar();
    public abstract DieselCar CreateDieselCar();
}

public class IranKhodro : CarFactory
{
    public override DieselCar CreateDieselCar() => new Arena();
    public override PetrolCar CreatePetrolCar() => new Peugeot206();
}

public class Saipa : CarFactory
{
    public override DieselCar CreateDieselCar() => new Foton();
    public override PetrolCar CreatePetrolCar() => new Pride();
}

public abstract class PetrolCar
{
    public abstract void AssembleSeats();
}

public class Pride : PetrolCar
{
    public override void AssembleSeats()=>
    Console.WriteLine("Pride seats assembled");
}

public class Peugeot206 : PetrolCar
{
    public override void AssembleSeats() =>
    Console.WriteLine("Peugeot206 seats assembled");
}

public abstract class DieselCar
{
    public abstract void AssembleDieselEngine();
}

public class Foton : DieselCar
{
    public override void AssembleDieselEngine()=>
    Console.WriteLine("Foton engine assembled");
}

public class Arena : DieselCar
{
    public override void AssembleDieselEngine()=>
    Console.WriteLine("Arena engine assembled");
}

Now, it is clear which car manufacturers produce which cars and how each of these is produced. The design is almost finished, and we only need a user to build the desired car using our defined abstractions. So, with these conditions, the class diagram changes as follows:

现在,哪些汽车制造商生产哪些汽车以及每辆汽车是如何生产的,这一点就很清楚了。设计几乎完成,我们只需要一个用户使用我们定义的抽象来构建所需的汽车。因此,在这些条件下,类图将发生如下变化:

alt text
Figure 2.6: Abstract Factory UML diagram
图 2.6: 抽象工厂 UML 图

According to Figure 2.6, the following code could be considered:
根据图 2.6,可以考虑以下代码:

public class Client
{
    private readonly CarFactory factory;
    public Client(CarFactory factory) => this.factory = factory;

    public void CreatePetrolCar()
    {
        var petrol = factory.CreatePetrolCar();
        petrol.AssembleSeats();
    }
        public void CreateDieselCar()
    {
        var diesel = factory.CreateDieselCar();
        diesel.AssembleDieselEngine();
    }
}

As it is clear in the preceding code, the Client receives the car factory in the constructor and creates the cars using CreatePetrolCar and CreateDieselCar methods.

如前面的代码中所示,客户端在构造函数中接收汽车工厂,并使用 CreatePetrolCar 和 CreateDieselCar 方法创建汽车。

Participants:
参与者:

  • AbstractFactory: In the preceding scenario, the CarFactory has the task of defining a template for tasks whose purpose is to create an AbstractProduct.
    AbstractFactory:在前面的场景中,CarFactory 的任务是为创建 AbstractProduct 的任务定义模板。

  • ConcreteFactory: In the preceding scenario, it is IranKhodro and Saipa which has the task of implementing that aim to create a ConcreteProduct.
    ConcreteFactory:在前面的场景中,IranKhodro 和 Saipa 的任务是实现创建 ConcreteProduct 的目标。

  • AbstractProduct: In the preceding scenario, PetrolCar and DieselCar are responsible for defining the format for all kinds of products.
    AbstractProduct:在前面的场景中,PetrolCar 和 DieselCar 负责定义各种产品的格式。

  • ConcreteProduct: In the preceding scenario, it is Pride, Peugeot206, Arena, and Foton which is responsible for defining the product.
    ConcreteProduct:在前面的场景中,是 Pride、Peugeot206、Arena 和 Foton 负责定义产品。

  • Client: This is also called Client, in the preceding scenario, is responsible for using abstractions.
    Client:这也称为 Client,在前面的场景中,它负责使用抽象。

Notes:
笔记:

  • In the preceding design, instead of the abstract class for CarFactory, PetrolCar, and DieselCar, we can also use interfaces.
    在前面的设计中,我们还可以使用接口,而不是 CarFactory、PetrolCar 和 DieselCar 的抽象类。

  • Usually, the classes in this design pattern can be implemented with the factory method or Prototype design pattern.
    通常,此设计模式中的类可以使用工厂方法或 Prototype 设计模式实现。

  • If the only purpose is to hide the sampling method, then an abstract factory can be considered an alternative to a Facade.
    如果唯一目的是隐藏采样方法,那么抽象工厂可以被视为 Facade 的替代方案。

  • The difference between the builder design pattern and abstract factory is that the Builder design pattern can be used to create complex objects. Also, it is possible to do additional tasks along with the sampling process when using the Builder design pattern. Still, the created sample is returned immediately in the abstract factory design pattern.
    Builder 设计模式和抽象工厂之间的区别在于 Builder 设计模式可用于创建复杂对象。此外,在使用 Builder 设计模式时,可以在采样过程中执行其他任务。尽管如此,创建的示例仍会立即以抽象的 Factory 设计模式返回。

  • Abstract factory design patterns can also be implemented as Singleton.
    抽象工厂设计模式也可以作为 Singleton 实现。

  • All products of the same family are implemented and integrated into a concrete factory related to that family.
    同一系列的所有产品都被实施并集成到与该系列相关的混凝土工厂中。

  • In C# language, to implement this design pattern, Generics can be used to design Factory and Product.
    在 C# 语言中,为了实现此设计模式,可以使用泛型来设计 Factory 和 Product。

Consequences: Advantages
后果:优势

  • There is always a guarantee that the products received from the factory are of the same family.
    始终保证从工厂收到的产品属于同一系列。

  • There is a loose connection between the Client and concrete products.
    客户和混凝土产品之间存在松散的联系。

  • The principle of SRP has been observed.
    已遵守 SRP 的原则。

  • The principle of OCP has been observed. New products can be defined without changing the existing codes.
    OCP 的原理已被遵守。可以在不更改现有代码的情况下定义新产品。

Consequences: Disadvantages
后果:缺点

  • This structure can become complicated to maintain over time and with the increase in the number of classes.
    随着时间的推移和类数量的增加,维护这种结构可能会变得复杂。

  • With the addition of a new product, the abstract factory must be changed, which leads to the change of all concrete factories.
    随着新产品的加入,抽象工厂必须改变,这导致了所有具体工厂的改变。

Applicability:
适用性:

  • When dealing with a family of products and do not want to make the code dependent on concrete products, we can use this pattern.
    当处理一系列产品并且不想使代码依赖于具体产品时,我们可以使用这种模式。

  • When faced with a class that consists of a collection of factory methods, the abstract factory pattern can be useful.
    当面对由工厂方法集合组成的类时,抽象工厂模式可能很有用。

Related patterns:
相关模式:
Some of the following design patterns are not related to abstract factory design patterns, but to implement this design pattern, checking the following design patterns will be useful:
以下一些设计模式与抽象工厂设计模式无关,但要实现此设计模式,检查以下设计模式将很有用:

  • Prototype 原型
  • Factory method 工厂方法
  • Singleton 单例
  • Facade 立面
  • Builder 生成器
  • Bridge Bridge

Builder

生成器模式

The builder design pattern is introduced and analyzed in this section, according to the structure presented in the GoF design patterns section in Chapter 1, Introduction to Design Pattern.

本节根据第 1 章 设计模式简介的 GoF 设计模式部分介绍的结构,介绍和分析了构建器设计模式。

Name:
Builder
生成器模式
Classification:
Creational design patterns
创建式设计模式
Also known as:
-
Intent:
意图:
This design pattern tries to create complex objects according to the requirement.
此设计模式尝试根据要求创建复杂对象。
Motivation, Structure, Implementation, and Sample code:
动机、结构、实现和示例代码:
Suppose we are building a residential unit. Residential units can have different construction processes according to the use case. A residential unit can be a villa or an apartment, which can be concrete, iron, or prefabricated. To model the residential unit, there are different ways. For example, we can define a parent class for a residential unit and connect different types of residential units to each other using the parent-child relationship. Although this method solves the design problem, in the end, it causes us to face many parent-child relationships that threaten the readability and maintainability of the code.

假设我们正在建造一个住宅单元。根据用例,住宅单元可以有不同的施工流程。住宅单元可以是别墅或公寓,可以是混凝土、铁或预制的。要对住宅单元进行建模,有多种方法。例如,我们可以为住宅单元定义一个父类,并使用父子关系将不同类型的住宅单元相互连接。虽然这种方法解决了设计问题,但最终还是导致我们面临很多父子关系,威胁到代码的可读性和可维护性。

We can use a class to define the residential unit and send all the necessary parameters to define the residential unit through the constructor, in the form of optional parameters, to the class. Although this method solves the design problem, too, it obviously causes the emergence of strange constructors with all the input parameters, which are not given a value when many of them are used.
我们可以使用一个类来定义住宅单元,并通过构造函数以可选参数的形式将定义住宅单元所需的所有参数发送到类。虽然这种方法也解决了设计问题,但它显然会导致出现具有所有输入参数的奇怪构造函数,当使用许多参数时,这些参数没有被赋予值。

For example, we come across the codes like the following:
例如,我们遇到了如下代码:

House house1 = new House(p1,p2,null,null,null,null,null,p3,null,null);

House house2 = new House(null,null,null,null,null,p4,null,null,null,null);

We can put different constructors in the definition of a class, for the residential unit, for different states. Although this method solves the problem, it causes the appearance of telescopic constructors and makes it difficult to maintain the class:
我们可以在类的定义中为住宅单元和不同的状态放置不同的构造函数。虽然这种方法解决了这个问题,但它会导致伸缩构造器的出现,并且很难维护类:

public class House{
Public House(string p1){}
Public House(string p1, string p2){}
Public House(string p1, string p2, string p3){}
Public House(string p1, string p2, string p3, string p4){}
// …
}

The builder design pattern tries to solve these problems. In the preceding example, this design pattern helps to define the stages and steps of building a residential unit so that it is possible to build a residential unit using these stages and steps. For example, separate the wall construction process from the door construction process and present each one separately.

构建器设计模式尝试解决这些问题。在前面的示例中,此设计模式有助于定义构建住宅单元的阶段和步骤,以便可以使用这些阶段和步骤构建住宅单元。例如,将墙构造过程与门构造过程分开,并分别呈现每个过程。

To make the matter clear, let us pay attention to another scenario in this regard:

为了弄清楚这个问题,让我们注意这方面的另一种情况:

A requirement is raised, and it is requested to provide the ability to make a mobile phone. Based on this requirement, we notice that mobile phones have different models. For example, we consider Samsung and Apple mobile phones. We understand that regardless of the manufacturing company, every mobile phone needs to be manufactured for the screen, body, camera, and so on, and the appropriate operating system is installed on the mobile phone. It is also clear that the manufacturing methods of both Samsung and Apple companies are completely different for each of these parts.

提出了一个要求,并要求提供制作手机的能力。根据这个要求,我们注意到移动电话的型号不同。例如,我们考虑三星和苹果手机。我们明白,无论制造公司是谁,每部手机都需要针对屏幕、机身、相机等进行制造,并在手机上安装适当的作系统。同样明显的是,三星和苹果公司的制造方法对于这些部件中的每一个都完全不同。

With these explanations, we are faced with two different elements in the design:
通过这些解释,我们在设计中面临着两个不同的元素:

  • The first element: The product we want to produce
    第一个元素:我们要生产的产品

  • The second element: The method of making the product
    第二个元素:制作产品的方法

For the first element, we can define two classes for Samsung and Apple, and for the second element, we can use an interface to format the steps of making a mobile phone. With these explanations, we can consider the following class figure:

对于第一个元素,我们可以为 Samsung 和 Apple 定义两个类,对于第二个元素,我们可以使用接口来格式化制作手机的步骤。通过这些解释,我们可以考虑以下类图:

alt text
Figure 2.7: Cellphone construction initial relations
图 2.7: 手机构造初始关系

According to Figure 2.7, the following code could be considered:
根据图 2.7,可以考虑以下代码:

public class CellPhone
{
    public string CameraResolution { get; set; }
    public string MonitorSize { get; set; }
    public string BodyMaterial { get; set; }
    public string OSName { get; set; }
}

public interface ICellPhoneBuilder
{
    public CellPhone Phone { get; }
    void BuildMonitor();
    void BuildBody();
    void BuildCamera();
    void PrepareOS();
}

public class Samsung : ICellPhoneBuilder
{
    public CellPhone Phone { get; private set; }
    public Samsung() => Phone = new CellPhone();
    public void BuildBody() => Phone.BodyMaterial = "Titanium";
    public void BuildCamera() => Phone.CameraResolution = "10 MP";
    public void BuildMonitor() => Phone.MonitorSize = "10 Inch";
    public void PrepareOS() => Phone.OSName = "iOS";
}

public class Apple : ICellPhoneBuilder
{
    public CellPhone Phone { get; private set; }
    public Apple() => Phone = new CellPhone();
    public void BuildBody() => Phone.BodyMaterial = "Aluminum";
    public void BuildCamera() => Phone.CameraResolution = "12 MP";
    public void BuildMonitor() => Phone.MonitorSize = "9.8 Inch";
    public void PrepareOS() => Phone.OSName = "Android 10";
}

Up to this design point, we have been able to define and implement the necessary steps to make a mobile phone. Now, what is the process of making a mobile phone? How should the defined steps be combined with each other to finally make a mobile phone? To answer these questions, we use the CellPhoneDirector class and define the work process in its format. With these explanations, the class diagram changes as follows:

到目前为止,我们已经能够定义和实施制造手机的必要步骤。那么,制作手机的过程是怎样的呢?定义的步骤应该如何相互结合,最终制作出手机呢?为了回答这些问题,我们使用 CellPhoneDirector 类并按其格式定义工作流程。通过这些说明,类图将更改如下:

alt text
Figure 2.8: Builder design pattern UML diagram
图 2.8.. Builder 设计模式 UML 图

According to Figure 2.8, the following code could be considered:
根据图 2.8,可以考虑以下代码:

public class CellPhoneDirector
{
private ICellPhoneBuilder builder;

public CellPhoneDirector(ICellPhoneBuilder builder) => this.builder = builder;

public CellPhone Construct()
{
    builder.BuildBody();
    builder.BuildMonitor();
    builder.BuildCamera();
    builder.PrepareOS();
    return builder.Phone;
}

}

According to the preceding code, it is clear that to build a mobile phone (Construct), its body should be built first (BuildBody), then the screen (BuildMonitor), and then the camera should be built (BuildCamera), and finally, the operating system should be prepared (PrepareOS). With these explanations, the design is finished, and to use this structure, you can use the following code:

根据前面的代码,很显然,要构建一部手机(Construct),首先要构建它的机身(BuildBody),然后构建屏幕(BuildMonitor),然后构建摄像头(BuildCamera),最后准备作系统(PrepareOS)。通过这些说明,设计就完成了,要使用此结构,您可以使用以下代码:

CellPhoneDirector director = new CellPhoneDirector(new Samsung());

var phone = director.Construct();
Console.WriteLine($"
    Body: {phone.BodyMaterial},
    Camera: {phone.CameraResolution},
    Monitor: {phone.MonitorSize},
    OS: {phone.OSName}"
);

Participants:
参与者:

  • Builder: In the preceding scenario, it is ICellPhoneBuilder, which is responsible for defining the format of the steps to build an object. In other words, it has the task of determining what stages and steps should be defined to build an object.
    Builder:在前面的场景中,它是 ICellPhoneBuilder,它负责定义构建对象的步骤的格式。换句话说,它的任务是确定应该定义哪些阶段和步骤来构建对象。

  • ConcreteBuilder: In the preceding scenario, it is Apple and Samsung that are responsible for implementing the steps announced by the builder and specifying how each step should be implemented.
    ConcreteBuilder:在前面的场景中,Apple 和 Samsung 负责实施生成器宣布的步骤,并指定每个步骤的实施方式。

  • Director: In the preceding scenario, it is CellPhoneDirector, which is responsible for implementing the object creation process. During the implementation of this process, the director uses the steps declared by the builder and produces the object.
    Director:在前面的场景中,是 CellPhoneDirector,它负责实现对象创建过程。在此过程的实施过程中,director 使用生成器声明的步骤并生成对象。

  • Product: In the preceding scenario, it is the CellPhone that is responsible for defining the complex object that we are trying to build. In fact, the product is built by the Director within ConceretBuilders through the steps defined in the Builder.
    Product:在前面的场景中,CellPhone 负责定义我们尝试构建的复杂对象。事实上,该产品是由 ConceretBuilders 中的 Director 通过 Builder 中定义的步骤构建的。

Notes:
笔记:

  • An abstract class can also be used to define Builder.
    抽象类也可用于定义 Builder。

  • Using this design pattern, the details of the object construction are hidden from the user's view. If we need to create an object differently, all we need to do is define a new builder.
    使用此设计模式,对象构造的细节对用户来说是隐藏的。如果我们需要以不同的方式创建对象,我们需要做的就是定义一个新的构建器。

  • Using this design pattern, and the director's use, there is more control over the object creation process.The Builder design pattern can also be implemented as a Singleton.
    使用此设计模式以及 director 的使用,可以更好地控制对象创建过程。Builder 设计模式也可以作为 Singleton 实现。

  • Builder and bridge design patterns can be combined. In this case, the director plays the role of abstraction, and various builders play the role of implementation.
    Builder 和 Bridge 设计模式可以组合使用。在这种情况下,director 扮演抽象的角色,而各种 builder 扮演 implementation 的角色。

Consequences: Advantages
后果:优势

  • The step-by-step construction of the object allows for better control over the construction process.
    对象的逐步构造可以更好地控制构造过程。

  • Single Responsibility Principle (SRP) is met. The code related to the construction of a complex object is separated from the business logic.
    满足单一责任原则 (SRP)。与复杂对象构造相关的代码与业务逻辑分离。

Consequences: Disadvantages
后果:缺点

  • With the increase in the number of concrete builders, the complexity of the code increases due to the increase in the volume of coding.
    随着混凝土构建器数量的增加,由于编码量的增加,代码的复杂性也随之增加。

Applicability:
适用性:

  • When we have telescopic constructors in our classes, this design pattern will likely improve the quality of code and design.
    当我们的类中有伸缩构造器时,这种设计模式可能会提高代码和设计的质量。

  • The product manufacturing has the same steps, but the output can be different, then this design pattern will be useful. For example, in the preceding scenario, creating the product in question had the same steps, but the output was different (the Apple company had its own output, and the Samsung company had its own output).
    产品制造具有相同的步骤,但输出可能不同,那么这种设计模式将很有用。例如,在前面的场景中,创建相关产品的步骤相同,但输出不同(Apple 公司有自己的输出,而 Samsung 公司有自己的输出)。

Related patterns:
Some of the following design patterns are not related to the Builder design pattern, but in order to implement this design pattern, checking the following design patterns will be useful:
以下一些设计模式与 Builder 设计模式无关,但为了实现此设计模式,检查以下设计模式将非常有用:

  • Singleton
  • Composite
  • Bridge
  • Abstract Factory

Prototype

原型

In this section, the prototype design pattern is introduced and analyzed according to the structure presented in the GoF design patterns section in Chapter 1, Introduction to Design Patterns.
在本节中,根据第 1 章 设计模式简介的 GoF 设计模式部分中介绍的结构介绍和分析原型设计模式。

Name:
Prototype
原型
Classification:
Creational design patterns
创建式设计模式
Also Known As:
-
Intent:
意图:
This design pattern tries to create objects using a prototype.
此设计模式尝试使用原型创建对象。
Motivation, Structure, Implementation, and Sample code:
动机、结构、实现和示例代码:
Suppose that we have a clothing factory, and we intend to produce different types of pants. The usual method is that the pants are designed first. Then, a pair of pants is produced as a sample based on this design. Finally, the rest of the pants are mass-produced. Pants that are designed and produced at the beginning are called prototypes. According to this scenario, it is very important to define a method to mass-produce the rest of the pants from the prototype.
假设我们有一家服装厂,我们打算生产不同类型的裤子。通常的方法是先设计裤子。然后,根据此设计生产一条裤子作为样品。最后,其余的裤子都进行了批量生产。一开始设计和生产的裤子称为原型。根据这个场景,定义一种方法来从原型中批量生产其余裤子是非常重要的。

There are different types of pants, such as jeans, linen, and cloth pants. The production of each prototype is different from the other. But in the meantime, having a method for mass production of any pants (copying) is common among different types of pants. According to this scenario, we face the following class diagram:
有不同类型的裤子,例如牛仔裤、亚麻布和布裤。每个原型的生产都不同。但与此同时,拥有一种大规模生产任何裤子(复制)的方法在不同类型的裤子中很常见。根据此方案,我们面对以下类图:

alt text
Figure 2.9: Pants initial relation

According to Figure 2.9, the following code could be considered:
根据图 2.9,可以考虑以下代码:

public interface IPant
{
    IPant Clone();
}

public class FabricPant : IPant
{
    public IPant Clone() => this.MemberwiseClone() as IPant;
}

public class CottonPant : IPant
{
    public IPant Clone() => this.MemberwiseClone() as IPant;
}

public class JeanPant : IPant
{
    public IPant Clone() => this.MemberwiseClone() as IPant;
}

According to the preceding structure, to copy the pants, the clone method is defined in the IPant interface, and each class copies the object using the MemberwiseClone method.

根据前面的结构,要复制 pants,在 IPant 接口中定义了 clone 方法,每个类都使用 MemberwiseClone 方法复制对象。

Now that the design is finished, the following structure can be used:
现在设计已完成,可以使用以下结构:

IPant jean1 = new JeanPant();

IPant jean2 = jean1.Clone();

The point in the preceding code is the method of copying the object. The objects are copied using the shallow method. In shallow copying, value types are copied bit by bit, but in the case of reference types, only the address is copied, and not the object, which causes another object to be affected by changing the value. To prevent this from happening, the deep method should be used to copy objects. Refer to the following figure:
上述代码中的要点是复制对象的方法。使用 shallow 方法复制对象。在浅层复制中,值类型是逐位复制的,但在引用类型的情况下,只复制地址,而不复制对象,这会导致另一个对象受到更改值的影响。为了防止这种情况发生,应该使用 deep 方法来复制对象。参考下图:

alt text
Figure 2.10: Shallow Copy
图 2.10.. 浅拷贝

The preceding figure shows the shallow copy, in which object X1 refers to B1 and object B1 also refers to C1. After copying, a new object named X2 is created, which still refers to B1. Therefore, applying a change in B1 through X1 will cause this change to be felt by object X2 as well:
上图显示了浅拷贝,其中对象 X1 引用 B1,对象 B1 也引用 C1。复制后,将创建一个名为 X2 的新对象,该对象仍引用 B1。因此,在 B1 到 X1 中应用更改将导致对象 X2 也能感受到此更改:

alt text
Figure 2.11: Deep Copy

The preceding figure also shows the copy by deep method, in which after the creation of object X2, new objects B2 and C2 are also created, and X2 refers to new objects B2 and C2. Therefore, applying changes in B1 through X1 will not cause these changes to be felt in object X2.

上图还显示了 deep 复制方法,其中在创建对象 X2 之后,还会创建新的对象 B2 和 C2,X2 引用新的对象 B2 和 C2。因此,在 B1 到 X1 中应用更改不会导致在对象 X2 中感觉到这些更改。

According to the preceding explanations, and to clarify the differences between the shallow and deep methods, we also define another method called DeepClone. There are different methods to implement deep copy. Here we use the following method. Also, we define a class called Cloth, and we define the characteristics of the fabric through this class. The following code can be considered:

根据前面的解释,为了明确 shallow 和 deep 方法之间的区别,我们还定义了另一种名为 DeepClone 的方法。有多种方法可以实现 Deep Copy。这里我们使用以下方法。此外,我们定义了一个名为 Cloth 的类,我们通过这个类来定义织物的特性。可以考虑以下代码:

public class Cloth
{
    public string Color { get; set; }
}

public interface IPant
{
    public int Price { get; set; }
    public Cloth ClothInfo { get; set; }

    IPant Clone();
    IPant DeepClone();
}

public class JeanPant : IPant
{
    public int Price { get; set; }
    public Cloth ClothInfo { get; set; }

    public IPant Clone() => this.MemberwiseClone() as IPant;

    public IPant DeepClone()
    {
    JeanPant pant = this.MemberwiseClone() as JeanPant;
    pant.ClothInfo = new Cloth() { Color = this.ClothInfo.Color };
    return pant;
    }

    public override string ToString() =>
    $"Color:{this.ClothInfo.Color}, Price: {this.Price}";
}

Now, to check the output of each method, we will test the copy methods using the following code. For Shallow copying, we have:
现在,为了检查每个方法的输出,我们将使用以下代码测试 copy 方法。对于浅拷贝,我们有:

IPant jean1 = new JeanPant()
{
    Price = 10000,
    ClothInfo = new Cloth(){ Color = "Red"}
};

IPant jean2 = jean1.Clone() ;//Shallow Copy
jean2.Price = 11000;
jean2.ClothInfo.Color = "Geen";
Console.WriteLine($"jean1: {jean1}");

Console.WriteLine($"jean2: {jean2}");

By running the preceding code, we get the following output:
通过运行上述代码,我们得到以下输出:

jean1: Color: Geen, Price: 10000
jean2: Color: Geen, Price: 11000

In this copy method, for reference types, only the reference address is copied; changing the value in ClothInfo through jean2 causes the ClothInfo value in jean1 to change as well.
在此 copy 方法中,对于引用类型,仅复制引用地址;通过 jean2 更改 ClothInfo 中的值会导致 jean1 中的 ClothInfo 值也发生更改。

But if we use DeepClone instead of the clone method in the preceding code, then we will see the following output:
但是,如果我们在前面的代码中使用 DeepClone 而不是 clone 方法,那么我们将看到以下输出:

jean1: Color: Red, Price: 10000
jean2: Color: Geen, Price: 11000

Now, if we return to the proposed scenario, we can slightly change the design provided for the Prototype design pattern.
现在,如果我们返回到建议的场景,我们可以稍微更改为 Prototype 设计模式提供的设计。

In the current design,It is not yet clear how the user can communicate.
目前尚不清楚用户如何进行通信。

Also, in the current design, when the number of prototypes is not a fixed value, and the prototypes can be dynamically created and destroyed, no solution has been provided.

此外,在当前设计中,当原型的数量不是固定值,并且可以动态创建和销毁原型时,没有提供任何解决方案。

To answer the first requirement, the user communication to produce objects in the preceding scenario should be through IPant. For the second requirement, another class can be used as Registry, and the user's relationship with the prototype can be promoted through the registry as the following:
为了满足第一个要求,在前面的方案中生成对象的用户通信应通过 IPant 进行。对于第二个要求,可以使用另一个类作为 Registry,并且用户与原型的关系可以通过 Registry 进行提升,如下所示:

alt text
Figure 2.12: Prototype design pattern UML diagram
图 2.12: 原型设计模式 UML 图

According to Figure 2.12, the following code could be considered:
根据图 2.12,可以考虑以下代码:

public class PantRegistry
{
    public PantRegistry() => Pants = new List<IPant>();
    public List<IPant> Pants { get; private set; }
    public void Add(IPant obj) => Pants.Add(obj);
    public IPant GetByColor(string color) =>
        Pants
        .OrderBy(x => Guid.NewGuid())
        .FirstOrDefault(x => x.ClothInfo.Color == color)
        .DeepClone();

    public IPant GetByType(Type type) =>
        Pants
        .OrderBy(x => Guid.NewGuid())
        .FirstOrDefault(x => x.GetType() == type)
        .DeepClone();

}

In the preceding code, a feature called Pants is presented, which has the role of a reservoir, and objects are stored in this reservoir. The Add method is used to store the new object in the repository, and the GetByColor and GetByType methods are used to search the repository and find the desired object and copy it. To use this code, you can do the following:
在上面的代码中,提出了一个名为 Pants 的功能,它具有 reservoir 的角色,并且对象存储在此 reservoir 中。Add 方法用于将新对象存储在存储库中,GetByColor 和 GetByType 方法用于搜索存储库并查找所需对象并复制它。要使用此代码,您可以执行以下作:

IPant jean1 = new JeanPant()
{
    Price = 10000,
    ClothInfo = new Cloth() { Color = "Red" }
};

IPant cotton1 = new CottonPant()
{
    Price = 7000,
    ClothInfo = new Cloth() { Color = "Red" }
};

IPant fabric1 = new FabricPant()
{
    Price = 12000,
    ClothInfo = new Cloth() { Color = "Blue" }
};

PantRegistry registry = new PantRegistry();
registry.Add(jean1);
registry.Add(cotton1);
registry.Add(fabric1);
Console.WriteLine($"{jean1}");

// Get a pair of jeans
Console.WriteLine($"{registry.GetByType(typeof(JeanPant))}");

// Get a pair of red pants regardless of the type of pants
Console.WriteLine($"{registry.GetByColor("Red")}");

As you can see in the preceding code, the objects are added to the repository after being created, searched, and copied through different methods.

如前面的代码所示,对象在通过不同的方法创建、搜索和复制后会添加到存储库中。

Participants:
参与者:

  • Prototype: In the preceding scenario, it is IPant that is responsible for defining the template for copying the object
    原型:在前面的场景中,是 IPant 负责定义用于复制对象的

  • Concrete prototype: In the preceding scenario, it is CottonPant, JeanPant, and FabricPant, which is responsible for implementing the provided template for copying the object.
    模板具体原型:在前面的场景中,是 CottonPant、JeanPant 和 FabricPant 负责实现提供的用于复制对象的模板。

  • Prototype registry: which is also called Prototype Manager. In the preceding scenario, it is PantRegistry whose task is to facilitate access to objects.
    Prototype registry:也称为 Prototype Manager。在前面的场景中,PantRegistry 的任务是促进对对象的访问。

  • Client: It is the same user who sends a request to create a new object through Prototype or Prototype Registry.
    客户端:通过 Prototype 或 Prototype Registry 发送创建新对象的请求的同一用户。

Notes:

  • In .NET, the ICloneable interface can also be used as a prototype.
    在 .NET 中,ICloneable 接口也可以用作原型。

  • Singleton can also be used in the implementation of the prototype design pattern.
    Singleton 也可以用于原型设计模式的实现。

  • In the command design pattern, if you need to save history, you can use the prototype design pattern.
    在命令设计模式中,如果需要保存历史记录,可以使用 prototype 设计模式。

  • If there is a need to store the state of the object and this object:
    如果需要存储对象和此对象的 state:

    • It is not a complex object.
      它不是一个复杂对象。
    • Does not have complex references or can be defined easily.
      没有复杂的引用或可以轻松定义。
    • Then the prototype design pattern can be used instead of the memento design pattern.
      然后,可以使用 prototype 设计模式来代替 memento 设计模式。
  • Prototype registry can also be implemented using generics:
    原型注册表也可以使用泛型实现:

    public interface IPrototypeRegistry
    {
    void Add<T>(T obj) where T : ICloneable;
    T Get<T>() where T : ICloneable;
    }
  • Consequences: Advantages
    后果:优势

  • Repetitive codes for creating objects are removed, and we are faced with a smaller amount of code.
    用于创建对象的重复代码被删除,我们面临的代码量较小。

  • Due to the presence of the copying method, making complex objects can be easy.
    由于存在复制方法,制作复杂对象很容易。

  • Objects can be copied, and a new object can be created without the need for a concrete class.
    可以复制对象,并且可以在不需要具体类的情况下创建新对象。

  • Consequences: Disadvantages
    结果:缺点

  • The process of copying objects can be complicated.
    复制对象的过程可能很复杂。

  • Implementing the copy process for classes that have circular dependencies can be complicated.
    为具有循环依赖关系的类实现复制过程可能很复杂。

Applicability:
适用性:

  • When the type of object to be created is determined at runtime. For example, Dynamic Loading
    在运行时确定要创建的对象的类型时。例如,Dynamic Loading
  • When the objects of a class have almost the same data content
    当一个类的对象具有几乎相同的数据内容时

Related patterns:
相关模式:
Some of the following design patterns are not related to prototype design patterns, but to implement this design pattern, checking the following design patterns will be useful:
以下一些设计模式与原型设计模式无关,但要实现此设计模式,检查以下设计模式将很有用:

  • Singleton
  • Command
  • Memento
  • Abstract factory

Singleton

单例

In this section, the singleton design pattern is introduced and analyzed according to the structure presented in the GoF design patterns section in Chapter 1, Introduction to Design Pattern.
在本节中,根据第 1 章 设计模式简介的 GoF 设计模式部分中介绍的结构介绍和分析了单例设计模式。

Name:
Singleton
单例
Classification:
Creational design patterns
创建式设计模式
Also known as:
-
Intent:
This design pattern tries to provide a structure in which there is always an object of the class. One of the most important reasons for having only one object of a class is to control access to common resources such as databases and the like.
此设计模式尝试提供一个结构,其中始终存在类的对象。只有一个类对象的最重要原因之一是控制对公共资源(如数据库等)的访问。
Motivation, Structure, Implementation, and Sample code:
动机、结构、实现和示例代码:
Suppose we want to design the appropriate infrastructure for communication with the SQL Server database. For this purpose, we have created the DbConnectionManager class. During the program, to use this class, we need to always have only one object of it. In this scenario, the database is the shared resource. With these explanations, the following design can be imagined:
假设我们想要设计适当的基础结构来与 SQL Server 数据库进行通信。为此,我们创建了 DbConnectionManager 类。在程序过程中,要使用这个类,我们只需要只有一个对象。在此方案中,数据库是共享资源。通过这些解释,可以想象出以下设计:

alt text
Figure 2.13: Database connection manager initial relation
图 2.13.. 数据库连接管理器初始关系

According to Figure 2.13, the following code could be considered:
根据图 2.13,可以考虑以下代码:

public class DbConnectionManager
{
    private DbConnectionManager() { }
    public static DbConnectionManager GetInstance() => new();
}

As you can see in Figure 2.13 class diagram and preceding code, the constructor of the class is defined with a private access modifier. The reason for this is to remove access to the constructor from outside the class. Because when the constructor of the class is called, an object must be returned, which can ruin the design. Therefore, to prevent this from happening, access to the class’s constructor is limited. Within the class, the GetInstance method is defined as public static. So, it is possible to access this method outside the class without creating an object of the desired class type. In fact, this method will be responsible for presenting the object to the outside.

如图 2.13 类图和前面的代码所示,类的构造函数是使用 private 访问修饰符定义的。这样做的原因是从类外部删除对构造函数的访问。因为当调用类的构造函数时,必须返回一个对象,这可能会破坏设计。因此,为了防止这种情况发生,对类的构造函数的访问受到限制。在该类中,GetInstance 方法定义为 public static。因此,可以在类外部访问此方法,而无需创建所需类类型的对象。实际上,此方法将负责将对象呈现给外部。

The preceding code still does not cover the main requirement, which is to have only one object. Because every time the GetInstance method is called, a new object is always created and returned. The following code shows how to use this class and the existing problem:

前面的代码仍然没有涵盖主要要求,即只有一个对象。因为每次调用 GetInstance 方法时,总是会创建并返回一个新对象。下面的代码演示如何使用此类和现有问题:

DbConnectionManager obj1 = DbConnectionManager.GetInstance();

DbConnectionManager obj2 = DbConnectionManager.GetInstance();

Console.WriteLine($"obj1: {obj1.GetHashCode()}, obj2: {obj2.GetHashCode()}");

Output:
输出:

obj1: 58225482, obj2: 54267293

As shown in the preceding code and its corresponding output, two different objects have been created. To prevent this, we need to slightly change the GetInstance method:
如前面的代码及其相应的输出所示,已经创建了两个不同的对象。为了防止这种情况,我们需要稍微更改 GetInstance 方法:

  • The first step of the change is to return the object directly to the user after it is created and save it in a variable, and
    更改的第一步是在创建对象后直接将对象返回给用户,并将其保存在变量中,
  • The second step is to check before creating the object; if this variable contains the object, no more new objects will be created.
    第二步是在创建对象之前进行检查;如果此变量包含对象,则不会再创建新对象。
  • Therefore, the class diagram and code will change as the following:
    因此,类图和代码将更改如下:

alt text
Figure 2.14: Singleton design pattern UML diagram
图 2.14: 单例设计模式 UML 图

According to Figure 2.14, the following code could be considered:

根据图 2.14,可以考虑以下代码:

public class DbConnectionManager
{
    private static DbConnectionManager _instance;
    private DbConnectionManager() { }
    public static DbConnectionManager GetInstance()
    {
        if (_instance == null)
        _instance = new();
        return _instance;
    }
}

Now if we run these codes again, we have the following output:
现在,如果我们再次运行这些代码,我们将得到以下输出:

obj1: 58225482, obj2: 58225482

As you can see, only one object of DbConnectionManager class is created and available. This way of implementing the singleton design pattern is suitable for single and multi-thread environments. This implementation needs changes, and this design pattern should be implemented in a thread-safe manner. If this design pattern is implemented using the early initialization method, the problem raised for multi-thread environments will not exist, and the desired object will be created for each AppDomain.

如您所见,仅创建一个 DbConnectionManager 类的对象且可用。这种实现单例设计模式的方式适用于单线程和多线程环境。此实现需要更改,并且此设计模式应以线程安全的方式实现。如果此设计模式是使用早期初始化方法实现的,则为多线程环境引发的问题将不存在,并且将为每个 AppDomain 创建所需的对象。

Before dealing with the implementation of thread Safe, let us first observe the problem by simulating the multi-thread environment:
在处理线程 Safe 的实现之前,我们先通过模拟多线程环境来观察问题:

Parallel.Invoke(() =>
    {
    DbConnectionManager obj1 = DbConnectionManager.GetInstance();
    Console.WriteLine($"obj1: {obj1.GetHashCode()}");
    }, () =>
        {
        DbConnectionManager obj2 = DbConnectionManager.GetInstance();
        Console.WriteLine($"obj2: {obj2.GetHashCode()}");
        });

The Invoke method in the Parallel class tries to perform the provided tasks in parallel. By running the preceding code, we can see the output as follows:
Parallel 类中的 Invoke 方法尝试并行执行提供的任务。通过运行上述代码,我们可以看到如下输出:

obj2: 6044116

obj1: 27252167

Pay attention that these outputs may be different on different machines. The preceding output shows that the first obj2 is prepared, and then obj1 is prepared. Since these two objects have different HashCodes, we can conclude that even though the singleton pattern is used, in the multi-thread environment, instead of just one object, we are faced with several objects. Now, to solve this problem, as mentioned before, we need to implement Singleton as thread Safe. There are different ways to implement Singleton as thread Safe. We do this using the lock block. For this purpose, the DbConnectionManager class is changed as follows:

请注意,这些输出在不同计算机上可能有所不同。从上述输出可以看出,先准备了第一个 obj2,然后准备了 obj1。由于这两个对象具有不同的 HashCodes,我们可以得出结论,即使使用了单例模式,在多线程环境中,我们面对的不仅仅是一个对象,而是多个对象。现在,为了解决这个问题,如前所述,我们需要将 Singleton 实现为 thread Safe。有多种方法可以将 Singleton 实现为 thread Safe。我们使用 lock 块来做到这一点。为此,DbConnectionManager 类更改如下:

public class DbConnectionManager
{
    private static DbConnectionManager _instance;
    private static object locker = new();
    private DbConnectionManager() { }
    public static DbConnectionManager GetInstance()
    {
        lock (locker)
            {
                if (_instance == null)
                _instance = new();
                return _instance;
            }
    }
}

In the preceding code, by using the lock block, the first thread that launches the method takes over the locker resource and enters the lock block. The remaining threads that enter the method, because the locker resource is not available are stopped so that the previous thread releases the resource. Immediately after releasing the resource, the next thread receives the resource and enters the lock block. The problem of not being thread-safe is solved, but the code still can be improved. The important problem of this implementation method is efficiency.

在上面的代码中,通过使用 lock 块,启动该方法的第一个线程将接管 locker 资源并进入 lock 块。由于 locker 资源不可用,因此进入该方法的其余线程将停止,以便前一个线程释放资源。释放资源后,下一个线程立即收到资源并进入 lock 块。线程不安全的问题得到了解决,但代码仍然可以改进。这种实现方法的重要问题是效率。

For example, thread number 1 enters the GetInstance method and receives the locker. Thread number 2 enters and stops behind the lock block. Thread number 1 creates the object and exits the block, and thread number 2 enters the lock block. Meanwhile, thread number 3 enters the GetInstance method, and since the locker is owned by thread number 2, it must wait. But the waiting is useless, and thread number 3 can directly receive the object and return it. With this explanation, the preceding code can be optimized as follows:

例如,线程 1 输入 GetInstance 方法并接收保险箱。线程 2 进入并停止在锁块后面。线程 1 创建对象并退出块,线程 2 进入 lock 块。同时,线程 3 进入 GetInstance 方法,并且由于锁由线程 2 拥有,因此它必须等待。但是等待没用,线程 3 可以直接接收对象并返回。通过此说明,可以按如下方式优化上述代码:

public static DbConnectionManager GetInstance()
{
    if (_instance != null)
    return _instance;
    lock (locker)
    {
        if (_instance == null)
        _instance = new();
        return _instance;
    }
}

This method is called double-check locking and can improve overall performance.
此方法称为 double-check locking,可以提高整体性能。

The second method to implement thread-safe is to mark the GetInstance method as Synchronized. Using this method, the GetInstance method is implemented as follows:
实现线程安全的第二种方法是将 GetInstance 方法标记为 Synchronized。使用此方法,GetInstance 方法的实现方式如下:

[MethodImpl(MethodImplOptions.Synchronized)]
public static DbConnectionManager GetInstance()
{
    if (_instance == null)
    _instance = new();
    return _instance;
}

This method has an important difference compared to the lock method; that is, only one thread is allowed to enter the method. The object is protected by using a lock, and the whole method is protected by using a synchronized method.

与 lock 方法相比,此方法具有重要区别;也就是说,只允许一个线程进入该方法。使用锁保护对象,并使用同步方法保护整个方法。

Apart from the preceding methods, there are other methods, such as using Lazy or Monitor class to implement a thread-safe singleton.
除了上述方法外,还有其他方法,例如使用 Lazy 或 Monitor 类来实现线程安全的单例。

Participants:
参与者:

  • Singleton: In the preceding scenario, it is the DbConnectionManager that is responsible for defining the object creation process. This class should always try to create only one object of the target class.
    Singleton:在前面的场景中,DbConnectionManager 负责定义对象创建过程。此类应始终尝试仅创建目标类的一个对象。

Notes:

  • By using this design pattern, in the object creation process, any changes which are needed can be applied by applying changes in the GetInstance method. For example, by having this structure, it is possible to provide conditions so that there are always two objects from a class. To implement this logic, it is enough to change the GetInstance method.
    通过使用这种设计模式,在对象创建过程中,可以通过在 GetInstance 方法中应用更改来应用所需的任何更改。例如,通过具有此结构,可以提供条件,以便一个类中始终有两个对象。要实现此逻辑,只需更改 GetInstance 方法即可。

  • In using this design pattern, you should pay attention to its difference from static classes. Among the most important differences between the static class and the use of the singleton design pattern, we can mention the impossibility of using static classes in inheritance, which makes static classes not extensible.
    在使用此设计模式时,应注意它与 static 类的区别。在 static 类和使用 singleton 设计模式之间最重要的区别中,我们可以提到在继承中使用 static 类是不可能的,这使得 static 类不可扩展。

  • Abstract factory, factory method, and builder design patterns can be implemented using Singleton.
    抽象工厂、工厂方法和构建器设计模式可以使用 Singleton 实现。

  • The initialization process can be implemented in both Lazy and Early. The difference between these two methods is when the desired object is initialized. In the Lazy method, the object is initialized when it is needed. In the Early method, the object is initialized when the class is loaded. The lazy method is used in the preceding scenario. To implement the early method, you can do the following:
    初始化过程可以在 Lazy 和 Early 中实现。这两种方法之间的区别在于初始化所需对象的时间。在 Lazy 方法中,对象在需要时初始化。在 Early 方法中,对象在加载类时初始化。在前面的场景中使用了 lazy 方法。要实现早期方法,您可以执行以下作:

    public class DbConnectionManager
    {
    private static DbConnectionManager _instance = new();
    private DbConnectionManager() { }
    public static DbConnectionManager GetInstance() => _instance;
    }

Consequences: Advantages
后果:优势

  • It can be ensured that there is one object in the class.
    可以确保类中有一个对象。

  • It is possible to access the object. Since a method is responsible for providing the object, it is possible to have better control over who requested the object and when.
    可以访问该对象。由于方法负责提供对象,因此可以更好地控制请求对象的人员和时间。

Consequences: Disadvantages
后果:缺点

  • The implementation of the thread-safe structure can be complicated, and this task can affect performance due to the use of locking mechanisms.
    线程安全结构的实现可能很复杂,并且由于使用锁定机制,此任务可能会影响性能。

  • Writing unit tests can be complicated.
    编写单元测试可能很复杂。

Applicability:
适用性:

  • When we need only one object of the class to be available to the user.
    当我们只需要 class 的一个对象可供用户使用时。
  • When we need to have more control over the object and its data content, we can use this design pattern instead of using Global Variables.
    当我们需要对对象及其数据内容进行更多控制时,我们可以使用这种设计模式而不是使用 Global Variables。

Related patterns:
相关模式:
Some of the following design patterns are not related to the Singleton design pattern, but to implement this design pattern, checking the following design patterns will be useful:
以下一些设计模式与 Singleton 设计模式无关,但要实现此设计模式,检查以下设计模式将很有用:

  • Facade
  • Abstract Factory
  • Factory Method
  • Builder
  • Prototype
  • Flyweight
  • Observer
  • State

Conclusion

总结

In this chapter, you got acquainted with various creational design patterns and learned how to manage the initialization of objects according to different conditions.
在本章中,您熟悉了各种创建设计模式,并学习了如何根据不同条件管理对象的初始化。

In the next chapter, you will learn about structural design patterns and learn how to manage relationships between classes and objects.
在下一章中,您将了解结构设计模式,并学习如何管理类和对象之间的关系。

NET 7 Design Patterns In-Depth 1. Introduction to Design Patterns

Chapter 1 Introduction to Design Patterns

Introduction

简介

One of the problems in understanding and using design patterns is the need for proper insight into software architecture and the reason for using design patterns. When this insight does not exist, design patterns will increase complexity. As they are not used in their proper place, the use of design patterns will be considered a waste of work. The reason for this is that the design patterns will not be able to have a good impact on quality because they need to be placed in the right place.

理解和使用设计模式的问题之一是需要正确了解软件体系结构以及使用设计模式的原因。当这种洞察力不存在时,设计模式将增加复杂性。由于它们没有在适当的位置使用,因此使用设计模式将被视为浪费工作。这样做的原因是,设计模式无法对质量产生好的影响,因为它们需要放在正确的位置。

In this chapter, an attempt has been made to briefly examine the software architecture and design patterns. The enterprise applications architecture has been introduced, and the relationship between software design problems and design patterns has been clarified. In the rest of the chapter, a brief look at .NET, some object-oriented principles, and the UML is given because, throughout the book, UML is used for modeling, and the .NET framework and C# language are used for sample codes.
在本章中,我们尝试简要地研究了软件体系结构和设计模式。介绍了企业应用程序体系结构,并阐明了软件设计问题和设计模式之间的关系。在本章的其余部分,简要介绍了 .NET、一些面向对象的原则以及 UML,因为在整本书中,UML 用于建模,而 .NET 框架和 C# 语言用于示例代码。

Structure

结构

In this chapter, we will cover the following topics:
在本章中,我们将介绍以下主题:

  • What is software architecture
  • What are design patterns
  • GoF design patterns
  • Enterprise application and its design patterns
    • Different types of enterprise applications
  • Design patterns and software design problems
    • Effective factors in choosing a design pattern
  • .NET
    • Introduction to object orientation in .NET
  • Object orientation SOLID principles
  • UML class diagram
  • Conclusion

Objectives

目标

By the end of this chapter, you will be able to understand the role and place of design patterns in software design, be familiar with software architecture, and evaluate software design problems from different aspects. You are also expected to have a good view of SOLID design principles at the end of this chapter and get to know .NET and UML.

通过本章的结尾,您将能够理解设计模式在软件设计中的作用和地位,熟悉软件架构,并从不同方面评估软件设计问题。在本章的末尾,您还应该对 SOLID 设计原则有一个很好的了解,并了解 .NET 和 UML。

What is software architecture

什么是软件架构

Today, there are various definitions for software architecture. The system’s basic structure, related to design decisions, must be made in the initial steps of software production. The common feature in all these definitions is their importance. Regardless of our attitude towards software architecture, we must always consider that suitable architecture can be developed and maintained. Also, when we want to look at the software from an architectural point of view, we must know what elements and items are of great importance and always try to keep those important elements and items in the best condition.

今天,软件架构有多种定义。系统的基本结构与设计决策相关,必须在软件生产的初始步骤中制定。所有这些定义的共同特征是它们的重要性。无论我们对软件架构的态度如何,我们都必须始终考虑可以开发和维护合适的架构。此外,当我们想从架构的角度来看软件时,我们必须知道哪些元素和项目非常重要,并始终尝试使这些重要的元素和项目处于最佳状态。

Consider software that needs to be better designed, and its essential elements must be identified. During the production and maintenance of this software, we will need help with various problems, including implementing changes, which will reduce the speed of providing new features and increase the volume of software errors and bugs. For example, pay attention to the following figure:

考虑需要更好设计的软件,并且必须确定其基本元素。在该软件的制作和维护过程中,我们将需要帮助解决各种问题,包括实施更改,这将降低提供新功能的速度并增加软件错误和错误的数量。例如,请注意下图:

alt text

Figure 1.1: An example of software without proper architecture
图 1.1: 没有适当架构的软件示例

In the preceding figure, full cells are the new features provided, and empty cells are the design and architectural problems and defects.

在上图中,full cells 是提供的新功能,而 empty cells 是设计和体系结构问题和缺陷。

If we consider one row of Figure 1.1, the following figure will be seen:
如果我们考虑图 1.1 的一行,将看到下图:

alt text

Figure 1.2: Sample feature delivery in software without proper architecture
图 1.2: 没有适当架构的软件中的功能交付示例

We see how much time it takes to provide three different features. If the correct design and architecture were adopted, new features would be delivered more quickly. The same row could be presented as the following figure:

我们了解提供三种不同功能需要多少时间。如果采用正确的设计和架构,新功能将更快地交付。同一行可以显示为下图:

alt text

Figure 1.3: Sample feature delivery in software WITH proper architecture
图 1.3: 具有适当架构的软件中的示例功能交付

The difference in length in the preceding two forms (Figure 1.2 and Figure 1.3) is significant. This shows the importance of the right design and architecture in the software. A high-quality infrastructure in the short term may indicate that production speed decreases. This natural and high-quality infrastructure will show its effect in the long run.

前两种形式(图 1.2 和图 1.3)的长度差异很大。这表明了软件中正确设计和架构的重要性。短期内高质量的基础设施可能表明生产速度会降低。从长远来看,这种天然和高质量的基础设施将显示出其效果。

The following figure shows the relationship between Time and Output:
下图展示了 Time 和 Output 之间的关系:

alt text

Figure 1.4: Time-Output Relation in Software Delivery
图 1.4: 软件交付中的时间-输出关系

In Figure 1.4, at the beginning of the work, reaching the output with a low-quality Infrastructure is faster than with a high-quality Infrastructure. However, with the passage of time and the increase in the capabilities and complexity of the software, the ability to maintain and apply software change is accelerated with better quality infrastructure. This will reduce costs, increase user satisfaction, and improve maintenance.

在图 1.4 中,在工作开始时,使用低质量的 Infrastructure 比使用高质量的 Infrastructure 更快地达到输出。但是,随着时间的推移以及软件功能和复杂性的增加,维护和应用软件更改的能力会随着更高质量的基础设施而加速。这将降低成本、提高用户满意度并改善维护。

In this regard, Gerald Weinberg, the late American computer science scientist, has a quote that says,

在这方面,已故的美国计算机科学科学家杰拉尔德·温伯格 (Gerald Weinberg) 有一句话说:

“If builders-built buildings the way programmers wrote programs, then the first woodpecker that came along would destroy civilization.”

“如果建筑商按照程序员编写程序的方式建造建筑物,那么出现的第一只啄木鸟就会摧毁文明。”

Weinberg tried to express the importance of infrastructure and software architecture. According to Weinberg’s quote, paying attention to maintainability in the design and implementation of software solutions is important. Today, various principles can be useful in reaching a suitable infrastructure.

Weinberg 试图表达基础设施和软件架构的重要性。根据 Weinberg 的引述,在软件解决方案的设计和实施中注意可维护性很重要。今天,各种原则都有助于实现合适的基础设施。

Some of these principles are as follows:
其中一些原则如下:

  • Separation of concerns: Different software parts should be separated from each other according to their work.
    关注点分离:不同的软件部分应根据其工作情况相互分离。

  • Encapsulation: This is a way to restrict the direct access to some components of an object, so users cannot access state values for all the variables of a particular object. Encapsulation can hide data members, functions, or methods associated with an instantiated class or object. Users will have no idea how classes are implemented or stored, and the users will only know that the values are being passed and initialized (Data Hiding). Also, it would be easy to change and adapt to new requirements (ease of use) using Encapsulation.
    封装:这是一种限制对对象某些组件的直接访问的方法,因此用户无法访问特定对象的所有变量的状态值。封装可以隐藏与实例化的类或对象关联的数据成员、函数或方法。用户将不知道类是如何实现或存储的,用户只知道值正在传递和初始化(数据隐藏)。此外,使用 Capsulation 很容易更改和适应新的需求(易用性)。

  • Dependency inversion: High-level modules should not depend on low-level modules, and the dependence between these two should only happen through abstractions. To clarify the issue, consider the following example:
    We have two different times in software production: compile and run time. Suppose that in a dependency graph at compile-time, the following relationship exists between classes A, B, and C:
    依赖反转:高级模块不应该依赖低级模块,这两者之间的依赖关系只能通过抽象来实现。为了澄清这个问题,请考虑以下示例:
    我们在软件生产中有两个不同的时间:编译和运行时。假设在编译时的依赖关系图中,类 A、B 和 C 之间存在以下关系:

alt text

Figure 1.5: Relationship between A, B, and C in compile-time
图 1.5: 编译时 A、B 和 C 之间的关系

As you can see, at compile-time, A is directly connected to B to call a method in B, and the exact relationship is true for the relationship between B and C. This connection will be established in the same way during runtime as follows:

如你所见,在编译时,A 直接连接到 B 以调用 B 中的方法,并且 B 和 C 之间的关系正是如此。此连接将在运行时以相同的方式建立,如下所示:

alt text
Figure 1.6: Relationship between A, B, and C in runtime
图 1.6.. 运行时 A、B 和 C 之间的关系

The problem in this type of communication is that there is no loose coupling between A-B and B-C, and these parts are highly dependent on each other and cause problems in maintainability. To solve this problem, instead of the direct connection between A and B, we consider the connection at compile-time based on abstractions as shown in the following figure:

这种类型的通信的问题在于 A-B 和 B-C 之间没有松散的耦合,并且这些部分彼此高度依赖,并导致可维护性问题。为了解决这个问题,我们在编译时根据抽象来考虑连接,而不是 A 和 B 之间的直接连接,如下图所示:

alt text

Figure 1.7: Relationship between A, B, and C based on abstractions
图 1.7: 基于抽象的 A、B 和 C 之间的关系

In the prior connection, A depends on an abstraction from B at the compile-time, and B has implemented the corresponding abstraction. This change in communication type will ultimately remain the same as at runtime. But it will cause a loose coupling in the sense that the implementation of B can be changed without changing A.

在前面的连接中,A 依赖于编译时来自 B 的抽象,并且 B 已经实现了相应的抽象。通信类型的这种更改最终将与运行时相同。但它会导致松散耦合,因为 B 的实现可以在不改变 A 的情况下改变。

The communication during runtime in the prior mode is shown in the following figure:

previous 模式下运行时的通信如下图所示:

alt text
Figure 1.8: Relationship between A, B, and C based on abstractions in runtime
图 1.8: 运行时中基于抽象的 A、B 和 C 之间的关系

  • Explicit dependencies: Classes and methods must be honest with their users. For example, attribute X must have a correct value for a class to function properly. This condition can be applied through the class constructor, and objects that cannot be used can be prevented from being created.
    显式依赖项:类和方法必须对其用户诚实。例如,属性 X 必须具有正确的值,类才能正常工作。可以通过类构造函数应用此条件,并且可以阻止创建无法使用的对象。

  • Single responsibility: This principle is proposed in object-oriented design as one of the architectural principles. This principle is like the separation of concerns and states that an object must have one task and reason for the change.
    单一责任:此原则在面向对象设计中作为架构原则之一提出。此原则类似于关注点分离,并声明对象必须具有一个更改的任务和原因。

  • DRY: The behavior related to a specific concept should not be given in several places. Failure to comply with this principle will cause all the code to change its behavior, increasing the probability of errors and bugs.
    DRY:与特定概念相关的行为不应在多个地方给出。不遵守此原则将导致所有代码改变其行为,从而增加错误和错误的可能性。

  • Persistence ignorance: Different business models in data sources should be able to store regardless of the type. These models are often called Plain Old CLR Object (POCO)s in .NET. This is because the storage resource can change over time (for example, from SQL Server to Azure Cosmos DB), and this should not affect the rest of the sections. Some signs of violation of this principle can be introduced as the following:
    持久性无知:数据源中的不同业务模型应该能够存储,而不管类型如何。这些模型在 .NET 中通常称为普通旧 CLR 对象 (POCO)。这是因为存储资源可能会随时间而变化(例如,从 SQL Server 更改为 Azure Cosmos DB),这不应影响其余部分。违反此原则的一些迹象可以引入如下:

    Binding to a specific parent class
    绑定到特定的父类

    The requirement to implement a specific interface
    实现特定接口的要求

    Requiring the class to store itself (as in Active Record)
    要求类存储自身(如 Active Record 中)

    The presence of mandatory parametric constructors
    存在强制性参数构造函数

    The presence of virtual features in the class
    类中存在虚拟特征

    The presence of unique attributes related to storage technology
    存在与存储技术相关的独特属性

    The preceding cases are introduced as violations of the principle of persistence ignorance because these cases often create a dependency between models and storage technology, making it difficult to adapt to new storage technology in the future.
    上述情况违反了持久化无知原则,因为这些情况通常会在模型和存储技术之间产生依赖关系,使其将来难以适应新的存储技术。

  • Bounded contexts: A more significant problem can be divided into smaller conceptual sub-problems. In other words, each sub-problem represents a context that is independent of other contexts. Communication between different contexts is established through programming interfaces. Any communication or data source shared between contexts should be avoided, as it will cause tight coupling between contexts.
    有界上下文:更重要的问题可以划分为更小的概念子问题。换句话说,每个子问题都代表一个独立于其他上下文的上下文。不同上下文之间的通信是通过编程接口建立的。应避免在上下文之间共享任何通信或数据源,因为这会导致上下文之间紧密耦合。

What are design patterns

什么是设计模式

As can be seen from the title of the word "design pattern", it is simply a pattern that can be used to solve an upcoming problem. This means that the completed design is not a finished design that can be directly converted into source code or machine code. During the design and production of software, we face various problems in design and implementation, which are repetitive. Therefore, the answer to these often has a fixed format. For example, developing a feature to send messages to the end user may be necessary for software production. Therefore, a suitable infrastructure must be designed and implemented for this requirement. On the other hand, there are different ways to send messages to end users, such as via email, SMS, and so on. The mentioned problem has fixed generalities in most software, and the answer often has a fixed design and format.
从“设计模式”这个词的标题可以看出,它只是一种可以用来解决即将到来的问题的模式。这意味着完成的设计不是可以直接转换为源代码或机器代码的已完成设计。在软件的设计和生产过程中,我们在设计和实现中面临各种问题,这些问题是重复的。因此,这些问题的答案通常具有固定的格式。例如,开发一项功能以向最终用户发送消息对于软件生产可能是必要的。因此,必须针对此要求设计和实施合适的基础设施。另一方面,有多种方法可以向最终用户发送消息,例如通过电子邮件、SMS 等。上述问题在大多数软件中具有固定的通用性,而答案通常具有固定的设计和格式。

A design pattern is a general, repeatable solution to common problems in software design. Therefore, if we encounter a new issue during software production, there may be no pattern introduced for that, and we need to solve it without the help of existing practices. This needs to be solved by designing a correct structure.

设计模式是针对软件设计中常见问题的通用、可重复的解决方案。因此,如果我们在软件生产过程中遇到新的问题,可能没有引入任何模式,我们需要在没有现有实践帮助的情况下解决它。这需要通过设计正确的结构来解决。

Using design patterns has several advantages:
使用设计模式有几个优点:

  • Increasing scalability
    提高可扩展性

  • Increasing expandability
    提高可扩展性

  • Increased flexibility
    提高灵活性

  • Increase the speed of development
    提高开发速度

  • Reduce errors and problems
    减少错误和问题

  • Reducing the amount of coding
    减少编码量

The important thing about design patterns is that they are not a part of the architecture of software systems, and they only provide the correct method of object-oriented coding. You can choose and implement the right way to solve a problem.
设计模式的重要一点是,它们不是软件系统架构的一部分,它们只提供正确的面向对象编码方法。您可以选择并实施正确的方法来解决问题。

GoF design patterns

GoF 设计模式

In the past years, Christopher Alexander introduced the design pattern. He was an architect and used patterns to build buildings. This attitude and thinking of Alexander made Eric Gama use design patterns to develop and produce software in his doctoral dissertation. After a short period, Chard Helm started working with Eric Gama. Later, John Vlissides and Ralph Johnson also joined this group. The initial idea was to publish the design patterns as an article, and due to its length, the full text was published as a book. This four-person group, which is also called Gang of Four (GoF), published a book called “Elements of Reusable Object-Oriented Software”, and they classified and presented 23 different design patterns in the form of 3 different categories (structural, behavioral, and creational). They tried to categorize it from the user's perspective. To fully present this, the GoF group developed a general structure to introduce the design patterns, which consisted of the following sections:

在过去的几年里,Christopher Alexander 引入了设计模式。他是一名建筑师,使用图案来建造建筑物。Alexander 的这种态度和思考使 Eric Gama 在他的博士论文中使用设计模式来开发和生产软件。不久之后,Chard Helm 开始与 Eric Gama 合作。后来,John Vlissides 和 Ralph Johnson 也加入了这个团体。最初的想法是将设计模式作为一篇文章发布,由于它的长度,全文被作为一本书发布。这个四人小组,也被称为 Gang of Four (GoF),出版了一本名为《Elements of Reusable Object-Oriented Software》的书,他们以 3 个不同类别(结构、行为和创建)的形式分类和呈现了 23 种不同的设计模式。他们试图从用户的角度对其进行分类。为了充分呈现这一点,GoF 小组开发了一个通用结构来介绍设计模式,其中包括以下部分:

  • Name and Classification: It shows the design pattern's name and specifies each design pattern's category.
    Name and Classification:它显示设计模式的名称并指定每个设计模式的类别。

  • Also Known As: If other names know the design pattern, they are introduced in this section.
    也称为:如果其他名称知道设计模式,则本节将介绍它们。

  • Intent: This section gives brief explanations about the design pattern.
    Intent:本节提供有关设计模式的简要说明。

  • Motivation, Structure, Implementation, and Sample Code: A description of the problem, main structure, implementation steps, and the source code of design patterns are presented.
    动机、结构、实现和示例代码:提供了问题描述、主要结构、实现步骤和设计模式的源代码。

  • Participants: This section introduces and describes different participants (in terms of classes and objects involved) in the design pattern.
    参与者:本节介绍和描述设计模式中的不同参与者(根据涉及的类和对象)。

  • Notes: Significant points are given in this section regarding the design and implementation of each design pattern.
    注意:本节中给出了有关每个设计模式的设计和实现的重要要点。

  • Consequences: Advantages and disadvantages of the discussed design pattern are given.
    结果:给出了所讨论的设计模式的优缺点。

  • Applicability: Places, where the discussed design pattern can be helpful, are briefly stated.
    适用性:简要说明所讨论的设计模式可能有用的位置。

  • Related Patterns: The relationship of each design pattern with other design patterns is mentioned.
    Related Patterns:提到了每种设计模式与其他设计模式的关系。

The 23 presented patterns can be divided in the form of the following table in terms of scope (whether the pattern is applied to the class or its objects) and purpose (what the pattern does):
提供的 23 种模式可以按范围(模式是应用于类还是其对象)和目的(模式的作用)以下表的形式进行划分:

Behavioral Structural Creational
Class Interpreter、Template Method Class Adapter Factory Method
Object Chain of Responsibility、Command、Iterator、Mediator、Memento、Observer、State、Strategy、Visitor Object Adapter、Bridge、Composite、Decorator、Façade、Flyweight、Proxy Abstract Factory、Builder Prototype、Singleton

Table 1.1: Classification of GoF Design Patterns
表 1.1:GoF 设计模式的分类

Every design pattern has four essential features as follows:
每个设计模式都有四个基本特征,如下所示:

  • Name: Every template must have a name. The name of the design pattern should be such that the application, problem, or solution provided can be reached from the name of the design pattern.
    名称:每个模板都必须有一个名称。设计模式的名称应使所提供的应用程序、问题或解决方案可以从设计模式的名称中访问。

  • Problem: The problem indicates how the design pattern can be applied.
    问题:该问题指示如何应用设计模式。

  • Solution: It deals with the expression of the solution, the involved elements, and their relationships.
    解决方案:它处理解决方案的表达式、涉及的元素及其关系。

  • Consequences: It expresses the results, advantages, disadvantages, and effects of using the design pattern.
    后果:它表示使用设计模式的结果、优点、缺点和效果。

The relationship of all these 23 patterns can be seen in the following figure:

alt text

Figure 1.9: Relationships of GoF Design Patterns
图 1.9: GoF 设计模式的关系

The design patterns provided by GoF are not the only design patterns available. Martin Fowler has also introduced a series of other design patterns with a different look at software production problems called Patterns of Enterprise Application Architecture (PofEAA). He tried to introduce suitable solutions for everyday problems in producing enterprise software. Although there is a rea meter and criteria for using design patterns, a small software may need to use PofEAA design patterns. Martin Fowler has also divided the provided design patterns into different categories, which include the following:

GoF 提供的设计模式并不是唯一可用的设计模式。Martin Fowler 还引入了一系列其他设计模式,这些模式对软件生产问题有着不同的看法,称为企业应用程序架构模式 (PofEAA)。他试图为生产企业软件中的日常问题引入合适的解决方案。尽管使用设计模式有严格的标准和标准,但小型软件可能需要使用 PofEAA 设计模式。Martin Fowler 还将提供的设计模式分为不同的类别,其中包括:

  • Domain-logic patterns
    域逻辑模式
  • Data-source architectural patterns
    数据源架构模式
  • Object-relational behavioral patterns
    对象关系行为模式
  • Object-relational structural patterns
    对象关系结构模式
  • Object-relational metadata-mapping patterns
    对象关系元数据映射模式
  • Web presentation patterns
    Web 表示模式
  • Distribution patterns
    分布模式
  • Offline concurrency patterns
    脱机并发模式
  • Session-state patterns
    会话状态模式
  • Base patterns
    基本模式

In this chapter, an attempt has been made to explain GoF and PofEAA design patterns with a simple approach, along with practical examples.
在本章中,我们尝试用简单的方法解释 GoF 和 PofEAA 设计模式,并提供了实际示例。

Enterprise application and its design patterns

企业应用程序及其设计模式

People construct types of different applications. Each of these has its challenges and complexities. For example, in one software, concurrency issues may be significant and critical, and in another category, the complexity of data structures might be necessary. The term enterprise application or information systems refers to systems in which we face the complexity of data processing and storage. To implement this software, special design patterns will be needed to manage business logic and data. It is important to understand that a series of design patterns can be useful for different types of software. However, a series will also be more suitable for enterprise applications.

人们构建不同类型的应用程序。每一项都有其挑战和复杂性。例如,在一个软件中,并发问题可能是重大和关键的,而在另一个类别中,数据结构的复杂性可能是必要的。术语企业应用程序或信息系统是指我们面临数据处理和存储复杂性的系统。要实现此软件,需要特殊的设计模式来管理业务逻辑和数据。了解一系列设计模式对于不同类型的软件非常有用,这一点很重要。但是,系列也将更适合企业应用程序。

Among the most famous enterprise applications, we can mention accounting software, toll payment, insurance, customer service, and so on. On the other hand, software such as text processors, operating systems, compilers, and even computer games are not part of the enterprise application category.

在最著名的企业应用程序中,我们可以提到会计软件、通行费支付、保险、客户服务等。另一方面,文本处理器、作系统、编译器甚至计算机游戏等软件不属于企业应用程序类别。

The important characteristic of enterprise applications is the durability of data. This data may be stored in data sources for years. The reason for the durability of these data will be needed at different times in different parts of the program at different steps of the process. During the lifetime of the data, we may encounter small and significant changes in operating systems, hardware, and compilers. The volume of data we face in an enterprise application is often large, and different databases will often be needed for storing them.

企业应用程序的重要特征是数据的持久性。此数据可能会在数据源中存储数年。这些数据持久性的原因将在程序的不同时间、流程的不同步骤中需要。在数据的生命周期内,我们可能会遇到作系统、硬件和编译器的微小而重大的变化。我们在企业应用程序中面临的数据量通常很大,并且通常需要不同的数据库来存储这些数据。

When we have a lot of data and have to present it to the users, graphic interfaces and different pages will be needed. The users who use these pages are different from each other and have different knowledge levels of software and computers. Therefore, we will use different methods and procedures to provide users with better data.

当我们有大量数据并且必须将其呈现给用户时,将需要图形界面和不同的页面。使用这些页面的用户彼此不同,并且对软件和计算机的知识水平不同。因此,我们将使用不同的方法和程序为用户提供更好的数据。

Enterprise application often needs to communicate with other software. Each software may have its technology stack. However, we face different interaction, communication, and software integration methods. Even at the level of business analysis, each software may have different analyses for a specific entity, leading to the emergence of different data structures. From another point of view, business logic can be complex, and it is very important to organize these effectively and change them over time.

企业应用程序通常需要与其他软件通信。每个软件都可能有其技术堆栈。但是,我们面临着不同的交互、通信和软件集成方法。即使在业务分析层面,每个软件也可能对特定实体有不同的分析,从而导致出现不同的数据结构。从另一个角度来看,业务逻辑可能很复杂,有效地组织这些逻辑并随着时间的推移改变它们非常重要。

When the word enterprise application is used, a mentality arises that we are dealing with a big software. In reality, this is not correct. A small software can create more value than a large software for the end user. One of the ways to deal with a big problem is to break and divide it into smaller problems. When these smaller issues are solved, they will lead to the solution of the bigger problem. This principle is also true about large software.

当使用企业应用程序这个词时,就会产生一种心态,即我们正在处理一个大型软件。实际上,这是不正确的。小型软件可以比大型软件为最终用户创造更多价值。处理大问题的方法之一是将其分解并划分为较小的问题。当这些较小的问题得到解决时,它们将导致更大问题的解决。此原则也适用于大型软件。

Different types of enterprise applications

不同类型的企业应用程序

It should always be kept in mind that every enterprise application has its own challenges and complexities. Therefore, one solution can be generalized for types of enterprise applications. Consider the following two examples:

应始终牢记,每个企业应用程序都有自己的挑战和复杂性。因此,对于企业应用程序类型,可以通用化一种解决方案。请考虑以下两个示例:

Example 1: In an online selling software, we face many concurrent users. In this case, the proposed solution should have good scalability in addition to the effective use of resources so that with the help of hardware enhancement, the volume of incoming requests can increase the volume of supported concurrent users. In this type of software, the end user can efficiently work with it, so it will be necessary to design a web application that can run on most browsers.

示例 1:在在线销售软件中,我们面临许多并发用户。在这种情况下,除了有效利用资源外,所提出的解决方案还应具有良好的可扩展性,以便在硬件增强的帮助下,传入请求的数量可以增加支持的并发用户的数量。在这种类型的软件中,最终用户可以有效地使用它,因此有必要设计一个可以在大多数浏览器上运行的 Web 应用程序。

Example 2: We may face software in which the volume of concurrent users is low, but the complexity of the business is high. For these systems, more complex graphical interfaces will be needed, which is necessary to manage more complex transactions.

示例 2:我们可能遇到并发用户量较低但业务复杂性较高的软件。对于这些系统,将需要更复杂的图形界面,这对于管理更复杂的事务是必要的。

As evident in the preceding two examples, having a fixed architectural design for every type of enterprise software will not be possible. As mentioned before, the choice of architecture depends on the precise understanding of the problem.

从前面的两个例子中可以明显看出,不可能为每种类型的企业软件都采用固定的架构设计。如前所述,架构的选择取决于对问题的精确理解。

One of the important points in dealing with enterprise applications and their architecture is to pay attention to efficiency, which can be different among teams. One team may pay attention to the performance issues from the beginning, and another may prefer to produce the software first and then identify and fix performance issues by monitoring various metrics. At the same time, a team might use a combination of these two methods. Whichever method is used to improve performance, the following factors are usually important to address:

处理企业应用程序及其架构的重要一点是关注效率,这可能因团队而异。一个团队可能从一开始就关注性能问题,而另一个团队可能更愿意先生产软件,然后通过监控各种指标来识别和修复性能问题。同时,团队可能会结合使用这两种方法。无论使用哪种方法提高性能,通常都需要解决以下因素:

  • Response time: The time it takes to process a request and return the appropriate response to the user.
    响应时间:处理请求并将适当的响应返回给用户所需的时间。

  • Responsiveness: For example, suppose the user is uploading a file. The response rate will be better if the user can work with the software during the upload operation. Another mode is that the user has to wait while performing the upload operation. In this case, the response rate will be equal to the time rate.
    响应能力:例如,假设用户正在上传文件。如果用户可以在上传作期间使用该软件,则响应率会更好。另一种模式是用户在执行上传作时必须等待。在这种情况下,响应率将等于时间率。

  • Latency: The minimum time it takes to receive any response. For example, suppose we are connected to another system through Remote Desktop. The time it takes for the appropriate request and response to move through the network and reach us will indicate the delay rate.
    Latency(延迟):接收任何响应所需的最短时间。例如,假设我们通过 Remote Desktop 连接到另一个系统。适当的请求和响应通过网络到达我们所需的时间将指示延迟率。

  • Throughput: It specifies the amount of work that can be done in a certain period. For example, when copying a file, the throughput can be set based on the number of bytes copied per second. Metrics such as the number of transactions per second or TPS can also be used for enterprise applications.
    吞吐量:指定在一定时间内可以完成的工作量。例如,在复制文件时,可以根据每秒复制的字节数设置吞吐量。每秒事务数或 TPS 等指标也可用于企业应用程序。

  • Load: Specifies the amount of pressure on the system. For example, the number of online users can indicate Load. The load is often an important factor in setting up other factors. For example, the response time for ten users may be 1 second, and for 20 users, it may be 5 seconds.
    负载:指定系统上的压力大小。例如,在线用户数可以指示 Load (负载)。负载通常是设置其他因素的重要因素。例如,10 个用户的响应时间可能是 1 秒,而 20 个用户的响应时间可能是 5 秒。

    • Load sensitivity: A proposition through which the change of response time based on load is specified. For example, assume that system A has a response time of 1 second for several 10-20 users. System B also has a response time of 0.5 seconds for ten users, while if the number of users becomes 20, its response time increases to 2 seconds. In this case, A has less load sensitivity than B.
      负载敏感度:一个命题,通过该命题指定基于负载的响应时间变化。例如,假设系统 A 对几个 10-20 个用户的响应时间为 1 秒。系统 B 对 10 个用户的响应时间也是 0.5 秒,而如果用户数量变为 20 个,则其响应时间将增加到 2 秒。在这种情况下,A 的负载敏感度低于 B。
  • Efficiency: Performance divided by resources. A system with a TPS volume equal to 40 on 2 CPU cores has better efficiency than a system that brings a TPS volume equal to 50 with 6 CPU cores.
    效率:性能除以资源。在 2 个 CPU 内核上 TPS 卷等于 40 的系统比在 6 个 CPU 内核上将 TPS 卷等于 50 的系统效率更高。

  • Capacity of system: A measure that shows the maximum operating power or the maximum effective load that can be tolerated.
    系统容量:显示可以承受的最大运行功率或最大有效负载的度量。

  • Scalability: A measure that shows how efficiency is affected by increasing resources. Often, two vertical (Scale Up) and horizontal (Scale Out) methods are used for scalability.
    可扩展性:显示增加资源如何影响效率的度量。通常,使用两种垂直 (纵向扩展) 和水平 (横向扩展) 方法来实现可扩展性。

The critical point is that design decisions will not necessarily have similar effects on different efficiency factors. Usually, when producing enterprise applications, an effort is made to give higher priority to scalability. Because it can have a more significant effect on efficiency and will be easier to implement. In some situations, a team may prefer to increase the volume rate by implementing a series of complex tasks so they do not have to bear the high costs of purchasing hardware.

关键是,设计决策不一定会对不同的效率因素产生类似的影响。通常,在生成企业应用程序时,会努力提高可伸缩性的优先级。因为它可以对效率产生更显着的影响,并且更容易实施。在某些情况下,团队可能更愿意通过实施一系列复杂的任务来提高卷率,这样他们就不必承担购买硬件的高成本。

The PofEAA presented in this book is inspired by the patterns presented in the Patterns of Enterprise Applications Architecture book written by Martin Fowler. The following structure is used in presenting PofEAA patterns:

本书中介绍的 PofEAA 受到 Martin Fowler 撰写的 Patterns of Enterprise Applications Architecture 一书中介绍的模式的启发。以下结构用于呈现 PofEAA 模式:

  • Name and Classification: It shows the design pattern's name and specifies each design pattern's category.
    Name and Classification:它显示设计模式的名称并指定每个设计模式的类别。

  • Also Known As: If the design pattern is known by other names, they are introduced in this section.
    也称为:如果设计模式有其他名称,则本节将介绍它们。

  • Intent: In this section, brief explanations about the design pattern are given.
    意图:本节简要介绍了设计模式。

  • Motivation, Structure, Implementation, and Sample Code: A description of the problem, main structure, implementation steps, and the source code of design patterns are presented.
    动机、结构、实现和示例代码:提供了问题描述、主要结构、实现步骤和设计模式的源代码。

  • Notes: Regarding the design and implementation of each design pattern, significant points are given in this section.
    注意:关于每种设计模式的设计和实现,本节中给出了重要的要点。

  • Consequences: Advantages and disadvantages of the discussed design pattern are given.
    结果:给出了所讨论的设计模式的优缺点。

  • Applicability: Places, where the discussed design pattern can be helpful, are briefly stated.
    适用性:简要说明所讨论的设计模式可能有用的位置。

  • Related Patterns: The relationship of each design pattern with other design patterns is mentioned.
    相关模式:提到了每种设计模式与其他设计模式的关系。

Design patterns and software design problems

设计模式和软件设计问题

When we talk about software design, we are talking about the plan, map, or structural layout on which the software is supposed to be placed. During a software production process, various design problems need to be identified and resolved. This behavior exists in the surrounding world and in real life. For example, when we try to present a solution, it is in line with a specific problem. The same point of view is also valid in the software production process. As mentioned earlier, in a software production process, design patterns solve many different problems. In order to identify and apply a suitable design pattern and a working method for a problem, it is necessary to determine the relationship between the design patterns and the upcoming software problem in the first step. In order to better understand this relationship, you can pay attention to the following:

当我们谈论软件设计时,我们谈论的是应该放置软件的计划、地图或结构布局。在软件生产过程中,需要识别和解决各种设计问题。这种行为存在于周围的世界和现实生活中。例如,当我们尝试提出解决方案时,它与特定问题一致。同样的观点也适用于软件生产过程。如前所述,在软件生产过程中,设计模式解决了许多不同的问题。为了识别并应用适合问题的设计模式和工作方法,有必要在第一步中确定设计模式与即将到来的软件问题之间的关系。为了更好地理解这种关系,您可以注意以下几点:

1.
Finding the right objects: In the world of object-oriented programming, there are many different objects. Each contains a set of data and performs certain tasks. The things that the object can do are called the behavior of the object or its methods. In order to change the content of the data that the object carries, it is necessary to act through methods. One of the most important and most difficult parts of designing and implementing an object-oriented program is decomposing a system into a set of objects. This is difficult because this analysis requires the boundaries of encapsulation, granularity, dependence, flexibility, efficiency, and so on.
查找正确的对象:在面向对象编程的世界中,有许多不同的对象。每个 VPN 都包含一组数据并执行某些任务。对象可以执行的作称为对象的行为或其方法。为了更改对象携带的数据内容,必须通过方法进行作。设计和实现面向对象的程序最重要和最困难的部分之一是将系统分解为一组对象。这很困难,因为这种分析需要封装、粒度、依赖性、灵活性、效率等界限。
When a problem arises, there are different ways to transform the problem into an object-oriented design. One of the ways is to pay attention to the structure of the sentences, convert the nouns into classes, and present the verbs in the form of methods. For example, in the phrase:
当出现问题时,有多种方法可以将问题转换为面向对象的设计。其中一种方法是注意句子的结构,将名词转换为类,并以方法的形式呈现动词。例如,在短语中:
"A user can log in to the system by entering the username and password."
“用户可以通过输入用户名和密码来登录系统。”
"User" has the role of the noun in the sentence, and "login" is the verb of the sentence. Therefore, you can create a class called User, which has a method called Login as the following output:
“User” 在句子中具有名词的角色,“login” 是句子的动词。因此,您可以创建一个名为 User 的类,该类具有一个名为 Login 的方法,输出如下:

public class User {
  public void Login(/*Inputs*/) {}
}

Another way is to pay attention to the connections, tasks, and interactions and thereby identify the classes, methods, and so on. No matter what method is used, at the end of the design, we may encounter classes for which we need help finding an equivalent in the real world or business environment. Design patterns help in abstractions, and classes can be placed in their proper place and used. For example, the class used to implement the sorting algorithm may not be identified in the early stages of analysis and design, but different design patterns can be designed correctly and connected with the rest of the system.
另一种方法是关注连接、任务和交互,从而识别类、方法等。无论使用哪种方法,在设计结束时,我们都可能会遇到需要帮助在现实世界或业务环境中找到等效项的类。设计模式有助于抽象,并且可以将类放置在适当的位置并使用。例如,在分析和设计的早期阶段可能无法识别用于实现排序算法的类,但可以正确设计不同的设计模式并与系统的其余部分连接。

2.
Recognizing the granularity of objects: An object has a structure and can be accompanied by various details, and the depth of these details can be very high or low. This factor can affect the size and the number of objects. It is an important decision to decide what boundaries and limits the object structure should have. Design patterns can help form these boundaries and limits accurately.
识别对象的颗粒度:一个对象有一个结构,可以伴随着各种细节,这些细节的深度可以很高,也可以很低。此因素会影响对象的大小和数量。决定对象结构应具有哪些边界和限制是一个重要的决定。设计模式可以帮助准确地形成这些边界和限制。

3.
Knowing the interface of objects: The behavior of an object consists of the name, input parameters, and output type. These three components together form the signature of a behavior. The set of signatures provided by an object is called a connection or interface of the object. The object interface specifies under what conditions and in what ways a request can be sent to the object. These interfaces are required to communicate with an object, although having information about these does not mean having information about how to implement them. Being able to connect a request to the appropriate object and appropriate behavior at the time of execution is called dynamic binding.
了解对象的接口:对象的行为由名称、输入参数和输出类型组成。这三个组件共同构成了行为的特征。对象提供的签名集称为对象的连接或接口。对象接口指定在什么条件下以及以什么方式可以向对象发送请求。这些接口是与对象通信所必需的,尽管拥有有关这些接口的信息并不意味着拥有有关如何实现它们的信息。能够在执行时将请求连接到适当的对象和适当的行为称为动态绑定。

public class Sample {
  public int GetAge(string name){}
  public int GetAge(string nationalNo, string name){}
}

Mentioning a request at the time of coding does not mean connecting the request for implementation. This connection will happen at the time of execution, which expresses its dynamic binding. This provides the ability to replace objects with each other at runtime. This is called polymorphism in object orientation. Design patterns also help in shaping such communications and interactions. This design pattern assistance may happen, for example, by placing a constraint on the structure of classes.
在编码时提及请求并不意味着连接 request 以进行实现。这个连接将在执行时发生,这表示它的动态绑定。这提供了在运行时将对象相互替换的功能。这在面向对象中称为多态性。设计模式还有助于塑造此类通信和交互。例如,通过对类的结构施加约束,可以实现这种设计模式帮助。

4.
Knowing how to implement objects: Objects are created by instantiating from a class which leads to the allocation of memory to the internal data of the object. New classes can also be created as a subset or child of a class using inheritance. In this case, the child class will contain all the accessible data and behaviors of its parent class. If the definition of a class is necessary to leave the implementation of behavior to the children (abstract behavior), then the class can be defined as an abstract class. Since this class is only an abstraction, it cannot be instantiated. If a class is not abstract, then it is called a real or intrinsic class.
知道如何实现对象:对象是通过从类实例化来创建的,这会导致将内存分配给对象的内部数据。还可以使用继承将新类创建为类的子集或子类。在这种情况下,子类将包含其父类的所有可访问数据和行为。如果类的定义是必要的,以便将行为的实现留给子类(抽象行为),则可以将该类定义为抽象类。由于此类只是一个抽象,因此无法实例化。如果一个类不是抽象的,那么它被称为实类或内部类。

public abstract class Sample {}// Abstract class 抽象类
public class Sample {}// Intrinsic class 内部类
public abstract class Sample {
  public abstract void Get() ;//Abstract method 抽象方法
}

How the objects are instantiated, and classes are formed and implemented are very important points that should be paid attention to. Several design patterns are useful in these situations. For example, one design pattern may help to create static implementations for classes, and another design pattern may help define static structure.
如何实例化对象,如何形成和实现类是应该注意的非常重要的点。在这些情况下,有几种设计模式很有用。例如,一种设计模式可能有助于为类创建静态实现,而另一种设计模式可能有助于定义静态结构。

5.
Development based on interfaces: With the help of inheritance, a class can access the accessible behavior and data of the parent class and reuse them. Being able to reuse an implementation and having a group of objects with a similar structure are two different stories, which is very important and shows its importance in polymorphism. This usually happens with the help of abstract classes or interfaces.
基于接口的开发:在继承的帮助下,类可以访问父类的可访问行为和数据并重用它们。能够重用一个实现和拥有一组具有相似结构的对象是两个不同的故事,这非常重要,并显示了它在多态性中的重要性。这通常是在抽象类或接口的帮助下发生的。
The use of abstract classes and interfaces makes the user unaware of the exact type of object used in the class. Because the object adheres to the provided abstraction and interface. Also, users are unaware of the classes that implement these objects and only know the abstraction that created the class. This makes it possible to write code based on interfaces and abstractions.
使用抽象类和接口会使用户不知道类中使用的对象的确切类型。因为对象遵循提供的抽象和接口。此外,用户不知道实现这些对象的类,而只知道创建该类的抽象。这使得基于接口和抽象编写代码成为可能。
The main purpose of creational design patterns is to provide different ways to communicate between interfaces and implementations. This category of design patterns tries to provide this communication in an inconspicuous way at the time of instantiating.
创建性设计模式的主要目的是提供不同的方式来在接口和实现之间进行通信。此类别的设计模式尝试在实例化时以不显眼的方式提供此通信。

6.
Attention to reuse: Another important problem in software design and implementation is to benefit from reusability and provide appropriate flexibility to the codes. For example, you should pay attention to the differences between inheritance and composition and use each one in the right place. These two are one of the most widely used methods to provide code reusability. Using inheritance, one class can be implemented based on another class. Reusability, in this case, is formed in the form of a child class definition. This type of reuse is called White Box Reuse:
注意重用:软件设计和实现中的另一个重要问题是从可重用性中受益,并为代码提供适当的灵活性。例如,您应该注意 inheritance 和 composition 之间的区别,并在正确的地方使用它们。这两种是提供代码可重用性的最广泛使用的方法之一。使用继承,一个类可以基于另一个类实现。在这种情况下,可重用性以子类定义的形式形成。这种类型的重用称为 White Box Reuse:

public class Parent {
  public void Show_Parent(){}
}

public class Child: Parent { // Inheritance
  public void Show_Child(){}
}

On the other hand, Composition provides reusability by installing an object in a class and adding new functionality in that class. This type of reuse is also called Black Box Reuse:
另一方面,Composition 通过在类中安装对象并在该类中添加新功能来提供可重用性。这种类型的重用也称为黑盒重用:

public class Engine {
  public void Get(){}
}

public class Car {
  private Engine _engine;
  public Car(Enging engine)=>_engine = engine;//Composition
}

Both inheritance and composition structures have advantages and disadvantages that should be considered while using them. However, empirically, most programmers overuse inheritance in order to provide reusability, and this causes problems in code development. Using composition can be very helpful in many scenarios. By using delegation, you can give double power to composition. Today, there are other ways that help to reach a code with suitable reusability. For example, in a language like C#, there is a feature called Generic, which can be very useful in this direction. Generics are also called parametrized types. With all these explanations, a series of design patterns help to provide reusability and flexibility well in the code.
继承结构和组合结构都有优点和缺点,使用它们时应考虑这些优点和缺点。但是,从经验上讲,大多数程序员过度使用继承以提供可重用性,这会导致代码开发出现问题。在许多情况下,使用组合可能非常有用。通过使用委派,您可以为组合提供双倍的能力。今天,还有其他方法可以帮助获得具有适当可重用性的代码。例如,在像 C# 这样的语言中,有一个叫做 Generic 的功能,它在这个方向上可能非常有用。泛型也称为参数化类型。通过所有这些解释,一系列设计模式有助于在代码中很好地提供可重用性和灵活性。

6.
Design for change: It is a suitable and good design that can predict future changes and is not vulnerable to those changes. If the design cannot make a good prediction of the future, it should be ready to apply extensive changes in the future. One of the functions and advantages of design patterns is that it allows the design to be flexible to future changes.
为变化而设计:这是一种合适且良好的设计,可以预测未来的变化,并且不会受到这些变化的影响。如果设计不能对未来做出良好的预测,它应该准备好在未来应用广泛的更改。设计模式的功能和优点之一是它允许设计灵活地适应未来的变化。

Effective factors in choosing a design pattern

选择设计模式的有效因素

When first faced with a list of 23 GoF design patterns, it can be difficult to know which pattern to choose for a particular problem. This difficulty increases when we add the PofEAA design patterns to this list of 23 design patterns. It is enough to make the selection process difficult and confusing. In order to make a suitable choice, it is recommended to consider the following points:

当第一次面对 23 个 GoF 设计模式的列表时,可能很难知道为特定问题选择哪种模式。当我们将 PofEAA 设计模式添加到这个 23 种设计模式列表中时,这种难度会增加。这足以使选择过程变得困难和混乱。为了做出合适的选择,建议考虑以下几点:

  • Understanding the problem space and how the design pattern can solve the problem: The first step in choosing a design pattern is to identify the problem correctly. Once the problem becomes clear, think about how the presence of the design pattern can help the problem.
    了解问题空间以及设计模式如何解决问题:选择设计模式的第一步是正确识别问题。一旦问题变得清晰,就想想设计模式的存在如何帮助解决问题。

  • Examining the generalities of design patterns using the purpose and scope: By doing this review, you can understand the degree of compatibility of the problem ahead with the design patterns.
    使用目的和范围检查设计模式的通用性:通过进行此审查,您可以了解问题与设计模式的兼容性程度。

  • Examining the interconnections of design patterns: For example, if the Abstract Factory design pattern is to be used by combining Singleton with this pattern, only one instance of Abstract Factory can be created. In order to apply dynamics to it, a Prototype can be used.
    检查设计模式的互连:例如,如果要通过将 Singleton 与此模式组合来使用 Abstract Factory 设计模式,则只能创建一个 Abstract Factory 实例。为了对其应用动力学,可以使用 Prototype。

  • Examining the similarities and differences of each design pattern: For example, if the problem ahead is a behavioral problem, you can choose the appropriate behavioral pattern among all the behavioral patterns.
    检查每种设计模式的相似之处和不同之处:例如,如果前面的问题是行为问题,则可以在所有行为模式中选择合适的行为模式。

  • Knowing the reasons that lead to redesign: In this step, the factors that can cause redesign should be known.
    了解导致重新设计的原因: 在此步骤中,应了解可能导致重新设计的因素。

  • Knowing the design variables: In this step, you should understand what can be changed in the design.
    了解设计变量:在此步骤中,您应该了解设计中可以更改的内容。

When the appropriate design pattern is chosen, it should be implemented. In order to use and implement a design pattern, you must first study that pattern completely. In this study, the application cases and consequences of the model should be carefully studied and examined. After understanding the generalities of the pattern, the details should be examined, and these details ensure that we know the elements involved have sufficient information about the interactions between these elements.

当选择了适当的设计模式时,应该实现它。为了使用和实现设计模式,您必须首先完整地研究该模式。在本研究中,应仔细研究和检查该模型的应用案例和后果。在了解了模式的一般性之后,应该检查细节,这些细节确保我们知道所涉及的元素有足够的信息来了解这些元素之间的交互。

In the next step, the way to implement the design pattern will be examined by the existing code samples. Then, we will select the appropriate names for each of the involved elements, taking into account the problem and the business ahead. The choice of name should be made according to the purpose of each element in the upcoming business. After choosing the name, various classes, interfaces, and relationships are implemented. During the implementation, there may be a need to change the codes in different parts of the system. Choosing appropriate names for methods and their implementation are the next steps that should be considered while implementing a design pattern.

在下一步中,将通过现有的代码示例来研究实现设计模式的方法。然后,我们将考虑到问题和未来的业务,为每个涉及的元素选择合适的名称。名称的选择应根据即将到来的业务中每个元素的目的进行。选择名称后,将实现各种类、接口和关系。在实施过程中,可能需要更改系统不同部分的代码。为方法及其实现选择合适的名称是实现设计模式时应考虑的下一步。

alt text

Figure 1.10: Choosing Design Pattern Process
图 1.10.选择 Design Pattern Process(设计模式流程)

.NET

In 2002, Microsoft released .NET Framework, a development platform for creating Windows apps. Today .NET Framework is at version 4.8 and remains fully supported by Microsoft. In 2014, Microsoft introduced .NET Core as a cross-platform, open-source successor to .NET Framework. This new implementation of .NET kept the name .NET Core through version 3.1. The next version was named .NET 5. The new versions continue to be released annually, with each version number higher. They include significant new features and often enable new scenarios.

2002 年,Microsoft 发布了 .NET Framework,这是一个用于创建 Windows 应用程序的开发平台。目前,.NET Framework 的版本为 4.8,并且仍然受到 Microsoft 的完全支持。2014 年,Microsoft 推出了 .NET Core 作为 .NET Framework 的跨平台开源后继产品。此 .NET 的新实现在版本 3.1 之前一直保留名称 .NET Core。下一个版本被命名为 .NET 5。新版本每年都会继续发布,每个版本号都更高。它们包括重要的新功能,并且通常支持新方案。

There are multiple variants of .NET, each supporting a different type of app. The reason for multiple variants is part historical and technical.

.NET 有多种变体,每种变体都支持不同类型的应用程序。多个变体的原因部分是历史和技术方面的。

.NET implementations (historical order):
.NET 实现 (历史顺序):

  • .NET Framework: It provides access to the broad capabilities of Windows and Windows Server. Also extensively used for Windows-based cloud computing. The original .NET.
    .NET Framework:它提供对 Windows 和 Windows Server 的广泛功能的访问。也广泛用于基于 Windows 的云计算。原始 .NET.

  • Mono: A cross-platform implementation of .NET Framework. The original community and open-source .NET used for Android, iOS, and Wasm apps.
    Mono:.NET Framework 的跨平台实现。用于 Android、iOS 和 Wasm 应用程序的原始社区和开源 .NET。

  • .NET (Core): A cross-platform and open-source implementation of .NET, rethought for the cloud age while remaining significantly compatible with the .NET Framework. Used for Linux, macOS, and Windows apps.
    .NET(核心):.NET 的跨平台开源实现,针对云时代进行了重新思考,同时保持与 .NET Framework 的显著兼容性。用于 Linux、macOS 和 Windows 应用程序。

According to the Microsoft .NET website, it is a free, cross-platform, open-source developer for building many different types of applications. With .NET, you can use multiple languages, editors, and libraries to build for web, mobile, desktop, games, IoT, and more. You can write .NET apps in C#, F#, or Visual Basic. C# is a simple, modern, object-oriented, and type-safe programming language. F# is a programming language that makes it easy to write succinct, robust, and performant code. Visual Basic is an approachable language with a simple syntax for building type-safe, object-oriented apps.

根据 Microsoft .NET 网站,它是一个免费的、跨平台的开源开发人员,用于构建许多不同类型的应用程序。借助 .NET,您可以使用多种语言、编辑器和库来构建 Web、移动、桌面、游戏、IoT 等。您可以使用 C#、F# 或 Visual Basic 编写 .NET 应用程序。C# 是一种简单、现代、面向对象且类型安全的编程语言。F# 是一种编程语言,可以轻松编写简洁、可靠且高性能的代码。Visual Basic 是一种易于使用的语言,具有简单的语法,用于构建类型安全、面向对象的应用程序。

Whether you are working in C#, F#, or Visual Basic, the code will run natively on any compatible operating system. You can build many types of apps with .NET. Some are cross-platform and target a specific set of operating systems and devices.

无论您是使用 C#、F# 还是 Visual Basic,代码都可以在任何兼容的作系统上本地运行。您可以使用 .NET 构建多种类型的应用程序。有些是跨平台的,面向一组特定的作系统和设备。

.NET provides a standard set of base class libraries and APIs that are common to all .NET applications. Each app model can also expose additional APIs that are specific to the operating systems it runs on and the capabilities it provides. For example, ASP.NET is a cross-platform web framework that provides additional APIs for building web apps that run on Linux or Windows.

.NET 提供了一组标准的基类库和 API,这些库和 API 是所有 .NET 应用程序通用的。每个应用程序模型还可以公开特定于其运行的作系统及其提供的功能的其他 API。例如,ASP.NET 是一个跨平台的 Web 框架,它提供其他 API 来构建在 Linux 或 Windows 上运行的 Web 应用程序。

.NET helps you develop high-quality applications faster. Modern language constructs like generics, Language Integrated Query (LINQ), and asynchronous programming make developers productive. Combined with the extensive class libraries, common APIs, multi-language support, and the powerful tooling provided by the Visual Studio family, it is the most productive platform for developers.

.NET 可帮助您更快地开发高质量的应用程序。泛型、语言集成查询 (LINQ) 和异步编程等现代语言结构使开发人员能够提高工作效率。结合 Visual Studio 系列提供的大量类库、通用 API、多语言支持和强大的工具,它是开发人员最高效的平台。

.NET 7, the successor to .NET 6, is Microsoft .NET’s latest version which is built for modern cloud-native apps, mobile clients, edge services, and desktop technologies. Creates mobile experiences using a single codebase without compromising native performance using .NET MAUI.

.NET 7 是 .NET 6 的继任者,是 Microsoft 。NET 的最新版本,专为现代云原生应用程序、移动客户端、边缘服务和桌面技术而构建。使用 .NET MAUI 使用单个代码库创建移动体验,而不会影响本机性能。

.NET apps and libraries are built from source code and project files using the .NET CLI or an Integrated Development Environment (IDE) like Visual Studio.

.NET 应用程序和库是使用 .NET CLI 或集成开发环境 (IDE)(如 Visual Studio)从源代码和项目文件构建的。

The following example is a minimal .NET app:
以下示例是一个最小的 .NET 应用程序:

Project file:
项目文件:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
  </PropertyGroup>
</Project>

Source Code:
源代码:

Console.WriteLine("Welcome to .NET 7 Design Patterns, in Depth!");

The app can be built and run with the .NET CLI:
可以使用 .NET CLI 构建和运行该应用程序:

% dotnet run

It can also be built and run as two separate steps. The following example is for an app that is named app:
它还可以作为两个单独的步骤构建和运行。以下示例适用于名为 app 的应用程序:

% dotnet build

% ./bin/Debug/net6.0/app

According to the Microsoft .NET website, new versions are released annually in November. .NET released in odd-numbered years are Long-Term Support (LTS) and are supported for three years. Versions that are released in even-numbered years are Standard-Term Support (STS) and are kept for 18 months. The quality level, breaking change policies, and all other aspects of the releases are the same. The .NET Team at Microsoft works collaboratively with other organizations such as Red Hat (for Red Hat Enterprise Linux) and Samsung (for Tizen Platform) to distribute and support .NET in various ways.

根据 Microsoft .NET 网站,新版本每年 11 月发布。奇数年发布的 .NET 是长期支持 (LTS),支持期限为三年。在偶数年发布的版本是标准期限支持 (STS),保留 18 个月。质量级别、中断性变更策略和版本的所有其他方面都是相同的。Microsoft 的 .NET 团队与其他组织合作,例如 Red Hat(用于 Red Hat Enterprise Linux)和 Samsung(用于 Tizen 平台),以各种方式分发和支持 .NET。

Introduction to object orientation in .NET

.NET中的面向对象简介

An object in the real world is a thing. For example, John's car, Paul's mobile, Sara's table, and so on are all objects in the real world. There is a similar view in the programming world where an object is a representation of something in the real world. For example, Tom's bank account in the financial software is the same representative of Tom's bank account in the real world. Dealing with the details of object orientation and object-oriented programming is beyond the scope of this chapter, but in the following, we will get to know some important concepts of object orientation.

现实世界中的对象是一个事物。例如,John 的汽车、Paul 的移动设备、Sara 的桌子等等都是现实世界中的对象。在编程世界中也有类似的观点,其中对象是现实世界中某物的表示。例如,Tom 在财务软件中的银行账户与现实世界中 Tom 的银行账户是同一个代表。处理面向对象和面向对象编程的细节超出了本章的范围,但在下文中,我们将了解面向对象的一些重要概念。

In the C# programming language, the class or struct keywords are used to define the type of an object that is actually the outline and format of the object. Object orientation has a series of main and fundamental concepts that are briefly discussed in the following:

在 C# 编程语言中,class 或 struct 关键字用于定义对象的类型,该类型实际上是对象的轮廓和格式。面向对象具有一系列主要和基本概念,下面将简要讨论这些概念:

Encapsulation: Deals directly with the data and methods associated with the object. By using encapsulation, we control access to data and methods and assert how the internal state of an object can be changed.

封装:直接处理与对象关联的数据和方法。通过使用封装,我们可以控制对数据和方法的访问,并断言如何更改对象的内部状态。

public class DemoEncap
{
  private int studentAge;

  // You can access the field only by using the following methods.
  //So, this field is encapsulated & access to it, is controlled
  public int Age
  {
    get => studentAge;
    set => studentAge = value;
  }
}

Composition: Describes what an object is made of. For example, a car consists of four wheels.
构图:描述对象的构成。例如,一辆汽车由四个轮子组成。

Aggregation: States what things can be mixed with the object. For example, a human is not part of a car, but a human can sit inside the car and try to drive.
聚合:说明哪些内容可以与对象混合。例如,人类不是汽车的一部分,但人类可以坐在车内并尝试驾驶。

Inheritance: By using inheritance, existing codes can be reused. This reuse happens in the form of defining a child class based on the parent class. In this case, all access methods and features of the parent class are available to the child class. Also, with the help of inheritance, you can develop the capabilities of the parent class. When using inheritance, two types of casting can occur.
继承:通过使用继承,可以重用现有代码。这种重用以基于父类定义子类的形式发生。在这种情况下,父类的所有访问方法和功能都可供子类使用。此外,在继承的帮助下,您可以开发父类的功能。使用继承时,可能会发生两种类型的强制转换。

Implicit casting: Means to store the child class object in a parent class variable
隐式强制转换:表示将子类对象存储在父类变量中

Explicit casting: In this type of casting, the type of destination should be stated explicitly. In this method, there is a possibility of an exception, so it is better to check whether Casting can be done or not by using the keyword before doing Casting.
显式强制转换:在这种类型的强制转换中,应显式说明目标的类型。在这种方法中,有出现异常的可能,所以最好在做 Casting 之前,先用关键词检查一下是否可以做 Casting。

Abstraction: By using abstraction, the main idea of the object is identified, and the details are ignored. The child classes have the chance to implement the details based on their own problem space. In C# language, you can use the abstract keyword to define an abstract class or method, which is usually considered as classes that continue to implement the introduced abstractions using the inheritance of child classes. The volume and extent of abstraction of a class are important points that should be taken into account. The more abstract the class, the more we can use it, but there will be less code to share.
抽象:通过使用抽象,可以识别对象的主体思想,忽略细节。子类有机会根据自己的问题空间实现细节。在 C# 语言中,可以使用 abstract 关键字定义抽象类或方法,该类或方法通常被视为使用子类的继承继续实现引入的抽象的类。类的抽象量和范围是应该考虑的重要点。类越抽象,我们可以使用它就越多,但要共享的代码会更少。

Polymorphism: By using polymorphism, the child class has the ability to change the implementation of its parent class. In order to change the parent class, the child class can change the implementation of the method using the override keyword in the C# programming language. In order for the implementation of the method to be changeable, the parent class must define the method as virtual. The members that are defined as abstract in the parent class will use the override keyword in the child class for implementation. If a method is defined in the parent class and a method with the same signature is defined in the child class, it is said that the process is hidden (Method Hiding). This type of inheritance is called non-polymorphic inheritance. In order to define this type of method, the new keyword can be used, although the use of this keyword is optional.
多态性:通过使用多态性,子类能够更改其父类的实现。为了更改父类,子类可以使用 C# 编程语言中的 override 关键字更改方法的实现。为了使方法的实现是可更改的,父类必须将方法定义为 virtual。在父类中定义为 abstract 的成员将使用子类中的 override 关键字进行实现。如果在父类中定义了方法,并且在子类中定义了具有相同签名的方法,则称该进程是隐藏的(方法隐藏)。这种类型的继承称为非多态继承。为了定义这种类型的方法,可以使用 new 关键字,尽管此关键字的使用是可选的。

P class A
{
  public void Print() => Console.WriteLine("I am Parent");
}

public class B: A
{
  public new void Print() => Console.WriteLine("I am Child");
}

When we are dealing with a large class, the implementation of the class can be written in several formats. In this case, the class is called partial. A class in C# can have different members, including the following:
当我们处理一个大型类时,类的实现可以用多种格式编写。在这种情况下,该类称为 partial。C# 中的类可以具有不同的成员,包括:

Field: The field is used to store data. Fields have three different categories:
字段:该字段用于存储数据。字段有三个不同的类别:
Constant: The data that is placed in these types of fields will never change, and the compiler copies the relevant data, where the constants are called.
常量:放置在这些类型的字段中的数据永远不会更改,编译器会复制调用常量的相关数据。

For example, consider the following code:
例如,请考虑以下代码:

public class A
{
  public const string SampleConst = ".NET Design Patterns";
}

public class B
{
  public B()
  {
    string test = A.SampleConst;
  }
}

After compiling the code, the compiler will generate the following code: (The generated IL code is captured by ILSpy software)
编译代码后,编译器会生成如下代码:(生成的 IL 代码被 ILSpy 软件捕获)

public class A
{
  public const string SampleConst = ".NET Design Patterns";
}

public class B
{
  public B()
  {
    string test =".NET Desig Patterns";
  }
}

As you can see, the compiler copies the value of SampleConst wherever the constant is used.
如您所见,编译器会在使用常量的位置复制 SampleConst 的值。
Read Only: The data in these types of fields cannot be changed after creating the object.
只读:创建对象后,无法更改这些类型字段中的数据。
Event: In these types of fields, the available data is actually a reference to one or more methods that are supposed to be executed when a specific event occurs.
事件:在这些类型的字段中,可用数据实际上是对一个或多个方法的引用,这些方法应该在特定事件发生时执行。
Method: These are used to execute expressions. The method defines and implements the expected behavior of the object. It has a name, input parameters, and output type. If two methods have the same name but different input parameters, they are said to be overloaded. Methods also have four different types:
方法:这些用于执行表达式。该方法定义并实现对象的预期行为。它具有名称、输入参数和输出类型。如果两个方法具有相同的名称但不同的输入参数,则称它们被重载。方法也有四种不同的类型:
Constructor: The constructor allocates memory to the object and initializes it. When the new keyword is used in the C# programming language, the associated constructor will be executed.
构造函数:构造函数为对象分配内存并对其进行初始化。在 C# 编程语言中使用 new 关键字时,将执行关联的构造函数。
Finalizer: These methods, also called destructors, are rarely used in the C# language. During execution, when an object is disposing and reclaiming memory, then these types of methods are executed.
终结器:这些方法也称为析构函数,在 C# 语言中很少使用。在执行期间,当对象释放和回收内存时,将执行这些类型的方法。

class Car
{
  ~Car() // finalizer
  {
    // cleanup statements...
  }
}

In the preceding code, the Finalizer implicitly calls the Finalize method in the Object class. So, calling Finalizer will result in calling the following manner:
在上面的代码中,Finalizer 隐式调用 Object 类中的 Finalize 方法。因此,调用 Finalizer 将导致以下方式调用:

protected override void Finalize()
  {
    try
  {
    // Cleanup statements...
  }
    finally
  {
    base.Finalize();
  }
}

Property: Statements in this type of method will be executed while setting or reading data. Behind the scenes of property, data is usually stored in Fields. There is no requirement for this purpose, and the data can be stored in an external data source or calculated during execution. Usually, the Property can be used for field encapsulation.
Property:在设置或读取数据时,将执行此类方法中的语句。在属性的幕后,数据通常存储在 Fields 中。没有此目的的要求,数据可以存储在外部数据源中或在执行期间进行计算。通常,Property 可用于字段封装。

  public string FirstName { get; set; }

Indexer: The expressions in this type of method are executed using “[]” indicator when setting or receiving data
索引器:在设置或接收数据时,此类方法中的表达式使用 “[]” 指示符执行

class StringDataStore
{
  private string[] strArr = new string[10]; // internal data storage
  public string this[int index]
  {
    get => strArr[index];
    set => strArr[index] = value;
  }
}

Operator: The expressions in this type of method are executed when operators like + are used on class operands.
运算符:当对类作数使用类似 + 的运算符时,将执行此类方法中的表达式。

  public static Box operator + (Box b, Box c) {
Box box = new Box();
box.length = b.length + c.length;
box.breadth = b.breadth + c.breadth;
box.height = b.height + c.height;
return box;
}

Apart from the preceding code, a class also contains an inner class:
除了前面的代码外,类还包含一个内部类:

 public class A{
public string GetName()=> $“Vahid is {new B().GetAge()} years old”;
private class B{
public int GetAge()=>10;
}
}

Regardless of the members of a class, part of encapsulation is to assign appropriate access levels to the class or its members. In C# language, there are different access levels which are:

无论类的成员如何,封装的一部分都是为类或其成员分配适当的访问级别。在 C# 语言中,有不同的访问级别,它们是:

  • Public: Members with this access level are available everywhere.
    公共:具有此访问级别的成员在任何地方都可用。

  • Private: Members with this access level are only available inside the class. This access level is the default for class members.
    Private:具有此访问级别的成员只能在类内使用。此访问级别是类成员的默认访问级别。

  • Protected: Members with this access level are only available inside the class, and inside classes are derived from this class.
    受保护:具有此访问级别的成员仅在类内部可用,并且内部类派生自此类。

  • Internal: Members with this access level are only available inside the same assembly.
    内部:具有此访问级别的成员仅在同一程序集中可用。

  • Internal protected: Members with this access level are available within the same class, assembly, or classes derived from this class. This access is internal or protected.
    Internal protected:具有此访问级别的成员在同一个类、程序集或从此类派生的类中可用。此访问权限是内部访问权限或受保护访问权限。

  • Private protected: Members with this access level are available within the same class or classes derived within the same assembly. This access is internal and protected.
    Private protected:具有此访问级别的成员在同一类或同一程序集中派生的类中可用。此访问权限是内部的,并且受到保护。

In addition to access levels, C# language also has a series of Modifiers through which you can slightly change the definition of the class or its members. For example, using sealed makes it impossible to inherit from a class or override a method. When a class is defined as closed, extension methods can be used to expand its capabilities.

除了访问级别之外,C# 语言还具有一系列修饰符,通过这些修饰符可以稍微更改类或其成员的定义。例如,使用 sealed 使得无法从类继承或重写方法。当类定义为 closed 时,可以使用扩展方法来扩展其功能。

When the class is defined statically, it is no longer possible to create an instance, and the class is always available to everyone. Also, the abstract is a modifier, when applied to a class, turns the class into an abstract class. When it is attributed to other members, such as methods, it eliminates the possibility of providing an implementation, and child classes are required to provide implementations.

当类是静态定义的时,就不再可能创建实例,并且该类始终可供所有人使用。此外,抽象是一个修饰符,当应用于类时,会将类转换为抽象类。当它归属于其他成员(如方法)时,它消除了提供实现的可能性,并且需要子类来提供实现。

Along with classes in C#, there are interfaces that are very similar to abstract classes. All members of interfaces are abstract. Among the similarities between the abstract class and interface, it can be mentioned that neither can be sampled. Along with all the similarities, they also have differences, including the following:

除了 C# 中的类外,还有一些与抽象类非常相似的接口。接口的所有成员都是抽象的。在抽象类和接口之间的相似之处中,可以提到两者都不能采样。除了所有相似之处外,它们也有不同之处,包括:

  • Interfaces can only inherit from interfaces, while abstract classes can inherit from other classes and implement different interfaces.
    接口只能继承自接口,而抽象类可以继承自其他类并实现不同的接口。

  • Abstract classes can include constructors and destructors, while this possibility is not available for interfaces
    抽象类可以包含构造函数和析构函数,但这种可能性不适用于接口

Since C# version 8, interfaces can have default implementations for methods, just like abstract classes.
从 C# 版本 8 开始,接口可以具有方法的默认实现,就像抽象类一样。

public interface IPlayable
{
  void Play();
  void Pause();
  void Stop() // default implementation 默认实现
  {
    WriteLine("Default implementation of Stop.");
  }
}

In fact, interfaces are a way to connect to each other. When a class implements an interface, it guarantees to provide a set of capabilities. The use of interfaces and abstract classes is very widely used in design patterns.
事实上,接口是一种相互连接的方式。当类实现接口时,它保证提供一组功能。接口和抽象类的使用在设计模式中得到了非常广泛的应用。

Object orientation SOLID principles

面向对象 SOLID 原则

C# programming language is an object-oriented language that provides good facilities for using object-oriented capabilities. Features such as the use of interfaces, inheritance, polymorphism, and so on. The fact that the C# programming language provides such facilities does not guarantee that every code written is by object-oriented principles and has an acceptable quality. Ideally, reaching an appropriate and correct object-oriented design in an extensive system will be challenging and require much scrutiny and precision.

C# 编程语言是一种面向对象的语言,它为使用面向对象的功能提供了良好的工具。功能,例如使用接口、继承、多态性等。C# 编程语言提供此类工具这一事实并不能保证编写的每段代码都遵循面向对象原则并具有可接受的质量。理想情况下,在一个广泛的系统中实现适当和正确的面向对象设计将具有挑战性,并且需要大量的审查和精确性。

Various principles have been introduced to produce the system according to the correct principles and guidelines of object orientation. One of these principles is the SOLID principle. SOLID actually consists of five different principles, which are:

已经引入了各种原则,以根据面向对象的正确原则和准则来生成系统。这些原则之一是 SOLID 原则。SOLID 实际上由五个不同的原则组成,它们是:

  • Single Responsibility Principle (SRP)
    单一责任原则 (SRP)

  • Open/Close Principle (OCP)
    开/关原则 (OCP)

  • Liskov Substitution Principle (LSP)
    里斯科夫替代原则 (LSP)

  • Interface Segregation Principle (ISP)
    接口分离原则 (ISP)

  • Dependency Inversion Principle (DSP)
    依赖关系倒置原则 (DSP)

The title SOLID also consists of the first letters of each of the preceding five principles. These principles help the written code to be of good quality and to maintain the code at an acceptable level. In the following, each of these principles is explained:
标题 SOLID 也由上述五个原则中每个原则的首字母组成。这些原则有助于编写的代码具有良好的质量,并将代码保持在可接受的水平。下面将解释这些原则中的每一个:

Single Responsibility Principle

单一责任原则

This principle states that each class should have only one task, which by nature will have one reason to change the class. When this principle is not followed, a class will contain a large amount of code to be changed if there is a need in the system. Making changes to this class will lead to the re-execution of the tests. On the other hand, by observing this principle, a big problem is divided into several smaller problems, and each issue is implemented in the form of a class. Therefore, making changes in the system will lead to making changes in one of these small classes, and it will only be necessary to run the tests related to this small class again. The principle of SRP is very similar to the principle in object orientation called SoC1.

该原则指出,每个类应该只有一个任务,而该任务本质上只有一个更改类的理由。如果不遵循此原则,如果系统有需要,一个类将包含大量需要更改的代码。对此类进行更改将导致重新执行测试。另一方面,通过遵守这个原则,一个大问题被分成几个小问题,每个问题都以类的形式实现。因此,在系统中进行更改将导致对其中一个小类进行更改,并且只需要再次运行与该小类相关的测试即可。SRP 的原理与面向对象的原理非常相似,称为 SoC1。

For example, consider the following code:
例如,请考虑以下代码:

public class WrongSRP
{

  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string Email { get; set; }
  public static List<WrongSRP> Users { get; set; } = new List<WrongSRP>();

  public void NewUser(WrongSRP User)
  {
  Users.Add(User);
  SendEmail(User.Email, "Account Created", "Your new account created");
  }

  public void SendEmail(string email, string subject, string body)
  {
  //Send email
  }
}

Suppose it is requested to design and implement a mechanism to create a new user. It is necessary to send an email after creating a user account. The preceding code has two methods called NewUser to create a new user and SendEmail to send an email. There are two different behaviors in the same class that are not directly related to each other. In other words, sending an e-mail is not directly related to the user entity, and the presence of this method in this class violates the SRP principle. Because this class is no longer responsible for only one task, and apart from managing user-related requests, it is also responsible for sending emails. The preceding design will cause the codes to change if the email-sending process changes. For example, the email service provider changes. In order to modify this code, the preceding code can be rewritten as follows:

假设请求设计和实现一种机制来创建新用户。创建用户帐户后,需要发送电子邮件。上述代码有两个方法,分别称为 NewUser 来创建新用户,另一个方法称为 SendEmail 来发送电子邮件。同一类中有两种不同的行为,它们彼此之间没有直接关系。换句话说,发送电子邮件与用户实体没有直接关系,并且此类中存在此方法违反了 SRP 原则。因为这个类不再只负责一个任务,除了管理与用户相关的请求外,它还负责发送电子邮件。如果电子邮件发送过程发生变化,上述设计将导致代码发生变化。例如,电子邮件服务提供商会发生变化。为了修改此代码,可以按如下方式重写上述代码:

public class SRP
{
  public string FirstName { get; set; }
  public string Email { get; set; }
  public string LastName { get; set; }
  public static List<WrongSRP> Users { get; set; } = new List<WrongSRP>();

  public void NewUser(WrongSRP User)
  {
  Users.Add(User);
  new EmailService()
  .SendEmail(User.Email,"Account Created","Your new account created");
  }
}

public class EmailService
{
  public void SendEmail(string email, string subject, string body)
  {
    //Send email
  }
}

As can be seen in the preceding code, the task of sending emails has been transferred to the EmailService class, and with this rewrite, the SRP principle has been respected, and it will not have the problems of the previous code.
从前面的代码中可以看出,发送邮件的任务已经转移到了 EmailService 类,通过这次重写,尊重了 SRP 原则,不会有之前代码的问题。

Open/Close Principal

开/关原则 (OCP)

This principle states that a class should be open for extension and closed for modification. In other words, when a class is implemented, and other parts of the system start using this class, it should not be changed. It is clear that making changes in this class can cause problems in the parts of the system. If there is a need to add new capabilities to the class, these should be added to it by expanding the class. In this case, the parts of the system that uses this class will not be affected by the applied changes, and in order to test new codes, only new parts will be needed to be tested.

此原则指出,类应为 open for extension,shut for modification。换句话说,当实现一个类,并且系统的其他部分开始使用这个类时,它不应该被改变。很明显,在此类中进行更改可能会导致系统的某些部分出现问题。如果需要向类添加新功能,则应通过扩展类来将这些功能添加到类中。在这种情况下,使用此类的系统部分将不会受到应用的更改的影响,并且为了测试新代码,只需要测试新部分。

For example, suppose you are asked to write a class to calculate employee salaries. In the initial plan of this requirement, it is stated that the working hours of all employees must be multiplied by 1000, and this way, salaries are calculated. With this explanation, the following code is written:

例如,假设您被要求编写一个类来计算员工工资。在此要求的初始计划中,规定所有员工的工作时间必须乘以 1000,这样就可以计算出工资。通过此说明,编写了以下代码:

public class WrongOCP
{
  public string Name { get; set; }
  public decimal CalculateSalary(decimal hours) => hours * 1000;
}

The preceding code has a method called CalculateSalary which calculates the salary of each person by receiving the working hours. After this code has been used for some time, it is said that a new type of employee called a manager has been defined in the system. For them, the working hours should be multiplied by 1500, and for others, it should be multiplied by 1000. Therefore, to cover this need, we change the preceding code as follows:

前面的代码有一个名为 CalculateSalary 的方法,它通过接收工作时间来计算每个人的工资。此代码使用一段时间后,据说系统中定义了一种称为经理的新型员工。对他们来说,工作时间应该乘以 1500,对其他人来说,应该乘以 1000。因此,为了满足这一需求,我们按如下方式更改了前面的代码:

public class WrongOCP
{
    public string Name { get; set; }
    public string UserType { get; set; }

    public decimal CalculateSalary(decimal hours)
    {
    if (UserType == "Manager")
    return hours * 1500;
    return hours * 1000;
    }
}

To add this new feature to the class, we changed the existing code, and this violates the OCP principle. By making these changes in the class, all parts of the system that use this class will be affected. To cover the requirement raised in the form of the original OCP, the preceding code can be rewritten as follows:

为了将这个新功能添加到类中,我们更改了现有代码,这违反了 OCP 原则。通过在类中进行这些更改,使用此类的系统的所有部分都将受到影响。为了涵盖以原始 OCP 形式提出的要求,可以按如下方式重写前面的代码:

public abstract class OCP
{
  protected OCP(string name) => Name = name;
  public string Name { get; set; }
  public abstract decimal CalculateSalary(decimal hours);
}

public class Manager : OCP
{
  public Manager(string name) : base(name) { }
  public override decimal CalculateSalary(decimal hours) => hours * 1500;
}

public class Employee : OCP
{
  public Employee(string name) : base(name) { }
  public override decimal CalculateSalary(decimal hours) => hours * 1000;
}

In the preceding code, if we want to add the role of a consultant, for example, it is enough to create a new class for the consultant and define the process of calculating his salary without touching the existing codes. With these words, new functionality is added without changing the current codes.

在上面的代码中,例如,如果我们想添加顾问的角色,只需为顾问创建一个新类并定义计算其薪水的过程就足够了,而无需触及现有代码。使用这些词,可以在不更改当前代码的情况下添加新功能。

Liskov Substitution Principle

里斯科夫替代原则 (LSP)

This principle states that the objects of the child class should be able to replace the parent class so there is no change in the final result. To make the matter clear, let us assume that we are asked to design an infrastructure through which the contents of various files can be read and written to these files. It is also stated that a message should be displayed to the user before reading and writing in text files. For this purpose, the following code can be considered:

该原则指出,子类的对象应该能够替换父类,因此最终结果没有变化。为了清楚地说明这个问题,让我们假设我们被要求设计一个基础设施,通过该基础设施,可以读取和写入各种文件的内容。还指出,在读取和写入文本文件之前,应向用户显示一条消息。为此,可以考虑以下代码:

public class FileManager
{
  public virtual void Read()=> Console.WriteLine("Reading from file...");
  public virtual void Write()=> Console.WriteLine("Writting to file...");
  }

  public class TextFileManager : FileManager
  {
    public override void Read()
  {
    Console.WriteLine("Reading text file...");
    base.Read();
  }
  public override void Write()
  {
    Console.WriteLine("Writting to text file...");
    base.Write();
  }
}

After some time, it is stated that the possibility of writing in XML files will be removed, and there is no need to present the writing behavior for XML files to the user. With these conditions, the preceding code changes as the following:

一段时间后,声明将消除写入 XML 文件的可能性,并且无需向用户提供 XML 文件的写入行为。在这些条件下,前面的代码将更改如下:

public class FileManager

{
  public virtual void Read() => Console.WriteLine("Reading from file...");
  public virtual void Write() => Console.WriteLine("Writting to file...");
}

public class TextFileManager : FileManager
  {
  public override void Read()
  {
  Console.WriteLine("Reading from text file...");
  base.Read();
  }

  public override void Write()
  {
  Console.WriteLine("Writting to text file...");
  base.Write();
  }
}

public class XmlFileManager : FileManager
{
  public override void Write()=> throw new NotImplementedException();
}

Now that the preceding class has been added for XmlFileManager, the following problem appears:
现在,已为 XmlFileManager 添加了前面的类,此时会出现以下问题:

FileManager fm = new XmlFileManager();
fm.Read();
fm.Write();// Runtime error 运行时错误

In the preceding code, when we want to call the Write method, we will encounter a NotImplementedException error, so it is not possible to replace the child class object, that is, the XmlFileManager class object, with the parent class object, that is, the FileManager class, and this replacement will change the final result. Because if we worked only with the parent class in the preceding code (FileManager fm = new FileManager()), a result would be obtained. In this case, the LSP principle is violated.

在上面的代码中,当我们要调用 Write 方法时,会遇到一个 NotImplementedException 错误,所以无法将子类对象(即 XmlFileManager 类对象)替换为父类对象(即 FileManager 类),而这种替换会改变最终的结果。因为如果我们只使用前面代码中的父类 (FileManager fm = new FileManager()),就会得到一个结果。在这种情况下,违反了 LSP 原则。

To modify the preceding structure, the code can be changed as the following:
要修改上述结构,可以按如下方式更改代码:

public interface IFileReader
{
  void Read();
}

public interface IFileWriter
{
  void Write();
}

public class FileManager : IFileReader, IFileWriter
{
  public void Read() => Console.WriteLine("Reading from file...");
  public void Write() => Console.WriteLine("Writting to file...");
}

public class TextFileManager : IFileReader, IFileWriter
{
  public void Read() => Console.WriteLine("Reading text file...");
  public void Write() => Console.WriteLine("Writting to text file...");
}

public class XmlFileManager : IFileReader
{
  public void Read() => Console.WriteLine("Reading from file...");
}

In the preceding code, two different interfaces called IFileReader and IFileWriter are introduced. Each class has implemented these interfaces according to its coverage level. Since there was no need to write the Xml files, this class only implemented IFileReader. According to the change in the preceding code, it can be used as the following:

在上面的代码中,引入了两个不同的接口,分别称为 IFileReader 和 IFileWriter。每个类都根据其覆盖率级别实现了这些接口。由于不需要编写 Xml 文件,因此此类仅实现 IFileReader。根据上述代码中的更改,可以按如下方式使用:

IFileReader xmlReader = new XmlFileManager();
xmlReader.Read();

As you can see, in the preceding code, the child class has replaced the parent class, and there has been no change in the result. In the prior structure, since XmlFileManager has not implemented the IFileWriter interface, there is no error or change in the final result. In this way, the LSP principle has been observed.

如您所见,在上面的代码中,子类已替换父类,结果没有变化。在前面的结构中,由于 XmlFileManager 尚未实现 IFileWriter 接口,因此最终结果中没有错误或更改。这样,就遵守了 LSP 原则。

Interface segregation principle

接口隔离原则

This principle states that users of an interface would not have to implement features and methods they do not need. Suppose we are implementing a payroll system. In order to calculate the salaries of employees, a series of attributes are considered for them and written as follows:

该原则指出,接口的用户不必实现他们不需要的功能和方法。假设我们正在实施一个工资单系统。为了计算员工的工资,考虑了一系列属性,并写成如下:

public interface IWorker
{
  public string Name { get; set; }
  public int MonthlySalary { get; set; }
  public int HourlySalary { get; set; }
  public int HoursInMonth { get; set; }
}

On the other hand, there are two types of employees in the system. Full-time and part-time employees. Salaries of full-time employees are calculated by adding 10% to MonthlySalary, and HourlySalary and HoursInMonth are useless for these employees. For part-time employees, salaries are calculated from the product of HourlySalary multiplied by HoursInMonth, and MonthlySalary is useless for this type of employee. To implement these types of employees, the following code is written:

另一方面,系统中有两种类型的员工。全职和兼职员工。全职员工的工资是通过在 MonthlySalary 上增加 10% 来计算的,HourlySalary 和 HoursInMonth 对这些员工毫无用处。对于兼职员工,工资是根据 HourlySalary 乘以 HoursInMonth 的乘积计算的,而 MonthlySalary 对这种类型的员工毫无用处。为了实现这些类型的员工,编写了以下代码:

public class FullTimeWorker: IWorker
{
    public string Name { get; set; }
    public int MonthlySalary { get; set; }
    public int HourlySalary {
    get => throw new NotImplementedException();
    set => throw new NotImplementedException();
    }

  public int HoursInMonth {
    get => throw new NotImplementedException();
    set => throw new NotImplementedException();
  }

  public int CalculateSalary()=>MonthlySalary+(MonthlySalary * 10 / 100);
}

public class PartTimeWorker : IWorker
{
  public string Name { get; set; }
  public int MonthlySalary {
  get => throw new NotImplementedException();
  set => throw new NotImplementedException();
    }

  public int HourlySalary { get; set; }
  public int HoursInMonth { get; set; }

  public int CalculateSalary() => HourlySalary * HoursInMonth;

}

As can be seen in the preceding code, the FullTimeWorker and PartTimeWorker classes have features that are useless for them, but since they need to implement the IWorker interface, these features are placed for them. Hence, the ISP principle is violated. In order to modify this structure, it is necessary to define smaller and more appropriate interfaces. Therefore, the following interfaces can be considered:

从前面的代码中可以看出,FullTimeWorker 和 PartTimeWorker 类具有对它们无用的功能,但由于它们需要实现 IWorker 接口,因此为它们放置了这些功能。因此,违反了 ISP 原则。为了修改此结构,有必要定义更小、更合适的接口。因此,可以考虑以下接口:

public interface IBaseWorker
{
  public string Name { get; set; }
  int CalculateSalary();
}

public interface IFullTimeWorker : IBaseWorker
{
  public int MonthlySalary { get; set; }
}

public interface IPartTimeWorker : IBaseWorker
{
  public int HourlySalary { get; set; }
  public int HoursInMonth { get; set; }
}

Then the FullTimeWorker and PartTimeWorker classes can be implemented as follows:
然后,可以按如下方式实现 FullTimeWorker 和 PartTimeWorker 类:

public class FullTimeWorke : IFullTimeWorker
{
  public string Name { get; set; }
  public int MonthlySalary { get; set; }
  public int CalculateSalary()=>MonthlySalary+(MonthlySalary * 10 / 100);
}

public class PartTimeWorker : IPartTimeWorker
{
  public string Name { get; set; }
  public int HourlySalary { get; set; }
  public int HoursInMonth { get; set; }
  public int CalculateSalary() => HourlySalary * HoursInMonth;
}

Now, the FullTimeWorker class has implemented the IFullTimeWorker interface. It does not need to provide its implementation for the HourlySalary and HoursInMonth features. The same condition is true for PartTimeWorker class and IPartTimeWorker interface. Therefore, with these changes, the ISP principle has been observed.

现在,FullTimeWorker 类已经实现了 IFullTimeWorker 接口。它不需要提供 HourlySalary 和 HoursInMonth 功能的实现。对于 PartTimeWorker 类和 IPartTimeWorker 接口,情况相同。因此,通过这些更改,已经遵守了 ISP 原则。

Dependency Inversion Principle

依赖关系反转原则

This principle states that high-level modules and classes should not depend on low-level modules and classes. In other words, a high-level module should not contain anything from a low-level module, and the bridge between these two modules should only be formed through abstractions. These abstractions should not be dependent on the details, and the details themselves should be dependent on the abstractions. In this way, the code written will be easily expandable and maintainable. For example, consider the following code:
该原则指出,高级模块和类不应依赖于低级模块和类。换句话说,高级模块不应包含来自低级模块的任何内容,并且这两个模块之间的桥梁只能通过抽象形成。这些抽象不应该依赖于细节,细节本身应该依赖于抽象。这样,编写的代码将易于扩展和维护。例如,请考虑以下代码:

public class User
{
  public string FirstName { get; set; }
  public string Email { get; set; }
  public static List<User> Users { get; set; } = new List<User>();
  public void NewUser(User user)
  {
    Users.Add(user);
    new EmailService()
    .SendEmail(user.Email,"Account Created","Your new account created");
  }
}

public class EmailService
{
  public void SendEmail(string email, string subject, string body)
  {
    //Send email
  }
}

In the preceding code, the high-level class User is dependent on the low-level class EmailService, and therefore the maintenance and development of this code always need help. With these specifications, DIP still needs to be met. In order to comply with DIP, the preceding code can be rewritten as the following:

在上面的代码中,高级类 User 依赖于低级类 EmailService,因此此代码的维护和开发始终需要帮助。对于这些规范,仍然需要满足 DIP。为了符合 DIP,可以将上述代码重写为以下内容:

public class User
{
  private readonly IEmailService _emailService;
  public string FirstName { get; set; }
  public string Email { get; set; }
  public static List<User> Users { get; set; } = new List<User>();
  public User(IEmailService emailService)=>this._emailService=emailService;
  public void NewUser(User user)
  {
    Users.Add(user);
    _emailService
    .SendEmail(user.Email,"Account Created","Your new account created");
  }
}

public interface IEmailService
{
  void SendEmail(string email, string subject, string body);
}

public class EmailService : IEmailService
{
  public void SendEmail(string email, string subject, string body)
  {
    //Send email
  }
}

In the preceding code, the User class is dependent on the IEmailService interface, and the EmailService class has also implemented this interface. In this way, while complying with DIP, code maintenance and development are improved.
在上面的代码中,User 类依赖于 IEmailService 接口,并且 EmailService 类也实现了此接口。这样,在遵守 DIP 的同时,代码维护和开发得到了改进。

UML class diagram

UML 类图

UML is a standard modeling language that consists of a set of diagrams. These diagrams help software developers to define software requirements, depict them and document them after construction. The diagrams in UML not only help software engineers during the software production process but also allow business owners and analysts to understand and model their needs more accurately.

UML 是一种由一组图组成的标准建模语言。这些图表可帮助软件开发人员定义软件需求、描述它们并在构建后记录它们。UML 中的图表不仅可以在软件生产过程中帮助软件工程师,还可以让企业主和分析师更准确地理解和建模他们的需求。

UML is very important in the development of object-oriented software, and for this, UML uses a series of graphical symbols. With the help of modeling UML, team members can talk about design and architecture with better and more accuracy and fix possible defects.

UML 在面向对象软件的开发中非常重要,为此,UML 使用一系列图形符号。在建模 UML 的帮助下,团队成员可以更好、更准确地讨论设计和架构,并修复可能的缺陷。

During the past years, UML has undergone various changes, which can be followed in the figure:

在过去的几年里,UML 发生了各种变化,如图所示:

alt text

Figure 1.11: UML versions
图 1.11. UML 版本

When UML is examined and studied, various diagrams can be seen. The reason for this diversity is that different people participate in the production process, and each person sees the product from a different angle according to their role in the team. For example, the use that a programmer makes of UML diagrams is very different from the use made by an analyst.

当检查和研究 UML 时,可以看到各种图表。这种多样性的原因是不同的人参与生产过程,每个人根据他们在团队中的角色从不同的角度看待产品。例如,程序员对 UML 图的使用与分析师对 UML 图的使用非常不同。

In a general classification, UML diagrams can be divided into two main categories:
在一般分类中,UML 图可以分为两大类:

  1. Structural diagrams: These diagrams show the static structure of the system along with different levels of abstraction and implementation and their relationship with each other. The following 7 are structural diagrams in UML:
    结构图:这些图显示了系统的静态结构以及不同级别的抽象和实现以及它们之间的关系。以下 7 个是 UML 中的结构图:
  • Class Diagram 类图
  • Component Diagram 组件图
  • Deployment Diagram 部署图
  • Object Diagram 对象图
  • Package Diagram 打包图
  • Composite Structure Diagram 复合结构图
  • Profile Diagram 轮廓图
  1. Behavioral diagrams: These diagrams show the dynamic behavior of objects in the system. This dynamic behavior can usually be displayed in the form of a series of changes over time. Types of behavioral charts are as follows:
    行为图: 这些图显示了系统中对象的动态行为。这种动态行为通常可以随时间推移的一系列变化的形式显示。行为图的类型如下:
  • Use Case Diagram 用例图
  • Activity Diagram 活动图
  • State Machine Diagram 状态机 图
  • Sequence Diagram 序列图
  • Communication Diagram 通信图
  • Interaction Overview Diagram 交互概述 图
  • Timing Diagram 时序图

Class diagram

类图

This diagram is one of the most popular and widely used UML diagrams. The class diagram describes the different types in the system and the static relationships between them. Also, with the help of this diagram, you can see the characteristics and behaviors of each class and even define limits on the relationship between classes. The following figure shows a class in a class diagram:
该图是最流行和最广泛使用的 UML 图之一。类图描述了系统中的不同类型以及它们之间的静态关系。此外,借助此图,您可以看到每个类的特征和行为,甚至可以定义类之间关系的限制。下图显示了类图中的一个类:

alt text
Figure 1 12: Class in a Class Diagram
图 12:类图中的类

As you can see, each class has a name (Class Name), some characteristics, and behaviors. Properties are given in the upper part of the class (prop1 and prop2). Behaviors are also given in the lower part (op1 and op2).
如您所见,每个类都有一个名称 (Class Name)、一些特征和行为。属性在类的上半部分(prop1 和 prop2)中给出。下半部分还给出了行为 (op1 和 op2)。

Characteristics in the class diagram are divided into two categories:
类图中的特征分为两类:

  • Attributes: This indicator presents the attribute in the form of a written text within the class, which is in the following format. In this format, only a name is required.
    Attributes:此指标在类中以书面文本的形式呈现属性,格式如下。在此格式中,只需要名称。
    visibility name : type multiplicity = default {property-string}
    For example, in the preceding class, the property called prop1 is defined. The access level of this property is private, and its type is an array of int.
    例如,在前面的类中,定义了名为 prop1 的属性。此属性的访问级别为 private,其类型为 int 数组。

  • Relationships: Another way to display features is to use the relationship indicator. Using this indicator, two classes are connected through a line. Relationships can be one-way or two-way.
    关系:显示特征的另一种方法是使用关系指示器。使用此指标,两个类通过一条线连接。关系可以是单向的,也可以是双向的。
    Behaviors are things that an object of the class should be able to do. The methods can be displayed in the following format in the class diagram:
    行为是类的对象应该能够执行的作。这些方法可以在类图中按以下格式显示:
    visibility name (parameter-list): return-type {property-string}
    For example, in the preceding class diagram, a method named op1 is defined with a public access level. Whose return type is Boolean. Also, an input parameter called param1 is defined for the op2 method.
    例如,在前面的类图中,名为 op1 的方法定义了一个 public 访问级别。其返回类型为 Boolean。此外,还为 op2 方法定义了一个名为 param1 的输入参数。

Each class diagram usually consists of several classes or interfaces and connections between them. There may be an inheritance relationship between classes. To show this type of relationship, Generalization is used:
每个类图通常由多个类或接口以及它们之间的连接组成。类之间可能存在继承关系。为了显示这种类型的关系,使用了泛化:

alt text
Figure 1.13: Generalization in Class Diagram
图 1.13.. 类图中的泛化

For example, the preceding diagram shows that Class2 inherits from Class1, so all the features and behaviors available to Class1 are also available to Class2.
例如,上图显示 Class2 继承自 Class1,因此 Class1 可用的所有功能和行为也可用于 Class2。

For another example, a class may use or depend on another class. To display this type of relationship, Dependency must be used. In this type of relationship, changes on the supplier side usually lead to changes on the client side. Classes can depend on each other for different reasons and types. One of the most used dependencies, which has been used many times in this chapter, is use:
再举一个例子,一个类可能使用或依赖于另一个类。要显示这种类型的关系,必须使用 Dependency 。在这种类型的关系中,供应商端的变化通常会导致客户端的变化。类可以由于不同的原因和类型而相互依赖。最常用的依赖项之一是 use:

alt text
Figure 1.14: Use relation in Class Diagram
图 1.14.. 在类图中使用关系

In the preceding figure, Class2 has the role of Supplier, and Class1 has the role of Client. According to the preceding diagram, Class1 is dependent on Class2 through the use of dependency. In other words, Class1 uses Class2.
在上图中,Class2 具有 Supplier 角色,Class1 具有 Client 角色。根据上图,Class1 通过使用依赖关系依赖于 Class2。换句话说,Class1 使用 Class2。

During software development, apart from inherent classes, we may also deal with abstract classes or interfaces:
在软件开发过程中,除了固有的类,我们还可以处理抽象类或接口:

alt text
Figure 1.15: Abstract classes and interfaces in Class Diagram
图 1.15.. 类图中的抽象类和接口

In the preceding diagram, there is an inherent class called Class1, which inherits from the AbstractClass. The name of the abstract class is written in italics. Also, Class1 has implemented the IClass interface. Visually, it is very easy to recognize the interface.

在上图中,有一个名为 Class1 的固有类,它继承自 AbstractClass。抽象类的名称以斜体书写。此外,Class1 还实现了 IClass 接口。从视觉上看,很容易识别界面。

Conclusion

结束语

In this chapter, software architecture and design patterns, the .NET framework, and UML were introduced in general. According to the points mentioned in this chapter, it should be possible to identify good architectural factors and produce software in accordance with some important programming principles.

在本章中,一般介绍了软件体系结构和设计模式、.NET 框架和 UML。根据本章中提到的要点,应该能够识别出好的架构因素,并根据一些重要的编程原则来生产软件。

In the next chapter, the first category of GoF design patterns (Creational design patterns) will be introduced and examined, and it will be investigated how to manage the object initialization according to different creational design patterns.

在下一章中,将介绍和研究 GoF 设计模式的第一类(Creational Design patterns),并研究如何根据不同的创建设计模式管理对象初始化。

NET 7 Design Patterns In-Depth Table of Contents

.NET 7 Design Patterns In-Depth

Enhance code efficiency and maintainability with .NET Design Patterns

Vahid Farahmandian

Table of Contents

目录

  1. Introduction to Design Patterns

  2. 设计模式简介

  3. Creational Design Patterns

  4. 创造式设计模式

  5. Structural Design Patterns

  6. 结构设计模式

  7. Behavioral Design Patterns – Part I

  8. 行为设计模式 – 第一部分

  9. Behavioral Design Patterns – Part II

  10. 行为设计模式 – 第二部分

  11. Domain Logic Design Patterns

  12. 域逻辑设计模式

  13. Data Source Architecture Design Patterns

  14. 数据源架构设计模式

  15. Object-Relational Behaviors Design Patterns

  16. 对象关系行为设计模式

  17. Object-Relational Structures Design Patterns

  18. 对象关系结构设计模式

  19. Object-Relational Metadata Mapping Design Patterns

  20. 对象关系元数据映射设计模式

  21. Web Presentation Design Patterns

  22. Web 表示设计模式

12 . Distribution Design Patterns
12 .分布设计模式

  1. Offline Concurrency Design Patterns

  2. 离线并发设计模式

  3. Session State Design Patterns

  4. 会话状态设计模式

  5. Base Design Patterns

  6. 基本设计模式


About the Author

关于作者
Vahid Farahmandian, who currently works as the CEO of Spoota company, was born in Urmia, Iran, in 1989. He got a BSc in Computer Software Engineering from Urmia University and an MSc degree in Medical Informatics from Tarbiat Modares University. He has more than 17 years of experience in the information and communication technology field and more than a decade of experience in teaching different courses of DevOps, programming languages, and databases in various universities, institutions, and organizations in Iran. Vahid also is an active speaker in international shows and conferences, including Microsoft .NET Live TV, Azure, .NET, and SQL Server conferences. The content published by Vahid was available through YouTube and Medium and had thousands of viewers and audiences.

Vahid Farahmandian 目前担任 Spoota 公司的首席执行官,于 1989 年出生于伊朗乌尔米亚。他获得了乌尔米亚大学的计算机软件工程学士学位和塔尔比亚特莫达雷斯大学的医学信息学硕士学位。他在信息和通信技术领域拥有超过 17 年的经验,并在伊朗的各所大学、机构和组织中教授 DevOps、编程语言和数据库的不同课程方面拥有十多年的经验。Vahid 还是国际节目和会议的积极演讲者,包括 Microsoft .NET Live TV、Azure、.NET 和 SQL Server 会议。Vahid 发布的内容可通过 YouTube 和 Medium 获得,并拥有成千上万的观众和观众。

About the Reviewers

关于审阅者

Kratika Jain is a senior software developer specializing in .NET technologies. She has a strong understanding of C#, ASP.NET, MVC, .NET Core, SQL, and Entity Framework. She has participated in agile project management, employs continuous integration/deployment (CI/CD) using Azure DevOps, and delivered robust and scalable software solutions. As a meticulous technical reviewer, she ensures accuracy and quality in technical content. Her attention to detail allows her to identify potential pitfalls and offer valuable insights for improvement. With her expertise in .NET development and dedication to enhancing technical content, she contributes to empowering developers and enabling their success in mastering the .NET ecosystem. She is a natural problem solver, team player, adaptable, and always seeking new challenges. You can connect with her on LinkedIn at www.linkedin.com/in/kratikajain29/ or on Twitter via @_KratikaJain.

Kratika Jain 是一位专门从事 .NET 技术的高级软件开发人员。她对 C#、ASP.NET、MVC、.NET Core、SQL 和实体框架有很强的理解。她参与了敏捷项目管理,使用 Azure DevOps 采用持续集成/部署 (CI/CD),并提供了强大且可扩展的软件解决方案。作为一名一丝不苟的技术审查员,她确保技术内容的准确性和质量。她对细节的关注使她能够识别潜在的陷阱并提供有价值的改进见解。凭借她在 .NET 开发方面的专业知识和对增强技术内容的奉献精神,她为增强开发人员的能力并帮助他们成功掌握 .NET 生态系统做出了贡献。她是一个天生的问题解决者、团队合作者、适应性强,并且总是寻求新的挑战。您可以通过 LinkedIn at www.linkedin.com/in/kratikajain29/ 或通过 @_KratikaJain 在 Twitter 上与她联系。

Gourav Garg is a Senior Software Engineer from India who has been helping companies to build scalable products. He holds a bachelor’s degree in software engineering and has been programming for 11 years. He is proficient in .net, C#, and Entity Framework. He has experience in delivering several products and many features at his work.

Gourav Garg 是来自印度的高级软件工程师,一直在帮助公司构建可扩展的产品。他拥有软件工程学士学位,从事编程工作已有 11 年。他精通 .net、C# 和 Entity Framework。他在工作中拥有交付多种产品和许多功能的经验。

Gourav has also experience with JavaScript-related tech stacks like Angular and React. He has developed quite a few open-source libraries using ES6 and Angular.

Gourav 还拥有 Angular 和 React 等 JavaScript 相关技术堆栈的经验。他使用 ES6 和 Angular 开发了不少开源库。

Acknowledgement

致谢
There are a few people I want to thank for the continued and ongoing support they have given me during the writing of this book. First and foremost, I would like to thank my parents for continuously encouraging me to write the book — I could have never completed this book without their support.

我想感谢一些人,他们在写这本书期间给予我持续的支持。首先,我要感谢我的父母一直鼓励我写这本书——如果没有他们的支持,我永远不可能完成这本书。

I also need to thank my dear wife, who has always supported me. Finally, I would like to thank all my friends and colleagues who have been by my side and supported me during all these years. I really could not stand where I am today without the support of all of them.

我还需要感谢我一直支持我的亲爱的妻子。最后,我要感谢这些年来一直陪伴在我身边并支持我的所有朋友和同事。如果没有他们所有人的支持,我真的无法站今天。

My gratitude also goes to the team at BPB Publications, who supported me and allowed me to write and finish this book.
我还要感谢 BPB Publications 的团队,他们支持我并允许我编写和完成这本书。

Preface

前言

This book has tried to present important design patterns (including GoF design patterns and Patterns of Enterprise Application Architecture) in software production with a simple approach, along with practical examples using .NET 7.0 and C#.

本书试图用简单的方法呈现软件生产中重要的设计模式(包括 GoF 设计模式和企业应用程序架构模式),以及使用 .NET 7.0 和 C# 的实际示例。

This book will be useful for software engineers, programmers, and system architects. Readers of this book are expected to have intermediate knowledge of C#.NET programming language, .NET 7.0, and UML.

这本书对软件工程师、程序员和系统架构师很有用。本书的读者应具备 C#.NET 编程语言、.NET 7.0 和 UML 的中级知识。

Practical and concrete examples have been used in writing this book. Each design pattern begins with a short descriptive sentence and is then explained as a concrete scenario. Finally, each design pattern's key points, advantages, disadvantages, applicability, and related patterns are stated.

在撰写本书时,使用了实际和具体的例子。每个设计模式都以一个简短的描述性句子开头,然后作为具体场景进行解释。最后,陈述了每种设计模式的关键点、优点、缺点、适用性和相关模式。

This book is divided into 15 chapters, including:

本书分为 15 章,包括:

Chapter 1: Introduction to Design Patterns- In this chapter, an attempt has been made to explain why design patterns are important and their role in software architecture, and basically, what is the relationship between design patterns, software design problems, and software architecture? In this chapter, various topics such as Design Principles, including SOLID, KISS, DRY, etc., and Introduction to .NET and UML are covered too.
第 1 章:设计模式简介 - 在本章中,我们试图解释为什么设计模式很重要以及它们在软件架构中的作用,基本上,设计模式、软件设计问题和软件架构之间的关系是什么?在本章中,还涵盖了各种主题,例如设计原则,包括 SOLID、KISS、DRY 等,以及 .NET 和 UML 简介。

Chapter 2: Creational Design Patterns- Creative design patterns, as the name suggests, deal with the construction of objects and how to create instances. In C# programming language, wherever an object is needed, the object can be created using the “new” keyword along with the class name. However, there are situations where it is necessary to hide the way the object is made from the user's view. In this case, creative design patterns can be useful. In this chapter, creational design patterns, one of the types of GoF design patterns, have been introduced, and it has been said that these design patterns are useful for what issues.
第 2 章:创造性设计模式 - 顾名思义,创意设计模式涉及对象的构造以及如何创建实例。在 C# 编程语言中,只要需要对象,就可以使用 “new” 关键字和类名创建对象。但是,在某些情况下,有必要从用户的视图中隐藏对象的创建方式。在这种情况下,创意设计模式可能很有用。在本章中,介绍了 GoF 设计模式的一种创建设计模式,并且据说这些设计模式对哪些问题很有用。

Chapter 3: Structural Design Patterns- Structural design patterns deal with the relationships between classes in the system. In fact, this category of design patterns determines how different objects can form a more complex structure together. In this chapter, structural design patterns, one of the types of GoF design patterns, have been introduced, and it has been said that these design patterns are useful for what issues.
第 3 章:结构设计模式 - 结构设计模式处理系统中类之间的关系。事实上,这类设计模式决定了不同的对象如何一起形成更复杂的结构。在本章中,介绍了 GoF 设计模式的一种结构设计模式,据说这些设计模式对什么问题很有用。

Chapter 4: Behavioral Design Patterns - Part I- This category of design patterns deals with the behavior of objects and classes. In fact, the main goal and focal point of this category of design patterns is to perform work between different objects using different methods and different algorithms. In fact, in this category of design patterns, not only objects and classes are discussed, but the relationship between them is also discussed. In this chapter, the most popular and famous behavioral design patterns, one of the types of GoF design patterns, have been introduced, and it has been said that these design patterns are useful for what issues.
第 4 章:行为设计模式 – 第一部分 - 这类设计模式涉及对象和类的行为。事实上,这类设计模式的主要目标和焦点是使用不同方法和不同算法在不同对象之间执行工作。事实上,在这类设计模式中,不仅讨论了对象和类,还讨论了它们之间的关系。在本章中,介绍了最流行和最著名的行为设计模式,这是 GoF 设计模式的一种,据说这些设计模式对什么问题很有用。

Chapter 5: Behavioral Design Patterns - Part II- In continuation of the previous chapter, in this chapter, more complex and less used behavioral design patterns are discussed, and it is shown how these design patterns can be useful in dealing with the behavior of objects and classes. Although these patterns are less known or less used, their use can make much more complex problems be solved in a very simple way. In this chapter, less popular or famous behavioral design patterns, one of the types of GoF design patterns, have been introduced, and it has been said that these design patterns are useful for what issues.
第 5 章:行为设计模式 – 第二部分 - 在上一章的延续中,本章讨论了更复杂和较少使用的行为设计模式,并展示了这些设计模式如何用于处理对象和类的行为。尽管这些模式鲜为人知或较少使用,但它们的使用可以以非常简单的方式解决更复杂的问题。在本章中,介绍了不太流行或不太著名的行为设计模式,这是 GoF 设计模式的一种类型,据说这些设计模式对什么问题很有用。

Chapter 6: Domain Logic Design Patterns- To organize domain logic, Domain Logic design patterns can be used. The choice of which design pattern to use depends on the level of logical complexity that we want to implement. The important thing here is to understand when logic is complex and when it is not! Understanding this point is not an easy task, but by using domain experts, or more experienced people, it is possible to obtain a better approximation. In this chapter, it is said how to organize the logic of the domain. And in this way, what are the design patterns that help us have a more appropriate and better design? These design patterns are among the PoEAA design patterns.
第 6 章:域逻辑设计模式 - 为了组织域逻辑,可以使用域逻辑设计模式。选择使用哪种设计模式取决于我们想要实现的逻辑复杂程度。这里重要的是了解逻辑何时复杂,何时不复杂!理解这一点并非易事,但通过使用领域专家或更有经验的人,可以获得更好的近似值。在本章中,将介绍如何组织域的逻辑。而这样一来,有哪些设计模式可以帮助我们有一个更合适、更好的设计呢?这些设计模式属于 PoEAA 设计模式。

Chapter 7: Data Source Architectural Design Patterns- One of the challenges of designing the data access layer is to implement how to communicate with the data source. In this implementation, it is necessary to address issues such as how to categorize SQL codes, how to manage the complexities of communicating with the data of each domain, and the mismatch between the database structure and the domain model. In this chapter, it has been said that in software architecture, communication with data sources can be considered and implemented in a suitable way. These design patterns are among the PoEAA design patterns.
第 7 章:数据源架构设计模式 - 设计数据访问层的挑战之一是实现如何与数据源通信。在此实现中,有必要解决诸如如何对 SQL 代码进行分类、如何管理与每个域的数据进行通信的复杂性以及数据库结构和域模型之间的不匹配等问题。在本章中,已经说过在软件架构中,可以考虑并以适当的方式实现与数据源的通信。这些设计模式属于 PoEAA 设计模式。

Chapter 8: Object-Relational Behaviors Design Patterns- Among the other challenges that exist when communicating with the database is paying attention to behaviors. What is meant by behaviors is how the data should be fetched from the database or how it should be stored in it. For example, suppose a lot of data is fetched from the database, and some of them have changed. It will be very important to answer the question of which of the data has changed or how to store the changes again in the database, provided that the data consistency is not disturbed. Another challenge is that when the Domain Model is used, most of the models have relationships with other models, and reading a model will lead to fetching all its relationships, which will again jeopardize the efficiency. In this chapter, an attempt has been made to explain how to connect business to data sources in a proper way. These design patterns are among the PoEAA design patterns.
第 8 章:对象关系行为设计模式 - 与数据库通信时存在的其他挑战之一是关注行为。行为的含义是应该如何从数据库中获取数据或应该如何将数据存储在数据库中。例如,假设从数据库中获取了大量数据,其中一些数据已更改。回答哪些数据已更改或如何将更改再次存储在数据库中的问题非常重要,前提是数据一致性不受干扰。另一个挑战是,当使用 Domain Model 时,大多数模型都与其他模型有关系,读取一个模型会导致获取它的所有关系,这将再次危及效率。在本章中,我们尝试解释如何以适当的方式将业务连接到数据源。这些设计模式属于 PoEAA 设计模式。

Chapter 9: Object-Relational Structures Design Patterns- Another challenge in mapping the domain to the database is how to map a record in the database to an object. The next challenge is how to implement all types of relationships, including one-to-one, one-to-many and many-to-many relationships. In the meantime, we may face some data that cannot and should not be mapped to any table, and we should think about this problem in our design. Finally, to implement the structure of the database, relationships such as inheritance may be used. In this case, it should be determined how this type of implementation should be mapped to the tables in the database. In this chapter, an attempt has been made to explain how to implement the data source structure in the software. These design patterns are among the PoEAA design patterns.
第 9 章:对象关系结构设计模式 - 将域映射到数据库的另一个挑战是如何将数据库中的记录映射到对象。下一个挑战是如何实现所有类型的关系,包括 1 对 1、1 对多和 many-to-many 关系。同时,我们可能会遇到一些不能也不应该映射到任何 table 的数据,我们应该在设计中考虑这个问题。最后,为了实现数据库的结构,可以使用继承等关系。在这种情况下,应确定如何将这种类型的实现映射到数据库中的表。在本章中,尝试解释如何在软件中实现数据源结构。这些设计模式属于 PoEAA 设计模式。

Chapter 10: Object-Relational Metadata Mapping Design Patterns- When we are producing software, we need to implement the mapping between tables and classes. For the software production process, this will be a process that contains a significant amount of repetitive code, and this will increase the production time. So, it will be necessary to stop writing duplicate codes and extract relationships from metadata. When this challenge can be solved, then it will be possible to generate queries automatically. Finally, when it is possible to automatically extract queries, the database can be hidden from the rest of the program. This chapter describes how to store object metadata in the data source, as well as how to create and manage queries to the data source. These design patterns are among the PoEAA design patterns.
第 10 章:对象关系元数据映射设计模式 - 当我们生产软件时,我们需要实现表和类之间的映射。对于软件生产过程,这将是一个包含大量重复代码的过程,这将增加生产时间。因此,有必要停止编写重复代码并从元数据中提取关系。当这个挑战可以解决时,就可以自动生成查询。最后,当可以自动提取查询时,数据库可以对程序的其余部分隐藏。本章介绍如何在数据源中存储对象元数据,以及如何创建和管理对数据源的查询。这些设计模式属于 PoEAA 设计模式。

Chapter 11: Web Presentation Design Patterns- One of the most important changes in applications in recent years is the penetration of web-based user interfaces. These types of interfaces come with various advantages, including that the client often does not need to install a special program to use them. The creation of web applications is often accompanied by the generation of server-side codes. The request is entered into the web server, and then the web server delivers the request based on the content of the request to the web application or the corresponding website. To separate the details related to the view from the data structure and logic, you can benefit from the design patterns presented in this chapter. In this chapter, the creation and handling of user interface requests are discussed, and it is stated how you can prepare and implement the view and how you can manage the requests in a suitable way. These design patterns are among the PoEAA design patterns.
第 11 章:Web 表示设计模式 - 近年来应用程序最重要的变化之一是基于 Web 的用户界面的渗透。这些类型的接口具有各种优点,包括客户端通常不需要安装特殊程序即可使用它们。Web 应用程序的创建通常伴随着服务器端代码的生成。将请求输入到 Web 服务器中,然后 Web 服务器根据请求的内容将请求投递到 Web 应用程序或相应的网站。要将与视图相关的细节与数据结构和逻辑分开,您可以从本章中介绍的设计模式中受益。在本章中,讨论了用户界面请求的创建和处理,并说明了如何准备和实现视图以及如何以适当的方式管理请求。这些设计模式属于 PoEAA 设计模式。

Chapter 12: Distribution Design Patterns- One of the problems of implementing communication between systems is observing the level of coarseness and fineness of communication. This level should be such that both the effectiveness and efficiency during the network are not disturbed, and the data structure delivered to the client is the structure that is expected and suitable for the client. In this chapter, design patterns that can be useful in building distributed software are discussed. These design patterns are among the PoEAA design patterns.
第 12 章:分布设计模式 - 在系统之间实现通信的问题之一是观察通信的粗略程度和精细度。这个级别应该是这样的,网络期间的有效性和效率都不会受到干扰,并且交付给客户端的数据结构是客户预期和适合的结构。本章讨论了在构建分布式软件时有用的设计模式。这些设计模式属于 PoEAA 设计模式。

Chapter 13: Offline Concurrency Design Patterns- One of the most complicated parts of software production is dealing with topics related to concurrency. Whenever several threads or processes have access to the same data, there is a possibility of problems related to concurrency, so one should think about concurrency in software production. Of course, there are different solutions at different levels for working and managing concurrency in enterprise software applications. For example, you can use transactions, internal features of relational databases, etc., for this purpose. Of course, this reason is not proof of the claim that concurrency management can basically be blamed on these methods and tools. In this chapter, design patterns that can be useful in solving these problems have been introduced. These design patterns are among the PoEAA design patterns.
第 13 章:离线并发设计模式 - 软件生产中最复杂的部分之一是处理与并发相关的主题。每当多个线程或进程可以访问相同的数据时,就可能存在与并发相关的问题,因此应该考虑软件生产中的并发性。当然,在企业软件应用程序中工作和管理并发在不同级别有不同的解决方案。例如,为此,您可以使用事务、关系数据库的内部功能等。当然,这个原因并不能证明并发管理基本上可以归咎于这些方法和工具的说法。本章介绍了可用于解决这些问题的设计模式。这些设计模式属于 PoEAA 设计模式。

Chapter 14: Session State Design Patterns- When we talk about transactions, we often talk about system transactions and business transactions. This discussion continues to the discussion of stateless or stateless sessions. Obviously, first, it should be determined what is meant by Stateful or Stateless. When we look at an object, this object consists of a series of data (status) and a series of behaviors. If we assume that the object does not contain any data, then we have accepted that the object in question does not have any data with it. If we bring this discussion to enterprise software, the meaning of Stateless will be a state in which the server does not keep any data of the request between two requests. If the server needs to store data between two requests, then we will face stateful mode. This chapter talks about how to manage user sessions. Some points have been raised regarding stateless and stateful sessions. These design patterns are among the PoEAA design patterns.
第 14 章:会话状态设计模式 - 当我们谈论事务时,我们经常谈论系统事务和业务事务。此讨论将继续讨论 stateless 或 stateless 会话。显然,首先,应该确定 Stateful 或 Stateless 的含义。当我们查看一个对象时,这个对象由一系列数据 (status) 和一系列 Behavior 组成。如果我们假设该对象不包含任何数据,则我们已接受该对象不包含任何数据。如果我们把这个讨论带到企业软件上,Stateless 的含义将是服务器在两个请求之间不保留请求的任何数据的状态。如果服务器需要在两个请求之间存储数据,那么我们将面临 Stateful 模式。本章讨论如何管理用户会话。已经提出了一些关于无状态和有状态会话的观点。这些设计模式属于 PoEAA 设计模式。

Chapter 15: Base Design Patterns- When we are designing software, we need to use different design patterns. To use these patterns, it is also necessary to use a series of basic design patterns to finally provide a suitable and better design. In fact, basic design patterns provide the foundation for designing and using other patterns. In this chapter, a series of basic design patterns have been introduced, and it has been shown how the use of these design patterns can be effective on the use of other design patterns. These design patterns are among the PoEAA design patterns.
第 15 章:基本设计模式 - 当我们设计软件时,我们需要使用不同的设计模式。要使用这些模式,还需要使用一系列基本的设计模式,以最终提供合适且更好的设计。事实上,基本设计模式为设计和使用其他模式提供了基础。在本章中,介绍了一系列基本设计模式,并展示了如何使用这些设计模式来有效地使用其他设计模式。这些设计模式属于 PoEAA 设计模式。

Code Bundle and Coloured Images

代码包和彩色图像

Please follow the link to download the Code Bundle and the Coloured Images of the book:https://rebrand.ly/g3mn07e
请点击链接下载代码包和书籍的彩色图像: https://rebrand.ly/g3mn07e

The code bundle for the book is also hosted on GitHub at https://github.com/bpbpublications/.NET-7-Design-Patterns-In-Depth. In case there's an update to the code, it will be updated on the existing GitHub repository.
该书的代码包也托管在 GitHub 上,网址为 https://github.com/bpbpublications/.NET-7-Design-Patterns-In-Depth。如果代码有更新,它将在现有的 GitHub 存储库上更新。

We have code bundles from our rich catalogue of books and videos available at https://github.com/bpbpublications. Check them out!
我们在 https://github.com/bpbpublications 上提供了丰富的书籍和视频目录中的代码包。看看他们吧!

Errata

勘误表
We take immense pride in our work at BPB Publications and follow best practices to ensure the accuracy of our content to provide with an indulging reading experience to our subscribers. Our readers are our mirrors, and we use their inputs to reflect and improve upon human errors, if any, that may have occurred during the publishing processes involved. To let us maintain the quality and help us reach out to any readers who might be having difficulties due to any unforeseen errors, please write to us at :errata@bpbonline.com

我们为我们在 BPB Publications 的工作感到非常自豪,并遵循最佳实践来确保我们内容的准确性,从而为我们的订阅者提供沉迷的阅读体验。我们的读者是我们的镜子,我们利用他们的意见来反映和改进在所涉及的发布过程中可能发生的人为错误(如果有)。为了让我们保持质量并帮助我们联系任何可能因任何不可预见的错误而遇到困难的读者,请写信给我们:errata@bpbonline.com

Your support, suggestions and feedbacks are highly appreciated by the BPB Publications’ Family.

BPB Publications 大家庭高度感谢您的支持、建议和反馈。