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课程. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |