设计模式之(一) 六大设计原则

版权声明:欢迎转载,转载请说明出处https://csdn.yanxml.com。大数据Github项目地址https://github.com/SeanYanxml/bigdata。 https://blog.csdn.net/u010416101/article/details/84654212

前言

人无常师,水无常形。兵无常势,文无定法。 --《鬼谷子》

写在设计模式总结之前。任何东西,都在在变化的。开发也不例外。所谓设计模式,就是前辈在开发过程中总结出来的一系列需要学习的地方。但是文无定法,开发中可以使用这样的设计模式,也可以不使用。关键在于灵活运用,以及具体问题,具体分析。最终的目的就是完成开发任务,实现项目的正常上线,不出问题。

附: 文中所有的样例都可以在:https://github.com/SeanYanxml/arsenal 项目内找到。


六大铁则

  • 单一指责原则(single responsibility principle, SRP)
There should never be more than one reason for a class to change.

一个类的更改原因常常不只一个。
  • 里氏替换原则(Liskov Substitution Principle, LSP)
If for each object o1 of type S, there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substitued for o2 then S is a subtype of T.
(如果对于每一个S的对象o1, 都有类型为T的类型o2, 使得以T定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有变化,那么类型S是T的子类型。)
  • 依赖倒置原则(Dependence Inversion Pinciple, DIP)
High level modules should not depend upon low modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.

高层模块不应该依赖底层模块,两者都应该依赖其抽象;
抽象不应该依赖细节;
细节应该依赖抽象;
  • 接口隔离原则(Interface Segregation Principle, ISP)
Clients should not be forced to depend upon interfaces that they don't use.(客户端不应该依赖它不需要的接口)
The dependency of one class to another one should depend on the smallest possible interface.(类间的依赖关系应该建立在最小的接口上。)
  • 迪米特法则(Law of Demeter, LoD) 最少知识原则(Least Knowledge Principle, LKP)
Only talk to your immediate friends.(只与直接的朋友通信。)
  • 开闭原则(Open Closed Principle)
Software entities like classes,modules and functions should be open for extension but closed for modification.(一个软件实体如类、模块和函数应该对扩展开发,对修改关闭。)

单一职责原则(Simple Responsibility Principle - SRP)

There should never be more than one reason for a class to change.
(类更改的原因不应该超过一个)。

  • 单一职责 Case 1 (用户管理-职责细化)

UserInfo类图1

UserInfo类图

  • 单一职责 Case 2 (通话管理-职责细化)

Phone类图(初始)

Phone类图(变化)

优点:1.复杂度低,容易理解。2. 便于维护,变更的风险低。

缺点:1.有时划分太细,导致维护花费过大。

But it is sometimes hard to say. 分割过大容易造成功能分割不清,分割过细容易造成维护困难。通常需要根据开发的人员情况、项目规模、开发成本(时间/人员)等视情况而定。

个人理解:本文例举的例子其实是关于一个类的细节的划分,这通常根据开发的规模,项目的预算等决定的。把握划分的度其实非常的重要,这也是我们在开发过程中最难以把握的部分。作为一个架构师,需要根据多方面的决定进行决策。


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

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
所有引用基类的地方必须能透明地使用其子类的对象。

父类能够使用的地方,子类必然可以替换。子类可以使用的地方,父类不一定可以替换。

里氏替换原则主要可以归纳为如下四点。前面2点主要讲的是继承的相关特性(即子类完全具有父类的特性,子类还可以有自己的特性),后面2点主要讲解在开发中需要注意的参数设置的问题。

  • 子类必须完全实现父亲类的方法

  • 子类可以有自己的个性

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

  • 覆写或实现父类方法时输出结果可以被缩小

  • 子类必须完全实现父类的方法

Gun类图(初始)

## AbstractGun
public void killEnemy(){
	gun.shoot();//gun为AbstractGun类型
}

## Client
public class client{
	public static void main(String []args){
		Soldier soldier = new Soldier;
		soldier.setGun(new Rifile());
		soldier.killEnemy();
	}
}

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

变种-玩具枪类
变种-玩具枪类

  1. 通过instanceof进行判断,但是维护性差。

  2. ToyGun脱离继承关系,建立独立的父类,为了代码的复用,可以与AbstractGun建立关联委托关系。

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

  • 子类可以有自己的个性

AUG

# 正确
public class Client{
	public static void main(String []args){
		Snipper snipper = new Snipper();
		snipper.setGun(new AUG());
		snipper.killEnemy();
	}
}

