设计模式-六大设计原则(附加实际开发建议以及计算器例子)

使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式使代码编制真正工程化,是软件工程的基石脉络,如同大厦的结构一样。

文章结构:

1.单一职责原则(SRP);
2.里氏替换原则(LSP);
3.依赖倒置原则(DIP);
4.接口隔离原则(ISP);
5.迪米特原则(LoD);
6.开闭原则(OCP)。
7.以计算器设计为例子综合展示各大原则以及相关设计模式。


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

定义:

应该有且仅有一个原因引起类的变更。也就是一个接口或类只负责一件事。

使用原因:

假设一个类负责两大职责,那么当业务变化,一个职责需要变化时就要修改这个类,所以就可能导致另一个职责会发生变化。

优点:

1.类的复杂度降低,实现什么职责都又清晰明确的定义;
2.可读性高,复杂性降低;
3.可维护性高,易维护;
4.变更引起的风险降低。(一个接口修改只对应的实现类有影响,对其他的接口无影响,对系统扩展性、维护性有多大帮助)。

例子:

//这样设计的话,有个后果可以看见:如果我要让帅哥类拥有一个跳舞的技能,那么我要修改哪里呢??修改People类?那就全崩了。所以我们应该把帅哥类拆分出来成为另一个类来继承People。
public class People{
	public void say(String name){
		System.out.println("我是"+name);
	}
}
//场景类
public class Client{
	public static void main(String []args){
		People people=new People();
		people.say("帅哥");
		people.say("美女");
		people.say("大叔")}
}

单一职责原则使用建议

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

但是呢,类的设计也要结合实际,很多时候非单一职责的情况的,我们就要去妥协一下子:
1.只有逻辑足够简单,才可以在代码级别上违背SRP;
2.只有类中方法数量足够少,才可以在方法级别上违背SRP;
3.实际应用中的类都要复杂的多,一旦发生职责扩散而需要修改类时,除非这个类本身非常简单,否则还是要遵循SRP。


二、里氏变换原则(Liskov Substitution Principle,LSP原则):

定义:

所有引用基类的地方必须能透明地使用其子类对象。即:只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或寻常。(子类可完整实现父类的业务)。

设计原因:

有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。

这个定义有四层含义:

### (一)、子类必须完全实现父类的方法:
也就是在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明违背LSP原则。(在编写类的实现时用父类对象,调用时传入其子类实现向上转型即可完成设计。是传入子类对象,在方法实现向上转型成父类)。

针对这一含义,实际的开发有些注意点:

1.业务增多时,类的设计思路:

(1)(不推荐)在moudle类中增加instanceof的判断(即每增加所有与这个类的父类有关系的类都要修改)。
(2)(推荐,维护人员经常干)新增类脱离继承,简历一个独立的父类,可与原来的那个父类建立关联委托关系。

2.具体应用场景类的设计关键:

子类是否能完整地实现父类的业务。(如果子类不能完整实现父类的方法,或者父类的某些方法在子类中已经发生变化,则应该简历断开父子继承关系,采用依赖、聚集、组合等关系。)

(二)子类可以有自己的个性:

也就是子类可以有自己的方法和属性。

注意:在调用那个类时,不要使用父类对象传递。因为向下转型是不安全的。

(三)覆盖或实现父类的方法时输入参数可以被放大。

相关概念:

(1)方法重载(Overload)子类拥有父类的所有属性和方法,方法名相同,但输入参数又不相同。要求方法的输入参数类或数量不相同,在LSP原则下,就是子类的输入参数宽于或等于父类的输入参数。
(2)方法覆写:父类和子类的同名方法的输入参数是相同的,而且两个方法的范围值为子类的小于父类的。

注意

1.子类中方法的前置条件(传递的参数)必须与父类中被覆写的方法的前置条件相同或者更宽松。

2.如果父类的输入参数类型宽于子类的输入参数类型,那么会出现父类存在的地方,子类就未必可存在。因为一旦把子类作为参数传入,调用者可能进入子类的方法。

(四)、覆写或实现父类的方法时输出结果可以被缩小:

含义:

父类的一方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么LSP原则要求S必须小于或等于T。也就是说S和T是同一类型或者S是T的子类。

LSP原则的目的:

增强程序的健壮性,使版本升级时,即使增加子类,原有的子类还可以继续运行。

实际项目做法:项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑。可是!!使用LSP原则的时候要尽量避免子类的“个性”,因为LSP原则只可以正着用不能反着用。

例子:

