本文结合b站和网上的一些资料,主要用于纪录知识。有错则请各位大哥多多指点。
文章目录
前言
不知道你是否曾被混乱的代码结构搞得很难受,无意中了解到SOLID
原则,特此纪录一下,希望对各位都有所帮助~
首先复习一下相关的知识
什么是面向对象?
百度百科:面向对象(Object Oriented)是软件开发方法,一种编程范式。面向对象的概念和应用已超越了程序设计和软件开发,扩展到如数据库系统、交互式界面、应用结构、应用平台、分布式系统、网络管理结构、CAD技术、人工智能等领域。面向对象是一种对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段后的产物。面向对象是相对于面向过程来讲的,面向对象方法,把相关的数据和方法组织为一个整体来看待,从更高的层次来进行系统建模,更贴近事物的自然运行模式。
在我的角度看来,面向对象就是将每个事物都抽象总结为一块积木,每块积木各司其职,共同堆砌成一个在计算机中运行的世界。
什么是SOLID?
SOLID 原则是由 5 个设计原则组成的,分别为:(S)单一职责原则、(O)开闭原则、(L)里式替换原则、(I)接口隔离原则和(D)依赖反转原则。
一、SOLID
SOLID 原则其实是用来指导软件设计的,它一共分为五条设计原则,分别是:
- 单一职责原则(SRP),Single Responsibility Principle
- 开闭原则(OCP),Open Closed Principle
- 里氏替换原则(LSP),Liskov Substitution principle
- 接口隔离原则(ISP),Interface Segregation Principle
- 依赖倒置原则(DIP),Dependence Inversion Principle
二、详解SOLID
场景:我们需要计算一堆图形的面积和,代码如下
// 圆
public class Circle {
private final int radius;
public Circle(int radius) {
this.radius = radius;
}
public int getRadius() {
return radius;
}
}
// 正方形
public class Square {
private final int length;
public Square(int length) {
this.length = length;
}
public int getLength() {
return length;
}
}
// 面积计算器
public class AreaCalculator {
public int sum(List<Object> shapes) {
int sum = 0;
for (int i = 0; i < shapes.size(); i++) {
Object shape = shapes.get(i);
if(shape instanceof Square) {
sum += Math.pow(((Square) shape).getLength(), 2);
}
if(shape instanceof Circle) {
sum += Math.PI * Math.pow(((Circle) shape).getRadius(), 2);
}
}
return sum;
}
}
// 主函数
public class Main {
public static void main(String[] args) {
AreaCalculator areaCalculator = new AreaCalculator();
Circle circle = new Circle(10);
Square square = new Square(10);
List<Object> circle1 = List.of(circle, square);
int sum = areaCalculator.sum(circle1);
System.out.println(sum);
}
}
1.单一职责原则(SRP)
需求提出:我们需要以不同的方式打印出面积和,包含但不限于:①Json,②普通格式…
问题引入:先不说计算机的概念,假如你去麦当劳兼职,机器坏了,店长让你去修一下(毕竟你是学计算机的,对吧=,=)。修完之后,让你去刷一下厕所,刷完厕所还得去倒一下垃圾。太多事情了!你晕头转向,上班是竖着进去上班的,下班是横着被人拉出去下班的。发生这种情况的原因就是你一个人干的事情太多了,如果能够分工合作,各司其职的话,就不会造成这样子的情况。那么在计算机里面的世界也是如此~
引入定义
单一职责原则:应该有且仅有一个原因引起类的变更。简单地说:接口职责应该单一,不要承担过多的职责。
反例
代码如下(示例):
// 直接在AreaCalculator中添加上相关的功能
public class AreaCalculator {
public int sum(List<Object> shapes) {
int sum = 0;
for (int i = 0; i < shapes.size(); i++) {
Object shape = shapes.get(i);
if(shape instanceof Square) {
sum += Math.pow(((Square) shape).getLength(), 2);
}
if(shape instanceof Circle) {
sum += Math.PI * Math.pow(((Circle) shape).getRadius(), 2);
}
}
return sum;
}
public String Json(List<Object> shapes) {
return "{sum: %s}".formatted(sum(shapes));
}
public String csv(List<Object> shapes) {
return "sum, %s".formatted(sum(shapes));
}
}
实话实话,这种写法其实是没啥问题的。但是当功能越来越多,代码就会越来越繁杂,直接导致的后果就是当要再看一遍代码的时候,我相信真的会看吐=,=。而且那么多的功能,灵活的胖子这种情况是不可能发生的。
改进
单一职责原则告诉我们,最好的做法是将这部分功能剥离出来,新建一个类去完成该项功能。但是会不会出现类过多的情况,这得看程序怎么设计了,这种事情都是仁者见仁智者见智的,毕竟没有什么东西可以十全十美╮(╯▽╰)╭。代码如下
public class shapePrinter {
public String Json(int sum) {
return String.format("{shapeSum: %s}", sum);
}
public String csv(int sum) {
return String.format("shapeSum, %s", sum);
}
}
2. 开闭原则(OCP)
需求引入:除了圆形、正方形以外,还得加入正方体,毕竟我是老板我任性对吧~
问题引入:说实话,这种问题根本难不倒我们对吧。作为CV工程师,这种小问题不是so easy吗。我要开始写(反例)了
反例
代码如下:
public class Circle {
private final int radius;
public Circle(int radius) {
this.radius = radius;
}
public int getRadius() {
return radius;
}
}
public class AreaCalculator {
public int sum(List<Object> shapes) {
int sum = 0;
for (int i = 0; i < shapes.size(); i++) {
Object shape = shapes.get(i);
if(shape instanceof Square) {
sum += Math.pow(((Square) shape).getLength(), 2);
}
if(shape instanceof Circle) {
sum += Math.PI * Math.pow(((Circle) shape).getRadius(), 2);
}
if(shape instanceof Cube) {
sum += 6 * Math.pow(((Square) shape).getLength(), 2);
}
}
return sum;
}
}
说实话,这种写法也是完全可以的~ 但是问题在于,任性的老板不定时提个需求,毕竟图形有辣么多对吧。然后我们就得一直写if
…最后又收获了一堆复杂的代码,哎
那么有什么好的设计方法可以避免这种情况的发生呢?
引入定义
一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。简单地说:就是当别人要修改软件功能的时候,使得他不能修改我们原有代码,只能新增代码实现软件功能修改的目的。
改进
针对这个例子,应该这样子修改
public interface Shape {
double area();
}
public class Circle implements Shape{
private final int radius;
public Circle(int radius) {
this.radius = radius;
}
public int getRadius() {
return radius;
}
@Override
public double area() {
return Math.PI * Math.pow(getRadius(), 2);
}
}
public class Square implements Shape{
private final int length;
public Square(int length) {
this.length = length;
}
public int getLength() {
return length;
}
@Override
public double area() {
return Math.pow(getLength(), 2);
}
}
public class Cube implements Shape{
private final int length;
public Cube(int length) {
this.length = length;
}
public int getLength() {
return length;
}
@Override
public double area() {
return 6 * Math.pow(this.length, 2);
}
}
public class AreaCalculator {
public int sum(List<Shape> shapes) {
int sum = 0;
for (int i = 0; i < shapes.size(); i++) {
sum += shapes.get(i).area();
}
return sum;
}
}
public static void main(String[] args) {
AreaCalculator areaCalculator = new AreaCalculator();
Circle circle = new Circle(10);
Square square = new Square(10);
List<Shape> circle1 = List.of(circle, square);
int sum = areaCalculator.sum(circle1);
System.out.println(sum);
}
代码分析:让我们看一下代码怎么变化,首先定义了一个接口Shape
这个接口定义了一个方法double area()
,然后给Circle
,Square
,等图形实现,同时AreaCalculator
也修改为sum += shapes.get(i).area()
。现在新增一个图形就不用改AreaCalculator
咯,只需要实现Shape
接口就好啦。很方便有木有!
所以开闭原则就是对修改关闭,对新增关闭。用人话就是说,新增功能不用修改已有的代码,这样子真的是太棒啦~
3.单一职责原则(SRP)
需求引入:奇怪的老板又来了,他买的股票带有
Shape
字样的跌成狗,就觉得Shape
这个类不顺眼,要全改。没办法,只能安装老板的意思改,有人想到可以把Shape
的子类给他放进去,对吧
这样子说好像只是感觉老板在发颠,实际不会出现这种情况。换优快云一篇文章的情景,有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
反例
大佬们说的头头是道,那我这个小菜鸡写一下代码:
public class NoShape implements Shape{
@Override
public double area() {
throw new RuntimeException("不是一个图形");
}
}
很明显,当我们把这段代码放进main
程序,必定会报错。因为我们抛出了异常=,=。这就违反了里氏替换原则,子类影响了父类的地方。
优快云这篇文章说的特别好,六大设计原则SOLID
在学习java类的继承时,我们知道继承有一些优点:
- 子类拥有父类的所有属性和方法,从而可以减少创建类的工作量。
- 提高了代码的重用性。
- 提高了代码的扩展性, 子类不但拥有了父类的所有功能,还可以添加自己的功能。
但有优点也同样存在缺点
- 继承是侵入性的。只要继承,就必须拥有父类的方法和属性。
- 降低了代码的灵活性。因为继承时,父类会对子类有一种约束。
- 增强了耦合性。 当需要对父类的代码进行修改时,必须考虑到对子类产生的影响。
如何扬长避短呢?方法是引入里氏替换原则。
里氏替换原则对继承进行了规则上的约束,这种约束主要体现在四个方面:
- 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
- 子类中可以增加自己特有的方法。
- 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。(即只能重载不能重写)
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
里氏替换原则 LSP 重点强调:对使用者来说,能够使用父类的地方,一定可以使用其子类,并且预期结果是一致的。换句话说, 所有基类在的地方, 都可以换成子类, 程序还可以正常的运行。这个原则是与面向对象语言的继承特性密切相关的。
当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。
4.接口隔离原则(ISP)
需求引入:立方体是个三维图形,辣么他有体积吧,你们帮我把它体积也求出来!老板如是说。
问题分析:这些图形都是实现了Shape
接口,那么在这个接口加上不就好了吗?聪明的我说干就干
反例
public interface Shape {
double area();
double volume();
}
public class Cube implements Shape{
private final int length;
public Cube(int length) {
this.length = length;
}
public int getLength() {
return length;
}
@Override
public double area() {
return 6 * Math.pow(this.length, 2);
}
@Override
public double volume() {
return Math.pow(this.length, 3);
}
}
坏了,平面的图形肿么办!?直接return 0
好了,╮(╯▽╰)╭。然后你会收获一堆繁杂而且没用的代码,给自己增加工作量啊!(ˉ▽ˉ;)…
那么有什么好的设计方法可以避免这种情况的发生呢?
引入定义
类间的依赖关系中,子类不应被强迫依赖它不需要的接口,而应该建立在最小的接口上。简单地说:接口的内容一定要尽可能地小,能有多小就多小。
注: 该原则中的接口, 是一个泛泛而言的接口, 不仅仅指java中的接口,还包括其中的抽象类。
其实通俗来理解就是, 不要在一个接口里面放很多的方法,这样会显得这个类很臃肿不堪。接口应该尽量的细化,一个接口对应一个功能模块,同时接口里面的方法应该尽量的少,使接口尽可能的轻便灵活。
改进
因此,正确的解决方法应该是这样子:
public interface ThreeDimensionalShape {
public double volume();
}
public class Cube implements Shape, ThreeDimensionalShape{
private final int length;
public Cube(int length) {
this.length = length;
}
public int getLength() {
return length;
}
@Override
public double area() {
return 6 * Math.pow(this.length, 2);
}
@Override
public double volume() {
return Math.pow(this.length, 3);
}
}
如上面的代码,Shape
只提供二维可能出现的情况,新开一个接口ThreeDimensionalShape
用于提供三维可能出现的情况。三维图形才去实现这个接口╮(╯▽╰)╭。
依赖反转原则(DIP)
需求引入,现在AreaCalculator不是求和了,是求积了,那么我们改怎么办呢?
根据开闭原则,我们应该先优化一下代码
public interface IAreaCalculator {
public int sum(List<Shape> shapes);
}
public class AreaCalculator implements IAreaCalculator{
@Override
public int sum(List<Shape> shapes) {
int sum = 0;
for (int i = 0; i < shapes.size(); i++) {
sum += shapes.get(i).area();
}
return sum;
}
}
public class ShapePrinter {
private final IAreaCalculator areaCalculator;
public ShapePrinter(IAreaCalculator areaCalculator) {
this.areaCalculator = areaCalculator;
}
public String Json(List<Shape> shapes) {
return String.format("{shapeSum: %s}", areaCalculator.sum(shapes));
}
public String csv(List<Shape> shapes) {
return String.format("shapeSum, %s", areaCalculator.sum(shapes));
}
}
public class Main {
public static void main(String[] args) {
AreaCalculator areaCalculator = new AreaCalculator();
Circle circle = new Circle(10);
Square square = new Square(10);
ShapePrinter shapePrinter = new ShapePrinter(areaCalculator);
List<Shape> circle1 = List.of(circle, square);
System.out.println(shapePrinter.Json(circle1));
System.out.println(shapePrinter.csv(circle1));
}
}
说实话,聪明的我随便就能做出来~
反例
public class AreaCalculatorV2 implements IAreaCalculator{
@Override
public int sum(List<Shape> shapes) {
int sum = 0;
for (int i = 0; i < shapes.size(); i++) {
sum *= shapes.get(i).area();
}
return sum;
}
}
public class Main {
public static void main(String[] args) {
AreaCalculatorV2 areaCalculator = new AreaCalculatorV2();
Circle circle = new Circle(10);
Square square = new Square(10);
ShapePrinter shapePrinter = new ShapePrinter(areaCalculator);
List<Shape> circle1 = List.of(circle, square);
System.out.println(shapePrinter.Json(circle1));
System.out.println(shapePrinter.csv(circle1));
}
}
简直轻而易举~太简单啦,我们这里啥都没改,只是新增了一个类去实现一个接口。然鹅,你会发现,如果又要改回AreaCalculator
, 主函数又得改,(ˉ▽ˉ;)…
那有什么方法可以避免这种情况呢?
引入定义
依赖反转原则:高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象。这里的高层模块为调用者,底层模块为被调用者;
改进
其实只需要小改一下就好了,改成介样子—>IAreaCalculator areaCalculator = new AreaCalculator();
public class Main {
public static void main(String[] args) {
IAreaCalculator areaCalculator = new AreaCalculator();
Circle circle = new Circle(10);
Square square = new Square(10);
ShapePrinter shapePrinter = new ShapePrinter(areaCalculator);
List<Shape> circle1 = List.of(circle, square);
// int sum = areaCalculator.sum(circle1);
System.out.println(shapePrinter.Json(circle1));
System.out.println(shapePrinter.csv(circle1));
}
}
这样子改完美的遵守了开闭原则。太牛啦!
依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。 以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。 在java中, 抽象指的契约,是接口或者抽象类, 细节就是具体的实现类, 使用接口或者抽象类的目的是制定好的规范和而不去设计任何具体的操作, 把展示细节的任务交给他们的实现类去完成。
总结
这篇文章总结的很好~
- 单一职责是所有设计原则的基础,开闭原则是设计的终极目标。
- 里氏替换原则强调的是子类替换父类后程序运行时的正确性,它用来帮助实现开闭原则。
- 而接口隔离原则用来帮助实现里氏替换原则,同时它也体现了单一职责。
- 依赖倒置原则是过程式编程与面向对象编程的分水岭,同时它也被用来指导接口隔离原则。
简单地说:依赖倒置原则告诉我们要面向接口编程。当我们面向接口编程之后,接口隔离原则和单一职责原则又告诉我们要注意职责的划分,不要什么东西都塞在一起。
当我们职责捋得差不多的时候,里氏替换原则告诉我们在使用继承的时候,要注意遵守父类的约定。而上面说的这四个原则,它们的最终目标都是为了实现开闭原则
相比于23种设计模式,SOLID原则更加通用,主要的思想还是解耦,真的可以认真学一下~