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

时间:2022-04-12 14:56:02

第一篇博文简单介绍面向对象设计,用来告诫自己以后规范编码,编写有质量的代码。

参考博文:S.O.L.I.D:The First 5 Principles of Object Oriented Design

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

Overview

Initial
Stands for
Concept
S
SRP
Single responsibility principle
“A class should have only one reason to change.“
O
OCP
Open/Closed principle
''
software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification''
L
LSP
Liskov substitution principle
''objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.”
I
ISP
Inrerface segregation principle
''many client-specific interfaces are better than one general-purpose interface.''
D
DIP
Dependency inversion principle
''one should “Depend upon Abstractions. Do not depend upon concretions.''















单一职责原则(Single-Resposibility Principle)。

一个类应该有且只有一个去改变它的理由。”

这意味着一个类应该只有一项工作,如果一个类承担的职责过多,那么这些职责就会相互依赖,一个职责的改变就可能会影响另一个职责的执行。

举一个例子(Java语言):有一个已知的二维坐标系,在坐标系中定义若干种规则的图形shape:圆、正方形、矩形和椭圆。

abstract class Shape {
    private double x, y;

    public Shape() {
    }

    public Shape(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public abstract double area();

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
    }

    public double getY() {
        return y;
    }

    public void setY(double y) {
        this.y = y;
    }

}

class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        super(radius, radius);
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return 3.1416 * radius * radius;
    }

}

class Square extends Shape {
    private double length;

    public Square(double length) {
             super(length,length);
        this.length = length;
    }

    public double getLength() {
        return length;
    }

    public void setLength(double length) {
        this.length = length;
    }

    @Override
    public double area() {
        return length * length;
    }
}

我们想求所有shape的面积的和,这很简单。接下来,我们继续通过创建AreaCalculator类,然后编写求取所提供的shape面积之和的逻辑。
class AreaCalculator {

	protected Shape[] shapes;

	public AreaCalculator(Shape[] shapes) {
		this.shapes = shapes;
	}

	public double sum() {
		// logic to sum the sumArea
		double sumArea = 0;
		for (int i = 0; i < shapes.length; i++) {
			sumArea = sumArea + shapes[i].area();
		}
		return sumArea;
	}

	public void OutPut() {
		System.out.println(sum());

	}
}
使用AreaCalculator类,我们简单地实例化类,同时传入一个shape数组,并在页面的底部显示输出。
             Shape[] shapes;
        shapes = new Shape[3];
        shapes[0] = new Circle(10.5);
        shapes[1] = new Square(20.3);
        shapes[2] = new Circle(3.14);
        
        AreaCalculator areas = new AreaCalculator(shapes);
        areas.OutPut();
结果:

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

如果用户想要以json或其他方式输出数据该怎么办?于是我们修改AreaCalculator类的代码,使得满足以json方式输出数据。

	public void OutPut() {
	//	System.out.println(sum());
		JSONObject jsobjcet = new JSONObject();    
                jsobjcet.put("areas", sum());  
                System.out.println(jsobjcet.toString()); 

	}
至此可以发现AreaCalculator类存在一些问题,不仅处理了计算总面积的逻辑,也处理了输出数据的逻辑,这是违反单一职责原则(SRP)的;AreaCalculator类应该只对提供的shape进行面积求和,它不应该关心用户是需要字符串还是json或者是XML。


因此,为了解决这个问题,我们可以创建一个SumCalculatorOutput类,使用这个类处理输出数据的逻辑,即对所提供的shape进行面积求和后如何显示。

SumCalculatorOutput类按如下方式工作:

class SumCalculatorOutput{
    private double areas;
    public SumCalculatorOutput(double areas) {
        this.areas = areas;
    }
    
    public void OutPutString() {
        System.out.println(areas);
        }
    
    public void OutPutJson() {
        JSONObject jsobjcet = new JSONObject();    
        jsobjcet.put("areas", areas);  
        System.out.println(jsobjcet.toString()); 
        }
}

结果:

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

现在,不管我们需要何种逻辑来输出数据给用户,皆由SumCalculatorOutput类处理。

开放封闭原则(Open-Closed principle)。

“对象或实体应该对扩展开放,对修改封闭。”

