目前正在实现我的课程到处都是重要的重构工作。我试图分解一些事情,更好地遵循SRP,但我总是发现很难评估一个班级是否有改变的原因#34;。我希望这个实际的例子可以帮助我理解。
有问题的代码旨在清理数据。目前这里有两个独立的进程 - 我们通过使用通过代码调用的外部应用程序来清理地址数据。我们使用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
接口,还是将其拆分为五个?
答案 0 :(得分:2)
免责声明:我不是专家,人们可能不同意我的一些想法。提供一个直接的答案很难,因为它在很大程度上取决于幕后的内容。可能还有很多"对"答案但这一切都取决于我们在这里缺少的信息。尽管如此,还没有人回答,我认为有些事情我可以指出可以指导你朝着正确的方向发展。
祝你好运!
您可以访问Pluralsight吗?购买快速月份完全值得通过Encapsulation and SOLID。 " ah-hah"我经历过的时刻就是看看你的方法签名,以帮助识别你可以提取的接口,以帮助简化代码。忽略名称,只需查看参数。
我会尝试使用您提供的代码进行练习,但我需要在可能不正确的路上做出假设。
在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
并假设实现将处理行分割/解析,然后我们得到与以前相同的结果 - 具有相同签名的两个方法:
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 { }
这些似乎与我类似,闻起来有些气味。根据您的原始方法名称(如清除所有字段),您可能正在使用循环来清除FileRow
中的某些列?但为什么还要依赖FileContents
?同样,我无法看到您的实施,所以我不太确定。也许您打算传递原始文件或数据库输入?
我也看不到你存储清理结果的位置 - 你以前的大部分方法都返回void
,这意味着调用方法有一些副作用(即它' sa命令)而一些方法只返回一个干净的字符串(查询)。
所以我假设整体意图是清理字符串,无论它们来自哪里和也将它们存储回某处。如果是这样的话,我们可以进一步简化我们的模型:
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
和{{1或者简化你所采用的依赖关系。
既然你只有一个接口FileStoreService
,你就可以宣布你需要的清洁工,然后将它们换掉/改变它们。
IStringCleaner
我对你的流程和代码做了很多假设,所以这可能比我想象的要复杂得多。没有所有信息,提供指导很困难 - 但是通过查看界面和名称仍然可以推断出基本的东西,我希望我已经证明了这一点。有时你只需要看过表面就可以看到矩阵后面的1和0然后一切都有意义;)
长篇大论道歉,但我完全明白你来自哪里。弄清楚如何重构事物是令人生畏,令人困惑的,似乎没有人能够提供帮助。希望在重构时,这会让你在某处开始。这是一项艰巨的任务,但只是坚持一些简单的指导方针和模式,并且根据你付出的努力,最终可能会更容易维护。再次,我绝对推荐PluralSight课程。