设计模式-六大设计原则详解(java 版)

设计模式-六大设计原则

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

 初次接触设计模式是在就读大学期间,或许那时候进入实验室有较好的导师及厉害点的同学,接的校外的商业代码都较为规范整洁,拗口的设计模式在学习中便没有下太大的精力,后续进入社会工作,发现尤其是新老系统在维护工程中重视出现各种奇怪的、难搞的bug,基本都是代码不遵循设计规范,不进行前期好的设计,出现了各种高耦合低内聚的垃圾产物。大改一番的成本是谁都不愿意承担的…由此学习设计模式是十分重要的事情,或许你有良好的代码习惯,但是这种习惯也只是知其然而不知其所以然的产物,故学习设计模式便是十分重要的东西。
 设计模式的学习重在设计方面,所谓工欲善其事必先利其器,好的设计能够大大降低更多的生产、运维成本,也能提高自己的思维能力,故本单元主要是对设计模式在整体中进行一次阐述,这里便从设计模式的六大设计原则开始说起:

  • 单一职责原则(Single Responsibility Principle);
  • 开闭原则(Open Closed Principle);
  • 里氏替换原则(Liskov Substitution Principle);
  • 迪米特法则(Law of Demeter),又叫“最少知道法则”;
  • 接口隔离原则(Interface Segregation Principle);
  • 依赖倒置原则(Dependence Inversion Principle)。

 把这 6 个原则的首字母(里氏替换原则和迪米特法则的首字母重复,只取一个)联合起来就是:SOLID(稳定的),其代表的含义也就是把这 6 个原则结合使用的好处:建立稳定、灵活、健壮的设计。

单一职责原则

单一职责原则的定义是:应该有且仅有一个原因引起类的变更。


 简而言之就是进行项目开发工程中,我们对类的设计及其定义上,一个类应该只承担一个职责,如何定义这个职责呢?每个人有不同的想法,我这里可以做一次统一的设计,即当如果这个类是对外的接口,那么接口路径公共命名上的区分即可作为一个类的划分。
