菜鸟成长系列-面向对象的6种设计原则

菜鸟成长系列拖了一周多了,今天继续以前的思路来进行。按照以前的规划,这篇主要来学习设计原则先关知识。经过本文学习,但愿你们一方面能是可以认识这些原则是什么,可以在平常的开发中起到怎样的约束,而且用这些原则来提升代码的复用性和可维护性,另外一方面是对后续的设计模式的学习可以有一些基础。java

菜鸟成长系列-概述
菜鸟成长系列-面向对象的四大基础特性
菜鸟成长系列-多态、接口和抽象类
算法


设计原则,在java与模式这本书中有提到,用于提升系统可维护性的同时,也提升系统的可复用性。这本书中主要讲了六种设计原则:编程

  • “开-闭”原则
  • 里氏替换原则
  • 依赖倒置原则
  • 接口隔离原则
  • 单一职责原则
  • 迪特米法则

这些设计原则首先都是复用的原则,遵循这些原则能够有效的提升系统的复用性,同时也提升了系统的可维护性。设计模式

“开-闭”原则

网上看到一我的的解释,他是这样来比喻的:一个本子,已经写完了,你不可能撕几张纸粘上去吧,最好的办法是买个新的。
道理就是这样,一个已经作好的程序,不支持修改的,由于修改的话,有可能形成程序没法运行或报错,因此,一般程序只支持扩展,不支持修改。bash

  • 1.为何会有这样一个原则来做为程序设计的一种约束呢?
    在软件的生命周期内,因为软件功能或者结构的变化、升级和维护等缘由须要对软件原有代码进行修改,在修改的过程当中可能会给旧代码中引入错误,也可能会使咱们不得不对整个功能进行重构,而且还须要进行软件的从新测试,所以咱们但愿在软件设计之初,可以用一种原则来进行一些基本的约束,使得在软件后期的功能变动、扩展或者维护更加容易
  • 2.开闭原则解决的问题是什么?
    当软件须要进行改变时,咱们应该尽可能经过扩展软件实体的行为来实现变化,而不是经过修改已有的代码来实现变化。经过这样一种原则,能够很好的实如今保证原有功能稳定的前提下扩展新的功能
  • 3.什么是开闭原则呢?
    一个软件实体(类、模块或函数)应当对扩展开放,对修改关闭。也就是说在扩展或者修改软件功能时,应尽可能在不修改原有代码的状况下进行

举个简单的栗子:如今有这样一个需求,系统须要经过QQ来进行验证登陆。OK,咱们来撸代码:微信

  • 用户类User
package com.glmapper.framerwork;
/**
 * 用户信息类
 * @author glmapper
 * @date 2017年12月9日下午10:54:09
 *
 */
public class User {
	private String userName;//用户名
	private String passWord;//密码
	public String getUserName() {
		return userName;
	}
	public void setUserName(String userName) {
		this.userName = userName;
	}
	public String getPassWord() {
		return passWord;
	}
	public void setPassWord(String passWord) {
		this.passWord = passWord;
	}
}

复制代码
  • QQ核心验证逻辑
package com.glmapper.framerwork;
/**
 * QQ验证器
 * @author glmapper
 * @date 2017年12月9日下午10:49:24
 */
public class QQAuther {
	/**
	 * 用于验证QQ登陆信息
	 */
    public boolean validateQQ(User user)
    {
        //模拟下逻辑
        return user.toString()==null?false:true;
    }
}

复制代码
  • 核心验证服务类
package com.glmapper.framerwork;
/**
 * 
 * 用于验证的核心服务
 * @author glmapper
 * @date 2017年12月9日下午10:47:04
 *
 */
public class AuthService {
	//持有一个QQ验证器对象
	private QQAuther qqAuther;
	//经过构造器注入qqAuther对象
	public AuthService(QQAuther qqAuther) {
		this.qqAuther = qqAuther;
	}
	/*
	 * 验证用户合法性
	 */
	public boolean validateUser(User user){
		return qqAuther.validateQQ(user);
	}
}

复制代码
  • 客户端
