c# – 当SRP违规似乎将复杂性传递到链中时,正确的方法

前端之家收集整理的这篇文章主要介绍了c# – 当SRP违规似乎将复杂性传递到链中时,正确的方法前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
在实现我的课程之后,目前处于实质性的重构工作中.我试图分解一些事情,更好地遵循SRP,但我总是发现很难评估一个班级是否有“改变的一个理由”的格言.我希望这个实际的例子可以帮助我理解.

有问题的代码旨在清理数据.目前这里有两个独立的进程 – 我们通过使用通过代码调用的外部应用程序来清理地址数据.我们使用C#中的内部算法清理其他数据字段.

当我被告知我们可能希望将来更改这两个进程时,这个重构就开始了 – 例如使用数据库存储过程来执行这两个作业而不是C#代码和外部应用程序.所以我的第一直觉是将这两个函数隐藏在接口后面(FileRow和FileContents只是DTO):

public interface IAddressCleaner
{
    string CleanAddress(StringBuilder inputAddress);
    void CleanFile(FileContents fc);
}

public interface IFieldCleaner
{
    string CleanPhoneNumber(string phoneToClean);
    void CleanAllPhoneFields(FileRow row,FileContents fc);
    void MatchObscentities(FileRow row,FileContents fc);
    void CleanEmailFields(FileRow row,FileContents fc);
}

哪个好.然而,实际上,我无法想象一个班级会在没有其他班级的情况下使用其中一个.因此将它们(及其实现)合并到一个类中似乎是有意义的.考虑到我们可能用一个解决方案(如数据库)替换这两个函数,这也是有意义的.

另一方面,似乎IFieldCleaner已经违反了SRP,因为它正在做三件事:清理电话号码,发送电子邮件和寻找粗鲁的单词,所有这些都是逻辑上不同的过程.因此似乎有理由将其拆分为IPhoneCleaner,IObscenityMatcher和IEmailCleaner.

对后一种方法特别困扰的是这些类在服务中使用,该服务已经具有愚蠢的接口依赖性:

public class ReadFileService : IExecutableObject
{
    private ILogger _log;
    private IRepository _rep;
    private IFileHelper _fileHelp;
    private IFieldCleaner _fieldCleaner;
    private IFileParser _fileParser;
    private IFileWriter _fileWriter;
    private IEmailService _mailService;
    private IAddressCleaner _addressCleaner;

    public ReadFileService(ILogger log,IRepository rep,IFileHelper fileHelp,IFileParser fileParse,IFileWriter fileWrite,IEmailService email,IAddressCleaner addressCleaner)
    {
        // assign to privates
    }

  // functions
}

而且,反过来看,它似乎也违反了SRP到一个荒谬的程度,而没有增加额外的两个接口.

这里的方法是什么?我应该有一个ICleaner接口,还是将其分成五个?

解决方法

免责声明:我不是专家,人们可能不同意我的一些想法.提供一个直接的答案是很难的,因为它在很大程度上取决于幕后的内容.可能还有很多“正确”的答案,但这完全取决于我们在这里缺少的信息.尽管如此,还没有人回答过,我认为有些事情我可以指出可以指导你朝着正确的方向前进.

祝你好运!

你有权使用Pluralsight吗?购买快速的月份是完全值得的,只需要经历Encapsulation and SOLID.我经历过的“啊哈”时刻之一就是看看你的方法签名,以帮助识别你可以提取的界面,以帮助简化代码.忽略名称,只需查看参数即可.

我将尝试使用您提供的代码进行练习,但我需要在可能不正确的方式上做出假设.

在IFieldCleaner上,您有3个具有相同签名的方法

void CleanAllPhoneFields(FileRow row,FileContents fc);
void MatchObscentities(FileRow row,FileContents fc);
void CleanEmailFields(FileRow row,FileContents fc);

注意这些方法是如何完全相同的.这表明您可以使用3个实现提取单个接口:

interface IFieldCleaner {
  void Clean(FileRow row,FileContents fc);
}
class PhoneFieldCleaner : IFieldCleaner { }
class ObscentitiesFieldCleaner : IFieldCleaner { }
class EmailFieldCleaner : IFieldCleaner { }

现在,这很好地将清理这些田地的责任分成了一口大小的课程.

现在您还有其他一些清洁方法

string CleanPhoneNumber(string phoneNumber);
string CleanAddress(StringBuilder inputAddress);

这些是非常相似的,除了一个采用StringBuilder可能是因为实现关心各个行?让我们把它切换成一个字符串并假设实现将处理行拆分/解析,然后我们得到与以前相同的结果 – 具有相同签名的两个方法

string CleanPhoneNumber(string phoneNumber);
string CleanAddress(string inputAddress);

因此,按照我们之前的逻辑,让我们创建一个与清理字符串相关的接口:

interface IStringCleaner {
  string Clean(string s);
}

class PhoneNumberStringCleaner : IStringCleaner { }
class AddressStringCleaner : IStringCleaner { }

现在我们将这些职责分离到他们自己的实现中.

在这一点上,我们只有一个方法可以解决

void CleanFile(FileContents fc);

我不确定这种方法是做什么的.为什么它是IAddressCleaner的一部分?因此,现在我将其排除在讨论之外 – 也许这是一种读取文件,查找地址,然后清理它的方法,在这种情况下,您可以通过调用我们的新AddressStringCleaner来完成.

