面向对象设计七大原则

最近团队在学习Agile 和 Clean Code。然后对面向对象设计的一些原则进行了一些学习和整理。包括SOLID、合成复用原则与迪米特法则。

可维护性

Robert C.Martin认为一个可维护性较低
的软件设计,通常由于如下四个原因造成:
• 过于僵硬(Rigidity)
• 过于脆弱(Fragility)
• 复用率低(Immobility)

• 黏度过高(Viscosity) 


可复用性

Peter Coad认为,一个好的系统

设计应该具备如下三个性质:
• 可扩展性(Extensibility)
• 灵活性(Flexibility)
• 可插入性(Pluggability) 


设计原则

先看下设计原则,这里我列举了常说的 SOLID 和 另外两大原则。

设计原则名称 简介 出处
单一职责原则
(Single Responsibility Principle, SRP)
就一个类而言,应该仅有一个引起它变化的原因。 Robert C. Martin《敏捷软件开发:原则、模式与实践》第八章
开闭原则
(Open-Closed Principle, OCP)
软件实体对扩展是开放的,但对修改是关闭的。即在不修改一个软件实体的基础上去扩展其功能。 Bertrand Meyer《面向对象软件构造(Object Oriented Software Construction)》
里氏代换原则
(Liskov Substitution Principle, LSP)
Subtypes must be substitutable for their base types。
子类必须能够替换成它们的基类。
Liskov女士《Data Abstraction and Hierarchy》
依赖倒转原则
(Dependency Inversion Principle, DIP)
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.

B. Abstractions should not depend on details. Details should depend on abstractions.

A.高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。

B.抽象不应该依赖于具体实现,具体实现应该依赖于抽象。

作用:降低了客户与实现模块间的耦合

《敏捷软件开发—原则、模式与实践》第十一章
接口隔离原则
(Interface Segregation Principle, ISP)
使用多个专门的接口来取代一个统一的接口。 《ISP: The Interface Segregation Principle》
合成复用原则
(Composite Reuse Principle, CRP)
就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。 简单就是:要尽量使用组合,尽量不要使用继承。  
迪米特法则
(Law of Demeter, LoD)
又叫作最少知识原则(Least Knowledge Principle 简写LKP),就是说一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。 《The Pragmatic Programmer》即《程序员修炼之道:从小工到专家

单一职责原则

所谓职责,即“变化的原因”。如果你能够想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责。就要拆分这个类。而拆分后的类内聚性提高。

相信你也看到了,类的拆分会导致产生大量短小的类。不过软件开发中有个说法:系统应该由许多短小的类而不是少量巨大的类组成。因此,个人觉得这不是个缺点

开闭原则

该原则要求,软件实现应该对扩展开放,对修改关闭。意思就是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化的。

举个例子:

打字员利用打印机打印文档,即可以打印黑白色的,也需要打印彩色的。

面向对象设计七大原则_第1张图片

之后如果系统增加一种新的打印机:UniversalPrinter。此时就对系统有不小的修改。

相反,如果采用下面的设计,Typist 依赖抽象类 Printer。这样有需求增加变化时,只需要增加一个子类即可。

面向对象设计七大原则_第2张图片

可能你也发现了,实现开闭原则的关键就是“抽象”。把可能的行为(这里是print)抽象成一个抽象层。之后的扩展都是对这个抽象的实现。


里氏代换原则

里氏代换原则是对开闭原则的补充,它讲的是基类和子类的关系。

“鸵鸟不是鸟",”正方形是长方形"都是理解里氏代换原则的最经典的例子。小学数学的时候就知道,正方形是长方形,即一个长宽相等的长方形。由此,应该让正方形继承自长方形。

面向对象设计七大原则_第3张图片

此时代码如下:

public class Rectangle {
    private int height;
    private int width;

    // 省略getter setter
}

要保证,正方形长和宽始终一样,要覆写两个setter:

public class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(int height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}

两个类在以下代码中,表现不一样:

public class TestSquare {
    public static void main(String[] args) {
        TestSquare test = new TestSquare();
        Rectangle rectangle = new Rectangle();
        rectangle.setHeight(5);
        rectangle.setWidth(4);
        test.zoom(rectangle, 2, 3);

        Square square = new Square();
        square.setHeight(5);
        square.setWidth(4);
        test.zoom(square, 2, 3);
    }

    public void zoom(Rectangle rectangle, int width, int height) {
        rectangle.setWidth(rectangle.getWidth() + width);
        rectangle.setHeight(rectangle.getHeight() + height);
    }
}


依赖倒转原则

所谓依赖倒转(Dependency Inversion Principle)有两条:
A.高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。