package com.glmapper.framerwork;
/**
 * 客户端调用验证
 * @author glmapper
 * @date 2017年12月9日下午10:50:13
 *
 */
public class AuthClient {
	
	public static void main(String[] args) {
		//获取用户信息
		User user = UserHolder.getUser();
		QQAuther qqAuther = new QQAuther();
		AuthService authService = new AuthService(qqAuther);
		//获取验证结果
		boolean isOK = authService.validateUser(user);
		System.out.println(isOK);
	}
}

复制代码

OK,完事了!可是如今须要接入微博的开放平台接口;修改代码...。 增长一个微博验证器:数据结构

package com.glmapper.framerwork;
/**
 * 微博核心验证器
 * @author glmapper
 * @date 2017年12月9日下午11:01:10
 */
public class WeiBoAuther {
	/**
	 * 用于验证QQ登陆信息
	 */
    public boolean validateWeiBo(User user)
    {
        return user.toString()==null?false:true;
    }
}

复制代码

核心验证服务修改:app

package com.glmapper.framerwork;
/**
 * 
 * 用于验证的核心服务
 * @author glmapper
 * @date 2017年12月9日下午10:47:04
 *
 */
public class AuthService {
	//持有一个QQ验证器对象
	private Object obj;
	//经过构造器注入qqAuther对象
	public AuthService(Object obj) {
		this.obj = obj;
	}
	/*
	 * 验证用户合法性
	 */
	public boolean validateUser(User user){
	    //这里仅做为模拟,通常状况下会经过使用定义枚举&工厂模式来完成
		if (obj instanceof QQAuther) {
			return new QQAuther().validateQQ(user);
		}
		if(obj instanceof WeiBoAuther){
			return new WeiBoAuther().validateWeiBo(user);
		}
		return false;
	}
}

复制代码

客户端改变:ide

package com.glmapper.framerwork;
/**
 * 客户端调用验证
 * @author glmapper
 * @date 2017年12月9日下午10:50:13
 *
 */
public class AuthClient {
	
	public static void main(String[] args) {
		//获取用户信息
		User user = UserHolder.getUser();
		
		//QQ
		QQAuther qqAuther = new QQAuther();
		boolean isQQOK = new AuthService(qqAuther).validateUser(user);
		System.out.println(isQQOK);
		
		
		//微博
		WeiBoAuther weiBoAuther = new WeiBoAuther();
		boolean isWeiBoOK = new AuthService(weiBoAuther).validateUser(user);
		System.out.println(isWeiBoOK);
	}
}
复制代码

OK,改进完成!可是又有新的需求,接入微信....。假如咱们如今把微信开放平台也接入了,而后又来需求要接入支付宝帐户、苏宁易购帐户等等。。。就须要不断的修改代码。那么这个时候就须要在设计之初用到咱们的开闭原则来作一个约束了。继续撸:
首先咱们须要须要定义一个接口用于约束:函数

  • 验证器接口,用于被QQ/WEIBO/微信/苏宁易购等开发平台验证器实现
package com.glmapper.framerwork;
/**
 * 定义一个约束接口 
 * @author glmapper
 * @date 2017年12月9日下午11:32:32
 *
 */
public interface ValidateInteface {
	/**
	 * 提供一个验证入口
	 */
	boolean validate(User user);
}

复制代码
  • QQ修改以后
package com.glmapper.framerwork;
/**
 * QQ验证器
 * @author glmapper
 * @date 2017年12月9日下午10:49:24
 */
public class QQAuther implements ValidateInteface{
	/**
	 * 用于验证QQ登陆信息
	 */
	@Override
	public boolean validate(User user) {
		return user.toString()==null?false:true;
	}
}

复制代码
  • 微博修改以后
package com.glmapper.framerwork;
/**
 * 微博核心验证器
 * @author glmapper
 * @date 2017年12月9日下午11:01:10
 */
public class WeiBoAuther implements ValidateInteface{
	/**
	 * 用于验证QQ登陆信息
	 */
	@Override
	public boolean validate(User user) {
		// TODO Auto-generated method stub
		 return user.toString()==null?false:true;
	}
}
复制代码
  • 核心验证服务