eg:

  • 简单的系统都有用户模块,我们对外提供接口一般是在管理层面来说是 /user/****[add/update/list/delete] ,对于其权限来说可以是 /user/role/ **** [add/update/list/delete],对于其登陆、登出、注册来说是 /{system}/*****[login/register/logout]等等,那么这三个方式便可以分成了三个controller类处理。
  • 对于service进行分层处理的时候,确实有点仁者见仁智者见智的意味了,我们往往要进行一些联合查询等相关操作,在进行单一职责原则的同时也会涉及到职责重复的部分,则我个人在这认为方法以何为主则放到哪里即可,在进行接口设计的时候,需要考虑单一职责原则,譬如通用的update可以分为updateAll、updateNotNull,但是涉及到两种更新逻辑的时候,还是分开不放置一个方法较好。
  • 对于中台系统的搭建,对于数据中台则进行丰富的数据处理,对于业务的处理则在业务中台进行组合即可,这里要区分单体架构中业务和数据混合过程。(一般这些不会处理的,系统就算是搞成了微服务,也只会增加维护成本,职责不进行很好的设计,容易出现高耦合低内聚的代码)

里氏替换原则

里氏替换原则的定义是:子类可以扩展父类的功能,但不能改变父类原有的功能。


简而言之,就是子类可以对父类的功能进行一些扩展而不能对父类的方法功能进行重写后对其作用进行变更。最为直观的体现就是Collection集合类,我们定义了List接口来确保其中的实现,对其进行实现时不管是ArrayList、LinkList还是其他的list,进行add操作时基本功能是将元素放到集合之中,其中因为其底层逻辑结构不同而使用不同的处理流程罢了。
eg:

public class test{

		public void test(){

       		List arrayList = new ArrayList();
       		List linkedList = new LinkedList();
       
       		arrayList.add(new Object());
       		linkedList.add(new Object());
       }

违法原则示例一:使用基类没有的异常:

public class Person{

		public void eatFood(Object food){
     	     if(food == null) throw new NoFoodException("there no food exception");
         	 // eat
    	}

		public class Father extend Person{
			@Override
			public void eatFood(Object food){
				//出现基类不存在的异常
         	  if(food == null) throw new NeedFoodException("there no food exception");
           	  //eat
			}
		}

}

违反原则示例二:擅自变更基类方法的处理逻辑

public class Person{

		public void eatFood(Object food){
     	     if(food == null) throw new NoFoodException("there no food exception");
         	 // eat
    	}

		public class  Father extend Person{
			@Override
			public void eatFood(Object food){
         	  if(food == null) throw new NoFoodException("there no food exception");
           	  //eat
			}
		}

		public class son extend Person{

			private Object toy;
		
			@Override
			public void eatFood(Object food){
				// 违反了里氏替换原则
           		if(toy== null) throw new NoToyException("there no toy exception ,son was crying");
            	// eat
			}
		}
}

开闭原则

开闭原则的定义是:子类可以扩展父类的功能,但不能改变父类原有的功能。


简而言之就是进行父类功能扩展的时候,我们不能够直接去修改父类的功能。具体落在实际如何处理呢?简而言之则是在进行设计时,譬如定义一个统一的操作接口,操作接口根据其实现子类进行实现其功能,这样我们扩展则只需要在一个新类进行实现即可。故这就需要我们在前期设计之中进行扩展设计考量了,后续进行处理的时候发现不符合开闭原则的话,需要进行架构的重新设计。


如何使用开闭原则

  1. 抽象约束
    第一,通过接口或者抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;(继续参考集合)
    第二,参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;(使用List list 而不是ArrayList list进行定义)
    第三,抽象层尽量保持稳定,一旦确定即不允许修改。

  2. 元数据(metadata)控制模块行为
    元数据就是用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。
    Spring容器就是一个典型的元数据控制模块行为的例子,其中达到极致的就是控制反转(Inversion of Control)

  3. 制定项目章程
    在一个团队中,建立项目章程是非常重要的,因为章程中指定了所有人员都必须遵守的约定,对项目来说,约定优于配置。
    复制

  4. 封装变化
    对变化的封装包含两层含义:
    第一,将相同的变化封装到一个接口或者抽象类中;
    第二,将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。


最好的示例就是集合类,Collection集合,List接口进行规范,慢慢演变出ArrayList LinkedList CopyOnRightArrayList…等

接口隔离原则

定义:客户端不应该依赖它不需要的接口,多数客户端特定接口是比单一总接口更好的。这里客户端不只是说用户使用的,比如说我们定义一个接口,那么对应的实现类便是接口的客户端。接口隔离原则其实也表明了面向对象的规范性。
eg:
  反例

public interface IPerson{

	public void eatFood();

	public void drinkWriter();

	public void sleep();

    public void goSchool(); 

}

public interface Father implement IPerson{
     .......
     @Override
     public void goSchool(){
		// 多余方法,违反了接口隔离原则	
	 }
       
}

  修改后:对业务进行接口上的拆分

public interface IPerson{

	public void eatFood();

	public void drinkWriter();

	public void sleep();

}

public interface IStudent{

	public void goSchool(); 

}
    

public class Father implement IPerson{
       .......     
}

public class StudentFather extend Father implement IStudent{
       .......     
}

public class Son implement IPerson,IStudent{
       .......     
}

依赖倒置原则

面向接口编程,不要面向实现编程。高层模块不应该依赖低层模块,二者都应该依赖其抽象。简而言之,就是父类(基类)不应该对其子类产生依赖,也就是我们在进行类扩展增强的时候,对于基类来说不管如何增强,其不应该被更改。这里做一个对比,里氏替换原则教会了我们不要篡改父类的逻辑,开闭原则教会了我们不要使用非类功能的public方法(使用接口约束,对功能增强可以考虑使用接口隔离原则),依赖倒置原则教会了我们基类不要依赖子类。
示例:
这里不写代码了吧,和之前的都差不多,如果是在基类进行条件适配的时候,不要再基类写if else语句,可以通过接口,通过type(譬如在Abstract 基类声明一个 abstract方法 getType() 处理等都可以)

迪米特法则

迪米特法则也就是最少知道原则,对于这个法则,其目的就是为了降低代码的耦合性来提高内聚,对于最少知道法则的实际使用上,基本上有两个原则遵守就可以满足,即:1. 只和直接朋友进行交流 2. 尽量减少不必要的交流(直达目的)
eg:
1.只和直接朋友交流:这个原则在管理学的体现就是不越级进行工作安排
场景:譬如我们进行房租租赁工作,一般有房子,房东,中介、房客四个角色进行参与,实际过程中我们可以找中介帮忙看房子,也可以自己找房子(这里既可以找到中介,也会找到房东直租)这里如果我们按照业务场景进行处理。
分析:进行租赁活动的时候既可以找中介,也可以直接找房东;如果按照这个思路处理就违反了迪米特法则。虽然业务确实可以直接找房东租赁,但是要知道房东在这里既担任了房主的角色,也担任着房屋出租者的角色,而中介仅仅只是扮演了房屋出租者的角色。故进行系统分析的时候需要进行权限的界定,外卖也算如此,需要有用户、食物、商家、骑手。商家自送则是将商家即承担了食品售卖者的角色,也承担了配送人员的角色。用户自取则是用户承担了购买者的角色,也承担了配送者的角色,软件系统需要对此分析到位。

public interface IShop{
	public Food earnFood();
}

/**
 *
 * 商家制作食物
 *
 **/
