在面向对象编程中,SOLID 是五个设计原则的首字母缩写,旨在使软件设计更易于理解、灵活和可维护。这些原则是由美国软件工程师和讲师罗伯特·C·马丁(Robert Cecil Martin)提出的许多原则的子集,在他2000年的论文《设计原则与设计模式》中首次提出。

SOLID 原则包含:

  • S:单一功能原则(single-responsibility principle)
  • O:开闭原则(open-closed principle)
  • L:里氏替换原则(Liskov substitution principle)
  • I:接口隔离原则(Interface segregation principle)
  • D:依赖反转原则(Dependency inversion principle)

本文我们来介绍单一功能原则

单一功能原则

在面向对象编程领域中,单一功能原则(Single responsibility principle)规定每个类都应该有且仅有一个单一的功能,并且该功能应该由这个类完全封装起来。所有它的(这个类的)服务都应该严密的和该功能平行(功能平行,意味着没有依赖)。

这个术语由罗伯特·C·马丁(Robert Cecil Martin)在他的《敏捷软件开发,原则,模式和实践》一书中的一篇名为『面向对象设计原则』的文章中提出。马丁表述该原则是基于《结构化分析和系统规格》一书中的内聚原则(Cohesion)之上的。

马丁把功能(职责)定义为:“改变的原因”,并总结出一个类或者模块应该有且只有一个改变的原因。一个具体的例子就是,想象有一个用于编辑和打印报表的模块。这样的一个模块存在两个改变的原因。第一,报表的内容可以改变(编辑)。第二,报表的格式可以改变(打印)。这两方面的改变会因为完全不同的起因而发生:一个是本质的修改,一个是表面的修改。单一功能原则认为这两方面的问题事实上是两个分离的功能,因此他们应该分离在不同的类或者模块里。把具有不同的改变原因的事物耦合在一起的设计是糟糕的。

保持一个类专注于单一功能点的一个重要的原因是,它可以使类更加的健壮。回顾上面的例子,如果有一个对于报表“编辑”流程的修改,那么将存在极大的危险性,因为假设这两个功能存在于同一个类中,修改报表的“编辑”流程会导致公共状态或者依赖关系的改变,从而可能使“打印”功能的代码无法正常运行。

C# 示例

例如,考虑这样一个应用程序,它接受一组形状(圆形和正方形),并计算该列表中所有形状的面积之和。

首先,创建形状类,并通过构造函数设置所需的参数。

对于正方形,需要知道它的边长:

/// <summary>
/// 正方形
/// </summary>
class Square
{
public Square(double length)
{
SideLength = length;
}
public double SideLength { get; init; }
}

对于圆形,需要它的半径:

/// <summary>
/// 圆形
/// </summary>
class Circle
{
public Circle(double radius)
{
Radius = radius;
} public double Radius { get; init; }
}

接下来,创建 AreaCalculator 类,然后编写逻辑以计算所有提供的形状的面积。正方形的面积是用边长的平方计算的,圆的面积由 π 乘以半径的平方来计算的。

糟糕的示范

class AreaCalculator
{
private List<object> _shapes; public AreaCalculator(List<object> shapes)
{
_shapes = shapes;
} /// <summary>
/// 计算所有形状的面积总和
/// </summary>
/// <returns></returns>
public double Sum()
{
List<double> areas = new List<double>(); foreach (var item in _shapes)
{
if (item is Square s)
{
areas.Add(Math.Pow(s.SideLength, 2));
}
else if (item is Circle c)
{
areas.Add(Math.PI * Math.Pow(c.Radius, 2));
}
} return areas.Sum();
} public string Output()
{
return $"Sum of the areas of provided shapes: {Sum()}";
}
}

要使用 AreaCalculator 类,您需要实例化这个类,并传入一个形状列表,并显示其输出。

在此,我们传入一个三个形状的列表:一个半径为 2 的圆,一个边长为 5 的正方形,一个边长为 6 的正方形。

