首页 > 代码库 > 设计模式之6大设计原则

设计模式之6大设计原则

设计模式之6大设计原则


原则一:单一职责原则(Single Responsibility Principle SRP)

定义:There should never be more than one reason for a class to change.(应该有且仅有一个原因引起类的变更)

好处

1.类的复杂性降低,实现什么职都有清晰明确的定义;

2.可读性高,负责性降低,当然可读性就提高了;

3.可维护性提高,可读性提高,自然就更容易维护了;

4.变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性。维护性都有非常大的帮助。

难点:职责的划分。

注意:单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计的是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。

    对于接口,再设计的时候一定要做到单一,但是对于实现类就需要多方面的考虑了。生搬硬套单一职责原则会引起类的剧增,给维护带来非常多的麻烦,而且过分细分类的职责也会人为的增加系统的复杂性。本来可以一个类实现的行为硬要拆成两个类,然后再使用聚合或者组合的方式耦合在一起,人为制造了系统的复杂性。

注意:单一职责适用于接口、类,同时也适用于方法,即一个方法尽可能只做一件事。

建议:接口一定做到单一职责,类的设计尽量做到只有一个原因引起变化。

例子:在基于角色的访问控制中,用户管理、修改用户信息、增加机构、增加角色等,我们将这些写到一个接口,如下:

技术分享

   以上类图中明显用户属性和行为没有分开,不满足单一职责原则。我们重新将该接口封装为两个接口,IuserBO负责用户属性,IUserBiz负责用户的行为,如下:

技术分享


原则二:里氏替换原则(Liskov Substitution Principle LSP)

面向对象语言中继承机制的有点:

1.代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;

2.提高代码的重用性;

3.子类可以形似父类,但是又异于父类;

4.提高代码的可扩展性,例如开源框架扩展接口都是通过继承父类来完成了;

5.提高产品或者项目的开放性。

面向对象语言中继承机制的缺点:

1.继承是侵入性的,只要有继承,就必须拥有父类的属性和方法;

2.降低代码的灵活性。子类必须拥有父类的方法和属性,让子类自由的世界中多了很多约束;

3.增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的情况下,这种修改可能带来非常糟糕的结果--大量代码需要重构。

第一种定义:If for each object o1 of type S there is an object o2 of type of T such that for all programs Pdefined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

    如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。

第二种定义:所有引用基类的地方必须能透明的使用其子类的对象。

里氏替换原则为良好的继承定义了一个规范,定义包含了四层含义

1.子类必须完全实现父类的方法;

例子

技术分享

CS游戏中的枪支类图

假设:在上述基础上增加玩具枪,类图如下 :

技术分享

但是很可惜玩具枪不能杀人,需要将玩具枪与真实枪分离,类图如下:

技术分享

注意:如果子类不能完整的实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议父子类断开继承关系,采用依赖、聚类、组合等关系代替继承。

注意:在类中调用其他类时务必要使用父类或者接口,如果不能使用父类或者接口,则说明类的设计已经违背LSP原则。

2.子类可以有自己的个性;

例子:在上述类图的基础上,给枪械子类增加自己特有的方法和属性,类图如下:

技术分享


3.覆盖或者实现父类的方法时输入参数可以被放大;

例子

父类

public class Father {
	public Collection doSomething(HashMap map){
		System.out.println("父类被执行........");
		return map.values();
	}
}

子类:

public class Son extends Father{
	public Collection doSomething(Map map){
		System.out.println("子类被执行.........");
		return map.values();
	}
}

场景类一:

public class Client {
	public static void invoker(){
		Father f=new Father();
		HashMap map=new HashMap();
		f.doSomething(map);
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		invoker();
	}
}

技术分享

里氏替换:

public class Client {
	public static void invoker(){
		Son f=new Son();
		HashMap map=new HashMap();
		f.doSomething(map);
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		invoker();
	}
}

技术分享

    里氏替换前后运行结果一致,父类中方法的参数为HashMap类型,子类的参数是Map类型,即子类的参数范围扩大,子类代替父类传递到调用者,子类的方法永远不会执行。反之,如果父类的参数类型比子类的参数类型范围大,则父类存在的地方,子类未必能够存在,因为一旦将子类作为参数传入,调用者就有可能进入子类的方法范畴。如下:

父类:

public class Father {
	public Collection doSomething(Map map){
		System.out.println("父类被执行........");
		return map.values();
	}
}

子类:

public class Son extends Father{
	public Collection doSomething(HashMap map){
		System.out.println("子类被执行.........");
		return map.values();
	}
}

场景类:

public class Client {
	public static void invoker(){
		Father f=new Father();
		HashMap map=new HashMap();
		f.doSomething(map);
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		invoker();
	}
}

技术分享

里氏替换:

