Design Patterns-encapsulation changes

Source: Internet
Author: User
The biggest enemy of software design is the constant change in demand. Sometimes there are endless changes, so the project development will delay the delivery date indefinitely During Repeated modifications and updates. Changes, such as the sword of damos hanging above the head, make many software engineering experts unable to do anything. Just as a silver bullet that cannot be used to solve software development, it seems impossible to completely eliminate changes in the cradle. Then, it is desirable to actively face "changes. As a result, XP advocates and evangelist Kent Beck proposed to "embrace change" and proposed a solution to "change" from the perspective of software engineering methods. This article tries to discuss how to solve future changes in the software design process from the perspective of the software design method. The method is encapsulation change. The encapsulation Change series consists of three parts:
Create mode-> abstract encapsulation of the created object
Structural Mode-> abstract encapsulation of the relationship between objects
Behavior patterns-> abstract encapsulation of object behavior the biggest enemy of software design is to cope with changing needs. Sometimes there are endless changes, so the project development will delay the delivery date indefinitely During Repeated modifications and updates. Changes, such as the sword of damos hanging above the head, make many software engineering experts unable to do anything. Just as a silver bullet that cannot be used to solve software development, it seems impossible to completely eliminate changes in the cradle. Then, it is desirable to actively face "changes. As a result, XP advocates and evangelist Kent Beck proposed to "embrace change" and proposed a solution to "change" from the perspective of software engineering methods. This article tries to discuss how to solve future changes in the software design process from the perspective of the software design method. The method is encapsulation change. The design pattern is the best explanation of the "encapsulation change" method. Whether it is the Creation Mode, the structure mode, or the behavior mode, the final analysis is to find possible "changes" in the software, and then encapsulate these changes in an abstract way. Since abstraction has no specific implementation, it represents an infinite possibility, making it possible to expand. Therefore, at the beginning of the design, in addition to the use cases set to meet the requirements, we also need to calibrate possible or existing "changes. The most important thing about encapsulation changes is to discover changes, or look for changes. The classification of design patterns by gof has demonstrated the connotation and essence of "encapsulation change. The purpose of the Creation Mode is to encapsulate changes in object creation. For example, in the factory method mode and abstract factory mode, a special abstract factory class is established to encapsulate the possible changes caused by the creation of future objects. The builder mode encapsulates the internal creation of objects. Because the details of the abstract can be used as a replacement, You can flexibly expand or replace the internal creation of objects in the future. As for the structural mode, it focuses on the combination of objects. In essence, if the object structure may change, it mainly lies in the change of its dependency. Of course, for the structural mode, the way to handle changes is not only as simple as encapsulation and abstraction, but also the rational use of inheritance and aggregation methods to flexibly express the dependency between objects. For example, in the decorator mode, the decorator mode describes multiple possible combinations of objects. This combination mode is a kind of relationship between the decorator and the decorator. Therefore, this combination mode is encapsulated, abstract A special decoration object is clearly the embodiment of "encapsulation change. Similarly, the Bridge Mode encapsulates the dependency of object implementation, while the composite mode solves the recursive relationship between objects. The behavior mode focuses on the behavior of objects. What behavior patterns need to do is to abstract the changed behaviors and encapsulate them to achieve the scalability of the entire architecture. For example, the policy mode abstracts a policy or algorithm that may change into an independent interface or abstract class to implement policy extension. Command mode, state mode, vistor mode, or iterator mode. It can also encapsulate a request (command mode), a State (State mode), or an access mode (visitor mode ), or encapsulate the "traversal" algorithm (iterator mode ). The behavior to be encapsulated is precisely the most unstable part of the software architecture, and its scalability is also the most likely. Encapsulate these behaviors and use abstract features to provide the possibility of expansion. The design pattern can be used to encapsulate changes to maximize the scalability of the software. In the face of complex changes in demand, although it is impossible to completely solve the terrible nightmare caused by changes, however, if some changes can be foreseen at the beginning of the design, to some extent, it is still possible to avoid the catastrophic harm that future changes will bring to the software architecture. From this point of view, although there is no "Silver Bullet", but from the perspective of the software design method, the design model is also a good "Copper Bullet.