这就意味着一个类应该无需修改类本身但却容易扩展。如果不必改动软件实体的源代码,就能扩充它的行为,那么这个软件实体设计就是满足开放封闭原则的。

我们shape面积求解的逻辑是附加到shape类上的。试想如果我们不采用这种设计方法,而是把面积求解的逻辑处理交由AreaCalculator 类的sum()方法来处理:
  

class AreaCalculator {
	protected Shape[] shapes;
	public AreaCalculator(Shape[] shapes) {
		this.shapes = shapes;
	}
	public double sum() {
		// logic to sum the sumArea
		double sumArea = 0;
		for (int i = 0; i < shapes.length; i++) {
			if(shapes[i].getClass().toString().equals("class Square")){
				sumArea = sumArea + Math.pow(shapes[i].getX(), 2);
			}else if (shapes[i].getClass().toString().equals("class Circle")) {
				sumArea = sumArea + (3.1416 *Math.pow(shapes[i].getX(), 2));
			}
		}
		return sumArea;
	}
}
如果我们希望sum方法能够对更多的shape进行面积求和,我们会添加更多的If / else块,这就违背了开放封闭原则。

所以,将每个shape面积的逻辑从sum方法中移出,将它附加到shape类上,当程序需要扩展求更多其它类型的图形的面积时,我们只需要添加相应的实体类,并实现类中的 public double area( ){ }方法即可,AreaCalculator 类中的sum( )方法完全不需要改变:

	public double sum() {
		// logic to sum the sumArea
		double sumArea = 0;
		for (int i = 0; i < shapes.length; i++) {
			sumArea = sumArea + shapes[i].area();
		}
		return sumArea;
	}

里氏替换原则(Liskov-Substitution Principle)。

"子类型必须能够替换掉它们的基类型。"

这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证运行期内识别子类,这是保证继承复用的基础,使得使用基类型模块无需修改就可扩充。如上例

        Shape[] shapes;
        shapes = new Shape[3];
        shapes[0] = new Circle(10.5);
        shapes[1] = new Square(20.3);
        shapes[2] = new Circle(3.14);

即:子类可以代替换父类,而父类不可以替换子类。

接口隔离原则(Interface-Segregation Principle)。

"多个专用接口优于一个单一的通用接口。"

每一个接口执行一个专门的功能,当某个客户程序的要求发生变化时不应该*依赖它们不使用的方法。

仍然以shape为例,我们知道也有立体shape,如果我们也想计算shape的体积,我们可以添加另一个合约到Shape:

abstract class Shape {
	private double x, y;
	public Shape() {
	}
	public Shape(double x, double y) {
		this.x = x;
		this.y = y;
	}
	public abstract double Area();
	
	public abstract double Volume();//添加计算体积的合约
}
一旦这样设计,任何我们创建的shape都必须实现Volume的方法,但我们知道 圆、正方形、矩形、椭圆等平面图形是没有体积的,所以这个接口将迫使平面图形也得实现计算体积的一个对于它完全没有什么意义的方法。

依赖反转原则(Dependecy-Inversion Principle)。

"依赖于抽象,而不是依赖于具体"

具体地说就是模块间的依赖是通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生;且高层次的模块不应该依赖于低层次的模块,它们都应该依赖于抽象。

举个例子:假设我们用以下的方法实现用户的登录验证,首先 LoginCheck 是高层次的模块,MySQLConnection 是低层次的模块,高层次模块直接依赖于低层次模块,这样的设计显然违背了依赖反转原则,此时,如果程序发生改变,系统改用Oracle或者SQLServer数据库而不是MySQL数据库,我们就必须修改LoginCheck模块,这又违背开放封闭原则。

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

依赖反转原则,“反转”并不是说 让低层次依赖于高层次,而是高层次模块、低层次模块均依赖于接口或抽象类。对上述例子,我们增加一个DBConnectionInterface接口:

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



在LoginCheck模块不直接依赖于MySQLConnection,而是使用接口替换,这样不管应用程序使用的是什么类型的数据库,LoginCheck模块都可以很容易地连接到数据库,没有任何问题,且不违反OCP。可见,依赖倒置原则的目的是为了避免这种高度耦合而介入了一个抽象层,并提高高层次模块的可重用性。