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

C# 了解使用单一责任原则的实际好处,c#,solid-principles,single-responsibility-principle,C#,Solid Principles,Single Responsibility Principle,我试图理解SRP,但是,虽然我理解如何应用它背后的原因,但我并没有真正看到这样做的好处。考虑这个例子,取自Robert Martin的: 他建议将其分为两个接口: interface IModemConnection { void Dial(string number); void Hangup(); } interface IModemDataExchange { void Send(char c); char Recv(); } 我也一直在阅读,这更进一步

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

他建议将其分为两个接口:

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();
}
此时,我理解了功能性(
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; }
}
在这一点上,让我再次引用罗伯特·马丁(Robert Martin)的话,尽管他所说的例子与PDF不同:

其次,如果对GraphicalApplication的更改导致矩形因某种原因发生更改,该更改可能会迫使我们重新构建、重新测试和重新部署CalculationAlgeMetricyApplication。如果我们忘记这样做,该应用程序可能会以不可预知的方式中断

这是我不明白的。如果我必须创建
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;
}

我看不出这对减少变化对班级的影响起到了什么作用。它仍然需要重新编译,那么有什么好处呢?这样做对生成高质量代码非常重要,您从中获得了什么好处?

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

其思想是IModemConnection实现不依赖于除名称以外的任何有关IModeDataExchange实现的信息

<>向前,我会考虑以下方法:

interface IModemConnection : IDisposable
{
    void Dial(string number);
}

interface IModemDataExchange
{
    void Send(char c);
    char Recv();
}

class ConcreteModemDataExchange : IModemDataExchange
{
    ConcreteModemDataExchange(IModemDataExchange);
}
因此,要创建ConcreteModemDataExchange实例,您需要有一个连接。仍然存在断开连接实例的可能性,但情况不同


作为侧节点,我建议在出现故障时在
拨号中抛出异常。

对我来说,上面的调制解调器示例似乎总是适用于而不是SRP,但这不是重点

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


在我的工作中,我试图遵循这些原则和TDD,我发现SRP使编写类测试变得简单,并且使类保持专注。遵循SRP的类通常非常小,这降低了代码和依赖项的复杂性。在删除类时,我会尽量确保一个类是“做一件事”,还是“协调两件(或更多)事情”。这让他们集中精力,让他们改变的理由只取决于他们做的一件事,这对我来说就是SRP的重点。

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

如果我必须创建IModemDataExchange的第二个实现,并且我想利用它,那么我仍然必须更改拨号方法

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


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

我对调制解调器的工作原理知之甚少,所以我努力想出一个有意义的例子。但是,考虑一下:

分离了拨号逻辑之后,现在如果程序的其他部分只需要拨号,我们可以只传入一个IModemConnection。 这甚至在调制解调器类本身中也很有用,使用依赖项注入:

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
    }
}


现在,如果您想更改调制解调器工作方式的某些方面(本例中的拨号方式),您所做的更改将在一个具有单一职责(拨号)的拨号器类中进行本地化。

我在连接引号和示例代码之间的点时遇到问题。这句话似乎引用了两个具有共享代码库的不同应用程序。@Gusdor它确实引用了两个不同的应用程序,但从我迄今为止读到的关于SRP的所有内容来看,它似乎是为了提高代码库的灵活性,同时减少更改的影响
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
    }
}