设计模式-6大设计原则

设计模式-6大设计原则

六大设计原则(SOLID)
  1. Single Responsibility Principle:单一职责原则

    应该有且仅有一个原因引起类的变更。

    ​ 类的复杂性降低,可读性提高,可维护性提高,风险降低。

    ​ Ex:有一个邮件接口,涉及到连接,断开连接,发送消息,接收消息;它就不符合单一职责的原则,连接管理和数据通信两者虽有关系,但关系不大,SRP因尽可能使类的功能单一。

    public class Email {
    
        void connection(){}
    
        void disconnection(){}
        
        void send(){}
        
        void receive(){}
    
    }
    

    ​ 通过职责划分为Connection,DataTransfer两个接口,每个接口仅涉及与自己相关的职责。

    public interface Connection {
    
        void connection();
    
        void disconnection();
    
    }
    
    public interface DataTransfer {
    
        void send();
    
        void receive();
    
    }
    
    public class Email implements Connection, DataTransfer {
    
        public void connection() {}
        
        public void disconnection() {}
        
        public void send() {}
        
        public void receive() {}
    
    }
    

    ​ 但我们还必须通过Email实现两个接口,将两个职责糅合到同一个类中,但是我们是面向接口编程,我们对外公布的是接口,而非实现类。

    ​ 单一职责针对的不仅仅是类,还有方法,将职责划分粒度变得更小,每个类、每个方法的功能尽可能单一,才能使得代码更加优雅。

    ​ 单一职责原则并不是规则,要根据开发进行部分取舍。


  2. Liskov Substitution Principle:里氏替换原则

    ​ 面向对象的语言中,继承是必不可少的、非常优秀的语言机制:

    代码共享、减少创建类的工作量,每个子类都拥有父类的方法和属性;

    提高的代码的可重用性;

    提高代码的可拓展性;

    提高产品或项目的开放性。

    ​ 继承的缺点:

    继承是侵入性。子类必须拥有父类所有的属性和方法;

    降低代码的灵活性,子类拥有父类的一切,也多了一些约束;

    增强藕合性,当父类的某些属性或方法修改时,需要考虑子类的修改。

    里氏替换原则就是为了尽可能发挥优点,而减少缺点带来的麻烦。

    ​ 所有引用基类的地方必须能透明地使用其子类的对象。也就是:只要父类能够出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本不需要是父类还是子类。但是反之不行。

    ​ 里氏替换原则为良好的继承定义了一个规范,一句简单的定义包含了四层含义。

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

      定义一个AbstractGun抽象类,拥有颜色、射击。两个实现类,一个士兵类,军人用枪杀敌,但具体什么枪只有调用才知道。

      AbstractGun.java、HandGun.java、MachineGun.java
             public abstract class AbstractGun {
             
                 abstract void getColor();
             
                 abstract void shoot();
             
             }
             
             public class HandGun extends AbstractGun {
                  @Override
                 void getColor() {
             
                 }
             
                 @Override
                 void shoot() {
                     System.out.println("手枪---->");
                 }
             }
             
             public class MachineGun extends AbstractGun{
                  @Override
                 void getColor() {
             
                 }
             
                 @Override
                 void shoot() {
                     System.out.println("机枪---->");
                 }
             }
      
      Soldier.java
      public class Soldier {
             
          AbstractGun gun;
      
          void setGun(AbstractGun gun) {
              this.gun = gun;
          }
      
          void killEnemy() {
              System.out.println("start");
              gun.shoot();
          }
      }
      

      在这里插入图片描述

      Main.java
      public class Main {
             
          public static void main(String[] args) {
              Soldier soldier = new Soldier();
              soldier.setGun(new MachineGun());
              soldier.killEnemy();
          }
      
      }
      

      输出结果:

      start 机枪---->

        当士兵需要使用其他类型的枪时,只需要更改Main类里传给soldier的枪的类型就行。即士兵并不知道传到自己手里的枪究竟是什么。
      
        士兵希望接收的枪的类型为AbstractGun,也并不关心具体的枪是什么类型,此时Soldier类中调用的Gun类为父类,而不是具体的子类,如果接收的对象为具体的类,说明类的设计已经违背了LSP原则。
      

      ​ 当我们有一个玩具手枪时,我们又该怎么可以呢,直接继承AbstractGun?传给在战场杀敌的士兵一把玩具枪?显然不行,但是玩具枪虽然不能开枪杀敌,但是它又有这和真枪一样的颜色属性。

        解决办法:我们在Soldier类中增加instanceof的判断,如果是玩具枪,我们就禁止射击。但是我们增加了一个类,导致与这个父类有关系的类我们都需要修改,这种方案显然不行。我们可以让ToyGun脱离继承,建立一个独立的父类,为了实现代码复用,可以与AbstractGun建立关联委托关系。
      
      public abstract class AbstractToy {
                 
          AbstractGun abstractGun;
      
          abstract void cannotShoot();
      
      }
      
    2. 子类可以有自己的个性

      ​ 继承的本质就是通过子类继承父类的功能并往外拓展,来实现复用和拓展。因此子类本来就需要有自己的个性。我们强调的是里氏替换原则原则可以正着用,但是不能反着用。在子类出现的地方,父类未必能够胜任!

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

      ​ 方法中的输入参数称为前置条件,这是什么意思呢?大家做过Web Service开发就应该知道有一个“契约优先”的原则,也就是先定义出WSDL接口,制定好双方的开发协议,然后再各自实现。里氏替换原则也要求制定一个契约,就是父类或接口,这种设计方法也叫做Design by Contract(契约设计),与里氏替换原则有着异曲同工之妙。契约制定了,也就同时制定了前置条件和后置条件,前置条件就是你要让我执行,就必须满足我的条件;后置条件就是我执行完了需要反馈,标准是什么。

      ​ 我们定义一个Father类,里面定义一个方法,将HashMap转为List。

      Father.java
      public class Father {
      
          List convertData(HashMap map) {
              System.out.println("父类执行");
              doSomeThing...
          }
      
      }
      

      ​ 然后定义Son类继承自Father,并重写父类的conventData方法。

      Son.java
      public class Son extends Father {
      
         @Override
         List convertData(HashMap map) {
             System.out.println("子类执行");
             doSomeThing...
         }
      }
      

      ​ 目前来看,代码并没有什么问题,那么我们能不能改变子类的convertData方法的输入参数类型呢?比如放大其输入参数改成Map。

      public class Son extends Father {
      
          List convertData(Map map) {
              System.out.println("子类执行");
              doSomeThing...
          }
      }
      

      ​ 我们去掉了@Override注解,因为当你改变了输入参数时,你的IDE就会报错,去除掉注解后,那这就不是对父类方法的重写,而是重载!此时父类出现的场景子类还能出现吗?

      public class Main {
      
          public static void main(String[] args) {
              Father f = new Father();
              f.convertData(new HashMap());
              Son s = new Son();
              s.convertData(new HashMap());
          }
      
      }
      

      ​ 执行结果:父类执行 父类执行

      ​ 此时我们发现,因为子类的输入参数是Map类型,也就是说之类的输入参数范围放大了,子类代替父类调用方法时,子类的方法永远都不会执行,这是正确的,因为你并没有重写父类的方法。如果我们缩小子类的前置条件时,上述的结果就会截然相反,因为在没有重写父类方法时执行了子类的方法,这会引起业务逻辑混乱。因此子类的前置条件必须与父类被重写的方法的前置条件相同甚至更宽松!

    4. 重写或实现父类的方法时输出结果可以被缩小

      ​ 里氏替换原则要求子类重载或重写父类方法的返回值必须小于等于父类方法的返回值。为什么?如果时重写,父类和子类的同名方法的入参相同,两个方法的子类的返回值是父类返回值的同类或子类,这是重写的要求。如果是重载,则要求方法签名不同,在里氏替换原则的要求下,就是子类的输入参数宽于或等于父类的输入参数,此时这个方法不会被调用!

    ​ 采用里氏替换原则的目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类从而完成不同的业务逻辑。


  3. Dependence Inversion Principle:依赖倒置原则

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

    三层含义:

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

    ​ 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;接口或抽象类不依赖于实现类;实现类依赖接口或抽象类。更加精简的定义就是“面向接口编程”——OOD(Object-Oriented Design,面向对象设计)的精髓之一。

    ​ 采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。

    ​ 依赖倒置原则的本质就是通过抽象使得各个类或模块实现彼此独立,不互相影响,实现模块间的松耦合。实现规则:

    1. 每个类都尽量要有抽象类或接口,或者两者都具备,有了抽象才有可能依赖倒置。
    2. 变量的表面类型尽量是接口或者是抽象类。某些util或者bean等是不需要抽象类的。
    3. 任何类都不该从具体类派生。非绝对,某些场景可能继承具体类更便于解决问题。
    4. 尽量不要覆写基类的方。如果父类是个抽象的类,并且方法已经实现,子类尽量不要覆写。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会产生一些影响。
    5. 结合里氏替换原则使用。接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。

    ​ 依赖正置是指我需要什么便依赖什么,也就是面向实现编程,当我们需要什么,就依赖什么,比如,我们需要iphone,就依赖iphone等。但是编写程序需要进行抽象,根据系统设计产生抽象间的依赖,代替了传统思维的事务的依赖,倒置就是这样。

    ​ 依赖倒置原则的核心就是“面向接口编程”!

  4. Interface Segregation Principle:接口隔离原则

    接口的含义:从实例类和实例对象的角度来看,实例对象需要遵从实例类的标准,实例类就是实例对象的接口。另一种就是通过interface关键字定义的接口。

    隔离的含义:客户端不应该依赖它不需要的接口。也就是保证客户端仅依赖其需要的接口,把不需要的接口剔除,也就是对接口进行细化,保证单个接口的纯洁性。类间的依赖关系应该建立在最小的接口上。也就是尽可能把接口进行细化。

    建立单一接口,接口尽量细化,同时接口内的方法尽可能少。

    ​ 从某种程度看,接口隔离原则与单一职责原则是相似的。但是两者针对的角度不同,单一职责要求从业务上划分,接口和类尽可能职责单一。而接口隔离要求接口的方法尽可能少。从单一职责来看,一个接口包含多个方法,并且提供给多个模块进行访问,从单一职责上来看,这个接口的设计是可以的,然而从接口隔离的角度来看,提供多个模块访问,应为每个模块提供一个接口,该接口所包含的方法仅供该接口访问。

    ​ 接口隔离原则是对接口进行规范约束,其包含以下四层含义:

    1. 接口尽量小

      这是接口隔离原则的核心定义,不出现臃肿的接口,但是首先不能违反单一职责原则。

    2. 接口要高内聚

      高内聚就是提高接口、类、模块的处理能力,减少对外的交互。具体就是在接口中尽量少公布public方法,承诺的越少,承担的风险越小。

    3. 定制服务

      一个系统或者模块间必然会有耦合,有耦合就有要互相访问的接口。定制服务就是只提供该访问者需要的方法。

    4. 接口设计是有限度的

      接口设计粒度越小,系统越灵活,但是也带来了结构的复杂化,开发难度增加,可维护性降低。因此应该遵守一些规则。一个接口只服务于一个子模块或者业务逻辑;已经污染的接口尽量修改,若风险太大,采用适配器模式转化处理。

  5. Law of Demeter:迪米特法则(最少知识原则)

    一个对象应该对其他对象有最少的了解

    要求:

    1. 只和朋友交流

      出现在成员变量、方法出入参的类称为成员朋友类,而出现在方法体内部的类不属于朋友类。尽量不要在一个方法中引入类中不存在的对象,JDK API提供的类除外。

    2. 朋友间是有距离的

      尽量压缩类中公开的public属性或方法,多使用private、default、protected。

    3. 是自己的就是自己的

      当一个方法出现放在本类也可以,放在其他类也没有错的情况。如果一个方法放置在本类中,既不增加类间关系,也不对本类产生负面影响,那就放置在本类中。

    4. 谨慎使用Serializable

      远程方法调用时传递一个VO,这个对象必须实现Serializable接口,如果客户端VO修改了属性的访问权限,而服务器没有做相应变更,就会序列化失败。

    迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。其要求的结果就是产生了大量的中转或跳转类,导致系统的复杂性提高,同时也为维护带来了难度。读者在采用迪米特法则时需要反复权衡,既做到让结构清晰,又做到高内聚低耦合。

  6. Open Closed Principle:开闭原则

    ​ 开闭原则:应尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。

    1. 抽象约束

      抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:第一,通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;第二,参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;第三,抽象层尽量保持稳定,一旦确定即不允许修改。

    2. 元数据(metadata)控制模块行为

      尽量使用元数据来控制程序的行为,减少重复开发。什么是元数据?用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。举个非常简单的例子,login方法中提供了这样的逻辑:先检查IP地址是否在允许访问的列表中,然后再决定是否需要到数据库中验证密码(如果采用SSH架构,则可以通过Struts的拦截器来实现),该行为就是一个典型的元数据控制模块行为的例子,其中达到极致的就是控制反转(Inversion of Control),使用最多的就是Spring容器。

    3. 制定项目章程

      在一个团队中,建立项目章程是非常重要的,因为章程中指定了所有人员都必须遵守的约定,对项目来说,约定优于配置。相信大家都做过项目,会发现一个项目会产生非常多的配置文件。举个简单的例子,以SSH项目开发为例,一个项目中的Bean配置文件就非常多,管理非常麻烦。如果需要扩展,就需要增加子类,并修改SpringContext文件。然而,如果你在项目中指定这样一个章程:所有的Bean都自动注入,使用Annotation进行装配,进行扩展时,甚至只用写一个子类,然后由持久层生成对象,其他的都不需要修改,这就需要项目内约束,每个项目成员都必须遵守,该方法需要一个团队有较高的自觉性,需要一个较长时间的磨合,一旦项目成员都熟悉这样的规则,比通过接口或抽象类进行约束效率更高,而且扩展性一点也没有减少。

    4. 封装变化

      对变化的封装包含两层含义:第一,将相同的变化封装到一个接口或抽象类中;第二,将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。封装变化,也就是受保护的变化(protected variations),找出预计有变化或不稳定的点,我们为这些变化点创建稳定的接口,准确地讲是封装可能发生的变化,一旦预测到或“第六感”发觉有变化,就可以进行封装。

6个原则结合使用的好处:建立稳定、灵活、健壮的设计,而开闭原则又是重中之重,是最基础的原则,是其他5大原则的精神领袖!

设计模式之禅(第二版)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值