那么让我们看看到目前为止我们所处的位置.

interface IFieldCleaner {
  void Clean(FileRow row,FileContents fc);
}

class PhoneFieldCleaner : IFieldCleaner { }
class ObscentitiesFieldCleaner : IFieldCleaner { }
class EmailFieldCleaner : IFieldCleaner { }

interface IStringCleaner {
  string Clean(string s);
}

class PhoneNumberStringCleaner : IStringCleaner { }
class AddressStringCleaner : IStringCleaner { }

这些似乎都与我相似,闻起来有些气味.根据您的原始方法名称(如CleanAllFields),您可能正在使用循环来清除FileRow中的某些列?但为什么还要依赖FileContents?再说一次,我看不到你的实现,所以我不太确定.也许您打算传递原始文件数据库输入?

我也无法看到存储清理结果的位置 – 大多数先前的方法返回void,这意味着调用方法有一些副作用(即它是一个Command),而一些方法只返回一个干净的字符串(一个Query).

因此,我将假设整体意图是清理字符串,无论它们来自何处,并将它们存储在某处.如果是这种情况,我们可以进一步简化我们的模型:

interface IStringCleaner {
  string Clean(string s);
}

class PhoneNumberStringCleaner : IStringCleaner { }
class AddressStringCleaner : IStringCleaner { }
class ObscenitiesStringCleaner : IStringCleaner { }
class EmailStringCleaner : IStringCleaner { }

请注意,我们已经删除了对IFieldCleaner的需求,因为这些字符串清除程序只处理要清理的输入字符串.

现在回到原始上下文 – 似乎您可以从文件获取数据并且这些文件可能有行?这些行包含我们需要清理其值的列.我们还需要坚持我们所做的清洁改变.

因此,基于您提供的服务,我看到了一些可能对我们有帮助的事情:

IRepository
IFileHelper
IFileWriter
IFileParser

我的假设是,我们打算将清理过的字段保留回来 – 我不确定的地方,我看到的是“存储库”,然后是“FileWriter”.

无论如何,我们知道我们需要最终从字段中获取字符串,也许IFileParser可以帮忙吗?

interface IFileParser {
  FileContents ReadContents(File file);
  FileRow[] ReadRows(FileContents fc);
  FileField ReadField(FileRow row,string column);
}

这可能比它需要的更复杂 – FileField可以负责存储字段值,因此可能你可以将所有这些组合在一起形成一个FileContents来保存回磁盘.

所以,现在我们已经将输入来自(文件,数据库等)的最终目标(干净的东西)与我们如何持久化(返回文件,数据库等)分开.

您现在可以使用您的服务根据需要撰写此流程.例如,您说当前您调用外部程序来清理地址?没问题:

class ExternalAddressStringCleaner : IStringCleaner {
  // depend on whatever you need here

  public string Clean(string s) {
    // call external program
    return cleanString;
  }
}

现在切换到存储过程?好的,也没问题:

class DatabaseAddressStringCleaner : IStringCleaner {

  // depend on database
  DatabaseAddressStringCleaner(IRepository repository) {
  }

  string Clean(string s) {
    // call your database sproc
    return cleanString;
  }
}

很难为您的服务推荐想法 – 但您可以将其拆分为单独的较小服务(FileReaderService,FileCleaningService和FileStoreService)或简化您所采用的依赖关系.

既然你只有一个接口IStringCleaner,你可以只声明你需要的清洁工并将它们换掉/改变它们.

public FileCleanerService {
  private IStringCleaner _addressCleaner;
  private IStringCleaner _phoneCleaner;
  private IStringCleaner _obscenityCleaner;
  private IStringCleaner _emailCleaner;

  ctor(IFileParser parser,/* deps */) {
    _parser = parser;

    _addressCleaner = new ExternalAddressStringCleaner(/* deps */);
    _phoneCleaner = new PhoneStringCleaner();
    _obscenityCleaner = new ObscenityStringCleaner();
    _emailCleaner = new EmailStringCleaner();
  }

  public void Clean(FileContents fc) {

    foreach(var row in _parser.ReadRows(fc)) {
      var address = _parser.ReadField(row,"Address");
      var phone   = _parser.ReadField(row,"Phone");
      var post    = _parser.ReadField(row,"PostContent");
      var email   = _parser.ReadField(row,"Email");

      // assumes you want to write back to the field?
      // handle this however you want
      address.Value = _addressCleaner.Clean(address.Value);
      phone.Value = _phoneCleaner.Clean(phone.Value);
      post.Value = _obscenityCleaner.Clean(post.Value);
      email.Value = _emailCleaner.Clean(email.Value);
    }

}

我对你的过程和代码做了很多假设,所以这可能比我想象的要复杂得多.没有所有信息,很难提供指导 – 但是通过查看界面和名称仍然可以推断出基本的东西,我希望我已经证明了这一点.有时你只需要看过表面看到矩阵后面的1和0然后它们都有意义;)

为长篇文章道歉,但我完全理解你来自哪里.弄清楚如何重构事物是令人生畏,令人困惑的,似乎没有人能够提供帮助.希望这能让你在重构时有所作为.这是一项艰巨的任务,但只是坚持一些简单的指导方针和模式,并且根据你投入的努力,最终可能会更容易维护.再次,我绝对推荐PluralSight课程.

原文链接:https://www.f2er.com/csharp/100317.html

猜你在找的C#相关文章