public class Client {
	public static void invoker(){
		Son f=new Son();
		HashMap map=new HashMap();
		f.doSomething(map);
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		invoker();
	}
}

技术分享

    可见,子类在没有覆写父类方法的前提下,子类方法被执行,违反里氏替换原则。

    因此,子类方法中的前置条件必须与超类中被重载的方法的前置条件更加宽松或相同。

4.覆写或实现父类的方法时输出的结果可能被缩小。

    假设父类的某个方法返回类型T,子类的相同方法(重载或者覆写)的返回值为S,那么历史替换原则要求S必须小于等于T。如果是覆写,父类和子类的同名方法的输入参数是相同的,两个方法的范围值S小于等于T,这是覆写的要求,这是重中之重,子类覆写父类的方法,天经地义。如果是重载,则要求方法的输入类型或者数量不同,在里氏替换原则要求下,就是子类的输入参数大于或等于父类的输入参数,也就是说你写的这个方法是不会被调用的。


原则三:依赖倒置原则(Dependence Inversion Principle)

    依赖倒置原则(Dependence Inversion Principle, DIP)原始定义:High level modules should not be depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.

含义

(1) 高层模块不应该依赖低层模块,两者都应该依赖其抽象;

(2) 抽象不应该依赖细节;

(3) 细节应该依赖抽象。

理解:高层模块和低层模块好理解,每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子模块就是低层模块,原子模块再组装就是高层模块。在Java中抽象就是指接口或者抽象类,二者都不能直接被实例化;细节就是类的实现,实现接口或者继承抽象类而产生的类就是细节,可以被直接实例化,即可以加上关键字new产生一个对象。

依赖倒置原则在Java中的表现:

(1) 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过借口或者抽象产生的;

(2) 接口或抽象不依赖于实现类;

(3) 实现类依赖接口或者抽象类。

依赖倒置反命题:不使用依赖倒置也可以减少类间耦合,提高系统稳定性,降低并行开发风险,提高代码的可读性可可维护性。

证明

奔驰车类:

public class Benz {
	public void run(){
		System.out.println("奔驰车开始运行.......");
	}
}

司机类:

public class Driver {
	public void drive(Benz benz){
		benz.run();
	}
}

场景类:

public class Client {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Driver zhangSan=new Driver();
		Benz benz=new Benz();
		zhangSan.drive(benz);
	}
}

运行结果:

技术分享

    可以发现,上述代码成功运行起来,但是当张三不仅需要开奔驰车还需要开宝马车时,上述代码不能很好的实现,司机类和奔驰车类之间是紧耦合关系,其导致的结果就是系统的可维护性大大降低。

    注意:设计是否具备稳定性,只要适当的“松松土”,观察“设计的蓝图”是否可以茁壮的成长就可以得出结论,稳定性较高的设计,在周围环境频繁变换的时候,依然可以做到“我自岿然不动”。

    接着证明“减少并行开发的风险”,并行开发最大的风险就是风险扩散,本来只是一段程序的错误或者异常,逐步波及一个功能,一个模块,最后毁了整个项目。例如,一个团队各人负责不同的模块,甲负责汽车类,乙负责司机类,在甲没有完成的情况下,乙是不能完全的编写代码的。在这种不使用依赖倒置原则的环境,所有的开发都是“单线程”的,甲做完乙做,乙做完丙做...

    以上足以证明如果不使用依赖倒置原则就会加重类间耦合,降低系统稳定性,增加并行开发的风险,降低代码的可读性和可维护性。

使用依赖倒置的例子

司机接口

public interface IDriver {
	public void drive(ICar car);
}

司机类

public class Driver implements IDriver {
	public void drive(ICar car){
		car.run();
	}
}

汽车接口

public interface ICar {
	public void run();
}

奔驰类

public class Benz implements ICar{
	public void run(){
		System.out.println("奔驰车开始运行.......");
	}
}

宝马类

public class BMW implements ICar {
	public void run(){
		System.out.println("宝马汽车开始运行.....");
	}
}

场景一

public class Client {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		IDriver zhangSan=new Driver();
		ICar benz=new Benz();
		zhangSan.drive(benz);
	}
}

技术分享

场景二

public class Client {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		IDriver zhangSan=new Driver();
		ICar bmw=new BMW();
		zhangSan.drive(bmw);
	}
}

技术分享

    注意:Java中只要定义变量就必然有类型,一个变量可以有两个类型:表面类型和实际类型,表面类型是在定义的时候赋予的类型,实际类型是对象的类型,如zhangSan表面类型是IDriver,实际类型是Driver。

依赖的三种写法

1.构造函数传递依赖对象

司机接口

public interface IDriver {
	public void drive();
}

司机

public class Driver implements IDriver {
	private ICar car;
	public Driver(ICar _car){
		this.car=_car;
	}
	public void drive(){
		this.car.run();
	}
}