public class Shop implement IShop{

	public IShop makeFood(){
		return this;
	}
 
	public Food earnFood(){
		// to do
		return food;
	}
 }

public interface ISendLunchPerson{
	public void getFood(IShop shop)}

/**
 *
 *外卖员进行取餐和送餐--这里甚至可以将外卖员封装成接口,上家和平台各种实现
 *
 **/
public class sendLunchPerson implement ISendLunchPerson{
	public void getFood(IShop shop){
		shop.makeFood()
	}
	public Food sendFood(...){
		// 核对用户信息并给出对应食物
	}
}

/**
 *
 *购买者只需要从外卖员手里获取食物
 *
 **/
public class Buyer{
	public Food getFood(ISendLunchPerson  sender){
		return sender.sendFood(...)
	}
 
}
  1. 尽量减少和朋友不必要的交流:这里旨在不需要管自己不归自己管的事情,譬如外卖员取餐只需要取餐,不需要管商家进行制作。生活中确实有帮商家炒菜配餐的外卖小哥,但是这个行为毕竟并非系统的约束行为,而且他帮人炒菜的同时也会降级自己的配送效率。
public interface IShop{
	public Food makeFood();
}

/**
 *
 * 商家制作食物
 *
 **/
public class Shop implement IShop{

	public IShopbuyIngredients(...){
		// buy Ingredients
		return this;
	} 

	 public IShopwash(){
   		// wash Ingredients
	 }

	public IShop makeFood(){
		// to do
		return food;
	}

	public Food earnFood(){
		return food;
	}

	// 这里包含了订单逻辑,为了防止把一个东西搞得太复杂,只是简单写在这里了
	public Food work(){
		// 1. 购买食材
		this.buyIngredients(...);
		// 2. 清洗食材 
		this.wash();
		// 3. 制作食物
		makeFood();
	}

 }

public interface ISendLunchPerson{
	public void getFood(IShop shop)}

/**
 *
 *外卖员进行取餐和送餐--这里甚至可以将外卖员封装成接口,上家和平台各种实现
 *
 **/
public class sendLunchPerson implement ISendLunchPerson{

	// 反例--骑手不需要管食物制作过程
	public void getFoodWarn(IShop shop){
		shop.buyIngredients().wash().earnFood()
	}

	// 正例,骑手只需要催商家出餐即可
	public Food getFoodTrue(IShop shop){
		  shop.work();
	}
}

基本上这几个原则使用的比较广泛,如果上文有不当之处还望指出,一起学习,共同成长。

你可能感兴趣的:(设计模式,java,设计模式,开发语言)