Consider a logging tool. Currently, you need to provide a convenient log so that you can easily record the log. This log must be recorded in a specified text file. The recorded content belongs to the string type and its value is provided by the customer. We can easily define a log object:
Public class log
{
Public void write (string target, string log)
{
// Implementation content;
}
} When you need to call the log function, you can create a log object to complete the log record:
Log = new log ();
Log. Write ("error. Log", "log"); however, with the frequent use of log records, more and more log-related files make it inconvenient to query and manage logs. In this case, the customer proposes to change the logging method and write the log content to the specified data table. Obviously, if you still follow the previous design, it has great limitations. Now, when we get back to the design stage, do you need to consider this change for the log API design? There are two design concepts: Progressive design and planning design. From the analysis in this example, it is not easy for the designer to consider the possible change of the logging method in the future at the beginning of the design. Furthermore, if a comprehensive design is considered at the very beginning, design redundancy will be generated. Therefore, the design of the plan is certainly forward-looking, but on the one hand, the designers are too demanding, but it will also produce some defects. So should we consider another change in the future when we adopt a progressive design and use the reconstruction method to improve the existing design when there is a change in demand? This is a matter of opinion and wisdom. In this example, we can directly modify the write () method and accept a parameter for type determination to solve this problem. However, such a design naturally bears the risk of a large number of code modifications due to future changes. For example, we need to record logs to the specified XML file. Therefore, changes are completely possible. As time and technical capabilities permit, I prefer to minimize the impact of changes on design. In this case, we need to encapsulate the changes. Before encapsulating changes, we need to figure out what changes have taken place? From the perspective of requirements, the logging method has changed. This concept may lead to two different results. One case is that we regard the log record method as an action. Specifically, it is a user request. In another case, we analyze the log from the perspective of objects. We regard logs in various ways as different objects, and they call the same interface behavior. The difference is that they create different objects. The former requires us to encapsulate "changes in user requests", while the latter requires us to encapsulate "Changes in log object creation ". Encapsulate "changes in user requests". Here, it encapsulates possible changes in log records. That is to say, we need to abstract the logging behavior into a separate interface, and then define different implementations respectively. 1:
Figure 1: encapsulate the changes in log records. If you are familiar with the design mode, you can see that the structure shown in Figure 1 is exactly the embodiment of the command mode. Because we abstract the log record behavior interfaces, You can freely expand the log record method, just implement the ilog interface. As for log objects, there is a weak dependency with the ilog interface: public class log
{
Private ilog log;
Public log (ilog log)
{
This. log = log;
} Public void write (string target, string logvalue)
{
Log. Execute (target, logvalue );
}
} We can also encapsulate "Changes in log object creation" to achieve log API scalability. In this case, logs are defined as different objects based on different recording methods. When we need to record the log, we will create the corresponding log object, and then call the write () method of the object to implement logging. In this case, the log object to be created may change. To encapsulate this change, you can define an abstract factory class to create a log object, as shown in Figure 2: figure 2: Changes in creating encapsulated log objects Figure 2 shows the factory method mode. The abstract class logfactory is responsible for creating log objects. If you need to record the corresponding logs, such as logging to the database, you must first create a specific logfactory object:
Logfactory factory = new dblogfactory (); when you need to record logs in an application, you can use the logfactory object to obtain the new log object:
Log = factory. Create ();
Log. Write ("errorlog", "log"); to change the log record mode to a text file, you only need to modify the creation of the logfactory object:
Logfactory factory = new txtfilelogfactory (); for a better understanding of "Changes in the creation of encapsulated objects", let's look at an example. Suppose we need to design a database component that can access Microsoft's SQL Server database. Based on ADO. Net knowledge, we need to use the following objects:
Sqlconnection, sqlcommand, sqldataadapter, etc.

 

