SOLID is the acronym for a set of best coding practices
- S Single Responsibility Principle
- O principles of openness and closure
- L Liskov (in-house) substitution principle
- I Interface Separation principle
- D Dependency Injection principle
By applying these best practices at the same time, you can improve your code's ability to adapt to changes. But all things have to be degree, over-use although you can make your code highly adaptive, it causes the granularity to be small and difficult to understand or use, and also affects the readability of your code.
Single principle of responsibility
Single Responsibility principle requires developers to write code with only one reason for change. If a class has multiple reasons for change, it has multiple responsibilities. This is the time to refactor, splitting the multi-responsibility class into multiple single-duty classes . Through delegation and abstraction, a class that contains multiple reasons for change should delegate one or more responsibilities to other single-responsibility classes .
Read an article before about why object-oriented is more adaptable to business change than process-oriented? It can also be seen from the benefits of a single responsibility principle, the responsibility is clear, only need to modify the local, will not affect the outside, the impact can be controlled in a sufficient scope.
Objects will be required to separate the needs of the class, as with the storage box to wrap things up one after another, the demand has changed, divided into several situations, the most serious is the big change, then each storage box to open the change, this method is not necessarily beneficial; So all we have to do is open these two lockers and change them, without affecting the rest of the locker.
The process is to put everything in a large storage box, modify a section later, will cause other parts of the instability, a bug fix, the new countless bugs, and finally the programmer into a burn.
Let's take a piece of code for example, and through the refactoring process, realize the benefits of a single responsibility principle.
Process-oriented coding
public class TradeRecord{ public int TradeAmount { get; set; } public decimal TradePrice { get; set; }}
public class tradeprocessor{public void Processtrades (Stream stream) {var lines = new list<string> (); using (var reader = new StreamReader (stream)) {string line; while (line =reader. ReadLine ()) = null) {lines. ADD (line); }} var trades = new list<traderecord> (); var linecount = 1; foreach (var line in lines) {var fields = line. Split (new char[] {', '}); if (fields. Length! = 3) {Console.WriteLine ("Warn:line {0} malformed. only {1} fields found ", LineCount, fields. Length); } int tradeamount; if (!int. TryParse (Fields[0], out Tradeamount)) {Console.WriteLine ("Warn:trade amount in line {0 } not a valid integer: {1} ", LineCount, Fields[0]); } decimal Tradeprice; if (!decimal. TryParse (Fields[1], out Tradeprice)) {Console.WriteLine (' Warn:trade price ' on line {0} Not a valid decimal: {1} ", LineCount, fields[1]); } var Traderecord = new Traderecord {tradeamount = Tradeamount, Tradeprice = Tradeprice}; Trades. ADD (Traderecord); linecount++; } using (var connection = new SqlConnection ("datasource= (local); Initial catalog=tradedatabase;integrated Se curity = True; ")) {connection. Open (); using (var transaction = connection. BeginTransaction ()) {foreach (var trade in trades) { var command = connection. CreateCommand (); Command. Transaction = Transaction; Command.commandtype = SYstem.Data.CommandType.StoredProcedure; Command.commandtext = "Insert_trade"; Command. Parameters.addwithvalue ("@tradeamount", trade. Tradeamount); Command. Parameters.addwithvalue ("@tradeprice", trade. Tradeprice); } transaction.commit (); } connection. Close (); } Console.WriteLine ("INFO: {0} trades processed", trades. Count); }}
The code above is not just a class with too much responsibility, but also a single method with too much responsibility. After a careful analysis of the code, the original Processtrades method code can be divided into three parts: reading the transaction data from the stream, converting the string data to a Traderecord instance, and persisting the transaction data to persistent storage.
the principle of single responsibility can be expressed at the class and method level . A method can only do one thing at the level of a method, and from a class level, a class can have only a single responsibility. Otherwise, split refactoring of classes and methods is necessary. For the method's split refactoring, the goal is clarity, which can improve the readability of the code, but it cannot improve the adaptive ability of the code. To improve the adaptive ability of code, it is necessary to do abstraction, dividing each responsibility into different classes.
Refactoring sharpness
Above we have analyzed the Processtrades method code can be divided into three parts, we can extract each part as a method, delegate the work to these methods, so the Processtrades method becomes:
public void ProcessTrade(Stream stream){ var lines = ReadTradeData(stream); var trades = ParseTrades(lines); StoreTrades(trades);}
The extracted method implementations are:
//<summary>///read transaction data from stream///</summary>///<param name= "Stream" ></param >///<returns></returns>private ienumerable<string> Readtradedata (Stream stream) {var tradeData = new List<string> (); using (var reader = new StreamReader (stream)) {string line; while (line = reader. ReadLine ()) = null) {Tradedata.add (line); }} return tradedata;}
//<summary>///string data to a replacement Traderecord instance///</summary>///<param name= " Tradedata "></param>///<returns></returns>private ienumerable<traderecord> ParseTrades (ienumerable<string> Tradedata) {var trades = new list<traderecord> (); var linecount = 1; foreach (var line in tradedata) {var fields = line. Split (new char[] {', '}); if (! Validatetradedata (Fields,linecount)) {continue; } var Traderecord = Maptradedatatotraderecord (fields); Trades. ADD (Traderecord); linecount++; } return trades;}
<summary>///Transaction Data Persistence///</summary>///<param name= "trades" ></param>private void Storetrades (ienumerable<traderecord> trades) {using (var connection = new SqlConnection ("datasource= (local); I nitial catalog=tradedatabase;integrated Security = True; ")) {connection. Open (); using (var transaction = connection. BeginTransaction ()) {foreach (var trade in trades) {var command = connection . CreateCommand (); Command. Transaction = Transaction; Command.commandtype = System.Data.CommandType.StoredProcedure; Command.commandtext = "Insert_trade"; Command. Parameters.addwithvalue ("@tradeamount", trade. Tradeamount); Command. Parameters.addwithvalue ("@tradeprice", trade. Tradeprice); } transaction.commit (); } connection. Close (); } Console.WriteLine ("INFO: {0} trades processed", trades. Count ());}
The implementation of the Parsetrades method is special and is responsible for converting string data to Traderecord instances, including validation of data and creation of instances. Similarly, these tasks are delegated to the Validatetradedata method and the Maptradedatatotraderecord method. The Validatetradedata method is responsible for validating the data, only valid data formats can continue to be assembled as Traderecord instances, and illegal data will be recorded in the log. The Validatetradedata method also delegates the work of logging to the LogMessage method, which is implemented as follows:
//<summary>///Verify transaction data///</summary>///<param name= "Fields" ></param >///<param name= "CurrentLine" ></param>///<returns></returns>private bool Validatetradedata (string[] Fields,int currentline) {if (fields). Length! = 3) {logmessage ("warn:line {0} malformed. only {1} fields found ", CurrentLine, fields. Length); return false; } int tradeamount; if (!int. TryParse (Fields[0], out Tradeamount)) {logmessage ("Warn:trade amount on line {0} not a valid integer: {1}", CU Rrentline, Fields[0]); return false; } decimal Tradeprice; if (!decimal. TryParse (Fields[1], out Tradeprice)) {logmessage (' warn:trade price ' on line {0} ' not a valid decimal: {1} ', Curr Entline, fields[1]); return false; } return true;}
/// <summary>/// 组装TradeRecord实例/// </summary>/// <param name="fields"></param>/// <returns></returns>private TradeRecord MapTradeDataToTradeRecord(string[] fields){ int tradeAmount = int.Parse(fields[0]); decimal tradePrice = decimal.Parse(fields[1]); var tradeRecord = new TradeRecord { TradeAmount = tradeAmount, TradePrice = tradePrice }; return tradeRecord;}
/// <summary>/// 记录日志/// </summary>/// <param name="message"></param>/// <param name="args"></param>private void LogMessage(string message,params object[] args){ Console.WriteLine(message,args);}
After refactoring the sharpness, the readability of the code is improved, but the adaptive ability does not improve much. Methods do only one thing, but the duties of a class are not singular. Also, continue to refactor the abstraction.
Refactoring abstraction
The first step in refactoring the TradeProcessor abstraction is to design one or a set of interfaces to perform the three highest-level tasks: reading data, processing data, and storing data.
public class TradeProcessor{ private readonly ITradeDataProvider tradeDataProvider; private readonly ITradeParser tradeParser; private readonly ITradeStorage tradeStorage; public TradeProcessor(ITradeDataProvider tradeDataProvider, ITradeParser tradeParser, ITradeStorage tradeStorage) { this.tradeDataProvider = tradeDataProvider; this.tradeParser = tradeParser; this.tradeStorage = tradeStorage; } public void ProcessTrades() { var tradeData = tradeDataProvider.GetTradeData(); var trades = tradeParser.Parse(tradeData); tradeStorage.Persist(trades); }}
The TradeProcessor class as a client is now unclear, and of course it should not be clear that the implementation details of the Streamtradedataprovider class can be obtained only through the Gettradedata method of the Itradedataprovider interface. The Tradeprocesso will no longer contain any details of the transaction process, instead the blueprint for the entire process .
For the implementation of the Itradeparser interface Simpleradeparser class, you can continue to extract more abstractions, after refactoring the UML diagram as follows. Itrademapper is responsible for mapping the data Format transformation, itradevalidator responsible for data validation.
public class TradeParser : ITradeParser{ private readonly ITradeValidator tradeValidator; private readonly ITradeMapper tradeMapper; public TradeParser(ITradeValidator tradeValidator, ITradeMapper tradeMapper) { this.tradeValidator = tradeValidator; this.tradeMapper = tradeMapper; } public IEnumerable<TradeRecord> Parse(IEnumerable<string> tradeData) { var trades = new List<TradeRecord>(); var lineCount = 1; foreach (var line in tradeData) { var fields = line.Split(new char[] { ',' }); if (!tradeValidator.Validate(fields, lineCount)) { continue; } var tradeRecord = tradeMapper.MapTradeDataToTradeRecord(fields); trades.Add(tradeRecord); lineCount++; } return trades; }}
A process similar to the one above that abstracts responsibilities into interfaces (and their implementations) is recursive . When examining each class, you need to determine whether it has multiple responsibilities. If it is, extract the abstraction until the class has only a single responsibility.
Refactoring the entire UML diagram after the completion of the abstraction is as follows:
It is important to note that logging, and so on, are generally dependent on third-party assemblies. for third-party references, it should be wrapped to convert to a first-party reference. such a dependency on a third party can be effectively controlled and, in the foreseeable future, it will be very easy to replace a third-party reference (just replace it), or the project may be full of direct dependencies on third-party references. Packaging is typically done through adapter mode, where the object adapter mode is used.
Note that the code implementation in the example is passed through the constructor for the dependent abstraction (interface), which means that the specific implementation of the object dependency has been determined at the time the object was created. There are two options: the client passes in the manually created dependent object (dependency injection of the poor edition), and the second is the use of the IOC container (dependency injection).
Changes in requirements
Refactoring the new version of the abstraction enables the following requirements enhancements without changing any existing classes. We can simulate requirements changes to experience the adaptive capabilities of the following code.
When validation rules for input data change
Modify the implementation of the Itradevalidator interface to reflect the latest rules.
When changing the logging mode, the window is printed to the file record mode
The FileLogger class that creates a file record implements the functionality of the file logging, replacing the specific implementation of the ILogger.
When a database has changed, such as replacing a relational database with a document database
The Create Mongotradestorage class uses MongoDB to store transaction data, replacing the specific implementation of Itradestorage.
At last
We found that code that conforms to the principle of a single responsibility is made up of more small, but more targeted, classes , and then the interface is abstracted and the responsibility for unrelated functions is delegated to the appropriate interfaces at runtime to achieve the goal. More small, but more targeted, classes can be used to complete tasks in a free-form combination , and each class is considered a small part, and the interface is the mold that produces the parts. When this part is no longer suitable for this task, you can consider replacing the part, provided that the replacement and before and after parts are produced by the same mold.
Smart people never put eggs in the same basket, but smarter people would consider putting them in different cars. We should be smarter, not every time the system has a problem, in the spaghetti code over and over again in Debug.
Reference
"C # Agile Development Practices"
Coderfocus
Public Number:
Statement: This article for Bo Master Learning sentiment Summary, the level is limited, if improper, welcome correction. If you think it's good, just click on the "recommend" button below, thanks for your support. Please specify the author and source of the reference.