static void Main(string[] args)
{
var shapes = new List<object> {
new Circle(2),
new Square(5),
new Square(6)
}; var areas = new AreaCalculator(shapes);
Console.WriteLine(areas.Output());
}

运行程序,您会看到如下的输出:

Sum of the areas of provided shapes: 73.56637061435917

输出正常,但这并不符合单一功能原则。因为 AreaCalculator 类既计算了所有形状的面积之和,又处理了输出数据的格式。

考虑这样一个场景,假如想要输出转换为另一种格式呢,如 JSON。我们就需要去修改 AreaCalculator 类,这样本来是为了修改输出数据的格式,却可能会影响到计算的逻辑,这明显违反了单一功能原则

正确的示范

AreaCalculator 类应该只关心计算提供的形状的面积之和,不应该关心输出什么格式。

下面我们来做一些修改,删除 AreaCalculator 类中的 Output 方法:

class AreaCalculator
{
private List<object> _shapes; public AreaCalculator(List<object> shapes)
{
_shapes = shapes;
} /// <summary>
/// 计算所有形状的面积总和
/// </summary>
/// <returns></returns>
public double Sum()
{
List<double> areas = new List<double>(); foreach (var item in _shapes)
{
if (item is Square s)
{
areas.Add(Math.Pow(s.SideLength, 2));
}
else if (item is Circle c)
{
areas.Add(Math.PI * Math.Pow(c.Radius, 2));
}
} return areas.Sum();
}
}

并新增一个 SumCalculatorOutputter 类来专门处理输出格式的逻辑:

class SumCalculatorOutputter
{
protected AreaCalculator _calculator; public SumCalculatorOutputter(AreaCalculator calculator)
{
_calculator = calculator;
} public string String()
{
return $"Sum of the areas of provided shapes: {_calculator.Sum()}";
} public string JSON()
{
var data = new { Sum = _calculator.Sum() };
return System.Text.Json.JsonSerializer.Serialize(data);
}
}

此时我们再来修改一下 Main 中的调用:

static void Main(string[] args)
{
var shapes = new List<object> {
new Circle(2),
new Square(5),
new Square(6)
}; var areaCalculator = new AreaCalculator(shapes);
var outputer = new SumCalculatorOutputter(areaCalculator);
Console.WriteLine(outputer.JSON());
Console.WriteLine(outputer.String());
}

运行程序,输出结果如下:

{"Sum":73.56637061435917}
Sum of the areas of provided shapes: 73.56637061435917

现在,AreaCalculator 类处理计算逻辑,SumCalculatorOutputter 类处理输出格式,它们各司其职,遵循了单一功能原则

总结

本文我介绍了 SOLID 原则中的单一功能原则(single-responsibility principle),并通过 C# 代码示例简明地诠释了它的含意和实现,希望对您有所帮助。

作者 : 技术译民

出品 : 技术译站

参考文档:

C# 实例解释面向对象编程中的单一功能原则的更多相关文章

  1. C# 实例解释面向对象编程中的开闭原则

    在面向对象编程中,SOLID 是五个设计原则的首字母缩写,旨在使软件设计更易于理解.灵活和可维护.这些原则是由美国软件工程师和讲师罗伯特·C·马丁(Robert Cecil Martin)提出的许多原 ...

  2. C# 实例解释面向对象编程中的里氏替换原则

    在面向对象编程中,SOLID 是五个设计原则的首字母缩写,旨在使软件设计更易于理解.灵活和可维护.这些原则是由美国软件工程师和讲师罗伯特·C·马丁(Robert Cecil Martin)提出的许多原 ...

  3. C# 实例解释面向对象编程中的接口隔离原则

    在面向对象编程中,SOLID 是五个设计原则的首字母缩写,旨在使软件设计更易于理解.灵活和可维护.这些原则是由美国软件工程师和讲师罗伯特·C·马丁(Robert Cecil Martin)提出的许多原 ...

  4. C# 实例解释面向对象编程中的依赖反转原则

    在面向对象编程中,SOLID 是五个设计原则的首字母缩写,旨在使软件设计更易于理解.灵活和可维护.这些原则是由美国软件工程师和讲师罗伯特·C·马丁(Robert Cecil Martin)提出的许多原 ...

  5. C#中面向对象编程中的函数式编程详解

    介绍 使用函数式编程来丰富面向对象编程的想法是陈旧的.将函数编程功能添加到面向对象的语言中会带来面向对象编程设计的好处. 一些旧的和不太老的语言,具有函数式编程和面向对象的编程: 例如,Smallta ...

  6. Dart编程实例 - Dart 面向对象编程

    Dart编程实例 - Dart 面向对象编程 class TestClass { void disp() { print("Hello World"); } } void main ...

  7. Python3学习之路~6.2 实例演示面向对象编程的好处

    首先建一个dog类,实例化为3个dog对象,并让它们都叫. class Dog: def bulk(self): print("xiaohuang:wang wang wang !" ...

  8. Go语言基础之面向对象编程中

    1 Golang面向对象编程基本介绍 Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OPP语言不一样,随后分别介绍Golang对面向对象编程的三大特性是如何实现的. 2 ...

  9. 面向对象设计之SRP(单一职责)原则

    SRP设计原则面向对象类设计的第一个原则,最优先考虑的因素 一个类应该有且仅有一个职责.所谓一个类的职责是指引起该类变化的原因,如果一个类具有一个以上的职责,那么就会有多个不同的原因 引起该类变化,其 ...

随机推荐

  1. [Elasticsearch] ES聚合场景下部分结果数据未返回问题分析

    背景 在对ES某个筛选字段聚合查询,类似groupBy操作后,发现该字段新增的数据,聚合结果没有展示出来,但是用户在全文检索新增的筛选数据后,又可以查询出来, 针对该问题进行了相关排查. 排查思路 首 ...

  2. 遍历 HashMap 的 5 种最佳方式

    使用 Iterator 遍历 HashMap EntrySet 使用 Iterator 遍历 HashMap KeySet 使用 For-each 循环迭代 HashMap 使用 Lambda 表达式 ...

  3. Solon 1.6.11 发布。类似 Spring 的生态体系

    关于官网 千呼万唤始出来: https://solon.noear.org .整了一个月多了,总体样子有了...还得不断接着整! 关于 Solon Solon 是一个轻量级应用开发框架.支持 Web. ...

  4. 自动化集成:Pipeline整合Docker容器

    前言:该系列文章,围绕持续集成:Jenkins+Docker+K8S相关组件,实现自动化管理源码编译.打包.镜像构建.部署等操作:本篇文章主要描述流水线集成Docker用法. 一.背景描述 微服务架构 ...

  5. 编写Java程序,判断输入的三条长度的边,是否能构成三角形

    需求说明: 编写Java程序,判断输入的三条长度的边,是否能构成三角形. (三角形第三边大于两边之和小于两边之差) 实现代码: package test; import java.util.Scann ...

  6. MySQL数据库常用命令汇总

    -- 查看mysql的当前登陆用户 select user(); -- 列出数据库 show databases; -- 使用数据库 use mysql; describe mysql.user; s ...

  7. [Atcoder Regular Contest 071 F & JZOJ5450]Neutral

    题目大意 一个无限长的序列\(a\), 需要满足 1.数列中的每一个数在\(1\)到\(n\)之间. 2.对于\(i>=n, j>=n\), \(a_i=a_j\). 3.对于\(i< ...

  8. centos一步一步搭建tendermint

    一.必要条件 1.安装go 请根据官方文档安装:https://golang.org/doc/install 要特别注意的是: /etc/profile 添加以下内容: export GOPATH=/ ...

  9. Ant 调用 Shell/CMD 命令

    Ant中调用Makefile,使用shell中的make命令 <?xml version="1.0" encoding="utf-8" ?> < ...

  10. for循环题目记录

    1.求1000以内的完数 /** * 一个数如果恰好等于它的因子之和,这个数就是完数,找出1000内的所有完数 * @author 努力Coding * @version * @data */ pub ...