B.抽象不应该依赖于具体实现,具体实现应该依赖于抽象。

看下图:

面向对象设计七大原则_第4张图片

在这个图中,从上到下的依赖是传递的,因此底层的任何修改都会影响到上层

再看下面这个图:

面向对象设计七大原则_第5张图片

这里,上层和下层都依赖抽象层。抽象层是稳定的,它的存在屏蔽了实现层修改带来的影响。


接口隔离原则

客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

下面类图中,三个客户端依赖一个大的接口 AbstractService。事实上,对ClientA 来说, operationB、operationC 都是不需要的。

面向对象设计七大原则_第6张图片

利用接口隔离原则,将大的接口进行拆分:

面向对象设计七大原则_第7张图片

这样对每个客户端就隐藏了它不需要的功能。


合成复用原则

这个原则很简单:多用组合,少用继承。
我们知道,继承是面向对象三大特征之一:封装、继承和多态。而且继承实现简单,易于扩展。
不过继承是有缺陷的:
1.父类变,子类就必须变。
2.继承破坏了封装,对父类而言,它的实现细节对子类来说都是透明的。
3.继承是一种强耦合关系。

1.下边类图,这里,麻雀、鸽子、鸭子都继承类-鸟,都拥有 鸣叫 方法。然后为 父类加 “飞”时,子类“鸭子”却不需要这个方法。

面向对象设计七大原则_第8张图片

2. 继承破坏了封装。网上有很多这样说的,但又不解释啥意思。查了很多资料,发现是这个意思。

对于子类来说,父类方法的细节是透明的,也就是不可见的。子类不知道里面的内容,但是当父类修改了自己的方法时,子类方法就会受影响发生变化。子类方法本来是封装的,但是父类的改变了这点。即父类破坏了子类的封装

那继承如何破坏封装的呢?看下面代码,来源于《Java编程的逻辑》

public class Base {
	private static final int MAX_NUM = 1000;
	private int[] arr = new int[MAX_NUM];
	private int count;

	public void add(int number) {
		if (count < MAX_NUM) {
			arr[count++] = number;
		}
	}

	public void addAll(int[] numbers) {
		for (int num : numbers) {
			add(num);
		}
	}
}
public class Child extends Base {
	private long sum;

	@Override
	public void add(int number) {
		super.add(number);
		sum += number;
	}

	@Override
	public void addAll(int[] numbers) {
		super.addAll(numbers);
		for (int i = 0; i < numbers.length; i++) {
			sum += numbers[i];
		}
	}

	public long getSum() {
		return sum;
	}

	public static void main(String[] args) {
		Child c = new Child();
		c.addAll(new int[] { 1, 2, 3 });
		System.out.println(c.getSum());
	}
}
addAll 接受参数1、2、3,期望输出是6。可是上边的输出是12!。代码里父类和子类共计算了两次。可以看出,如果子类不知道父类方法实现的细节,他就不能正确的扩展。

修改父类:

	public void addAll(int[] numbers) {
		for (int num : numbers) {
			if (count < MAX_NUM) {
				arr[count++] = num;
			}
		}
	}
这里修改了父类的addAll 方法,但是子类再次运行为 0。出错了!

总结下来:
1.对于子类而言,通过继承实现是没有安全保障的。因为父类修改的内部实现细节,子类的功能就可能被破坏。
2.对于父类而言,有子类继承和重写它的方法时,父类的方法就不能任意修改。


迪米特法则

即最少知道原则。在《代码整洁之道》中翻译为得墨忒耳法则。设计模式中的外观模式(Facade)和中介模式(Mediator),都是迪米特法则应用的例子。

得墨忒耳法则认为,类C的方法f 只应该调用以下对象的方法:
1. C
2. 由f 创建的对象
3. 作为参数传递给f的对象
4. 由C的实体遍历持有的对象

简单来说:只与朋友谈话,不与陌生人谈话。

从这点来讲,下面我们常用的这行代码就违反了这个规则:

System.out.println("Hello world");

这是外观模式的类图,Facade 提供统一的接口,Client只与 Facade 进行通信,与子系统之间的耦合很低。

面向对象设计七大原则_第9张图片

1.在类的划分上,应该创建有弱耦合的类
1.在类的结构设计上,每一个类都应当尽量降低成员的访问权限
3.在类的设计上,只要有可能,一个类应当设计成不变类
4.在对其他类的引用上,一个对象对其它对象的引用应当降到最低
5.尽量降低类的访问权限

6.不要暴露类成员,而应该提供相应的访问器

缺点:

迪米特法则有个缺点:系统中会产生大量的小方法。


你可能感兴趣的:(Design,Pattern)