我正在努力了解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();
}
我也一直在阅读this article,这更进了一步:
interface IModemConnection : IDisposable
{
IModemDataExchange Dial(string number);
}
interface IModemDataExchange
{
void Send(char c);
char Recv();
}
此时,我理解功能(Send / Recv
)和非功能(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;
}
我无法看到这样做是为了减少改变对课程的影响。它仍然需要重新编译,那么有什么好处呢?你从这样做中获得了什么,这对于生成高质量的代码非常重要?
答案 0 :(得分:10)
对我来说,上面的调制解调器示例似乎总是interface segregation principle而不是SRP的情况,但除了这一点之外。
在你关于Rectangle
的部分中,我认为你只是误解了它。 Martin使用Rectangle作为共享库的示例。如果GraphicalApplication
需要新方法或Rectangle
类中的语义更改,则会影响ComputationalGeometryApplication
,因为它们都“链接”到Rectangle
库。他说它违反了SRP,因为它负责定义渲染边界和代数概念。想象一下,如果GraphicalApplication
从DirectX更改为OpenGL,其中y坐标被反转。您可能希望更改Rectangle
上的某些内容以促进此操作,但您可能会在ComputationalGeometryApplication
中导致更改。
在我的工作中,我尝试遵循SOLID原则和TDD,并且我发现SRP使得为类编写测试变得简单并且还使课程集中。遵循SRP的类通常非常小,这降低了代码和依赖性的复杂性。在删除课程时,我会尝试确保课程要么“做一件事”,要么“协调两件(或更多件事)”。这使他们保持专注,并使他们改变的理由仅取决于他们所做的一件事,对我而言,这是SRP的重点。
答案 1 :(得分:3)
主要好处是显而易见的。通过拆分,您可以为您的模型提供更好的逻辑分组,从而使意图更清晰,维护更容易。
如果我必须创建IModemDataExchange的第二个实现,并且我想要使用它,我仍然需要更改Dial方法
是的必须,但那不是好处。一个好处是,当您对IModemDataExchange
接口本身进行任何修改时,您只需要更改接口的具体实现,而不是ConcreteModem
本身,这将使Dial
方法的订阅者的维护更容易。另一个好处是,即使你必须编写一个额外的IModemDataExchange
实现,然后在ConcreteModem
类中需要的更改被最小化,没有直接耦合。 通过分离责任,您可以最大限度地减少修改的副作用。
不需要重新编译不是这里的本质。从严格意义上讲,如果其中一个接口在另一个项目中呢?它节省了一个项目的重新编译。压力在于不需要在很多地方改变代码。当然,任何更改都需要重新编译。
答案 2 :(得分:1)
如果您使用抽象工厂,则无需更改ConcreteModem
。或者,如果您通过应在成功时创建的具体类型参数化通用Modem<TModemDataExchange>
(或通用Dial<TModemDataExchange>()
方法)。
这个想法是IModemConnection实现不依赖于有关IModeDataExchange实现的任何信息,除了它的名称。
继续前进,我会考虑采用以下方法:
interface IModemConnection : IDisposable
{
void Dial(string number);
}
interface IModemDataExchange
{
void Send(char c);
char Recv();
}
class ConcreteModemDataExchange : IModemDataExchange
{
ConcreteModemDataExchange(IModemDataExchange);
}
因此,要创建ConcreteModemDataExchange实例,您需要建立连接。仍然有可能断开连接的实例,但这是一个不同的故事。
作为一个副节点,我建议在失败时在Dial
中抛出异常。
答案 3 :(得分:1)
我不太了解调制解调器的工作原理,所以我努力想出一个有意义的例子。但是,请考虑一下:
分离了拨号逻辑,现在如果程序的其他部分只需要拨号,我们只能传入一个IModemConnection。 即使在使用依赖注入的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
}
}
现在,如果您想要更改调制解调器工作方式的某些方面(在这种情况下拨打号码的方式),您的更改将在具有单一责任(拨号)的拨号程序类中进行本地化。