package com.glmapper.framerwork;
/**
 * 用于验证的核心服务
 * @author glmapper
 * @date 2017年12月9日下午10:47:04
 */
public class AuthService {
	//持有一个QQ验证器对象
	private ValidateInteface validate;
	//经过构造器注入qqAuther对象
	public AuthService(ValidateInteface validate) {
		this.validate = validate;
	}
	/*
	 * 验证用户合法性
	 */
	public boolean validateUser(User user){
		return validate.validate(user);
	}
}

复制代码
  • 客户端
package com.glmapper.framerwork;
/**
 * 客户端调用验证
 * @author glmapper
 * @date 2017年12月9日下午10:50:13
 *
 */
public class AuthClient {
	public static void main(String[] args) {
		//获取用户信息
		User user = UserHolder.getUser();
		//QQ
		ValidateInteface qqAuther = new QQAuther();
		boolean isQQOK = new AuthService(qqAuther).validateUser(user);
		System.out.println(isQQOK);
		//微博
		ValidateInteface weiBoAuther = new WeiBoAuther();
		boolean isWeiBoOK = new AuthService(weiBoAuther).validateUser(user);
		System.out.println(isWeiBoOK);
	}
}

复制代码

改进以后咱们能够发现,对于原来的核心验证服务类、各验证器类,不管增长什么方式接入,咱们都不须要去修改它的代码了。而此时咱们须要作的就是新增一个验证器(例如苏宁易购验证器),而后继承ValidateInterface接口就好了。整体来首,开闭原则的核心是:

  • 抽象化
  • 对可变性的封装原则(1.不可变性不该该散落在代码的多处,而应当被封装到一个对象里面;2.一种可变性不该当与另一种可变性混合在一块儿)

(你们若是有更简单暴力的例子,能够留言;这个例子想了不少都感受不是很恰当,仍是从工做中抽象出来的)。

里氏替换原则

任何父类能够出现的地方,子类必定能够出现
里氏替换原则算是对“开闭”原则的补充,上面也提到,实现“开闭”原则的关键步骤是抽象化,而父类与子类的继承关系就是抽象化的一种具体体现,因此里氏替换原则是对实现抽象化的具体步骤的规范。

摘自java与模式中的定义:若是对每个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的全部程序 P 在全部的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。

下图中描述了一种继承关系,从最高层的动物一直衍生出具体的动物。OK,写一段断码来看看:

  • 顶层抽象父类-Animal
package com.glmapper.framework.model.lsp;
/**
 * 顶层抽象父类动物类
 * @author glmapper
 * @date 2017年12月10日上午10:51:30
 */
public abstract class Animal {
	//提供一个抽象方法,以供不一样子类来进行具体的实现
	public abstract void eatFood(String foodName);
}
复制代码
  • 具体动物类型-Dog
package com.glmapper.framework.model.lsp;
/**
 *子类-小狗
 * @author glmapper
 * @date 2017年12月10日上午10:54:17
 *
 */
public class Dog extends Animal{
	@Override
	public void eatFood(String foodName) {
		System.out.println("小狗吃"+foodName);
	}
}
复制代码
  • 具体动物-哈士奇
package com.glmapper.framework.model.lsp;
/**
 * 具体小狗的种类-子类哈士奇
 * @author glmapper
 * @date 2017年12月10日上午10:56:59
 *
 */
public class HSQDog extends Dog{
	/**
	 * 重写父类方法
	 */
	@Override
	public void eatFood(String foodName) {
		System.out.println("哈士奇吃"+foodName);
	}
}
复制代码
  • 客户端
package com.glmapper.framework.model.lsp;
//客户端程序
public class ClientMain {
	public static void main(String[] args) {
		//子类
		HSQDog hsqdog=new HSQDog();
		hsqdog.eatFood("饼干");
		//父类
		Dog dog = new HSQDog();
		dog.eatFood("饼干");
		//顶层父类
		Animal animal = new HSQDog();
		animal.eatFood("饼干");
	}
}
复制代码
  • 运行结果
哈士奇吃饼干
哈士奇吃饼干
哈士奇吃饼干
复制代码

