面向对象设计的五项原则

S.O.L.I.D:面向对象设计的五项原则

原文地址 https://www.digitalocean.com/community/conceptual_articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design

SOLID 是由罗伯特·C·马丁在 21 世纪早期引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则

将这些原理结合在一起,可使程序员轻松开发易于维护和扩展的软件。它们还使开发人员可以轻松避免冗余代码,轻松重构代码,并且还属于自适应软件开发和敏捷软件开发的一部分。

S.O.L.I.D 表示:

  • S - Single-responsiblity principle 单一功能原则
  • O - Open-closed principle 开闭原则
  • L - Liskov substitution principle 里氏替换原则
  • I - Interface segregation principle 接口隔离原则
  • D - Dependency Inversion Principle 依赖反转原则

让我们逐一查看每个原则,以了解 S.O.L.I.D 为什么可以帮助我们成为更好的开发人员。

单一功能原则

A class should have one and only one reason to change, meaning that a class should have only one job.

S.R.P是单一功能原则的缩写,它规定每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来。所有它的(这个类的)服务都应该严密的和该功能平行(功能平行,意味着没有依赖)。

例如,假设我们有一些形状,我们想对形状的所有面积求和。

public class Circle
{
    public double Radius;

    public Circle(double radius)
    {
        Radius = radius;
    }
}

public class Square
{
    public  double Length;

    public Square(double length)
    {
        Length = length;
    }
}

首先,我们创建形状类,并让构造函数设置所需的参数。接下来,我们继续创建 AreaCalculator 类,然后编写逻辑以计算所有提供的形状的面积。

public class AreaCalculator
{
    protected object[] Shapes;

    public AreaCalculator(object[] shapes)
    {
        Shapes = shapes;
    }

    public double Sum()
    {
        //计算面积和
        return 0;
    }

    public void Output()
    {
        Console.WriteLine("面积和是:" + Sum());
    }
}

要使用AreaCalculator类,我们只需实例化该类并传递形状数组,然后将输出显示在页面底部。

object[] shapes =
{
    new Circle(2),
    new Square(5),
    new Square(6),
};
var areas=new AreaCalculator(shapes);
areas.Output();

输出方法的问题在于,现在是AreaCalculator处理逻辑以输出数据。这时如果用户希望将数据输出为 json 或其他格式内容怎么办?

所有这些逻辑将由AreaCalculator类处理,这是 SRP 反对的。 AreaCalculator 类应仅对提供的 ​​ 形状的面积求和,而不管用户是否需要 json 或 HTML。

因此,要解决此问题,您可以创建一个SumCalculatorOutputter类,并使用该类来处理处理所有提供的形状的总面积如何显示所需的逻辑。

SumCalculatorOutputter 类的工作方式如下:

object[] shapes =
{
    new Circle(2),
    new Square(5),
    new Square(6),
};
var areas=new AreaCalculator(shapes);
var output = new SumCalculatorOutputter(areas);

output.JsonOutput();
output.HtmlOutput();

现在,SumCalculatorOutputter类现在可以处理将数据输出到用户所需的任何逻辑。

Open-closed Principle 开闭原则

Objects or entities should be open for extension, but closed for modification.

软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的

这只是意味着一个类应该易于扩展,而无需修改类本身。让我们看一下AreaCalculator类,尤其是sum方法。

public double Sum()
{
    double areas = 0;
    //计算面积和
    foreach (var shape in Shapes)
    {
        switch (shape)
        {
            case Circle circle:
                areas += Math.PI * Math.Pow(circle.Radius, 2);
                break;
            case Square square:
                areas +=  Math.Pow(square.Length, 2);
                break;
        }
    }
    return areas;
}

如果我们希望sum方法能够求和更多形状的区域,则必须添加更多的** if / else 块**,这违背了 Open-closed 原理。

更好的一种方法是从求和方法中删除用于计算每个形状的面积的逻辑,并将其附加到形状的类中。

