了解使用单一责任原则的实际好处

我正在努力理解SRP,但是,虽然我理解了如何应用它的原因,但我并没有真正看到这样做的好处。 考虑这个例子,取自Robert Martin的SRP PDF :

interface IModem { void Dial(string number); void Hangup(); void Send(char c); char Recv(); } 

他建议将其分为两个界面:

 interface IModemConnection { void Dial(string number); void Hangup(); } interface IModemDataExchange { void Send(char c); char Recv(); } 

我也一直在阅读这篇文章 ,它更进了一步:

 interface IModemConnection : IDisposable { IModemDataExchange Dial(string number); } interface IModemDataExchange { void Send(char c); char Recv(); } 

在这一点上,我理解function( Send / Recv )和非function( Dial / Hangup )方面的含义,但我没有看到在这个例子中分离它们的好处。 考虑到这个基本实现:

 class ConcreteModem : IModemConnection { public IModemDataExchange Dial(string number) { if (connection is successful) { return new ConcreteModemDataExchange(); } return null; } public void Dispose() { // } public bool IsConnected { get; private set; } } 

在这一点上,让我再次引用罗伯特马丁,即使他正在谈论与PDF不同的例子:

其次,如果对GraphicalApplication的更改导致Rectangle由于某种原因而发生更改 ,则该更改可能会强制我们重建,重新测试和重新部署ComputationalGeometryApplication 。 如果我们忘记这样做,该应用程序可能会以不可预测的方式破坏。

这是我不明白的。 如果我必须创建IModemDataExchange的第二个实现,并且我想使用它,我仍然需要更改Dial方法,这意味着该类还需要重新编译:

 public IModemDataExchange Dial(string number) { if (some condition is met) { return new ConcreteModemDataExchange(); } else if (another condition is met) { return new AnotherConcreteModemDataExchange(); } return null; } 

我无法看到这样做是为了减少改变对class级的影响。 它仍然需要重新编译,那么有什么好处呢? 你从这样做中获得了什么,这对于生成高质量的代码非常重要?

对我来说,上面的调制解调器示例似乎总是看起来像接口隔离原则而不是SRP,但除此之外。

在关于Rectangle的部分中,我认为你只是误解了它。 Martin使用Rectangle作为共享库的示例。 如果GraphicalApplicationRectangle类中需要新方法或更改语义,则会影响ComputationalGeometryApplication因为它们都“链接”到Rectangle库。 他说它违反了SRP,因为它负责定义渲染边界和代数概念。 想象一下,如果GraphicalApplication从DirectX更改为OpenGL,其中y坐标被反转。 您可能希望更改Rectangle上的某些内容以促进此操作,但您可能会在ComputationalGeometryApplication导致更改。

在我的工作中,我尝试遵循SOLID原则和TDD,并且我发现SRP使得为类编写测试变得简单并且还使类集中注意力。 遵循SRP的类通常非常小,这降低了代码和依赖性的复杂性。 在删除课程时,我会尝试确保课程“做一件事”,或“协调两件(或更多件事)”。 这使他们专注并使他们改变的原因仅取决于他们所做的一件事,对我而言,这是SRP的重点。

主要好处是显而易见的。 通过拆分,您可以为您的模型提供更好的逻辑分组,从而使意图更清晰,维护更容易。

如果我必须创建IModemDataExchange的第二个实现,并且我想使用它,我仍然需要更改Dial方法

是必须的,但这不是那里的好处。 一个好处是,当您对IModemDataExchange接口本身进行任何修改时,您只需要更改接口的具体实现,而不是ConcreteModem本身,这将使Dial方法的订户的维护更容易。 另一个好处是,现在即使你必须编写一个额外的IModemDataExchange实现,然后更改它将在ConcreteModem类中最小化,也没有直接耦合。 通过分离责任,您可以将修改的副作用降至最低。

不需要重新编译不是这里的本质。 从严格意义上说,如果其中一个接口在另一个项目中呢? 它节省了一个项目的重新编译。 压力在于不需要在很多地方改变代码。 当然,任何更改都需要重新编译。

如果使用抽象工厂,则无需更改ConcreteModem 。 或者,如果您通过应在成功时创建的具体类型参数化通用Modem (或通用Dial()方法)。

我们的想法是IModemConnection实现不依赖于有关IModeDataExchange实现的任何信息,除了它的名称。

outlook未来,我会考虑采用以下方法:

 interface IModemConnection : IDisposable { void Dial(string number); } interface IModemDataExchange { void Send(char c); char Recv(); } class ConcreteModemDataExchange : IModemDataExchange { ConcreteModemDataExchange(IModemDataExchange); } 

因此,要创建ConcreteModemDataExchange实例,您需要建立连接。 仍然有可能断开连接的实例,但这是一个不同的故事。

作为一个副节点,我建议在Dial失败时抛出exception。

我不太了解调制解调器的工作原理,所以我努力想出一个有意义的例子。 但是,考虑一下:

分离了拨号逻辑,现在如果程序的其他部分只需要拨号,我们就可以传入一个IModemConnection。 即使在使用dependency injection的Modem类本身中,这也很有用:

 public class Modem : IModemConnection, IModemDataExchange { public IModemConnection Dialer {get; private set;} public Modem(IModemConnection Dialer) { this.Dialer=Dialer; } public void Dial(string number) { Dialer.Dial(number); } public void Hangup() { Dialer.Hangup(); } // .... implement IModemDataExchange } 

现在你可以:

 public class DigitalDialer : IModemConnection { public void Dial(string number) { Console.WriteLine("beep beep"); } public void Hangup() { //hangup } } 

 public class AnalogDialer : IModemConnection { public void Dial(string number) { Console.WriteLine("do you even remember these?"); } public void Hangup() { //hangup } } 

现在,如果您想要更改调制解调器工作方式的某些方面(在这种情况下拨打号码的方式),您的更改将在具有单一职责(拨号)的拨号程序类中进行本地化。