能够看出咱们最开始说的那句话任何父类能够出现的地方,子类必定能够出现,反过来是不成立的。个人理解是子类经过集成获取的父类的属性和行为,而且子类自身也具备本身的属性和行为;父类能够出现的地方必然是须要用到父类的属性或者行为,而子类都涵盖了父类的这些信息,所以能够作到替换。反过来不行是由于父类在上述的例子中只是充当了一种类型约束,它可能不具备子类的某些特征,所以就没法作到真正的替换。

里氏替换原则是继承复用的基石,只有当子类能够替换掉基类,软件单位的功能不会受到影响时,基类才能被真正的复用,而子类也才可以在基类的基础上增长新的功能。

依赖倒转原则

实现“开闭”原则的关键是抽象化,而且从抽象化导出具体化实现。若是说开闭原则是面向对象设计的目标的话,依赖倒转原则就是面向对象设计的主要机制(java与模式)。
依赖倒转原则:要依赖与抽象,不依赖于具体实现。

怎么理解呢?

  • 1)高层模块不该该直接依赖于底层模块的具体实现,而应该依赖于底层的抽象。换言之,模块间的依赖是经过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是经过接口或抽象类产生的。

  • 2)接口和抽象类不该该依赖于实现类,而实现类依赖接口或抽象类。这一点其实不用多说,很好理解,“面向接口编程”思想正是这点的最好体现

首先是第一点,从复用的角度来讲,高层次的模块是设计者应当复用的。可是在传统的过程性的设计中,复用却侧重于具体层次模块的复用。好比算法的复用,数据结构的复用,函数库的复用等,都不可避免是具体层次模块里面的复用。较高层次的结构依赖于较低层次的结构,而后较低层次的结构又依赖于更低层次的结构,直到依赖到每一行代码为止。而后对低层次修改也会逐层修改,一直到最高层的设计模块中。

对于一个系统来讲,通常抽象层次越高,它的稳定性就越好,所以也是做为复用的重点

“倒转”,实际上就是指复用应当将复用的重点放在抽象层上,若是抽象层次的模块相对独立于具体层次模块的话,那么抽象层次的模块的复用即是相对较为容易的了。

在不少状况下,一个java程序须要引用一个对象,若是这个对象有一个抽象类型的话,应当使用这个抽象类型做为变量的静态类型。 在上面咱们画了动物和小狗的类图关系,在客户端调用的时候有三种方式:

//子类(方式1)
HSQDog hsqdog=new HSQDog();
hsqdog.eatFood("饼干");
//父类(方式2)
Dog dog = new HSQDog();
dog.eatFood("饼干");
//顶层父类(方式3)
Animal animal = new HSQDog();
animal.eatFood("饼干");
复制代码

若是咱们须要一个哈士奇(HSQDog)的话,咱们不该当使用方式1,而是应当使用方式2或者方式3。

接口隔离原则

接口隔离原则:使用多个专门的接口比使用单一的总接口要好。换句话说,从一个客户类的角度来说:一个类对另一个类的依赖性应当是创建在最小的接口上的。 这个其实在咱们实际的开发中是常常遇到的。好比咱们须要编写一个完成一个产品的一些操做接口。

package com.glmapper.framework.model.isp;
/**
 * 一个产品服务接口
 * @author glmapper
 * @date 2017年12月10日下午12:01:31
 */
public interface ProductService {
	//增长产品
	public int addProduct(Product p);
	//删除产产品
	public int deleteProduct(int pId);
	//修改产品
	public int updateProduct(Product p);
	//查询一个产品
	public Product queryProduct(int pId);
}
复制代码

OK,咱们在ProductService中提供了对产品的增删改查;可是随着需求升级,咱们须要能够增长对产品新的批量导入和导出。OK,这时在接口中继续新增两个方法:

//从excel中批量导入
public void batchImportFromExcel();
//从excel中批量导导出
public void batchExportFromExcel();
复制代码

而后需求又须要扩展,须要增长增长购买产品、产品订单生产、查询订单、订单详情....;这样一来,咱们的ProductService就会慢慢的急速膨胀。与此对应的具体的实现逻辑ProductServiceImpl类也会变得很是的庞大,可能单类会超过数千行代码。