public class Circle
{
    public double Radius;

    public Circle(double radius)
    {
        Radius = radius;
    }

    public double Area()
    {
        return Math.PI * Math.Pow(Radius, 2);
    }
}

对于 Square 类做同样的事情,应该添加 area 方法。现在,要计算提供的任何形状的总和应该很简单:

public double Sum()
{
    double areas = 0;
    //计算面积和
    foreach (var shape in Shapes)
    {
        switch (shape)
        {
            case Circle circle:
                areas += circle.Area();
                break;
            case Square square:
                areas +=  square.Area();
                break;
        }
    }
    return areas;
}

现在,我们可以创建另一个形状类,并在计算总和时传递它,而不会破坏我们的代码。但是,现在又出现了另一个问题,我们必须判断传递到AreaCalculator中的对象实际上是一个形状,代码会显得冗余,这时候需要接口的帮助了。

接口是S.O.L.I.D不可或缺的一部分,一个简单的例子是我们创建一个接口,每种形状都可以实现这个接口:

public interface IShapeInterface
{
    public double Area();
}

public class Circle : IShapeInterface
{
    public double Radius;

    public Circle(double radius)
    {
        Radius = radius;
    }

    public double Area()
    {
        return Math.PI * Math.Pow(Radius, 2);
    }
}

修改我们的AreaCalculator

public class AreaCalculator
{
    protected IShapeInterface[] Shapes;

    public AreaCalculator(IShapeInterface[] shapes)
    {
        Shapes = shapes;
    }

    public double Sum()
    {
        //计算面积和
        return Shapes.Sum(shape => shape.Area());
    }

    public void Output()
    {
        Console.WriteLine("面积和是:" + Sum());
    }

    internal void JsonOutput()
    {
        throw new NotImplementedException();
    }

    internal void HtmlOutput()
    {
        throw new NotImplementedException();
    }

    internal void HamlOutput()
    {
        throw new NotImplementedException();
    }

    internal void JadeOutput()
    {
        throw new NotImplementedException();
    }
}

Liskov substitution principle 里氏替换原则

Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.

派生类(子类)对象可以在程序中代替其基类(超类)对象。

仍然使用AreaCalculator类,但是我们有一个VolumeCalculator类扩展了AreaCalculator类:

public class VolumeCalculator :AreaCalculator
{
    public VolumeCalculator(IShapeInterface[] shapes) : base(shapes)
    {
    }

    public new double[] Sum()
    {
        var areas = new List<double>();
        //分别计算每个图形的面积,返回面积数组
        foreach (var shape in Shapes)
        {
            areas.ToList().Add(shape.Area());
        }
        return areas.ToArray();
    }
}

如果我们尝试运行这样的示例:

private static void Main()
{
    IShapeInterface[] shapes =
    {
        new Circle(2),
        new Square(5),
        new Square(6),
    };

    var volume = new VolumeCalculator(shapes);
    AreaCalculator areas = volume;
    var aS=areas.Sum();//double
    var vS=volume.Sum();//double[]
}

其中子类 VolumeCalculator 对象可以赋给父类对象 AreaCalculator,也可以说 VolumeCalculator 替换 AreaCalculator,并且出现在 AreaCalculator 能够出现的任何地方。

Interface segregation principle 接口隔离原则

A client should never be forced to implement an interface that it doesn’t use or clients shouldn’t be forced to depend on methods they do not use.

接口隔离原则指明客户(client)不应被迫使用对其而言无用的方法或功能。

仍然使用之前的例子,我们知道我们也有实体形状,我们还想计算形状的体积,因此可以向IShapeInterface添加另一个协定:

public interface IShapeInterface
{
    public double Area();
    public double Volume();
}

我们创建的任何形状都必须实现volume方法,但是我们知道正方形是扁平形状并且它们不具有体积,因此此接口将强制Square类毫无意义。