For SQL Server alone, we can directly create these objects when accessing the database:
Sqlconnection connection = new sqlconnection (strconnection );
Sqlcommand command = new sqlcommand (connection );
Sqldataadapter adapter = new sqldataadapter (); A database component is filled with the preceding statements, which is obviously unreasonable. It is full of rigidity and bad taste. Once it is required to support other databases, the original design needs to be completely modified, which makes expansion difficult. So let's think about how to modify the above design? Assuming that the database component requires or supports multiple databases in the future, objects such as connection, command, and dataadapter cannot be embodied as objects of SQL Server. That is to say, we need to create an inherited hierarchy for these objects to create abstract parent classes or interfaces for them respectively. Then define different classes for different databases. These classes inherit or implement their parent classes, such as the connection object: <etettings>
<Add key = "DB" value = "sqldbfactory"/>
</Appsettings>
Figure 3: the connection object hierarchy I abstracted a unified iconnection interface for the connection object, and the iconnection interface is implemented for various database connection objects. Similarly, the command object and dataadapter object adopt a similar structure. Now, we can use the principle of polymorphism to create an object:
Iconnection connection = new sqlconnection (strconnection); from this structure, we can see that object creation may change depending on the accessed database. That is to say, the database components we need to design are still unable to cope with changes in object creation in the current structure. With the principle of "encapsulation change", it is necessary to extract the responsibility for creating objects separately for effective encapsulation. For example, the Code for creating an object above should be the responsibility of a special object. We can still create a specialized abstract factory class dbfactory, which is responsible for creating connection, command, and dataadapter objects. As for the specific class that implements this abstract class, it has the same structure as the target object. Different factory classes are defined based on different database types, as shown in Figure 4:
Figure 4: dbfactory class diagram 4 shows a typical Abstract Factory mode. Every method in dbfactory class is an abstract method, so we can also use interfaces to replace the definition of this class. Inherit the specific classes of the dbfactory class, and create the corresponding database type object. Take the sqldbfactory class as an example. The code for creating an object is as follows:
Public class sqldbfactory: dbfactory
{
Public override iconnection createconnection (string strconnection)
{
Return new sqlconnection (strconnection );
} Public override icommand createcommand (iconnection connection)
{
Return new sqlcommand (connection );
} Public override idataadapter createdataadapter ()
{
Return new sqldataadapter ();
}
} Now you can use the factory class to create related objects to access the SQL Server database. First, we can create a factory object in the initialization part of the program:
Dbfactory factory = new sqldbfactory (); then, use the factory object to create the corresponding connection, command, and other objects:
Iconnection connection = factory. createconnection (strconnection );
Icommand command = factory. createcommand (connection); because we use the principle of encapsulation changes, we have created a dedicated factory class to encapsulate the changes created by the object. As you can see, after we introduce the factory class, the connection, command, and other object creation statements have successfully eliminated the dependency between them and the specific database type. In the above Code, there are no specific types such as SQL, such as sqlconnection and sqlcommand. That is to say, the object creation method is completely abstract and irrelevant to the specific implementation. No matter which database you access, these lines of code are irrelevant. The changes to the database types involved are all abstracted into the dbfactory abstract class. To change the database access type, we only need to modify the line of code that creates the factory object. For example
Modify server type to Oracle type:
Dbfactory factory = new oracledbfactory (); obviously, this method improves the scalability of database components. We encapsulate the parts that may change into a fixed part of the program, such as the initialization part, or as a global variable. We can also put the parts that may change into the configuration file, create an object by reading the value of the configuration file. In this way, you can change the database type without modifying the code or re-compiling the XML file. For example, create the following configuration file: modify the code for creating a factory object as follows:
String factoryname = configurationsettings. receivettings ["DB"]. tostring ();
// Dblib is the database component assembly:
Dbfactory factory = (dbfactory) activator. createinstance ("dblib", factoryname ). unwrap (); is the database component assembly: When we need to change the database type to Oracle database, we only need to change the value in the configuration file to "oracledbfactory. This structure is highly scalable and can effectively solve the problems arising from future demand changes. To imagine such a requirement, we need to provide a sort component for our framework. Currently, we need to implement Bubble sorting algorithms and quick sorting algorithms. Based on the idea of "interface-oriented programming", we can provide a unified interface isort for these sorting algorithms, there is a method sort () in this interface, which can accept an object array parameter. Returns the array after sorting the array. The interface is defined as follows: public interface isort
{
Void sort (ref object [] besorted );
} The class diagram is as follows: however, for sorting, sorting is ordered, for example, ascending or descending, and the returned results are also different. In the simplest way, we can use the if statement to achieve this purpose. For example, in the quicksort class: public class quicksort: isort
{
Private string m_sorttype; Public quicksort (string sorttype)
{
M_sorttype = sorttype;
} Public void sort (ref object [] besorted)
{
If (m_sorttype.toupper (). Trim () = "ascending ")
{
// Execute fast sorting in ascending order;
}
Else
{
// Execute a fast sort in descending order;
}
}
} Of course, we can also define sorttype of string type as Enumeration type to reduce the possibility of errors. However, after carefully reading the code, we can find that such code is very rigid. If we need to extend it, if we want to add a new sort order, such as the dictionary order, the work we are facing will be very heavy. That is to say, changes have taken place. Through analysis, we found that the so-called sorting order is exactly the most critical part of the sorting algorithm, which determines who is ranked first and who is ranked after. However, it is not a sort algorithm, but a comparison strategy. The latter is a comparison behavior. If you carefully analyze the classes that implement the isort interface, such as the quicksort class, it needs to compare the two objects when implementing the sorting algorithm. According to the refactoring method, we can extract a private method compare () in the sort method, and use the returned Boolean value to determine which object is in the front and which object is in the back. Obviously, what may change is this comparative behavior. Using the principle of "encapsulation abstraction", we should establish a proprietary interface icompare for this behavior, however, class objects that implement ascending, descending, or dictionary sorting are defined respectively. We introduce the icompare interface object in every class constructor that implements the isort interface, this establishes a weak coupling relationship between the Sorting Algorithm and the comparison algorithm (because this relationship is related to the abstract icompare Interface), for example, quicksort class: public class quicksort: isort
{
Private icompare m_compare;
Public quicksort (icompare compare)
{
M_compare = compare;
}
Public void sort (ref object [] besorted)
{
// Implementation
For (INT I = 0; I <besorted. Length-1; I ++)
{
If (m_compare.compare (besorted [I], besorted [I + 1 ))
{
// Omitted;
}
}
// Implementation
}
} The final class diagram is as follows: The stategy pattern is obviously designed by encapsulating the comparison policy to cope with its changes. In fact, the Sorting Algorithm here may also change, for example, binary tree sorting. Because we have introduced the idea of "interface-oriented programming", we can easily add a new class binarytreesort to implement the isort interface. For the caller, the implementation of the isort interface is also a strategy mode. At this time, the class structure is completely a state of extension development, which can fully adapt to the changes in the new requirements of class library callers. Taking petshop as an example, this project involves order management, such as inserting orders. Considering the relationship between visits, petshop provides synchronous and asynchronous order management methods. Obviously, only one of the two methods can be used in actual applications, and is determined by the specific application environment. In response to such frequent changes, we still need to use the principle of "encapsulation changes" to create abstract-level objects, that is, iorderstrategy interface: public interface iorderstrategy
{
Void insert (petshop. model. orderinfo order );
} Then define two classes: ordersynchronous and orderasynchronous. The class structure is as follows: In petshop, because the user may change the order insertion policy at any time, the Order domain objects at the business layer cannot be coupled with specific order policy objects. That is to say, in the domain object order class, a specific order policy object cannot be created. The code below is: iorderstrategy orderinsertstrategy = new ordersynchronous (); in Martin Fowler's article IOC container and dependency injection mode, he proposed a solution to this type of problem, which is called dependency injection. However, because petshop does not use IOC containers such as sping. net, the dependency issue is usually solved by using the configuration file and reflection. In the order class of the domain object, it is implemented as follows: public class order
{
Private Static readonly iorderstategy orderinsertstrategy = loadinsertstrategy ();
Private Static iorderstrategy loadinsertstrategy ()
{
// Look up which strategy to use from config file
String Path = configurationmanager. etettings ["orderstrategyassembly"];
String classname = configurationmanager. appsettings ["orderstrategyclass"]; // load the appropriate assembly and class
Return (iorderstrategy) Assembly. Load (PATH). createinstance (classname );
}
} In the configuration file web. config, configure the following section:
<Add key = "orderstrategyassembly" value = "petshop. BLL"/>
<Add key = "orderstrategyclass" value = "petshop. BLL. ordersynchronous"/> This is actually a compromise of the service locator mode. Put the logic of locating and creating dependent objects directly into the object. In the petshop example, it is a good method. After all, in this example, there are not many objects that require dependency injection. However, we can also think of it as a helpless compromise. Once the logic of dependency injection increases, it will bring some trouble to programmers, A dedicated lightweight IOC container is required. It seems that the topic of "encapsulation change" has been removed. But in fact, we need to understand that the use of abstract methods to encapsulate changes is certainly the king of response to changes in demand, but it can only lift the coupling relationship between the caller and the called, as long as the creation of specific objects is involved, even if the factory mode is introduced, the creation of specific factory objects is still indispensable. Then, for such objects that have been encapsulated and changed, we should also take full advantage of the "dependency injection" method to completely remove the coupling between the two.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.