# 错误
public class Client{
	public static void main(String []args){
		Snipper snipper = new Snipper();
		snipper.setGun( (AUG) new Rifle());
		snipper.killEnemy();
	}
}

java.class.ClassCastException异常,父类转换成子类异常。

注意: 子类自己的个性,会导致有子类的地方不能出现父类。继而造成在某些类的使用时,只能依赖子类,而不是父类。

  • 覆盖父类方法时输入参数可以被放大(重载)

子类中方法的前置条件必须与超类中被覆写的方法的前置条件相同或者更宽松。

Father-Son类图(初始)

#调用

public class Client{
	// 调用父类doSomething方法
	public static void invoker1(){
		Father f = new Father();
		HashMap map = new HashMap();
		f.doSomething(map);
	}
	
	// 调用父类doSomething方法
	public static void invoker2(){
		Son s = new Son();
		HashMap map = new HashMap();
		s.doSomething(map);
	}
	
}

Father-Son类图(变化)

#调用

public class Client{
	// 调用父类doSomething方法
	public static void invoker1(){
		Father f = new Father();
		HashMap map = new HashMap();
		f.doSomething(map);
	}
	
	// 调用子类doSomething方法
	public static void invoker2(){
		Son s = new Son();
		HashMap map = new HashMap();
		s.doSomething(map);
	}
	
}

这里主要注意子类中,覆写(Override)和重载(Overload)的区别。我们在日常使用过程中,经常是父类和子类的输入参数一样(即覆写),常常没有意识到这类问题的存在。这里主要表明的是,在重载父类方法时一定要注意参数的变化。因为父类通常是抽象的,子类是具体的。我们不能在调用父类方法时,在子类中进行抽象化。导致逻辑的混乱。

如果一直使用覆写,是不会出现如下的问题的。

  • 覆写或实现父类的方法时输出结果可以被缩小
class Father{
public Object doSomething(...)
}

class Son extends Father{
public String doSomething(...)
}

继承的优缺点主要如下所示:

优点:1.代码共享,减少工作量。2. 提高代码的重用性。3. 提高代码的扩展性和开放性。

缺点:1.继承是具有侵入性的。子类必须具有和父类相同的方法和成员。2. 降低代码的灵活性。子类必须具有和父类相同的方法和成员。3. 增加和耦合。修改父类时要考虑子类的变化和修改。

个人理解:里氏替换原则通常讲的是继承,以及继承过程中需要注意的事项。主要讲解的是替换这一操作的正确性。它和依赖倒置原则讲的是开发中继承的两个方面。对于里氏替换原则来说,只要了解父类存在的地方都可以使用子类。这个原则即可。

另外,在开发过程中通常都希望,使用父类,进而通过重写父类方法进行控制和使用。但是一旦子类中具有特性,使用子类独有方法时有时就无法通过父类进行调用了。

  1. 开发过程通常不需要子类的特性。
    2.父类中进行冗余创建对于其它子类无用的接口和方法。
    3.在子类一定具有特性时造成无法收场的情况下,违反里氏替换原则,这也是无法避免的事情。

依赖倒置原则(Dependence Inversion Principle -DIP)

High level modules should not depend upon low modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.

高层模块不应该依赖底层模块,两者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象;

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

面向接口编程,OOD(Object-Oriented Design)面向对象设计的核心之一。

  • 面向实体

驾驶汽车

  • 面向接口

驾驶汽车2

  • 依赖的几种写法

上文中的driver(ICar car)变表明了IDriver对于ICar的依赖关系。主要的依赖的写法主要包括三种,下面以代码的形式表现出来。(构造参数传递对象/Setter方法传递对象/接口调用时传递)

# 1.构造参数传递对象
public interface IDriver{
	public void drive();
}

public class Driver implements IDriver {
	private ICar car;
	public Driver(){car=null;}
	public Driver(ICar car){this.car=car;}
	public void drive(){if(null != car){car.dirve();}}
}

# 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(){if(null != car){car.dirve();}}
}

# 3. 接口声明依赖对象
public interface IDriver{
	public void drive(ICar car);
}

public class Driver implements IDriver {
	public void drive(ICar car){if(null != car){car.dirve();}}
}

依赖倒置原则实现的原则:

  1. 每个类尽量都有接口或接口类,或者抽象类和接口两者都具备;
  2. 变量的表面类型尽量时接口或抽象类;
  3. 任何类都不应该从具体类派生;
  4. 尽量不要覆写基类的方法;
  5. 结合里氏替换原则使用。

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

