单一职责原则(C#)
翻译、参考:https://www.dotnetcurry.com/software-gardening/1148/solid-single-responsibility-principle |
在用面向对象编程很多年后,我发现很多程序员总是违反这个原则。
是的,我们写类和方法,企图以面向过程的方式将逻辑写到同一个方法内,而不是把它们拆分成很多单一的类,每个只负责做一件事。
这里有一些典型的示例代码
public class CsvFileProcessor { public void Process(string filename) { TextReader tr = new StreamReader(filename); tr.ReadToEnd(); tr.Close(); var conn = new SqlConnection("server=(local);integrated security=sspi;database=SRP"); conn.Open(); string[] lines = tr.ToString().Split(new string[] {@"\r\l"}, StringSplitOptions.RemoveEmptyEntries); foreach( string line in lines) { string[] columns = line.Split(new string[] {","}, StringSplitOptions.RemoveEmptyEntries); var command = conn.CreateCommand(); command.CommandText = "INSERT INTO People (FirstName, LastName, Email) VALUES (@FirstName, @LastName, @Email)"; command.Parameters.AddWithValue("@FirstName", columns[0]); command.Parameters.AddWithValue("@LastName", columns[1]); command.Parameters.AddWithValue("@Email", columns[2]); command.ExecuteNonQuery(); } conn.Close(); } }
这个类做了几件事?一件?两件?三件?或者更多?
你可能会忍不住说一个。也就是说,这个类处理一个CSV文件。
从另一个角度看这个类。如何进行单元测试?
这并不容易。
如果有其他东西,比如数据验证和错误日志记录,该怎么办?那么你如何进行单元测试呢?
事实是,这个类做了三个事情:
- 读取CSV文件
- 转换CSV文件
- 储存数据
在类中做很多事情是不好的,不仅因为很难进行单元测试,而且它增加了引入bug的几率。
如果您在解析部分更改了代码,并且添加了一个错误,那么读取和存储也会中断。而且,因为单元测试将不存在或非常复杂,所以跟踪和修复错误也需要更长的时间。
添加单一责任原则
为了解决这个问题,我们需要将代码分解成单独的部分。
您可能认为可以只使用三个方法,每个方法对应一个功能块。但是回到SRP的定义。它说一个类应该只有一个目的。
因此,我们需要三个类来完成这项工作。好的,我们马上会看到更多。
修复这段代码的方法是通过代码重构。最初,我们将把每个功能块放入它自己的方法中。
public class CsvFileProcessor { public void Process(string filename) { var csvData = ReadCsv(filename); var parsedData = ParseCsv(csvData); StoreCsvData(parsedData); } public string ReadCsv(string filename) { TextReader tr = new StreamReader(filename); tr.ReadToEnd(); tr.Close(); return tr.ToString(); } public string[] ParseCsv(string csvData) { return csvData.ToString().Split(new string[] { @"\r\l" }, StringSplitOptions.RemoveEmptyEntries); } public void StoreCsvData(string[] csvData) { var conn = new SqlConnection("server=(local);integrated security=sspi;database=SRP"); conn.Open(); foreach (string line in csvData) { string[] columns = line.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries); var command = conn.CreateCommand(); command.CommandText = "INSERT INTO People (FirstName, LastName, Email) VALUES (@FirstName, @LastName, @Email)"; command.Parameters.AddWithValue("@FirstName", columns[0]); command.Parameters.AddWithValue("@LastName", columns[1]); command.Parameters.AddWithValue("@Email", columns[2]); command.ExecuteNonQuery(); } conn.Close(); } }
正如你所看到的,事情仍然不太对。
我们用 ParseCsv() 这个方法 CSV文件转换为一行一行的 ,但是StoreCsvData() 这个方法是将行转换为一列一列的。
解决这个问题的方法是 采用ContactDTO 来储存每一行的数据。
下一步是添加DTO,但我将跳过一个步骤,并将每个方法分解到它自己的类中。
但是我同时也预想到一些问题,如果这些数据不是来自CSV呢?如果它来自XML、JSON或者其它数据格式怎么办?
你用 接口 解决了这个问题
public interface IContactDataProvider { string Read(); } public interface IContactParser { IList<ContactDTO> Parse(string contactList); } public interface IContactWriter { void Write(IList<ContactDTO> contactData); } public class ContactProcessor { public void Process(IContactDataProvider cdp, IContactParser cp, IContactWriter cw) { var providedData = cdp.Read(); var parsedData = cp.Parse(providedData); cw.Write(parsedData); } } public class CSVContactDataProvider : IContactDataProvider { private readonly string _filename; public CSVContactDataProvider(string filename) { _filename = filename; } public string Read() { TextReader tr = new StreamReader(_filename); tr.ReadToEnd(); tr.Close(); return tr.ToString(); } } public class CSVContactParser : IContactParser { public IList<ContactDTO> Parse(string csvData) { IList<ContactDTO> contacts = new List<ContactDTO>(); string[] lines = csvData.Split(new string[] { @"\r\l" }, StringSplitOptions.RemoveEmptyEntries); foreach (string line in lines) { string[] columns = line.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries); var contact = new ContactDTO { FirstName = columns[0], LastName = columns[1], Email = columns[2] }; contacts.Add(contact); } return contacts; } } public class ADOContactWriter : IContactWriter { public void Write(IList<ContactDTO> contacts) { var conn = new SqlConnection("server=(local);integrated security=sspi;database=SRP"); conn.Open(); foreach (var contact in contacts) { var command = conn.CreateCommand(); command.CommandText = "INSERT INTO People (FirstName, LastName, Email) VALUES (@FirstName, @LastName, @Email)"; command.Parameters.AddWithValue("@FirstName", contact.FirstName); command.Parameters.AddWithValue("@LastName", contact.LastName); command.Parameters.AddWithValue("@Email", contact.Email); command.ExecuteNonQuery(); } conn.Close(); } } public class ContactDTO { public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } }
我们使用通用的方法名Read,Parse,Write,因为我们不知道我们将得到什么类型的数据。
现在我们可以轻松地对这段代码进行单元测试。
我们还可以轻松地修改解析代码,如果引入新的错误,它不会影响读取和写入代码。
另一个好处是我们实现了 低耦合。
这就是结果。我们采用了相当常见(。。。。常见吗?)的面向过程代码,并使用单一责任原则对其进行重构。
下次查看一个类时,问问自己是否可以重构它以使用SRP。应用SOLID的S将帮助您的代码变得绿色、繁茂和有活力,您正在走向拥有一个软件花园的道路上。
把软件开发比作建筑,我们可以看出软件是坚固的,而且很难改变。相反,我们应该把软件开发比作园艺,因为花园总是在变化的。
软件园艺包含实践和工具,可以帮助您为您的软件创建尽可能好的花园,花最少的努力让它增长和改变。了解更多关于什么是软件园艺。