设计模式—六大设计原则

本文详细介绍了设计模式中的六大原则:单一职责原则、里氏替换原则、依赖倒置原则、接口隔离原则、迪米特法则和开闭原则。通过实例分析,阐述了每个原则的含义、应用和优点,旨在帮助开发者理解并更好地实践这些原则,提高代码质量和可维护性。
摘要由CSDN通过智能技术生成

本文开始对《设计模式之禅》进行学习总结,首先是六大设计原则。

单一职责原则

单一职责原则(Single Responsibility Principle)简称SRP,这个原则存在的争议之处是“职责”的定义、划分。

先举例说明什么事单一职责:以用户管理、修改用户的信息、增加机构(一个人属于多个机构)、增加角色等维护,用户有这么多的行为维护。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Gc13Bfv-1639452643322)(1.%20%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E2%80%94%E5%85%AD%E5%A4%A7%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99%EF%BC%88%E4%B8%80%EF%BC%89.assets/%E7%94%A8%E6%88%B7%E4%BF%A1%E6%81%AF%E7%BB%B4%E6%8A%A4%E7%B1%BB%E5%9B%BE.png)]

  • 假如我们把他们写到一个接口中,类图如图最左侧,从图中可以很容易看出,类的定义没有划分用户的属性和用户的行为,按照抽取属性和行为的思路可以进行职责划分。

  • 如图中间,职责划分后将接口拆为两个IUserBoIUserBiz,现在是面向接口编程,所以产生UserInfo对象后可以既把它当做IUserBo接口获得用户属性,也可以当做IUserBiz操作维护用户相关信息。

    ....
    IUserInfo userInfo = new UserInfo();
    // 赋值
    IUserBO userBO = (IUserBo) userInfo;
    userBO.setPassword("abc");
    // 执行动作
    IUserBiz userBiz = (IUserBiz) userInfo;
    userBiz.deleteUser();
    ....
    
  • 项目实际习惯更倾向于右侧使用两个不同的类或者接口进行操作。

SRP的原话解释是:There should never be more than one reason for a class to change

这句话很好看懂,但是看懂是一回事,实践又是另一回事了。举例打电话:电话通话有4个过程:拨号、通话、回应。类图如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-psRmBlS6-1639452643323)(1.%20%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E2%80%94%E5%85%AD%E5%A4%A7%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99%EF%BC%88%E4%B8%80%EF%BC%89.assets/%E7%94%B5%E8%AF%9D%E7%B1%BB%E5%9B%BE.png)]

public interface IPhone{
   
  // 拨通电话
  public void dial(String phoneNumber);
  // 通话
  public void chat(Object o);
  // 通话完毕,挂电话
  public void hangup();
}

实现类比较简单,这个接口起始已经接近完美,看清楚是”接近“!一个接口或类只有一个原因引起变化,也就是一个接口只负责一件事,但是上面的接口只负责一件事吗?只有一个原因引起变化吗?好像不是!

这个接口包含了两个职责:一个是协议管理(dialhanup),另一个是数据传输(chat),所以考虑职责的划分和互不影响,拆分为两个接口如下图左侧:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jcJVgylV-1639452643323)(1.%20%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E2%80%94%E5%85%AD%E5%A4%A7%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99%EF%BC%88%E4%B8%80%EF%BC%89.assets/%E8%81%8C%E8%B4%A3%E5%88%86%E6%98%8E%E7%9A%84%E7%94%B5%E8%AF%9D%E7%B1%BB%E5%9B%BE.png)]

但是图中左侧的设计,一个手机要把两个类组合才能使用,这种组合是一种强耦合关系,并且增加了两个类。你和我都有共同的生命周期,这种强耦合的组合方式还不如使用接口的实现方式,如图右侧,这才是完美的设计!

你可能会觉得这个IPhone有两个原因引起变化了呀!但是别忘了我们是面向接口编程,对外提供的是接口而不是实现类,而且要是如果真要实现类的单一职责,就必须使用上面的组合模式,这会引起类间的耦合过重、类的数量增加等问题。

通过上面的例子,总结一下单一职责原则有什么好处:

  • 类的复杂性降低,实现什么职责有清晰明确的定义;
  • 可读性提高,复杂性降低,当然可读性提高了;
  • 可维护性提高,复杂性降低当然更容易维护;
  • 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做的好,一个接口修改只对相应的实现类有影响,对其他接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

反思:

看过电话的例子后是不是陷入了深思?不要怀疑自己的能力,单一职责最难划分的就是职责。一个职责一个接口,但是”职责“没有一个量化标准。从功能上说IPhone一个接口,一个实现类没有错,实现了电话的功能而且设计很简单,实际的项目我想大家都会这么设计。项目要考虑可变因素和不可变因素,以及相关的收益成本比。

一个方法承担多个职责,下图仅仅是个简单的逻辑尽管按左边的编写可能后续维护人员也可以看懂,但是如果是非常复杂的类作为参数,那么维护起来会浪费很多时间。方法职责不清晰,不单一,不要让别人猜测这个方法可能是用来处理什么逻辑的。比如图中右边每个方法的职责非常明确,不仅开发简单而且日后维护也非常容易。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yRhrOZ5J-1639452643323)(1.%20%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E2%80%94%E5%85%AD%E5%A4%A7%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99%EF%BC%88%E4%B8%80%EF%BC%89.assets/%E4%B8%80%E4%B8%AA%E6%96%B9%E6%B3%95%E6%89%BF%E6%8B%85%E5%A4%9A%E4%B8%AA%E8%81%8C%E8%B4%A3.png)]