public class Father {
	public Collection doSomeing(HashMap map){
		System.out.println("父类被执行。。。");
		return map.values();  
		}
}
public class Son extends Father{
	public Collection doSomeing(Map map){
		System.out.println("到子类啦");
		return map.values();
	}
}
//场景类一。
public class Client{
	public static void invoker(){
		Father f=new Father();     //这里就是直接使用父类的对象
		HashMap map=new HashMap();
		f.doSomeing(map);
	}
	public static void main(String []args){
		invoker();
	}
}
//跟场景类一结果一样的场景二:
//分析:为啥会一样结果呢??因为我们设计子类的时候,输入的参数的范围相对于父类扩大了,子类就可以代替父类传递到父类的调用者中,子类的方法就不会被执行啦。但是呢,如果想让子类的方法被执行,就必须覆写父类的方法了。
public class Client{
	public static void invoker(){
		Son son=new Son();
		HashMap map=new HashMap();
		son.doSomeing(map);
	}
	public static void main(String []args){
		invoker();
	}
}

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

定义:简而言之,就是面向接口编程。

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

设计原因:

类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。

DIP原则优点:

减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。

相关概念:

java中一个变量有两种类型:表面类型和实际类型。表面类型是定义的时候赋予的类型,实际类型是对象的类型。

使用:

分析!!分析两个类是否有依赖关系,有则制定出两者之间的接口(或抽象类)即可独立开发。

依赖的三种写法 :

(一)、在类中通过构造函数声明依赖对象。(按照依赖注入的说法,叫做构造函数注入)。

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();  //对象事务一级级地传递,传递到车对象
	}
}

(二)在抽象中设置Setter方法声明依赖关系(依照依赖注入的说法,这叫Setter依赖注入)。

public interface IDriver{
	public interface IDriver{
		public void setCar(ICar car); //车辆型号
		public void drive();
	}
	public class Driver implements IDriver{
		private ICar car;
		public void setCar(ICar car){   //setter函数注入依赖
			this.car=car;	
		}
		public void driver(){
			this.car.run();
		}
	}
}

(三)接口声明依赖对象

//分析:在IDriver中,通过传入ICar接口实现**抽象之间**的依赖关系。Driver实现类也传入了ICar接口,而是什么类型Car就需要在高层声明了。
public interface ICar {
	//是汽车就会跑
	public void run();
}
public interface IDriver{
	//是司机就会开车
	public void drive(ICar car);   //在接口就注入依赖,注入的是接口之间的依赖
}
publi class Driver implements IDriver{
//司机主要职责就是开车
	public void drive(ICar car){
		car.run();
	}
}

使用DIP原则的规则以及注意点

(一)、每个类尽量都又接口或抽象类,或者两者都具备。

(依赖倒置的基本要求,有了抽象才可能依赖倒置)

(二)变量的表面类型尽量是接口或者抽象类。

(但不是所有类型都要抽象,比如一个工具类一般不需接口或抽象类)

(三)任何类都不应该从具体类派生。

(比如:项目处于开发状态,不应从具体类派生出子类。但有维护的口占情况,这时则可以不考虑,但要求不超过两层继承)。

(四)尽量不要覆写基类的方法

(如果基类是一个抽象类,而且方法已经实现,子类应尽量不要覆写。类间依赖的是抽象,覆写抽象方法对依赖的稳定性有影响。)

(五)结合里氏替换原则使用

(原则:接口负责定义public属性和方法,并且声明与其他对象的的依赖关系,抽象类负责公共构造部分的实现,是实现类准确地实现业务逻辑,同时在适当的时候对父类进行细化)。

DIP原则是6个设计原则中最难实现的原则,是实现开闭原则的重要途径。


四、接口隔离原则(Interface Segregation Principle,ISP原则):

定义:

客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。(一句话:接口尽量细化,同时接口中的方法尽量少。)

相关定义:接口分类:

(一)实例接口。在Java中声明一个类,然后new产生一个实例,它是对一个类型的事物的描述(一个角度:Java中的类也是一种接口)。
(二)类接口。Java中经常使用的interface关键字定义的接口。

做法:

把大接口分解为几个小接口。(例子:美女的接口分解为外貌美女、气质型美女)

注意:

单一职责要求的是类和接口职责单一,注重的是职责!!!这属于业务逻辑的划分。

接口隔离原则是对接口进行规范约束:(四层含义)开发注意点

(1)接口要尽量小。

根据接口隔离原则拆分接口时,首先必须满足单一职责原则!

(2)接口要高内聚。

高内聚:提高接口、类、模块的处理能力,减少对外的交互。要求接口–尽量少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利。

(3)定制服务:

原因:模块之间必然会有耦合,有耦合就要有相互访问的接口(可能是Java中的interface,也可能为一个类或单纯数据交换)设计时需要为各个访问者(客户端)定制服务。