那么咱们就须要进行接口隔离,将产品的基本操做如增删改查放在一个接口,将产品订单处理放在一个接口,将产品申购放在一个接口,将批量操做放在一个接口等等...对于每个接口咱们只关心某一类特定的职责,这个其实就是和单一职责原则有点挂钩了。 经过这种设计,下降了单个接口的复杂度,使得接口的“内聚性”更高,“耦合性”更低。由此能够看出接口隔离原则的必要性。

迪特米法则

迪特米法则:又称为最少知识原则,就是说一个对象应当对其余对象尽量少的了解;看下迪特米法则的几种表述:
1.只与你直接的朋友们通讯
2.不跟陌生人说话
3.每个软件单位对其余的单位都只有最少知识,并且局限于那些与本单位密切相关的软件单位

也就是说,若是两个雷没必要彼此直接通讯,那么这两个类就不该当发生直接的相互做用。若是其中一个类须要电泳另外一个类的某一个方法的话,能够经过第三者进行消息的转发。代码看下:

  • 某我的
package com.glmapper.framework.model.isp;
/**
 * 某我的
 * @author glmapper
 * @date 2017年12月10日下午12:39:45
 */
public class SomeOne {
	//具体oprateion行为
	public void oprateion(Friend friend){
		Stranger stranger =friend.provide();
		stranger.oprateion3();
	}
}
SomeOne具备一个oprateion方法,该方法接受Friend为参数,根据上面的定义能够知道Friend是SomeOne的“朋友”(直接通讯了)
复制代码
  • 朋友
package com.glmapper.framework.model.isp;
/**
 * 朋友
 * @author glmapper
 * @date 2017年12月10日下午12:40:09
 */
public class Friend {
	private Stranger stranger = new Stranger();
	public Stranger provide(){
		return stranger;
	}
	public void opration2(){
	}
}
很明显SomeOne的opration方法不知足迪特米法则,由于这个方法中涉及到了陌生人Stranger,Stranger不是SomeOne的朋友
复制代码

OK,咱们来经过迪特米法则进行改造。

  • 改造以后的SomeOne
package com.glmapper.framework.model.isp;
/**
 * 某我的
 * @author glmapper
 * @date 2017年12月10日下午12:39:45
 *
 */
public class SomeOne {
	//具体oprateion行为
	public void oprateion(Friend friend){
		friend.forward();
	}
}
复制代码
  • 改造以后的朋友
package com.glmapper.framework.model.isp;
/**
 * 朋友
 * @author glmapper
 * @date 2017年12月10日下午12:40:09
 *
 */
public class Friend {
	private Stranger stranger = new Stranger();
	public void opration2(){
		
	}
	//进行转发
	public void forward() {
		stranger.oprateion3();
	}
}
复制代码

因为调用了转发,所以SomeOne中就不会和陌生人Stranger直接的关系就被忽略了。知足了直接和朋友通讯、不与陌生人说话的条件。
可是迪特米法则带来的问题也是很明显的:即会在系统中造出大量的小方法散落在系统的各个角落,这些方法仅仅是传递消息的调用,与系统的业务逻辑没有任何关系。

单一职责

上面在接口隔离中有提到过,单一职责其实很好理解,解释尽可能的使得咱们的每个类或者接口只完成本职工做之内的事情,不参与其余任何逻辑。好比说苹果榨汁机我就只用来榨苹果汁,若是你须要榨黄瓜汁的话,你就得买一个黄瓜榨汁机。

总结

OK ,至此,设计原则部分就复习完了。总结一下:

    1. 单一职责原则要求实现类要职责单一;
    1. 里氏替换原则要求不要去破坏继承系统;
    1. 依赖倒置原则要求面向接口编程;
    1. 接口隔离原则要求在设计接口的时候要精简单一;
    1. 迪米特法则要求要下降耦合;
    1. 开闭原则是总纲,要求对扩展开发,对修改关闭。

你们周末愉快!(若是有不当之处,但愿你们及时指出,多谢!)

相关文章
相关标签/搜索