单一职责原则(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文件。

从另一个角度看这个类。如何进行单元测试?

这并不容易。

如果有其他东西,比如数据验证和错误日志记录,该怎么办?那么你如何进行单元测试呢?

事实是,这个类做了三个事情:

  1. 读取CSV文件
  2. 转换CSV文件
  3. 储存数据

在类中做很多事情是不好的,不仅因为很难进行单元测试,而且它增加了引入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将帮助您的代码变得绿色、繁茂和有活力,您正在走向拥有一个软件花园的道路上。

把软件开发比作建筑,我们可以看出软件是坚固的,而且很难改变。相反,我们应该把软件开发比作园艺,因为花园总是在变化的。

 软件园艺包含实践和工具,可以帮助您为您的软件创建尽可能好的花园,花最少的努力让它增长和改变。了解更多关于什么是软件园艺。

 

posted @ 2020-12-19 13:02  超难微猫  阅读(172)  评论(0编辑  收藏  举报