2.setter方法传递依赖对象

司机接口

public interface IDriver {
	public void setCar(ICar car);
	public void drive();
}
司机类

public class Driver implements IDriver {
	private ICar car;
	public void setCar(ICar car){
		this.car=car;
	}
	public void drive(){
		this.car.run();
	}
}

3.接口声明依赖对象

见上面依赖倒置的例子。

总结:依赖倒置的本质是通过抽象(接口或者抽象类)使各个领域或模块的实现彼此独立,互不影响,实现模块间的松耦合。

1.每个类尽量有接口或抽象类,或者抽象类和接口两者都具备(基本要求);

2.变量的表面类型尽量是接口或者抽象类;

3.任何类都不应该从具体类派生,一般具体项目中只要不超过两层的继承都是可以忍受的;

4.尽量不要覆写基类的方法,如果基类是一个抽象类,而这个方法已经实现,子类尽量不要覆写,类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会产生一定的影响;

5.结合里氏替换原则使用,接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。

原则四:接口隔离原则

两种接口

1.实例接口,在Java中通过new关键字产生的一个实例,它是对一个类型的事物的描述;

2.类接口,在Java中使用interface关键字定义的接口。

两种隔离

1.Clients should not be forced to be depend upon interfaces that they don‘t use.(客户端不应该依赖它不需要的接口)

2.The dependence of one class to another one should denpend on the smallest possible interface.(类间的依赖关系应该建立在最小的接口上)

总结:建立单一接口,不要建立臃肿庞大的接口。接口尽量细化,同时接口中的方法尽量少。

接口隔离与单一职责的区别:接口隔离原则与单一职责原则的审视角度不同,单一职责原则要求的是类和接口的职责单一,注重的是职责,这是业务逻辑上的划分,而接口隔离原则要求接口的方法尽量少。

例子:

美女接口

public interface IPettyGirl {
	public void goodLooking();
	public void niceFigure();
	public void greatTemperament();
}

美女类

public class PettyGirl implements IPettyGirl{
	private String name;
	
	public PettyGirl(String _name){
		this.name=_name;
	}
	
	public void goodLooking(){
		System.out.println(this.name+"--脸蛋漂亮");
	}
	
	public void greatTemperament(){
		System.out.println(this.name+"--气质好");
	}
	
	public void niceFigure(){
		System.out.println(this.name+"--身材好");
	}
}

抽象星探类

public abstract class AbstractSeacher {
	protected IPettyGirl pettyGirl;
	
	public AbstractSeacher(IPettyGirl _pettyGirl){
		this.pettyGirl=_pettyGirl;
	}
	
	public abstract void show();
}

星探类

public class Seacher extends AbstractSeacher{
	public Seacher(IPettyGirl _pettyGirl){
		super(_pettyGirl);
	}
	
	public void show(){
		System.out.println("信息如下");
		super.pettyGirl.goodLooking();
		super.pettyGirl.niceFigure();
		super.pettyGirl.greatTemperament();
	}
}

场景类

public class Client {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		IPettyGirl yanZi=new PettyGirl("燕子");
		AbstractSeacher seacher=new Seacher(yanZi);
		seacher.show();
	}
}

技术分享

    人们审美的观点在不断变化,可以发现当审美条件发生变化时,IPettyGirl的设计是有缺陷的,过于庞大,容纳了一些可变因素,根据接口隔离原则,星探AbstractSeacher应该依赖于具有部分特质的女孩子,而我们却将这些特质全部封装了起来,放到了一个接口中,造成封装过渡。

改进:将IPettyGirl拆分为两个接口

两种美女接口:

public interface IGoodBodyGirl {
	public void goodLooking();
	public void niceFigure();
}
public interface IGreatTemperamentGirl {
	public void greatTemperamentGirl();
}

美女类

public class PettyGirl implements IGoodBodyGirl,IGreatTemperamentGirl{
	private String name;
	
	public PettyGirl(String _name){
		this.name=_name;
	}
	
	public void goodLooking(){
		System.out.println(this.name+"--脸蛋漂亮");
	}
	
	public void greatTemperament(){
		System.out.println(this.name+"--气质好");
	}
	
	public void niceFigure(){
		System.out.println(this.name+"--身材好");
	}
}
接口隔离原则的4层含义

1.接口要尽量最小,这是接口隔离原则的核心定义,但是“小”是有限度的,不能违反单一职责原则;

2.接口要高内聚,提高接口、类、模块的处理能力,减少对外的交互,具体就是要求在接口中尽量少用public方法,接口是对外承诺的,承诺越少对系统的开发越有利,变更的风险越小,同时也有利于降低成本;

3.定制服务,即单独为一个个体提供优良的服务,我们在系统设计时也需要考虑对系统之间或模块之间的接口采用定制服务,只提供访问者需要的方法;