"倒置"的含义?
“正置”含义在于面向实体。我开车,我会选择一辆实体的车。而倒置在于,我会选择一辆抽象的车,车的类型可以更换。我依赖的在于车的抽象,而不是实体车。

个人理解: 依赖倒置原则,说的在于开发的主体思想。面向接口,面向抽象,而不是面向具体。这样可以增加很多的变化,开发更加灵活和多样化。主要是利用面向对象中继承和多态的个性。

就好像我们常说的物质决定意识,意识反作用于物质。领导人讲话时,都将东西说的更加的抽象,这样可以适用于各行各业,而免去了一一列举的烦恼。


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

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

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

这里的接口在Java中既包括实体接口(实体类Class),也包括虚拟接口(接口interface)。

总结为:建立单一接口,不要建立臃肿庞大的接口。再通俗一点,接口尽量细化,同时接口内的方法尽可能的少。

与单一职责的区别?

单一职责要求的是类和接口职责单一,注重的是职责,这是业务的划分;接口隔离原则要求的是接口的方法尽可能的少。比如:一个接口的职责包括10种方法,把10种方法放在一个接口内,这对于单一职责原则来说是可以的,但是对于接口隔离原则则不然。接口隔离原则要求接口简单,不要臃肿的系统。

  • 美女类的类图(初始)

美女类的类图(初始)

  • 美女类的类图(变化 形体美/气质美)

美女类定义(变化)

  • 注: 从表现来看,将形体美和内在美的评价区分开来。但是在某些设计力度较粗时,本做法多次一举。

保证接口的纯洁性:

  1. 接口尽量小,但是不要过小。(比如CURD分成4个子接口,多次一举。根据经验、项目规模和开发需要进行划分即可。)
  2. 接口要高内聚。高内聚就是提升接口、类、模块的处理能力,减少对外的交互。(接口是对外的承诺,承诺越少,变更风险越小,对开发越有利。)
  3. 定制服务。(当某些需要时,需要定制服务。比如淘宝网站的物品查询服务。查询使用次数频繁,牵连广泛,可以定制服务。)
  4. 接口设计有限度。(“度”来定量。)

实践要求:

  1. 一个接口只服务于一个子模块或业务逻辑;
  2. 通过业务逻辑压缩接口中的public方法,接口时常回顾和维护。
  3. 已经被污染的接口,尽量去修改,若变更风险较大,则采用适配器模式进行转化处理。
  4. 了解环境,拒绝盲从。了解环境、深入业务逻辑。

个人理解: 接口隔离原则和单一职责原则非常相似。但是两者的角度不同,单一职责原则是从实体在使用的角度进行划分,而接口隔离原则则是从开发接口管理的角度进行划分。具体哪一种优先,个人决定单一职责原则优先,但是还是那句老话根据实际的场景具体把握拆分的度


迪米特法则(Law of Demeter, LoD) 最少知识原则(Least Knowledge Principle, LKP)

Only talk to your immediate friends.(只与直接的朋友通信。)

一个对象应该对其它对象有最小的了解。(即:一个类应该对自己耦合或调用的类知道的最少。)

  • 只和朋友交流 & 朋友间也是有距离的

  • 是自己的就是自己的:

  • 如果一个方法放在本类中,既不增加类间关系,也不对本类产生负面影响,那就放置在本类中。

  • 谨慎使用Serializable

  • 只和朋友交流

清点人数(初始)

# class Teacher
public class Teacher{
	public void commond(GroupLeader groupLeader){
		List<Girl> girlList = new ArrayList();
		for(int i=0;i<10;i++){girlList.add(new Girl())}
		groupLeader.count(girlList);
	} 
}

# class GroupLeader
public class GroupLeader{
	public void count(List<Girl> listGirls){
		System.out.println(listGirls.size());
	}
}

由上述类图可以知道,老师类可以完全将清点任务委托给GroupLeader类,可以避免与Girl类之间的耦合。这有点像后面将要说的代理模式?``命令模式?

清点人数(变化)

# class Teacher
public class Teacher{
	public void commond(GroupLeader groupLeader){
		groupLeader.count();
	} 
}

# class GroupLeader
public class GroupLeader{
	private List<Girl> listGirls;
	public GroupLeader(List<Girl> listGirls){
		this.listGirls = listGirls;
	}
	public void count(){
		System.out.println(listGirls.size());
	}
}