ISP对此表示不接受,相反,您可以创建另一个名为ISolidShapeInterface的接口,该接口具有volume项,诸如圆柱之类的实体可以实现此接口:

public interface ISolidShapeInterface
{
    public double Volume();
}

public interface ISolidShapeInterface
{
    public double Volume();
}


public class Cuboid : IShapeInterface, ISolidShapeInterface
{
    public double Area()
    {
        throw new System.NotImplementedException();
    }

    public double Volume()
    {
        throw new System.NotImplementedException();
    }
}

这是一种更好的方法,但要注意如果共同的方法,不要直接在 IShapeInterface 或 ISolidShapeInterface 中写出。而是创建另一个接口(可能是 IManageShapeInterface),并在平面和实体形状上都实现它。例如:

public interface IManageShapeInterface
{
    public string Calculate();
}
public class Square : IShapeInterface,IManageShapeInterface
{
    public double Length;

    public Square(double length)
    {
        Length = length;
    }
    public double Area()
    {
        return Math.Pow(Length, 2);
    }

    public double Calculate()
    {
        return Area();
    }
}

public class Cuboid : IShapeInterface, ISolidShapeInterface,IManageShapeInterface
{
    public double Area()
    {
        throw new System.NotImplementedException();
    }

    public double Volume()
    {
        throw new System.NotImplementedException();
    }

    public double Calculate()
    {
        return Area() + Volume();
    }
}

接口隔离原则(ISP)拆分非常庞大臃肿的接口成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。这种缩小的接口也被称为角色接口(role interfaces)。接口隔离原则(ISP)的目的是系统解开耦合,从而容易重构,更改和重新部署。

Dependency Inversion principle 依赖反转原则

Entities must depend on abstractions not on concretions. It states that the high level module must not depend on the low level module, but they should depend on abstractions.

该原则规定:

  • 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
  • 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

听起来可能很绕口,但确实很容易理解。该原理允许去耦,举个例子解释:

public class PasswordReminder
{
    public MySqlConnection DbConnection { get; }

    public PasswordReminder( MySqlConnection dbConnection)
    {
        DbConnection = dbConnection;
    }
}

MySQLConnection 是低级别的模块,而 PasswordReminder 是高级别的模块,但是根据 S.O.L.I.D 中 D 的定义。声明依赖于抽象而不依赖于具体内容,上面的代码片段违反了该原理,因为 PasswordReminder 类被强制依赖于 MySQLConnection 类。

以后,如果您要更改数据库引擎,则还必须编辑 PasswordReminder 类,从而违反了打开关闭原则。

PasswordReminder 类不必关心您的应用程序使用什么数据库,因为高级和低级模块应依赖抽象,因此我们可以创建一个接口:

public interface IDbConnectionInterface
{
    public void Connect();
}

该接口具有一个 connect 方法,而 MySqlConnection 类实现了此接口,并且也没有直接在 PasswordReminder 的构造函数中直接提示 MySqlConnection 类,而是改为提示该接口,无论您的应用程序使用哪种数据库类型,PasswordReminder 类可以轻松连接到数据库而不会出现任何问题,并且不会违反 OCP。

public class MySqlConnection:IDbConnectionInterface
{
    public void Connect()
    {
        throw new System.NotImplementedException();
    }
}

public class PasswordReminder
{
    public IDbConnectionInterface DbConnection { get; }

    public PasswordReminder(IDbConnectionInterface dbConnection)
    {
        DbConnection = dbConnection;
    }
}

这下高级模块和低级模块都依赖于抽象。

总结

S.O.L.I.D乍看起来有点太抽象了,但是对于 S.O.L.I.D.的每个实际应用原则,遵守其准则的好处将变得很明显。 S.O.L.I.D.之后的代码可以更轻松地与协作者共享,扩展,修改,测试和重构,而不会出现任何问题。

posted @ 2020-12-02 14:43  _zxmax  阅读(134)  评论(0)    收藏  举报