面向对象设计七大原则

最近团队在学习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》即《程序员修炼之道:从小工到专家

单一职责原则

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

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

开闭原则

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

举个例子:

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


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

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


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


里氏代换原则

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

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


此时代码如下:

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.抽象不应该依赖于具体实现,具体实现应该依赖于抽象。

看下图:


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

再看下面这个图:


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


接口隔离原则

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

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


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


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


合成复用原则

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

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


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 进行通信,与子系统之间的耦合很低。


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

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

缺点:

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


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值