定义:就是单独为一个getItem提供优良的服务

要求做法:只提供访问者需要的方法。


项目开发时接口设计规则:

(一)一个接口只服务于一个子模块或业务逻辑
(二)通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨肉“,而不是肥嘟嘟的一堆方法。
(三)已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理。
(四)了解环境,不要盲从。每个项目或产品都有特定的环境因素,别看到大师做接口就做接口。环境不同,接口拆分的标准不同,有一点点的妥协原则的。

例子:

//外貌型美女
public interface GoodBodyGirl{
	//美丽的外貌
	public void goodLooking{
	}
	//好身材
	public void niceFigure{
	}
}
//气质型美女
public interface GreatTemperametGirl{
	//气质
	public void greatTemperament();
}

(五)迪米特原则(Law Of Demeter,LOD原则):

定义:

一个对象应该只对其他对象有最少的了解。(也就是说你的类多复杂都不关我的事,我只关心你的public事物)

设计原因:

类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

含义有四层:

(一)只和朋友交流:类与类的联系不要跨越。

(也就是我想叫我的朋友的朋友去干活,就必须通过我的朋友去告诉我的朋友的朋友)
类与类之间的关系是建立在类间的,而不是方法之间,因此一个方法尽量不引入一个类中不存在的对象。

(二)朋友之间也是有距离的:

一个雷公开的public属性或方法越多,修改时设计的面也就越大,变更引起的风险扩散也越大。

注意:尽量不公布太多public方法和非静态的public变量,多使用private–>类的高内聚。

技巧:一个类暴露过多方法,而发现这堆方法有关系时,可以设计一个public方法,把那堆方法统率在新的public方法,并设置为private。

(三)是自己的就是自己的:

方法设计原则:如果一个方法放在本类中,即不增加类间的关系,也对本类不产生负面影响,就可以放置在本类中。

(四)谨慎使用Serializable(序列化机制,具体看我另一篇博客输入输出机制)。

例子:一个项目使用RMI(远程方法调用)方式传递VO值对象,其实就是个网络请求过程,那么这个对象就必须实现Serializable接口。(也就是把需要网络传输的对象进行序列化,否则会出现Not Serializable Exception)。

核心观念:类间解耦,实现弱耦合。

可能导致的后果:然而其结果会产生大量中转类或跳转类,导致系统的复杂性提高,为维护带来难度。所以使用LOD原则时要反复权衡。跳转不超过两次。

例子:

//场景是:老师叫班长清点女生人数,班长就去清点了。
//分析:所以这个场景,根据LOD原则,老师是不能调用女生的类的。然后我们知道只能够让班长去调用女生。而老师去调用班长。
public class Teacher{
	public void commond(GroupLeader groupleader){
		groupleader.countGirls();
	}
}
public class Girl{

}
public class GroupLeader{
	private List<Girl> listGirls;
	//传递全班的女生进来
	public GroupLeader(List<Girl> _listGirls){
		this.listGirls = _listGirls;
	}
	//清查女生数量
	public void countGirls(){
		System.out.println("女生数量是:"+this.listGirls.size());
	}
}

六、开闭原则:

定义:

一个软件实体应通过扩展来实现变化,而不是通过修改已有的代码来实现变化。

设计原因:

在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

相关概念:

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

业务变化:(三种类型)

(1)逻辑变化。

只变化一个逻辑而不涉及其他模块,可通过修改原有类中的方法的方式来完成。但是有个前提条件:所有依赖或关联类都按照相同逻辑处理。

(2)子模块变化:

一个模块变化,必会对其他模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化。

(3)可见视图变化:

可见视图是提供给客户使用的界面,该部分的变化一般会引起连锁反应。因为多了一项显示数据,意味着你的其他部分代码也要增多一项这样的数据。

针对软件产品的需求业务变化:有三大扩展方案

(一)修改接口,增加方法:(否定)

后果:所有拥有该接口的实现类必须作修改,场景类也要修改。
否定这个方案的原因:接口应该是稳定且可靠的,不应经常发生变化,否则接口作为契约的作用就失去了。

(二)修改实现类:(不推荐)

做法:通过class文件替换的方式完成业务变化。
后果:修改了的信息可能会导致其他类获得的信息不对称。

(三)通过扩展实现变化:(推荐)

做法:增加子类,覆写方法,高层次的模块(static静态模块区)通过新增子类产生新的对象。
代码例子:

public interface Book{
	//书籍有名称
	public String getName();
	//书籍有售价
	public int getPrice();
	//书籍有作者
	public String getAuthor();
}
public class NovelBook implements Book{
	//书籍名称
	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;
	}
}
//然后到了阅读月啦,我们书店就要打折啦!!怎么办??正如我们刚刚提到的三种方案。
//分析:如果选择第一种方案,在接口里面新增方法,那么我们的小说类NovelBook也要修改,而且违背接口的稳定性原则。如果选择第二种方案,直接修改实现类,也就是修改NovelBook的类啦,是的,我们可以做一个打折的方法,然后完成打折的业务扩展,但是!!其他业务就有可能有影响,比如设计模式里面说的采购人员也看价格,而当这个方法实现了,采购人员只会看到打折后的价格。
//所以我们采用第三种方案。来完成业务扩展。
public class OffNovelBook extends NovelBook{
	public offNovelBook(String _name,int _price,String _author){
		super(_name,_price,_author);
	}
	@Override
	public int getPrice(){
		//原价
		int selfPrice = super.getPrice();
		int offPrice = 0;
		if(selfPrice>4000){
			offPrice = selfPrice * 9/10;
		}else{
			offPrice = self * 8/10;
		}
		return offPrice;
	}
}

开闭原则是最基础的一个原则,也就是说前五个原则是指导设计的工具和方法,而开闭原则才是精神领袖。可以说,只要遵守前五大原则,设计出来的软件就会符合开闭原则。

思想核心:用抽象构建框架,用实现扩展细节。

联系其他原则:单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。


七、计算器(结合了六大设计原则与工厂模式)

//这个就是运算类的工厂模式。因为运算太多了,如果每个都在运算类里面再写个几行,那样会导致运算类的繁重累赘而且不清晰。以多态特性来设计。
public class OperationFactory{
	public static Operation createOperate(String operate){
		Operation oper = null;
		swich(operate){
			case "+":
				oper = new OperationAdd();
				break;
			case "-":
				oper = new OperationSub();
				break;
			case "*":
				oper = new OperationMul();
				break;
			case "/":
				oper = new OperationDiv();
				break;
		}
		return oper;
	}
}
//而这个运算类,把他给抽象处理就体现出 用抽象构建架构,用实现扩展细节了。
//分析:我们把运算类的核心抽取出来:两个数,一个结果,还需一个运算规则。这几个就是本质!而运算规则有很多种,我们需要交给子类去实现
//根据 单一职责原则--我们把每个运算规则的类分开成了单独的类。加减乘除均为一个类。
//根据 里氏替换原则--加减乘除的四个类均继承一个抽象出来的运算父类,获得一样的特性。两个数,一个结果。
//根据 依赖导致原则--面向接口编程,也就是面对抽象的结果编程。而我们抽象出的运算父类,拥有各个运算规则子类的共同特征。
//根据 接口隔离原则--呃,,这里只作出了一步抽象,没有多余的接口。
//根据 迪米特原则--松耦合。当然啦,都把加减乘除分离开来成一个类了,以后你想要修改加法,比如根据职位不同去加不同的薪水,这样更改,你只需改加法这个类。
//根据 开闭原则。核心--用抽象构建框架,用实现扩展细节。以上原则都实现了,这不就好了咩。
public abstract class Operation{
	private double numberA = 0;
	private double numberB = 0;
	private double result = 0;
	public void setNumberA(Double _numberA){
		this.numberA = _numberA;
	}
	public double getNumberA(){
		return numberA;
	}
	public void setNumberB(Double _numberB){
		this.numberB = _numberB;
	}
	public double getNumberB(){
		return numberB;
	}
	public abstract double getResult();
}
//加法类
class OperationAdd extends Operation {
	public double getResult(){
		result = numberA + numberB;
		return result;
	}
}
//减法类
class OperationSub extends Operation {
	public double getResult(){
		result = numberA - numberB;
		return result;
	}
}
//乘法类
class OperationMul extends Operation {
	public double getResult(){
		result = numberA - numberB;
		return result;
	}
}
//除法类
class OperationDiv extends Operation {
	public double getResult(){
		try{
			result = numberA / numberB;
		}catch(Exception e){
			System.out.println("除数不能为0");
			e.printStackTrace();
		}
		return result;
	}
}
//场景类代码:
Operation oper;

oper = OperationFactory.createOperate(+);

oper.setNumberA(1.0);

oper.SetNumberB(2.0);

double result = oper.GetResult();

好了,设计模式-六大设计原则(附加实际开发建议以及计算器例子)讲完了。本博客借鉴了一部分设计模式之禅,这是一本很好的书,只是我在这里总结以及写出自己的理解。欢迎在下面指出错误,共同学习!

转载请注明:【JackFrost的博客】
更多内容,可以访问JackFrost的博客

你可能感兴趣的:(设计模式,设计模式,软件工程,工厂模式之计算器,Java设计模式,软件六大设计原则)