SOLID是一组最佳编码实践的首字母缩写程序员
同时应用这些最佳实践,能够提高代码适应变动的能力。可是凡事要有度,过分使用虽然可让代码有很高的自适应能力,可是会致使层次粒度太小而难以理解或使用,还会影响代码的可读性。数据库
单一职责原则(Single Responsibility principle)要求开发人员编写的代码有且只有一个变动理由。若是一个类有多个变动理由,那么它就具备多个职责。这个时候就要进行重构,将多职责类拆解为多个单职责类。经过委托和抽象,包含多个变动理由的类应该把一个或多个职责委托给其余的单职责类。c#
以前看过一篇文章,讲为何面向对象比面向过程更能适应业务变化?从其中也能够看出单一职责原则带来的好处,职责明确,只须要修改局部,不会对外部形成影响,影响能够控制在足以掌控的范围内。微信
对象将需求用类一个个隔开,就像用储物箱把东西一个个封装起来同样,需求变了,分几种状况,最严重的是大变,那么每一个储物箱都要打开改,这种方法就不见得有好处;可是这种状况发生几率比较小,大部分需求变化都是局限在一两个储物箱中,那么咱们只要打开这两个储物箱修改就能够,不会影响其余储物柜了。app
而面向过程是把全部东西都放在一个大储物箱中,修改某个部分之后,会引发其余部分不稳定,一个BUG修复,引起新的无数BUG,最后程序员陷入焦头烂额。ide
咱们一段代码为例,经过重构的过程,体会一下单一职责原则的好处。函数
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 on 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 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);
}
}
复制代码
上面的代码不只仅是一个类拥有太多的职责,也是一个单一方法拥有太多的职责。仔细分析一下代码,原始的ProcessTrades方法代码能够分为三个部分:从流中读取交易数据、将字符串数据转换为TradeRecord实例、将交易数据持久化到永久存储。this
单一职责原则能够表如今类和方法层面上。从方法的层面上,一个方法只能作一件事情;从类的层面上,一个类只能有一个职责。不然,就要对类和方法进行拆分重构。对于方法的拆分重构,目标是清晰度,能提高代码的可读性,可是不能提高代码的自适应能力。要提高代码的自适应能力,就要作抽象,将每一个职责划分到不一样的类中。编码
上面咱们分析过ProcessTrades方法代码能够分为三个部分,咱们能够将每一个部分提取为一个方法,将工做委托给这些方法,这样ProcessTrades方法就变成了:spa
public void ProcessTrade(Stream stream)
{
var lines = ReadTradeData(stream);
var trades = ParseTrades(lines);
StoreTrades(trades);
}
复制代码
提取的方法实现分别为:
/// <summary>
/// 从流中读取交易数据
/// </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>
/// 将字符串数据装换位TradeRecord实例
/// </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>
/// 交易数据持久化
/// </summary>
/// <param name="trades"></param>
private void StoreTrades(IEnumerable<TradeRecord> trades)
{
using (var connection = new SqlConnection("DataSource=(local);Initial 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());
}
复制代码
其中ParseTrades方法的实现比较特殊,负责的是将字符串数据转换为TradeRecord实例,包含数据的验证和实例的建立。同理,将这些工做委托给了ValidateTradeData方法和MapTradeDataToTradeRecord方法。ValidateTradeData方法负责数据的验证,只有合法的数据格式才能继续组装为TradeRecord实例,不合法的数据将会被记录在日志中。ValidateTradeData方法将记录日志的工做也委托给了LogMessage方法,具体实现以下:
/// <summary>
/// 验证交易数据
/// </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}", currentLine, 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}", currentLine, 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);
}
复制代码
重构清晰度以后,代码的可读性提升了,可是自适应能力并无提高多少。方法作到了只作一件事情,可是类的职责并不单一。还因此,要继续重构抽象。
重构TradeProcessor抽象的第一步就是设计一个或一组接口来执行三个最高级别的任务:读取数据、处理数据和存储数据。
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);
}
}
复制代码
做为客户端的TradeProcessor类如今不清楚,固然也不该该清楚StreamTradeDataProvider类的实现细节,只能经过ITradeDataProvider接口的GetTradeData方法来获取数据。TradeProcesso将再也不包含任何交易流程处理的细节实现,取而代之的是整个流程的蓝图。
对于ITradeparser接口的实现Simpleradeparser类,还能够继续提取更多的抽象,重构以后的UML图以下。ITradeMapper负责数据格式的映射转换,ITradeValidator负责数据的验证。
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;
}
}
复制代码
相似于上面将职责抽象为接口(及其实现)的过程是递归的。在检视每一个类时,你须要判断它是否具有多重职责。若是是,提取抽象直到该类只具有单个职责。
重构抽象完成后的整个UML图以下:
须要注意的是,记录日志等通常须要依赖第三方程序集。对于第三方引用,应该经过包装的方式转换为第一方引用。这样对于第三方的依赖能够被有效控制,在可预见的未来,替换第三方引用将会变得十分容易(只须要替换一处),不然项目中可能处处是对第三方引用的直接依赖。包装通常是经过适配器模式,此处使用的是对象适配器模式。
注意,示例中的代码实现对于依赖的抽象(接口),都是经过构造函数传入的,也就是说对象依赖的具体实如今对象建立时就已经肯定了。有两种选择,一是客户端传入手动建立的依赖对象(穷人版的依赖注入),二是使用IOC容器(依赖注入)。
重构抽象后的新版本能在无需改变任何现有类的状况下实现如下的需求加强功能。咱们能够模拟需求变动来体验如下代码的自适应能力。
当输入数据的验证规则变化时
修改ITradeValidator接口的实现以反映最新的规则。
当更改日志记录方式时,由窗口打印方式改成文件记录方式
建立一个文件记录的FileLogger类实现文件记录日志的功能,替换ILogger的具体实现。
当数据库发生了变化,例如使用文档数据库替换关系型数据库
建立MongoTradeStorage类使用MongoDB存储交易数据,替换ITradeStorage的具体实现。
咱们发现,符合单一职责原则的代码会由更多的小规模但目标更明确的类组成,而后经过接口抽象以及在运行时将无关功能的责任委托给相应的接口来达成目标的。更多的小规模但目标更明确的类经过自由组合的形式配合完成任务,每一个类均可以看作是一个小零件,而接口就是生产这些零件的模具。当这个零件再也不适合完成此任务时,就能够考虑替换掉这个零件,前提是替换先后的零件都是经过同一个模具生产出来的。
聪明的人历来不会把鸡蛋放到同一个篮子里,可是更聪明的人会考虑把这些篮子放到不一样的车上。咱们应该作更聪明的人,而不是每次系统出现问题时,在乎大利面条式的代码里一遍又一遍的DeBug。
《C#敏捷开发实践》