Bob*_*way 7 c# architecture interface single-responsibility-principle solid-principles
在实现我的课程之后,目前处于实质性的重构工作中.我试图分解一些事情,更好地遵循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);
}
Run Code Online (Sandbox Code Playgroud)
哪个好.然而,实际上,我无法想象一个班级会在没有其他班级的情况下使用其中一个.因此将它们(及其实现)合并到一个类中似乎是有意义的.这也是有道理的,因为我们可以用一个解决方案(如数据库)替换这两个函数.
另一方面,似乎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
}
Run Code Online (Sandbox Code Playgroud)
反过来,看起来它似乎违反了SRP到一个荒谬的程度,而没有增加额外的两个接口.
这里有什么正确的方法?我应该有一个ICleaner界面,还是将它分成五个?
免责声明:我不是专家,人们可能不同意我的一些想法。很难提供直接的答案,因为这在很大程度上取决于幕后的情况。可能还有很多“正确”的答案,但这一切都取决于我们在这里缺少的大量信息。尽管如此,还没有人回答,我想我可以指出一些事情,可能会引导您走向正确的方向。
祝你好运!
您可以访问 Pluralsight 吗?仅仅为了经历封装和固体,购买快速一个月是完全值得的。我在经历它时遇到的“啊哈”时刻之一是查看您的方法签名,以帮助识别您可以提取的接口以帮助简化代码。忽略名字,只看参数。
我将尝试使用您提供的代码完成练习,但我需要在此过程中做出可能不正确的假设。
你IFieldCleaner有 3 个具有相同签名的方法:
void CleanAllPhoneFields(FileRow row, FileContents fc);
void MatchObscentities(FileRow row, FileContents fc);
void CleanEmailFields(FileRow row, FileContents fc);
Run Code Online (Sandbox Code Playgroud)
请注意这些方法都是完全相同的。这表明您可以提取具有 3 个实现的单个接口:
interface IFieldCleaner {
void Clean(FileRow row, FileContents fc);
}
class PhoneFieldCleaner : IFieldCleaner { }
class ObscentitiesFieldCleaner : IFieldCleaner { }
class EmailFieldCleaner : IFieldCleaner { }
Run Code Online (Sandbox Code Playgroud)
现在,这已经很好地将清理这些字段的责任划分为小类。
现在您还有其他几种清洁方法:
string CleanPhoneNumber(string phoneNumber);
string CleanAddress(StringBuilder inputAddress);
Run Code Online (Sandbox Code Playgroud)
它们非常相似,除了一个StringBuilder可能因为实现关心单独的行而需要一个?让我们将其切换到 astring并假设实现将处理行分割/解析,然后我们得到与之前相同的结果 - 具有相同签名的两个方法:
string CleanPhoneNumber(string phoneNumber);
string CleanAddress(string inputAddress);
Run Code Online (Sandbox Code Playgroud)
因此,按照我们之前的逻辑,我们还创建一个与清理字符串相关的接口:
interface IStringCleaner {
string Clean(string s);
}
class PhoneNumberStringCleaner : IStringCleaner { }
class AddressStringCleaner : IStringCleaner { }
Run Code Online (Sandbox Code Playgroud)
现在我们已经将这些职责分离到它们自己的实现中。
此时,我们只剩下一种方法可以解决:
void CleanFile(FileContents fc);
Run Code Online (Sandbox Code Playgroud)
我不确定这个方法的作用。为什么它是 的一部分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 { }
Run Code Online (Sandbox Code Playgroud)
这些对我来说都很相似,而且有些味道。根据您的原始方法名称(例如“清理所有字段”),您似乎可能正在使用循环来仅清理FileRow?中的某些列。但为什么还要依赖呢FileContents?同样,我看不到你的实现,所以我不太确定。也许您打算传递原始文件或数据库输入?
我也看不到你存储清理后的结果的位置——大多数以前的方法都返回void,这意味着调用该方法有一些副作用(即它是一个命令),而一些方法只返回一个干净的字符串(一个查询)。
因此,我假设总体目的是清理字符串,无论它们来自何处,并将它们存储回某个地方。如果是这样的话,我们可以进一步简化我们的模型:
interface IStringCleaner {
string Clean(string s);
}
class PhoneNumberStringCleaner : IStringCleaner { }
class AddressStringCleaner : IStringCleaner { }
class ObscenitiesStringCleaner : IStringCleaner { }
class EmailStringCleaner : IStringCleaner { }
Run Code Online (Sandbox Code Playgroud)
请注意,我们已经不再需要 a IFieldCleaner,因为这些字符串清理器仅处理要清理的输入字符串。
现在回到原来的上下文 - 似乎您可以从文件中获取数据并且这些文件可能有行?这些行包含我们需要清理其值的列。我们还需要坚持我们所做的清理更改。
因此,根据您提供的服务,我发现一些可能对我们有帮助的事情:
IRepository
IFileHelper
IFileWriter
IFileParser
Run Code Online (Sandbox Code Playgroud)
我的假设是,我们打算将清理后的字段保留回我不确定的地方,因为我看到了“存储库”,然后还有“文件写入器”。
不管怎样,我们知道我们最终需要将字符串从字段中取出,也许IFileParser会有所帮助?
interface IFileParser {
FileContents ReadContents(File file);
FileRow[] ReadRows(FileContents fc);
FileField ReadField(FileRow row, string column);
}
Run Code Online (Sandbox Code Playgroud)
这可能比它需要的更复杂——FileField可以负责存储字段值,因此大概您可以将所有这些重新组合在一起以形成一个FileContents持久化回磁盘的值。
所以,现在我们已经将最终目标(干净的东西)与输入的来源(文件、数据库等)以及我们如何持久化它(返回到文件、数据库等)分开。
您现在可以使用您的服务来编写您认为合适的流程。例如,您说目前您调用外部程序来清理地址?没问题:
class ExternalAddressStringCleaner : IStringCleaner {
// depend on whatever you need here
public string Clean(string s) {
// call external program
return cleanString;
}
}
Run Code Online (Sandbox Code Playgroud)
现在您切换到存储过程了吗?好吧,也没问题:
class DatabaseAddressStringCleaner : IStringCleaner {
// depend on database
DatabaseAddressStringCleaner(IRepository repository) {
}
string Clean(string s) {
// call your database sproc
return cleanString;
}
}
Run Code Online (Sandbox Code Playgroud)
很难为您的服务推荐想法,但您可以将其拆分为单独的较小服务(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);
}
}
Run Code Online (Sandbox Code Playgroud)
我对您的流程和代码做了很多假设,因此这可能比我假设的要复杂得多。如果没有所有信息,就很难提供指导——但仍然可以通过查看接口和名称来推断出一些基本的事情,我希望我已经证明了这一点。有时你只需要透过表面看到矩阵后面的 1 和 0,然后一切就都有意义了;)
对这么长的帖子表示歉意,但我完全理解你的想法。弄清楚如何重构事物是令人畏惧的、令人困惑的,而且似乎没有人能够提供帮助。希望这能为您在重构时提供一个起点。这是一项艰巨的任务,但只要坚持一些简单的准则和模式,根据您投入的努力,它最终可能会更容易维护。再次强调,我绝对推荐PluralSight 课程。