朋友类:出现成员变量、方法的输入输出参数中的类成为朋友类,而出现在方法体内部的类不属于朋友类。

一个类只和朋友类交流,不和陌生类交流。类与类之间的关系是建立在类间的,而不是方法间,因此,一个方法尽量不要引用类中不存在的对象。(JDK API除外)

  • 朋友间也是有距离的

安装软件

#尽量减少类间距离

# 错误
class InstallSoftware{
	public void intallWizard(Wizard wizard){
		wizard.first();
		wizard.second();
		wizard.third();
	}
}

class Wizard{
	public void first(){}
	public void second(){}
	public void third(){}
}


# 正确
class InstallSoftware{
	public void intallWizard(Wizard wizard){
		wizard.intallWizard();
	}
}

class Wizard{
	private void first(){}
	private void second(){}
	private void third(){}
	public void intallWizard(){
		first();
		second();
		third();
	}
}

一个类公开的public属性的方法越多,修改涉及的面也越大,变更会引起的风险也越大。因此,为保持朋友间的距离,设计时需要反复衡量。尽量不要对外公布过多的public方法和非静态public变量,尽量内敛,多使用private、package-private、protected等访问权限。

个人理解: 与单一职责原则接口隔离原则不同讲述的类内关系不同。迪米特法则主要讲解的是类间关系,并且从类中方法实现的角度给出:"少使用类中不存在的类"的建议。主要的目标还是,降低耦合,降低风险。高内聚,低耦合的口号。

本章的主要目的主要是2个:

  1. 方法应该放置在哪个类中合适;
  2. 使用什么样的权限合适(private/public/protected/private-package)。

开闭原则(Open Closed Principle -OCP)

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

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

“拥抱变化”。开闭原则告诉我们尽量通过扩展软件实体的行为来实现变化,而不是修改已有的代码来完成变化。

  • 书店售书类图(初始)

书店售书

  • 书店售书类图(变化-打折)

书店展开打折购书活动

  1. 修改接口IBook-造成冗余
  2. 修改实现类NovelBook-与原有逻辑冲突
  3. 通过扩展实现变化

书店售书类图(变化-打折)

主要的变化类型:

  1. 逻辑变化: a+b -> a-b
  2. 子模块变化,例子如上所示
  3. 可见视图的变化,JSP、Html页面等

个人感悟: 之前在开发过程经常都是直接改原来类和方法。1是当初的需求不太明确,老是修改 2.是项目尚未上线。所以,可以支持这样的修改方法。但是,从上线后的长远考虑,应该慎重修改。修改过多了,可以定时进行重构。

  • 书店售书类图(变化-打折)

书店售书类图(变化-新类别)

为什么要实现开闭原则?

  1. 开闭原则对测试的影响;
  2. 开闭原则可以提高复用性;
  3. 开闭原则可以提高可维护性;
  4. 面向对象开发的要求。

如何实现开闭原则?

  1. 抽象约束(依赖倒置原则)
  2. 元数据控制模块行为(例如:Spring注入)
  3. 制定项目章程
  4. 封装变化

抽象约束包括: 1.通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的非public方法(有时难以实现?);2.参数类型、引用对象尽量使用接口或者抽象类(以前没有注意到,直接使用PO或BO?);3. 抽象层尽量保持稳定。


总结

六大设计原则和23中设计模式

  • 开闭原则(Open Closed Principle - OCP)
  • 单一职责原则(Simple Responsibility Principle - SRP)
  • 接口隔离原则(Interface Seperate Principle - ISP)
  • 迪米特法则(Law of Demester)
  • 里氏替换原则(Liskv Subsitution Principle - LSP)
  • 依赖倒置原则(Dependence Inversion Principle - DIP)

六种原则,用于应对开发中的"变化"。

  1. 开闭原则为首,是主要目标。
  2. 单一职责原则接口隔离原则从业务和开发两个方面讲述类和接口的划分;
  3. 迪米特法则从类间关系的角度讲述接口中方法的划分和对象间的调用。
  4. 里氏替换原则依赖倒置原则主要讲解开发中如何使用继承和多态的特性。

开闭原则是一个终极目标,任何人包括大师级人物都无法百分百做到,但朝这个方向努力,可以非常显著地改善一个系统的架构,做到真正地拥抱变化。

猜你喜欢

转载自blog.csdn.net/u010416101/article/details/84654212