里氏替换原则

里氏替换原则(Liskov Substitution Principle),面向对象的语言中,继承是必不可少的。继承在带来代码共享、提高扩展性的同时,带来的是代码的侵入、降低灵活、增加耦合,没有规范的继承会使得代码难以维护。

里氏替换原则为良好的继承定义了一个规范,包含一下4层含义:

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

举一个士兵拿枪射击的例子:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SO74NTML-1639452643324)(1.%20%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E2%80%94%E5%85%AD%E5%A4%A7%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99%EF%BC%88%E4%B8%80%EF%BC%89.assets/%E5%A3%AB%E5%85%B5%E5%B0%84%E5%87%BB%E7%B1%BB%E5%9B%BE%E7%A4%BA%E4%BE%8B-6369438.png)]

士兵具有枪,可以设置拿什么枪,并且有杀敌动作。枪是一个抽象类,可以有很多个实现如手枪、步枪、机枪。

枪支抽象类

public abstract class AbstractGun{
   
  // 枪用来干什么
  public abstract void shoot();
}

枪支实现类

public class HandGun extends AbstractGun{
   
  @Override
  public void shoot(){
   
    System.out.println("手枪射击...");
  }
}

public class Rifle extends AbstractGun{
   
  @Override
  public void shoot(){
   
    System.out.println("步枪射击...");
  }
}

public class MachineGun extends AbstractGun{
   
  @Override
  public void shoot(){
   
    System.out.println("机枪射击...");
  }
}

士兵的实现类

public class Soldier{
   
  private AbstractGun gun;
  public void setGun(AbstractGun gun){
   
    this.gun = gun;
  }
  public void killEnemy(){
   
    System.out.println("士兵开始射杀敌人...");
    gun.shoot();
  }
}

在这个程序中给士兵什么武器将用什么射击,编写程序时候士兵根本不知道是哪个型号的枪(子类)被传入。

【注意】在类中调用其他类时务必使用父类或接口,如果不能则说明类的设计已经违背里氏替换原则

从上例子中再想一想如果我们有一个玩具手枪,该如何定义?是否是直接继承抽象枪类?

答案是否定的,因为玩具枪不能用来杀人,这个不应写在shoot()方法中。那么如何解决呢?

  • 如果已经将玩具枪继承自AbstractGun那么就需要再Soldier中增加instanceof的判断,如果是玩具枪就不能用来杀人。这是一种方案,但是如果你的产品出现问题,因为修正了一个这样的Bug就要求所有与这个父类有关系的类都增加一个判断,这显然是不可行的。

  • ToyGun脱离继承,建立一个独立的父类,为了实现代码复用,可以与AbstractGun建立关联委托关系,将声音、形状等委托给其处理,如图

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GinDkFap-1639452643324)(1.%20%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E2%80%94%E5%85%AD%E5%A4%A7%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99%EF%BC%88%E4%B8%80%EF%BC%89.assets/%E5%A3%AB%E5%85%B5%E5%B0%84%E5%87%BB%E7%B1%BB%E5%9B%BE%E7%A4%BA%E4%BE%8B%20(1)].png)

2、子类可以有自己的个性

子类可以有自己的方法和属性,里氏替换原则可以正着用,但不能反过来用,比如子类出现的地方,父类未必可以胜任。再举例狙击手类图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-isCDy80l-1639452643324)(1.%20%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E2%80%94%E5%85%AD%E5%A4%A7%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99%EF%BC%88%E4%B8%80%EF%BC%89.assets/%E7%8B%99%E5%87%BB%E6%89%8B.png)]

很简单,AUG继承了Rifle类,狙击手(Snipper)则直接使用AUG狙击步枪

public class Aug extends Rifle {
   
  // 瞄准
  public void zoomOut() {
   
    System.out.println("通过望远镜查看敌人...");
  }
  public void shoot() {
   
    System.out.println("AUG射击...");
  }
}

这样狙击手设计杀人的实现如下:

public class Client {
   
  public static void main(String[] args) {
   
    Snipper ab = new Snipper();
    ab.setRifle(new Aug());
    // 错误代码
    // ab.setRifle((Aug) new Rifle());
    ab.killEnemy();
  }
}

这样狙击手就可以先瞄准,再射击了。看一下错误代码,显然是不行的,会在运行时抛出java.lang.ClassCastException异常,这也是大家经常说的向下转型(downcast)是不安全的,从里氏替换原则来看,就是有子类可以实现的地方父类未必可以胜任。

3、重写或实现父类的方法时输入参数可以被放大

这句话是什么意思呢?举例说明你就会明白

// 父类
public class Father {
   
  public Collection doSomething(HashMap map) {
   
    System.out.println("父类被执行...");
    return map.Values();
  }
}
// 子类
public class Son extends Father {
   
  public Collection doSomething(Map map) {
   
    System.out.println("子类被执行..."
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值