Package change (Part One)
The biggest enemy in software design is the continuous change in demand. Changes are sometimes infinite, so project development is resequently modified, and the date indefinitely delayed delivery indefinitely. Changes such as the Sword of Damock's sword hanging on the head, making many software engineering experts. Just as I can't find "silver bomb" to solve the software development, it is necessary to thoroughly change the change in the cradle, it seems impossible task. Then, facing "changing" actively, it is a desirable attitude. Thus, the advocator of the Extreme Programming (XP) proposes to "hug changes", from the perspective of software engineering methods, and propose solutions to "change" from the perspective of software engineering methods. This article tries to explore how to solve future possible changes in the software design process from the perspective of software design methods, and its method is - package changes.
The design pattern is the best explanation of the "package change" method. Whether it is creation mode, structural mode or behavioral mode, the end of the root is the "change" that can exist in the software, and then package these changes by using abstract ways. Since there is no specific implementation of abstraction, an unlimited possibility is made, making it possible to expand it. Therefore, in the design, we need to calibrate or already exist "changes" in addition to the use cases set by the demand. The most important point is to discover changes, or find changes.
GOF's classification of design patterns has highlighted the connotation and essence of "package changes". The purpose of the creation mode is the change of the encapsulation object creation. For example, the Factory Method mode and the Abstract Factory mode have established a special abstract factory class to encapsulate possible changes caused by the creation of future objects. The Builder mode is packaged in the creation of the object, which makes it possible to expand or replace the change of the object's internal creation of the object due to the details of the abstraction.
As for the structural mode, it is concerned about the combination of objects. In essence, if the object structure may change, it is mainly the change in its dependency. Of course, for the structural mode, the mode of processing changes is not only as simple as the package and abstraction, but also reasonably utilizes the method of inheritance and polymerization, and flexibly expresses the dependencies between objects. For example, Decorator mode, described is a variety of combined methods where objects may exist, such a combination is a relationship between a decorator and a decorator, so encapsulating such a combination, abstract special decorative object is obviously It is an embodiment of "encapsulation changes". Similarly, the Bridge mode package is an object-implemented dependency, and the Composite mode is to solve is the recursive relationship between the objects.
The behavior pattern is concerned about the behavior of the object. The behavioral mode needs to be done, and the behavior of the change is abstract, by packaging to achieve the scalability of the entire architecture. For example, policy mode is to abstract the policies or algorithms that may have changed to be an independent interface or abstraction class to achieve the purpose of policy extension. Command mode, State mode, Vistor mode, Iterator mode. Or encapsulate a request (Command mode), or encapsulate a state (State mode), or package the "access" method (ViTOR mode), or package the "traversal" algorithm (Iterator mode). The behavior of these to package is precisely the most unstable part in the software architecture, and its extension is the largest. Package these behaviors, use abstract characteristics, it provides the possibility of expansion.
Using the design mode, through the method of encapsulation, the scalability of the software can be maximized. In the face of complex demand changes, although it is impossible to completely solve the terrible dreams of changes due to changes, if they can foresee certain changes in design, it is still possible to avoid changes in the future to a software architecture. Classic catastrophic injury. From this point, although there is no "silver bomb", but from the perspective of software design methods, the design model is also a good "copper". Package change (Part To)
Filed under: design & pattern - Bruce zhang @ 8:16 PM
Consider a logging tool. A convenient log is now available, allowing customers to easily complete log records. The log requires that the content recorded in the specified text file is a string type, which is provided by the customer. We can easily define a log object: public class log {public void write (string target, string log) {// implement content;}}
When the customer needs to call the log, you can create a log object, complete the log record: log log = new log (); log.write ("error.log", "log");
However, with the frequent use of logging, there are more and more files related to logs, and the query and management of logs are also inconvenient. At this point, the customer proposes that it is necessary to change the log's recording mode, and the log content is written to the specified data table. Obviously, if it is still in the previous design, it has a large limit.
Now we return to the beginning of the design, imagine that the design of the log API needs to consider this change? There are two design philosophy, namely the gradual design and plan design. From this example, the designer is required to consider the possible changes in the future in the design, which is not easy. Furthermore, if you consider a comprehensive design at the beginning, you will produce redundancy. Therefore, the design of the planned design has a certain forward-looking, but on the one hand, the designer is too high, and some defects will be produced. So, when you have gradually design, when you encounter a change in demand, use the reconstruction method to improve the existing design, and need to consider the future change? This is a question of seeing benevolence. For this example, we can completely modify the Write () method, accept the parameters of a type judgment to resolve this issue. But such a design, naturally should be responsible for the future possible change, resulting in a large number of changes in the code, for example, we require the log to record in the specified XML file.
Therefore, changes are completely possible. In the case of time and technical capabilities, I prefer to reduce the impact of changes to the design to the lowest. At this point, we need to package changes.
Before the package changes, we need to figure out what has changed? From demand, it is a change in logging. From this conceptual analysis, it may result in two different results. One situation is that we regard the way of logging as a behavior, it is precisely that it is a request for users. Another situation analyzes from the perspective of the object, we regard the logs of various ways as different objects, they call the same behavior as the interface, the difference is only to create different objects. The former needs us to package "changes in user requests", and the latter requires our packaging "changes created by log objects."
Package "Changes in User Request", here is the possible changes in the package log record. That is, we need to abstract the logging behavior into a separate interface and then define different implementations. As shown in Figure 1: Figure 1: Changes in package logging behavior
If you are familiar with the design pattern, you can see the structure shown in Figure 1 is an embodiment of a Command mode. Since we have an interface to log record behavior, users can freely expand the way logging, only need to implement the ILOG interface. As for the log object, there is a weak dependency between the ILOG interface:
Public class log {private ilog; public log (ilog log) {this.log = log;}
Public void write (String target, string logvalue) {log.execute (target, logvalue);}}
We can also achieve the scalability of the log API by encapsulating the "change of log objects". In this case, the log will be defined as different objects depending on the record mode. When we need to record the log, create a corresponding log object, then call the Write () method of the object to realize the log record. At this point, it may change the log object that needs to be created, so to encapsulate this change, you can define an abstract factory class, specifically responsible for the creation of the log object, as shown in Figure 2:
Figure 2: Changes of Covered Log Objects
Figure 2 is the embodiment of the Factory Method mode, which is dedicated to the creation of the log object. If the user needs to record the corresponding log, for example, the log is required to record the database, you need to create a specific logfactory object: logfactory factory = new dblogfactory ();
When in the application, you need to record the log, then get a new log object through the logFactory object: log log = factory.create (); log.write ("ErrorLog", "log");
If the user needs to change the log record as a text file, you only need to modify the creation of the logFactory object: logfactory factory = new txtFactory Factory ();
In order to better understand the "Change Creation of Package Objects", let's take a look at an example. If we need to design a database component, it can access Microsoft's SQL Server database. According to ADO.NET knowledge, we need to use the following objects: SqlConnection, SqlCommand, SqlDataAdapter, etc.
If only in terms of Sql Server, when accessing the database, we can create these objects directly: SqlConnection connection = new SqlConnection (strConnection); SqlCommand command = new SqlCommand (connection); SqlDataAdapter adapter = new SqlDataAdapter ();
In a database component, it is obviously unreasonable. It is full of rigidity. Once you ask for other databases, the original design needs to be completely modified, which has brought difficulties for expansion.
So let's think about it, what should I do? Assuming the database component requirements or future requests support multiple databases, for objects such as Connection, Command, DataAdapter, the objects of SQL Server cannot be particularly embodied. That is, we need to establish a inheritance hierarchy for these objects, and build an abstract parent class, or interface. Then define different specific classes for different databases, these specific classes inherit or achieve their parent class, such as the Connection object: Figure 3: Hierarchy of the Connection object
I abstract a unified Iconnection interface for the Connection object, and the Connection object that supports various databases implements the IConnection interface. Similarly, Command objects and DataAdapter objects also use similar structures. Now, when we want to create an object, you can create a polymorphic principle: iconnection connection = new sqlConnection (STRCONNECTION);
As can be seen from this structure, the creation of the object may change depending on the database of access. That is to say, we need to design a database component, with the current structure, there is still a problem that the object cannot be copened. With the principle of "package changes", we need to abstract the responsibilities of the creation object to be enabling them to make effective packages. For example, if the code for creating an object, it should be responsible by a special object. We can still create a special abstract factory class DBFActory, and it is responsible for creating Connection, Command, DataAdapter objects. As for the specific class of the abstract class, the same structure is the same as the structure of the target object, and the different factory classes are defined according to the different database types, and the class diagrams are shown in Figure 4:
Figure 4: Class diagram of dbfactory
Figure 4 is an embodiment of a typical Abstract Factory mode. Each method in class dbFactory is the Abstract method, so we can also use the interface to replace the definition of this class. Inherit the specific classes of the DBFactory class, create objects of the corresponding database type. Take the SQLDBFActory class as an example, creating the code for the respective objects as follows: public class sqldbfactory: dbfactory {public override iconnection createConnection {Return NEW SQLCONNECTION (STRCONNECONNECTION);}
Public override iCommand CreateCommand (iConnection Connection) {Return New SqlCommand (connection);
Public override idataadapter createDataAdapter () {return new sqldataadapter ();}}
Now you have to create a related object accessing the SQL Server database, you can use the factory class. First, we can create factory objects in the initial 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); Due to the principle of packaging changes, a special factory class is established. Changes created by package objects. It can be seen that when we introduce the factory class, the CONNECTION, COMMAND and other objects have successfully eliminated its relationship with the specific database type. In the above code, there is no specific type such as SQL, such as SqlConnection, SqlCommand, etc. That is, the way to create an object is completely abstract, which is independent of the specific implementation. Whether it is accession, it is not related to these lines of code. As for changes in the type of database involved, all abstractions are in the DBFactory abstraction class. You need to change the type of access to the database, and we only need to modify the line of code that created the factory object, such as modifying the SQL Server type to Oracle Type: dbfactory factory = new oracledbFactory ();
Obviously, this way improves the scalability of database components. We will populate the partial encapsulation of changes, put them into the fixed part, such as the initialization part, or as a global variable, more ways to change, in the configuration file, by reading the value of the configuration file, Create a corresponding object. As a result, don't need to modify the code, you don't need to recompile, just modify the XML file, you can change the change of the database type. For example, we create the following profile:
The code to create a factory object is modified as follows: string factoryName = configurationSettings.appsettings ["db"]. ToString (); // dblib is the assembly of the database component: dbfactory factory = (dbfactory) Activator.createInstance ("dblib", FactoryName) .unwrap ();
For database components: When we need to change access to the database type to an Oracle database, you only need to modify the value value in the configuration file to "OracleDbFactory". This structure has good scalability, better solves the problems caused by future possible demand changes.
Package change (Part THREE)
Filed Under: design & pattern - Bruce Zhang @ 6:35 PM
Imagine such a requirement, we need to provide a component responsible for the framework. It is currently required to achieve bubble sort algorithm and fast sort algorithm. According to "Interface Programming", we can provide a unified interface ISORT for these sorting algorithms, there is a method sort () in this interface, it can accept An Object array parameter. After the array is sorted, the array is returned. The definition of the interface is as follows:
Public interface isort {void sort (ref object [] bass;}
Its class diagram is as follows:
However, it is generally arranged in order, such as ascending, or descending sequencing, and the result is not the same. The simplest way we can use the IF statement to achieve this, 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") {/ / Executive quick sorting;} else {// Perform descending quick sorting;}}}}}}} }
Of course, we can also define the SortType of the String type as an enumeration type and reduce the possibility of an error. However, read the code carefully, we can find such a code very rigid, once you need to expand, if we ask us to add new sort order, such as the dictionary order, then our work will be very heavy. That is, the change is produced. Through analysis, we found that the order of so-called sorting is precisely the most critical part of the sorting algorithm, which determines who is arranged before, who is arranged. However, it does not belong to the sorting algorithm, but a comparative strategy, the latter is a comparative behavior.
If you carefully analyze the class of the ISORT interface, for example, a Quicksort class, it needs to be compared to both objects when the sorting algorithm is implemented. According to the practice, in essence, we can draw a private method Compare () in the sort method, and determine which object is before, which object is behind. Obviously, this comparison behavior can be changed, using the principle of "encapsulation abstraction", should establish a proprietary interface iCompare for this behavior, but define a class object that implements ascending, descending or dictionary.
In each class constructor that implements the ISORT interface, we introduce the ICompare interface object, thereby establishing the weak coupling relationship between the sorting algorithm and the comparative algorithm (because this relationship is related to the abstract ICompare interface), such as the Quicksort class:
public class QuickSort: ISort {private ICompare m_Compare; public QuickSort (ICompare compare) {m_Compare = compare;} public void Sort (ref object [] beSorted) {// achieve slightly for (int i = 0; i The final class diagram is as follows: By encapsulation of comparison strategies, it is obviously the design of the Stategy mode. In fact, the sorting algorithm here may also be changed, such as implementing binary tree sorting. Since we have introduced the idea of "interface 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 in which the extension development is fully, it is fully able to adapt to changes in new demand for class librarists. Take PETSHOP as an example, in this project involves the management of orders, such as inserting orders. Taking into account the relationship between the visits, PETSHOP provides synchronous and asynchronous ways for order management. Obviously, only one of these two modes can be used in practical applications, and is determined by a specific application environment. So in order to cope with such a possible change, we still need to use the principle of "encapsulation changes", establish an abstract level object, which is the IRDERSTRATEGY interface: Public interface iorderstrategy {void insert (petshop.model.orderinfo order); Then define two class ordersynchronous and orderrasynchronous. The class structure is as follows: In PETSHOP, since the user may change the strategy inserted into the order, it is not possible to have a coupling relationship with the specific order policy object for the order domain object of the business layer. That is, in the domain object ORDER class, you cannot New a specific order policy object, as follows: IRDERSTRATEGY ORDERINSERTSTRATEGY = New ORDERSYNCHRONOUS (); In Martin Fowler's article "IOC container and Dependency Injection Mode", the solution to this type of problem is proposed, which is called dependency injection. However, since PETSHOP did not use IOC containers such as Sping.Net, solve the dependence problem, usually done with the configuration file combined with reflection. This is achieved in the domain object ORDER class: 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.AppSettings [ "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: Writing here seems to have been separated from the theme of "packaging changes". But in fact, we need to understand that the abstract way is encapsulated, although the king of responding to demand changes, but it can only relieve the coupling relationship of the caller and the caller, as long as it also involves the creation of the specific object, even Factory model, but the creation of specific factory objects is still essential. Then, for such a subject that has been encapsulated, we should completely use the "dependency injection" mode to completely relieve the coupling between the two.