4.接口设计是有限度的,接口粒度越小,系统越灵活,但是灵活的同时也带来了结构的复杂化,开发难度增加,可维护性降低。


原则五:迪米特法则(Law of Demeter, LoD)

    迪米特法则(Law of Demeter)也成最少知识原则(Least Knowledge Principle):一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,只知道其public方法,其余一概不关心。

    总结:迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才会提高。其要求的结果就是产生了大量的中转或跳转类,导致系统的复杂性提高,同时也给维护带来了难度。在使用时要做到反复权衡,既要做到结构清晰,又做到高内聚低耦合。


原则六:开闭原则

    开闭原则定义:Software entities like classes, modules and functions should be open for extension but closed for modifications.(一个软件实体如类、模块和函数应该对扩展开放,对修改关闭)

    理解:软件实体应该对扩展开放,对修改关闭,其含义是说一个软件实体应该是通过扩展来实现变化,而不是通过修改已有的代码来实现变化。

    软件实体包括:1)项目或软件产品中按照一定的逻辑规则划分的模块;2)抽象和类;3)方法。

例子:

书籍接口

public interface IBook {
	public String getName();
	public int getPrice();
	public String getAuthor();
}

小说类

public class NovelBook implements IBook{
	private String name;
	private int price;
	private String author;
	
	public NovelBook(String _name,int _price,String _author){
		this.name=_name;
		this.price=_price;
		this.author=_author;
	}
	
	public String getAuthor(){
		return this.author;
	}
	
	public String getName(){
		return this.name;
	}
	
	public int getPrice(){
		return this.price;
	}
}

书店类

public class BookStore {
	private final static ArrayList<IBook> bookList=new ArrayList<IBook>();
	
	static{
		bookList.add(new NovelBook("天龙八部",3200,"金庸"));
		bookList.add(new NovelBook("巴黎圣母院",5600,"y"));
		bookList.add(new NovelBook("悲惨世界",3500,"雨果"));
		bookList.add(new NovelBook("金瓶梅",4300,"兰陵笑笑生"));
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		NumberFormat formatter=NumberFormat.getCurrencyInstance();
		formatter.setMaximumFractionDigits(2);
		System.out.println("卖书记录如下");
		for(IBook book:bookList){
			System.out.println("书籍名称:"+book.getName()+"\t书籍作者:"+
		book.getAuthor()+"\t书籍价格:"+formatter.format(book.getPrice()/100.0)+"元");
		}
	}

}

    技术分享

上述代码正常运行,但是当书籍需要打折出售时,书籍类需要作出变化,一般有如下三种方法解决此类问题:

1.修改接口,在IBook上增加一个getOffPrice()方法,专门进行打折处理。但是这样处理,NovelBook和BookStore都需要修改;

2.修改实现类,直接在NovelBook中getPrice()方法中实现打折处理,但是这样仍是有缺陷的;

3.通过扩展类实现变化,增加一个子类offNovelBook,覆写getPrice()方法,修改少,风险小。

打折小说类

public class offNovelBook extends NovelBook{
	public offNovelBook(String _name,int _price,String _author){
		super(_name,_price,_author);
	}
	
	public int getPrice(){
		int selfPrice=super.getPrice();
		int offPrice=0;
		if(selfPrice>4000){
			offPrice=selfPrice*90/100;
		}else{
			offPrice=selfPrice*80/100;
		}
		return offPrice;
	}
}
书店类

public class BookStore {
	private final static ArrayList<IBook> bookList=new ArrayList<IBook>();
	
	static{
		bookList.add(new offNovelBook("天龙八部",3200,"金庸"));
		bookList.add(new offNovelBook("巴黎圣母院",5600,"y"));
		bookList.add(new offNovelBook("悲惨世界",3500,"雨果"));
		bookList.add(new offNovelBook("金瓶梅",4300,"兰陵笑笑生"));
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		NumberFormat formatter=NumberFormat.getCurrencyInstance();
		formatter.setMaximumFractionDigits(2);
		System.out.println("卖书记录如下");
		for(IBook book:bookList){
			System.out.println("书籍名称:"+book.getName()+"\t书籍作者:"+
		book.getAuthor()+"\t书籍价格:"+formatter.format(book.getPrice()/100.0)+"元");
		}
	}

}

技术分享

开闭原则的重要性

    开闭原则非常著名,只要是面向对象编程,不管是什么编程语言,开发时都会提及开闭原则。另外,开闭原则是最基础的一个原则,前五个原则都是开闭原则的具体形态,也就是说前五个原则就是指导设计的工具和方法,而开闭原则才是精神领袖。

1.开闭原则方便测试

2.开闭原则可以提高复用性

3.开闭原则可以提高可维护性

4.面向对象开发的要求


设计模式之6大设计原则