创作不易,如果觉得这篇文章对你有帮助,欢迎各位老铁点个赞呗,您的支持是我创作的最大动力!
经过博主几个夜晚的努力,终于完成了这篇博文,带你理解Java中设计模式的基本原则,希望对老铁们会有所帮助。
设计模式是在软件开发中,经过无数编程先辈在软件开发中的血泪教训,以及总结出的一套用于解决问题的最佳方案。也就是说,本来并不存在所谓的设计模式,用的人多了,也便成了设计模式。
学习常用的设计模式,就是学习其他开发人员的经验与智慧,会让你慢慢的脱离苦海。为什么这么说呢,因为如果不用设计模式,需求经常发生变化,那么迎来的肯定是改改改
,频繁的改动代码,会让你怀疑人生。如果使用了设计模式,或许扩展性更强,可插拔,就不用频繁修改了。
本篇主要整理下,设计模式的几个基本原则,为后面学习设计模式打下基础。
设计模式定义:
软件设计模式(Design pattern
),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编写、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。(摘自-百度百科)
设计模式(Design Pattern)是一套被反复使用、多数人知晓的代码设计经验
的总结。总的来说,掌握了设计模式,你就可以设计出具有弹性
、可复用性
、可维护性(可扩展)
的系统。
设计模式追求的目标
代码复用性
:即相同功能的代码只用写一遍不用重复编写
可读性
:编写的代码易于理解,遵循了统一的规范
可扩展性(也叫可维护性)
:当需要增加和扩展新的功能的时候能够很容易的去扩展,扩展新功能成本低
可靠性
:增加的新功能,不会影响到原来功能的使用,追求软件的高内聚,低耦合
的状态。
七大原则定义汇总:
设计模式原则名称 | 简单定义 |
---|---|
开闭原则(OCP) | 对扩展开放,对修改关闭 |
单一职能原则(SRP) | 一个类只负责一个功能领域中的相应职责 |
里氏替换原则(LSP) | 所有引用基类的地方必须能透明地使用其子类的对象 |
依赖倒置(倒转)原则(DIP) | 依赖于抽象,不能依赖于具体实现 |
接口隔离原则(ISP) | 类之间的依赖关系应该建立在最小的接口上 |
合成/聚合复用原则(CARP) | 尽量使用合成/聚合,而不是通过继承达到复用的目的 |
迪米特法则(LOD) | 一个软件实体应当尽可能少的与其他实体发生相互作用 |
下面逐步对每个基本原则进行介绍。
OCP原则定义:在面向对象编程领域中,开闭原则规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”,这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。该特性在产品化的环境中是特别有价值的,在这种环境中,改变源代码需要代码审查,单元测试以及诸如此类的用以确保产品使用质量的过程。遵循这种原则的代码在扩展时并不发生改变,因此无需上述的过程。(百度百科)
OCP是编程中最基础、也是最核心的一种设计原则。一个软件实体,比如说类、功能模块和函数应该对扩展开放
,对修改关闭
。模块应尽量保证,在不修改原(指原来的代码)代码的情况下进行新功能的扩展。
该原则说的是类或者模块或者软件应该对扩展开放(对提供方)
,对修改关闭(对使用方)
。应该用抽象去搭建框架,用实现去扩展细节。当软件功能需要发生变化时,应该使用扩展的方式去应对变化,而不是去修改原有的代码逻辑去应对变化。
不遵循OCP原则的弊端
在软件的生命周期内,因为需求经常发生变化、升级和维护等原因需要对软件原有代码进行修改时,经常会在原有功能基础上修改功能和扩展新功能,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且原有代码需要经过重新测试。
OCP原则好处
对扩展开放
,对修改关闭
,这样可以大大提高代码的可维护(可扩展)性,使程序更加的健壮。
当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
无论如何,需要保证老功能不受影响的前提下,去扩展新功能。不能因为需求变化,就去修改原来的代码,这样重新修改后的代码,不仅需要额外的测试,也有可能导致原有功能不可用。
所以,开闭原则说的就是这个,即对扩展开放
,对修改关闭
。
假设有一个简单的场景,一个顾客点餐,厨师根据不同的菜单,为顾客做不通的饭菜。
下面给出不同的实现,帮助理解该OCP原则。
不遵循OCP的代码设计
这里有一个顾客类Customer 、一个食品类Food 、有一个厨师类Cooker 。
/**
* 顾客
*/
@Getter
@Setter
@AllArgsConstructor
public class Customer {
/**
* 客户id
*/
private Long customerId;
/**
* 客户名字
*/
private String customerName;
public Customer(String customerName) {
this.customerName = customerName;
}
}
/**
* 食品种类
*/
@Getter
@Setter
@AllArgsConstructor
public class Food {
/**
* 客户点餐某食品的序号
*/
private Long orderNo;
/**
* 菜品名称
*/
private String foodName;
/**
* 菜品单价
*/
private BigDecimal foodPrice;
}
/**
* 厨师
*/
@Getter
@Setter
@AllArgsConstructor
public class Cooker {
/**
* 厨师根据客户信息,以及客户点餐的食品信息,为客户做可口的饭菜(*********不遵循OCP的代码设计**********)
*
* @param customer
* @param food
*/
public void cookMenuFood(Customer customer, Food food) {
if (customer == null) {
return;
}
if (food == null) {
return;
}
if ("rice".equals(food.getFoodName())) {
System.out.println("The cooker cooked delicious rice for " + customer.getCustomerName());
} else if ("noodles".equals(food.getFoodName())) {
System.out.println("The cooker cooked delicious and inexpensive noodles for " + customer.getCustomerName());
}
}
}
测试用例:
public class CookerTest {
private Cooker cooker;
{
cooker = new Cooker();
}
@Test
public void testCooker() {
Customer customerOne = new Customer("张三");
Food foodOne = new Food(1L, "rice", new BigDecimal(5));
cooker.cookMenuFood(customerOne, foodOne);
Customer customerTwo = new Customer("李四");
Food foodTwo = new Food(2L, "noodles", new BigDecimal(10));
cooker.cookMenuFood(customerTwo, foodTwo);
}
}
执行结果:
The cooker cooked delicious rice for 张三
The cooker cooked delicious and inexpensive noodles for 李四
以上的代码实现,看起来比较容易理解,简单易懂,也满足了功能需要。但是,过了一周,产品经理来找你了,告诉你说,老李啊,本周咱们店推出了一个新的菜谱,现在客户可以点餐可口的饺子了,这时候,你仍然可以快速的完成功能,只需要对Cooker类进行简单的修改即可,实现如下:
/**
* 厨师
*/
public class Cooker {
/**
* 厨师根据客户信息,以及客户点餐的食品信息,为客户做可口的饭菜(*********不遵循OCP的代码设计**********)
*
* @param customer
* @param food
*/
public void cookMenuFood(Customer customer, Food food) {
if (customer == null || food == null) {
return;
}
if ("rice".equals(food.getFoodName())) {
System.out.println("The cooker cooked delicious rice for " + customer.getCustomerName());
} else if ("noodles".equals(food.getFoodName())) {
System.out.println("The cooker cooked delicious and inexpensive noodles for " + customer.getCustomerName());
} else if ("dumpling".equals(food.getFoodName())) {
System.out.println("The cooker cooked delicious dumpling for " + customer.getCustomerName());
}
}
}
测试用例如下:
@Test
public void testCooker() {
Customer customerOne = new Customer("张三");
Food foodOne = new Food(1L, "rice", new BigDecimal(5));
cooker.cookMenuFood(customerOne, foodOne);
Customer customerTwo = new Customer("李四");
Food foodTwo = new Food(2L, "noodles", new BigDecimal(10));
cooker.cookMenuFood(customerTwo, foodTwo);
Customer customerThree = new Customer("王五");
Food foodThree = new Food(3L, "dumpling", new BigDecimal(15));
cooker.cookMenuFood(customerThree, foodTwo);
}
执行结果:
The cooker cooked delicious rice for 张三
The cooker cooked delicious and inexpensive noodles for 李四
The cooker cooked delicious and inexpensive noodles for 王五
以上代码虽然实现了功能,但是每次扩展都需要修改原有的功能代码,进行if else判断处理,扩展性极差,违反了OCP原则。又过了一个月,产品经理又来找老李了,老李呀,这次又得麻烦你了,本周咱们店请来了两个大厨,需要再增加2个菜谱,牛肉面和重庆小面,估计老李的内心有点崩溃了,频繁的修改代码,还得测试原有的代码,这时候老李想了想,以后每周都增加菜谱,每次都需要动原来的逻辑,这样的设计也太low了,决定重构,把原来的代码重新设计如下。
遵循OCP的代码设计
这里在原有顾客类Customer 、一个食品类Food 基础上,把原有的厨师类Cooker替换为抽象厨师接口类AbstractCooker,该接口中定义一个抽象做饭菜的接口 。另外,增加三个实现类CookerDumpling、CookerNoodles和CookerRice,分别实现接口类AbstractCooker。
代码实现如下:
/**
* 厨师抽象接口类
*/
public interface AbstractCooker {
/**
* 厨师根据客户信息,以及客户点餐的食品信息,为客户做可口的饭菜
*
* @param customer
* @param food
*/
void cookMenuFood(Customer customer, Food food);
}
/**
* 厨师负责做可口的水饺
*/
@Service
public class CookerDumpling implements AbstractCooker {
/**
* 厨师为客户做可口的饺子
*
* @param customer
* @param food
*/
@Override
public void cookMenuFood(Customer customer, Food food) {
if (customer == null || food == null) {
return;
}
System.out.println("The cooker cooked delicious dumpling for " + customer.getCustomerName());
}
}
/**
* 厨师负责做可口的面条
*/
@Service
public class CookerNoodles implements AbstractCooker {
/**
* 厨师为客户做可口的面条
*
* @param customer
* @param food
*/
@Override
public void cookMenuFood(Customer customer, Food food) {
if (customer == null || food == null) {
return;
}
System.out.println("The cooker cooked delicious and inexpensive noodles for " + customer.getCustomerName());
}
}
/**
* 厨师负责做可口的米饭
*/
@Service
public class CookerRice implements AbstractCooker {
/**
* 厨师为客户做可口的米饭
*
* @param customer
* @param food
*/
@Override
public void cookMenuFood(Customer customer, Food food) {
if (customer == null || food == null) {
return;
}
System.out.println("The cooker cooked delicious rice for " + customer.getCustomerName());
}
}
单元测试类:
@RunWith(SpringRunner.class)
@SpringBootTest
public class CookerTest {
@Qualifier("cookerRice")
@Autowired
private AbstractCooker cookerRice;
@Qualifier("cookerNoodles")
@Autowired
private AbstractCooker cookerNoodles;
@Qualifier("cookerDumpling")
@Autowired
private AbstractCooker cookerDumpling;
@Test
public void testAbstractCooker() {
Customer customerOne = new Customer("张三");
Food foodOne = new Food(1L, "rice", new BigDecimal(5));
cookerRice.cookMenuFood(customerOne, foodOne);
Customer customerTwo = new Customer("李四");
Food foodTwo = new Food(2L, "noodles", new BigDecimal(10));
cookerNoodles.cookMenuFood(customerTwo, foodTwo);
Customer customerThree = new Customer("王五");
Food foodThree = new Food(3L, "dumpling", new BigDecimal(15));
cookerDumpling.cookMenuFood(customerThree, foodTwo);
}
}
执行结果:
The cooker cooked delicious rice for 张三
The cooker cooked delicious rice for 李四
The cooker cooked delicious rice for 王五
本周增加的两个菜谱,只需要新添加两个牛肉面类和重庆小面类,分别实现抽象厨师类AbstractCooker 即可。
以上修改后的代码设计符合开闭原则,因为整个系统在扩展时原有的代码没有做任何修改,新功能上线后,不用对以前的功能做测试。
这样的设计,不仅极大地提高了程序或者应用系统的可扩展性,而且遵循了OCP原则。
说明: 以上测试代码只是为了帮助了理解OCP原则,这里可以面向抽象类或者抽象接口编程,不要面向具体编程。以上的代码示例中,虽然遵循了OCP原则,但是违反了SRP-单一职能原则,下文SRP原则介绍结尾,会对这里作简要说明
。
OCP原则是编程中最基础、也是最核心的一种设计原则。对扩展开放,对修改关闭,这样可以大大提高代码的可维护(可扩展)性,使程序更加的健壮
。
SRP原则
定义:单一职能原则
,有的人也称为单一职责原则
。
SRP的原话解释是:
There should never be more than one reason for a class to change。SRP说的是,一个类或者一个方法只具有一项职能,不应具有多项职能。如果具有多项职能,会导致日后需求变动,要修改起来或者变化起来的成本非常高,也非常麻烦。
就一个类而言,应该仅有一个引起它变化的原因。具体说,在做编程的时候,很自然的会在一个类加上各种各样的功能。这样就意味着,无论任何需求发生了变化,都需要更改这个类,这样其实是很糟糕的,不仅维护麻烦,复用也不太可能,同时也缺乏灵活性。如果一个类承担的职责过多,就等于把这些职责耦合在一起了。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当发生变化时,设计会遭受到意想不到的破坏。而如果想要避免这种现象的发生,就要尽可能的遵守单一职责原则。此SRP原则的核心就是解耦合
以及增强内聚性
。
遵循单一职责原的优点有:
需要说明的一点是: 单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。
单一职责原则要求我们: 一个类不能做太多的东西。在软件系统中,一个类(一个模块、或者一个方法)承担的职责越多,那么其被复用的可能性就会越低
。一个很典型的例子就是万能类,任何一个常规的MVC项目,在极端的情况下,可以用一个类(甚至一个方法)完成所有的功能。但是这样做就会严重耦合,甚至牵一发动全身。一个类(一个模块、或者一个方法)承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中
。
不过说实话,其实有的时候很难去衡量一个类的职责,主要是很难确定职责的粒度
。这一点不仅仅体现在一个类或者一个模块中,也体现在采用微服务的分布式系统中。这也就是为什么我们在实施微服务拆分的时候经常会撕逼:"这个功能不应该发在A服务中,它不做这个领域的东西,应该放在B服务中"诸如此类的争论。存在争论是合理的,不过最好不要不了了之,而应该按照领域定义好每个服务的职责(职责的粒度最好找业务和架构专家咨询),得出相对合理的职责分配。
下面通过一些简单的示例,说明一下单一职责原则:
假设有一个场景:开发中,项目经理让你负责维护客户模块和产品模块。
不遵循SRP的代码设计
一个典型的例子,在Spring MVC中,一个Action(Controller)类完成所有的代码,这样理论上也是可行的,可以实现功能,但是,耦合度太高了,会导致牵一发而动全身,扩展性极差。
另外一个代码示例:
创建CrmCustomer
类、Product
类、CrmCustomerService
接口类、以及CrmCustomerService的实现类CrmCustomerServiceImpl
/**
* 客户对象
*/
@Getter
@Setter
@AllArgsConstructor
public class CrmCustomer implements Serializable {
/**
* 主键id
*/
private Long id;
/**
* 客户id
*/
private Long customerId;
/**
* 客户名字
*/
private String customerName;
}
/**
* 产品对象
*/
@Getter
@Setter
@AllArgsConstructor
public class Product implements Serializable {
/**
* 主键id
*/
private Long id;
/**
* 产品id
*/
private Long productId;
/**
* 产品名称
*/
private String productName;
}
/**
* 维护系统中,客户服务类
*/
public interface CrmCustomerService {
/**
* 新增客户信息和产品信息
*
* @param crmCustomer
*/
void insertCustomerOrProduct(CrmCustomer crmCustomer, Product product);
}
/**
* 维护系统中,客户服务实现类
*/
public class CrmCustomerServiceImpl implements CrmCustomerService {
/**
* 新增客户信息和产品信息
*
* @param crmCustomer
*/
@Override
public void insertCustomerOrProduct(CrmCustomer crmCustomer, Product product) {
//调用dao层,新增客户信息
if (crmCustomer != null) {
//mapper.insertCustomer(crmCustomer);
}
//调用dao层,新增产品信息
if (product != null) {
//mapper.insertProduct(product);
}
}
}
以上代码分析:
上面的代码中,CrmCustomerService接口中,insertCustomerOrProduct()
方法维护了客户和产品的新增功能,这种设计看着没什么问题,也能实现产品或者客户的新增。但是,这个接口中,不仅维护了客户的信息,还维护了产品的信息,这个接口的这个方法具有了多项职能,违反了SRP单一职能原则。
当需求发生变化时,在新增产品时,添加判断,如果存在历史的产品信息,不走新增产品信息,作修改产品信息。这样,你不得不修改insertCustomerOrProduct()这个方法中的代码,这时候重新发布时需要编译这个类,有可能会导致客户相关的功能不可用。这种一个类中维护了多个职能的行为,就是违反了SRP原则
,不仅导致类之间的高度耦合,代码复杂性提高了,而且可读性变差了,不方便维护,后续新功能(需求)扩展时也不好扩展,牵一发而动全身说的就是这种。
遵循SRP的代码设计
这里,产品Product就应该维护产品相关的信息,客户CrmCustomer 应该维护客户相关的信息,修改后的代码如下:
/**
* 维护系统中,客户服务类
*/
public interface CrmCustomerService {
/**
* 新增客户信息
*
* @param crmCustomer
*/
void insertCustomer(CrmCustomer crmCustomer);
}
/**
* 维护系统中,客户服务实现类
*/
public class CrmCustomerServiceImpl implements CrmCustomerService {
/**
* 新增客户信息
*
* @param crmCustomer
*/
@Override
public void insertCustomer(CrmCustomer crmCustomer) {
//调用dao层,新增客户信息
if (crmCustomer != null) {
//mapper.insertCustomer(crmCustomer);
}
}
}
/**
* 维护系统中,产品服务类
*/
public interface ProductService {
/**
* 新增产品信息
*
* @param product
*/
void insertProduct(Product product);
}
/**
* 维护系统中,产品服务实现类
*/
public class ProductServiceImpl implements ProductService {
/**
* 新增产品信息
*
* @param product
*/
@Override
public void insertProduct(Product product) {
//调用dao层,新增产品信息
if (product != null) {
//mapper.insertProduct(product);
}
}
}
以上的代码实现,遵循了SRP原则,客户和产品互不干扰,各自维护自己相关的职能,即使以后需求变化了,代码进行了修改,也不会互相影响。
说明:
在上文OCP开闭原则的代码示例中,虽然遵循了OCP原则
,但是违反了SRP单一职能原则
,因为厨师为客户做可口的饭菜时,public void cookMenuFood(Customer customer, Food food)
这个方法耦合了Customer 和 Food,不方便以后的扩展,应该在Food对象中,使用依赖
、聚合
或者组合
的关系,依赖Customer对象,而不应该在cookMenuFood()方法中耦合Customer 对象。
单一职能原则的好处是巨大的:
单一职能原则的目的主要是降低类的复杂度,即一个类或者一个方法只负责一项职能,
与其无关的职能工作不要冗余地写到一个这个类或者这个方法中。
提高类的可读性和可维护性。
降低了变更带来的不稳定风险。
通常情况下应该在类的层面要遵守单一职能原则,
通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则。 只有在类中的方法数量足够少,逻辑足够简单的情况下,才在方法层面遵守单一职能原则。
在实际项目开发中,有时候赶项目或者项目开发不规范,项目依赖
,组合
,聚合
这些关系都没有用UML类图去表达,很多类(接口)设计的不符合单一职责。但是,我们在编写代码的过程中,尽可能地让接口和方法保持单一职责,对我们后期的项目维护是很有好处的。
本文中的示例比较简单,它只有一个方法,所以,无论是在代码级别上违反单一职责原则,还是在方法级别上违反,都不会造成太大的影响。实际应用中的类都要复杂的多,一旦职责发生变化而需要修改类时,除非这个类本身非常简单,否则还是遵循单一职责原则的好。
里氏替换原则(Liskov Substitution Principle LSP)是面向对象设计的基本原则之一。 里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为
。摘自百度百科
里氏替换原则(里氏代换原则),是对OCP-开闭原则
的补充,OCP作为OO的高层原则,主张使用抽象(Abstraction)
和多态(Polymorphism)
,将设计中的静态结构改为动态结构,维持设计的封闭性。实现开闭原则的关键步骤就是抽象化
,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范
。当然,如果反过来,软件单位使用的是一个子类对象的话,那么它不一定能够使用基类对象。
举个很简单的例子说明这个问题: 如果一个方法接收Map类型参数,那么它一定可以接收Map的子类参数例如HashMap、LinkedHashMap、ConcurrentHashMap类型的参数。但是返过来,如果另一个方法只接收HashMap类型的参数,那么它一定不能接收所有Map类型的参数,而可以接收LinkedHashMap、ConcurrentHashMap类型的参数。
不遵循LSP原则的弊端
如下示例中,不遵循LSP,可能导致不合理的设计。里氏替换原则告诉我们,继承实际上让两个类耦合性增强了
,在适当的情况下,可以通过聚合,组合,依赖来解决问题
。继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性
,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障。
父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵循这些契约,但是如果子类对这些已经实现的方法任意修改,就会对整个继承体系造成破坏。
遵循LSP原则的好处
遵循LSP原则的坏处
缺点:侵入性
、不够灵活
、高耦合
LSP就是面向对象编程中对继承的一种规范和要求,要求子类尽量不要去重写父类已经实现的方法
。
极端例子: 如果子类全部重写了父类的方法,那么子类继承父类这个操作就毫无意义。
LSP要求,派生类(子类)对象能够替换其基类(父类)对象被调用,其实就是面向接口、面向抽象编程,以多态的方式实现功能
。在程序中,任何调用基类对象实现的功能,都可以调用派生类对象来替换,当然,反过来是不行的。其实这里主要说的是继承问题,既然派生类继承基类,那它的对象也应该相应继承基类对象的实现,当然也就应该能替换基类对象。如果无法替换,就说明这个派生类继承有问题,需要修改设计。
不遵循LSP的代码设计
/**
* 动物抽象类
*/
public abstract class Animal {
/**
* 动物抽象的方法,飞行
*/
protected abstract void fly();
/**
* 动物公共方法
*/
protected void breathe() {
System.out.println("动物都需要呼吸氧气...");
}
}
public class Bird extends Animal {
@Override
protected void fly() {
System.out.println("小鸟儿有翅膀,可以在天空中飞翔...");
}
}
public class Person extends Animal {
/**
* 超人不会飞
* 里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现,Person类不能出现这个fly方法,所以违背了LSP原则
*/
@Override
protected void fly() {
System.out.println("人类其实不会飞翔,超人不会飞...");
}
}
测试用例:
/**
* 违反LSP-里氏替换原则的设计
*/
private void test1() {
Animal animal = new Person();
animal.fly();//人类其实不会飞翔,超人不会飞...
}
以上示例分析: 里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现,Person类不能出现这个fly()方法,所以违背了LSP原则。所以说,在进行抽象设计时,一定要保证,子类需要实现这个抽象方法,否则把他放到基类中就是不合理的。
遵循LSP的代码设计
新增飞行行为抽象接口类FlyBehavior
、FlyNoWays
类、FlyWithWings
类这三个类,另外,对原有的三个类进行修改如下:
/**
* 抽象飞行行为
*/
public interface FlyBehavior {
/**
* 抽象方法
*/
void fly();
}
public class FlyNoWays implements FlyBehavior {
@Override
public void fly() {
System.out.println("人类其实不会飞翔,超人不会飞...");
}
}
public class FlyWithWings implements FlyBehavior {
@Override
public void fly() {
System.out.println("小鸟儿有翅膀,可以在天空中飞翔...");
}
}
/**
* 动物抽象类
*/
public abstract class AnimalChange {
/**
* 动物公共方法
*/
public void breathe() {
System.out.println("动物都需要呼吸氧气...");
}
}
/**
* 小鸟服务类
*/
public class BirdChange extends AnimalChange {
private FlyBehavior flyBehavior;
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
public void flyBehavior() {
flyBehavior.fly();
}
}
public class PersonChange extends AnimalChange {
private FlyBehavior flyBehavior;
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
public void flyBehavior() {
flyBehavior.fly();
}
}
测试用例:
/**
* 遵循LSP-里氏替换原则的设计
*/
private void test2() {
BirdChange birdChange = new BirdChange();
birdChange.breathe();
birdChange.setFlyBehavior(new FlyNoWays());
birdChange.flyBehavior();
PersonChange personChange = new PersonChange();
personChange.setFlyBehavior(new FlyWithWings());
personChange.flyBehavior();
}
测试结果:
动物都需要呼吸氧气...
人类其实不会飞翔,超人不会飞...
小鸟儿有翅膀,可以在天空中飞翔...
以上代码分析: 上面的示例中,抽象类AnimalChange中,有个抽象的呼吸方法,这个是动物都有的,所以放到了抽象类AnimalChange中。因为飞行不是所有动物都特有的行为特征,所以不应该把飞放到抽象方法上。
新增了抽象接口类FlyBehavior
、FlyNoWays
类、FlyWithWings
类这三个类,对飞行行为进行了抽象,在BirdChange类和PersonChange类中,使用聚合的方式,注入了FlyBehavior飞行行为,具体能不能飞,在运行阶段决定(即多态的动态绑定)。
LSP里氏替换原则
就是面向对象编程中对继承的一种规范和要求,是实现开闭原则的基础,它告诉我们,在设计程序的时候,尽可能使用基类进行对象的定义和引用,在运行时再决定基类的具体子类型,其实也即是多态(父类型的引用指向子类型,运行时再决定具体子类型)。
如果一个类需要使用到另一个类的功能,在合理的情况下可以使用继承的方式去设计实现,使用继承那么要遵循里氏替换原则
。但是要考虑耦合度是否达到要求,如果要降低耦合度可以使用组合、聚合、依赖的方式去解决
。
关于LSP原则,推荐一篇文章:https://www.cnblogs.com/duanxz/archive/2012/10/18/2729111.html
LSP的原定义比较复杂
,我们一般对里氏替换原则 LSP的解释为:子类对象能够替换父类对象,而程序逻辑不变。里氏替换原则有至少以下两种含义:
里氏替换原则是针对继承而言的,如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事。
如果继承的目的是为了多态,而多态的前提就是子类覆盖并重新定义父类的方法,为了符合LSP,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里,也就不存在子类替换父类实例时逻辑不一致的可能。
不符合LSP的最常见的情况是,父类和子类都是可实例化的非抽象类,且父类的方法被子类重新定义,这一类的实现继承会造成父类和子类间的强耦合,也就是实际上并不相关的属性和方法牵强附会在一起,不利于程序扩展和维护。 如何符合LSP?
总结一句话就是: `尽量不要从可实例化的父类中继承,而是要使用基于抽象类和接口的继承。
参考链接:https://www.jianshu.com/p/e6a7bbde8844
依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现
。简单的说就是要求对抽象进行编程,不要对实现进行编程
,这样就降低了客户与实现模块间的耦合。(百度百科)
这条原则说的是程序设计应该依赖抽象接口,而不应该依赖具体实现。经常听到的接口编程思想,其实说的主要就是这个原则。接口是稳定的,实现是不稳定的。
不遵循DIP原则的弊端
编程时,如果不遵循DIP原则,使用面向具体编程的话,当需求变化时,你会发现,你会很痛苦。因为频繁的需求变化,会导致你不停的修改原来的代码(违反OCP开闭原则),不仅扩展性极差,而且每次修改后,也需要测试这次的功能,以及原有的代码的功能,不好维护。
遵循DIP原则的好处
设计的时候,程序依赖于抽象接口,没有依赖于具体实现,面向抽象编程,使得项目扩展性,可维护性好。面对需求变化时,不需要影响原有的功能逻辑,而进行扩展。
程序设计时,高层模块不应该依赖于低层模块,二者都应该依赖其抽象。说的就是程序要依赖于抽象接口,不要依赖于具体实现,遵循DIP原则,其核心就是面向接口编程
,在继承时需要遵循LSP里氏替换原则
。
依赖倒转原则要求我们,在程序代码中传递参数时或在关联关系的时候,尽量引用高层次的抽象层类,即使用接口
和抽象类
进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。这也就要求,一旦接口确定,就不应该再进行修改了。使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,子类可以根据接口(抽象类)的定义,以及不同的使用场景,采用不同的实现方式
。
在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入(DependencyInjection, DI)
的方式注入到其他对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象
。常用的注入方式有三种,分别是:构造注入
,设值注入(Setter注入)
和接口注入
,Spring的IOC是此实现的典范。
假设有一个场景:跟朋友沟通聊天时,会使用一些聊天工具,通过不同的聊天工具,都可以实现沟通。
不遵循DIP的代码设计
使用面向具体编程,设计了三个QQ 类、WeChat 类和Person 类
/**
* 使用QQ聊天工具进行沟通
*/
public class QQ {
public void speak() {
System.out.println("My chat tool is QQ,The chat information is 'hello,jack'!");
}
}
/**
* 使用微信聊天工具进行沟通
*/
public class WeChat {
public void speak() {
System.out.println("My chat tool is WeChat,The chat information is 'hello,jack'!");
}
}
/**
* person类
*/
public class Person {
/**
* 使用QQ进行聊天
*/
public void speakUserQQ(QQ qq) {
qq.speak();
}
/**
* 使用微信进行聊天
*/
public void speakUserWx(WeChat weChat) {
weChat.speak();
}
}
测试用例:
/**
* 违反DIP-依赖倒置原则的聊天方式
*/
public void againstDependencyInversionPrinciple() {
Person person = new Person();
person.speakUserQQ(new QQ());
person.speakUserWx(new WeChat());
}
以上代码实现分析,功能实现看着是没什么问题,但是扩展性极差。如果日后想用Email工具聊天,需要修改Person类,添加speakUserEmail()方法,这种方式面向具体编程了,违反了DIP依赖倒置原则
。
以后每一次扩展,都需要修改person类,也违反了OCP开闭原则(对扩展开放(对提供方),对修改关闭(对使用方-Person))
。
遵循DIP的代码设计
对以上代码修改后,如下:
/**
* 抽象聊天工具服务
*/
public interface ChatTool {
/**
* 跟朋友聊天
*/
void speak();
}
/**
* 使用QQ聊天工具进行沟通
*/
public class QQChange implements ChatTool {
@Override
public void speak() {
System.out.println("My chat tool is QQ,The chat information is 'hello,jack'!");
}
}
/**
* 使用微信聊天工具进行沟通
*/
public class WeChatChange implements ChatTool {
@Override
public void speak() {
System.out.println("My chat tool is WeChat,The chat information is 'hello,jack'!");
}
}
/**
* personPersonChange类
*/
public class PersonChange {
private ChatTool chatTool;
public PersonChange(ChatTool chatTool) {
this.chatTool = chatTool;
}
/**
* 使用QQ、微信、Email...进行聊天
*/
public void speakUserChatTools() {
chatTool.speak();
}
}
测试用例:
/**
* 遵循DIP-依赖倒置原则的聊天方式
*/
public void followDependencyInversionPrinciple() {
PersonChange person = new PersonChange(new QQChange());
person.speakUserChatTools();
System.out.println("-----------------华丽的分割线------------------");
person = new PersonChange(new WeChatChange());
person.speakUserChatTools();
}
以上代码实现分析,抽象了聊天工具ChatTool
,PersonChange中,使用构造注入的方式,依赖了抽象接口ChatTool,不再面向具体的QQ,WeChat编程,而是面向抽象ChatTool编程
。如果日后想用Email工具聊天,这时候就非常简单了,只需要增加一个Email类,实现抽象工具ChatTool,而Person类不需要修改,即可实现功能。
这里面向接口编程,面向抽象编程,遵循了DIP原则
,另外,这里对扩展开放(对提供方-ChatTool),对修改关闭(对使用方-Person)也遵循了OCP开闭原则
。
编程的时候,在程序设计阶段,要面向接口编程,面向抽象编程,而不要面向具体编程,这样,不仅可以遵循OCP开闭原则,还能遵循DIP原则。这样,你就可以设计出具有弹性
、可复用性
、可维护性(可扩展)
的系统。
接口隔离原则说的是,使用多个隔离接口,比使用单个接口要好,可以降低类之间的耦合度。客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上
。经常提到的降低耦合,降低依赖,主要也是通过这个ISP原则来达到的
。另外,这样设计接口也可以给使用者带来方便,因为,越小的接口,就越好实现,复用性也越高。
通俗来讲,就是一个客户端类使用到的一个接口引用的对象,应该使用到其接口的所有方法,如果接口中的方法没有完全被使用到,这个接口的实现类又不得不实现其所有的抽象方法,这样就导致了实现类实现了接口所有的抽象方法,在客户端类中没有使用到的方法就是冗余的。那么这个接口就不是最小接口
,这样的情况就没有遵守接口隔离原则。
不遵循ISP原则的弊端
一个接口中,写了好多业务处理方法,但是,在客户端中仅使用了一部分方法,那么,这个接口就违反了ISP原则。后续不仅不好维护,而且可读性比较差。
遵循ISP原则的好处
对接口进行细化可以提高程序设计灵活性,代码设计不仅方便其他人阅读,也可以提高程序的可读性。
接口隔离原则(ISP)要求: 当一个接口太大时,我们需要将它分割成一些更细小的接口,只暴露给调用的类需要的方法,使用该接口的客户端仅需知道与之相关的方法即可,也就是说,客户端不应该依赖它不需要的接口,也不应该知道接口中不需要的方法
。
接口尽量小,但是要有限度,如果过小,则会造成接口数量过多,使设计变得复杂化,所以一定要适度。
假设有一个场景:维护一个客户相关的增删改查功能
不遵循ISP原则的代码设计
这里,我设计了CrmCustomer
类、CrmCustomerService
接口类和客户action服务类CustomerAction
,实际开发中,肯定有CrmCustomerService的实现类CrmCustomerServiceImpl
,这里笔者为了帮助理解ISP原则问题,就不写实现类了,实际开发中,业务方法很复杂,笔者这里只是简单写了一些方法, 来说明问题。
/**
* 顾客
*/
@Getter
@Setter
@AllArgsConstructor
public class CrmCustomer {
/**
* 客户id
*/
private Long customerId;
/**
* 客户名字
*/
private String customerName;
}
/**
* 客户服务类
*/
public interface CrmCustomerService {
/**
* 单条新增客户
*
* @param crmCustomer
* @return
*/
CrmCustomer addCustomer(CrmCustomer crmCustomer);
/**
* 批量新增客户
*
* @param crmCustomerList
* @return
*/
List<CrmCustomer> addCustomerBatch(List<CrmCustomer> crmCustomerList);
/**
* 通过id删除客户信息
*
* @param id
* @return
*/
Long deleteById(Long id);
/**
* 通过客户id修改客户信息
*
* @param id
* @return
*/
CrmCustomer updateCustomerById(Long id);
/**
* 通过客户id,获取客户信息
*
* @param id
* @return
*/
CrmCustomer getById(Long id);
/**
* 通过客户名字,获取最新的一条记录(因为可能有重名的)
*
* @param name
* @return
*/
CrmCustomer getByName(String name);
//获取分页列表数据...
}
/**
* 客户action服务类
*/
@RestController
@RequestMapping("/customer")
public class CustomerAction {
@Autowired
private CrmCustomerService crmCustomerService;
/**
* 单条新增客户
*
* @param crmCustomer
* @return
*/
@PostMapping("addCustomer")
public CrmCustomer addCustomer(@RequestBody CrmCustomer crmCustomer) {
return crmCustomerService.addCustomer(crmCustomer);
}
/**
* 批量新增客户
*
* @param crmCustomerList
* @return
*/
@PostMapping("addCustomerBatch")
public List<CrmCustomer> addCustomerBatch(@RequestBody List<CrmCustomer> crmCustomerList) {
return crmCustomerService.addCustomerBatch(crmCustomerList);
}
/**
* 通过id删除客户信息
*
* @param id
* @return
*/
@PostMapping("deleteById")
public Long deleteById(@RequestParam("id") Long id) {
return crmCustomerService.deleteById(id);
}
/**
* 通过客户id修改客户信息
*
* @param id
* @return
*/
@PostMapping("updateCustomerById")
public CrmCustomer updateCustomerById(@RequestParam("id") Long id) {
return crmCustomerService.updateCustomerById(id);
}
/**
* 通过客户id,获取客户信息
*
* @param id
* @return
*/
@GetMapping("getById")
public CrmCustomer getById(@RequestParam("id") Long id) {
return crmCustomerService.getById(id);
}
/**
* 通过客户名字,获取最新的一条记录(因为可能有重名的)
*
* @param name
* @return
*/
@GetMapping("getByName")
public CrmCustomer getByName(@RequestParam("name") String name) {
return crmCustomerService.getByName(name);
}
//获取分页列表数据...
}
以上代码分析: 以上代码基本上实现了一个基础的客户关系,虽然功能实现了,但是有一个问题,就是我所有的客户相关的处理,都封装到了一个CrmCustomerService接口服务中,业务越来越多,过一段时间,你就会发现,这个接口的实现类,上万行的代码,越来越难维护。在暴露接口时,你在action中,调用这个接口的方法时,会不会感觉客户实现类代码量这么大,如果是你自己写的还勉强能接受,如果是你接手的别人的代码,需要看业务逻辑时,你会不会很崩溃?
以上代码虽然只维护了客户这一个功能,在模块层面,这个CrmCustomerService接口类遵循了SRP单一职责原则
,但是,却违反了ISP接口隔离原则
,客户端不应该依赖它不需要的接口,也不应该知道接口中不需要的方法。因为这个接口方法太多了,别人调用你的接口时,可能只是用到几个小接口,不应该把大而全的方法全暴露给别人。
遵循ISP原则的代码设计
对以上代码进行修改,如下:
把CrmCustomerService
接口服务,按照功能类型
(操作客户和查询客户)进行拆分,拆分为CrmCustomerSelectService
接口查询服务类和CrmCustomerOperationService
接口操作服务类。笔者这里只是简单地拆分成了操作相关的接口服务,和查询服务类,实际开发中,要根据实际情况,进行合理的拆分。
/**
* 客户查询服务类
*/
public interface CrmCustomerSelectService {
/**
* 通过客户id,获取客户信息
* @param id
* @return
*/
CrmCustomer getById(Long id);
/**
* 通过客户名字,获取最新的一条记录(因为可能有重名的)
* @param name
* @return
*/
CrmCustomer getByName(String name);
//获取分页列表数据...
}
/**
* 客户操作服务类
*/
public interface CrmCustomerOperationService {
/**
* 单条新增客户
*
* @param crmCustomer
* @return
*/
CrmCustomer addCustomer(CrmCustomer crmCustomer);
/**
* 批量新增客户
*
* @param crmCustomerList
* @return
*/
List<CrmCustomer> addCustomerBatch(List<CrmCustomer> crmCustomerList);
/**
* 通过id删除客户信息
*
* @param id
* @return
*/
Long deleteById(Long id);
/**
* 通过客户id修改客户信息
*
* @param id
* @return
*/
CrmCustomer updateCustomerById(Long id);
}
这里,暴露接口服务的action类,这里就不贴代码了。以上的代码设计,把负责的一个接口,按照功能拆分成了两个接口服务,日后维护的时候会稍微好一些,最起码,不用打开一个实现类,上万行的代码。在设计接口的时候,我们要合理的设计接口的粒度大小,接口设计的过大或过小都不好。
运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。
合成(Composition)
和聚合(Aggregation)
都是关联(Association)的特殊种类。
合成也即是组合,比聚合的关联程度更深,具体的关系,不清楚的童鞋们,可以参考我的另一篇博文 类和类之间的关系详解,比较详细的介绍了几种关系。
在面向对象设计中,有两种基本的办法可以实现复用:
第一种是通过合成/聚合,即合成复用原则,含义是指,尽量使用合成/聚合,而不是使用继承
第二种就是通过继承
该原则说的是: 尽量使用聚合的方式而不要使用继承的方式达到复用原有代码目的。在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新的对象通过这些对象的委派达到复用已有功能的目的。
CARP设计原则是: 尽量使用合成/聚合,尽量不要使用继承
。
迪米特法则(Law of Demeter,LOD)又叫作最少知识原则(Least Knowledge Principle 简写LKP),说的是一个类对于自己依赖的其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。(百度百科)
对于依赖的类而言,类不管多么复杂也尽量将逻辑封装在其内部,对外只提供public的方法,但是不能对外直接提供访问属性的权限。通俗来说就是只使用直接关联、联系的类,不要使用没有直接联系的陌生的类。
直接联系的类包括: 是类的成员变量、是类中方法的返回值、是类中方法的参数,最直接的说法就是不要让一个既不是成员变量类型,也不是类中方法的返回值类型,还不是类中方法的参数类型的类,却成为在局部变量的类型。
一个实体应当尽可能少的与其他实体之间发生相互作用。这样做的目的在于减少依赖
,独立功能
,以便更好的复用
。
Java中的设计原则,还是必须掌握的,这样,才能更好的理解和掌握Java中的设计模式,鉴于笔者的理解能力,本文介绍的可能有不合理的地方,欢迎老铁们评论,多多指教,大家一起学习,进步!
本文参考链接:
https://www.cnblogs.com/zhaye/p/11176906.html
https://www.cnblogs.com/throwable/p/9315318.html
写博客是为了记住自己容易忘记的东西,另外也是对自己工作的总结,希望尽自己的努力,做到更好,大家一起努力进步!
如果有什么问题,欢迎大家评论,一起探讨,代码如有问题,欢迎各位大神指正!
给自己的梦想添加一双翅膀,让它可以在天空中自由自在的飞翔!