美文网首页
java核心技术笔记

java核心技术笔记

作者: 日落_3d9f | 来源:发表于2022-08-07 16:02 被阅读0次

    第一章、面向对象五大基本原则 SOLID原则

    1.1 单一职责

    参考:https://www.jianshu.com/p/63bd557f6ca4
    单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP。他的英文描述是:A class or module should have a single reponsibility。翻译过来就是:一个类或者模块只负责完成一个职责(或者功能)。

    意思是一个类只负责完成一个职责或者功能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。

    这个原则看似比较简单,但实际在开发中判断一个类是否单一是比较难拿捏的。因为在不同的应用场景和业务的不同阶段,对同一个类职责判断是否单一都是有可能不一样的。在当前需求下一个类可能已经满足了单一职责的判断,但如果换一个应用场景或者未来的某个需求背景下可能就不满足了,需要继续拆分成粒度更细的类。所以通常我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展如果粗粒度的类越来越庞大的时候,我们再将这个粗粒度的类拆分成几个更细粒度的类。

    判断一个类是否需要拆分有以下几个原则供参考:

    • 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
    • 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
    • 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;
    • 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;
    • 类中大量的方法都是集中操作类中的某几个属性,那就可以考虑将这几个属性和对应的方法拆分出来。

    1.2 开闭原则

    开闭原则的英文全称是 Open Closed Principle,简写为 OCP。它的英文描述是:software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。翻译过来就是:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。

    这个描述比较简略,详细表述一下就是:添加一个新的功能应该是在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

    这里说的不修改已有代码并不是指完全杜绝修改代码,在软件开发需求迭代中修改代码是在所难免的,我们要做的是尽量让修改操作更集中、更少、更上层,尽量让最核心、最复杂的那部分逻辑代码满足开闭原则。

    打个比方说,有一个插入用户信息:姓名,年龄,电话的方法。

    class Demo{
          public void insertUserInfo(String name,int age,String tel){
              ....
          }
    
    }
    

    如果随着业务的发展这个方法需要把用户的email也插入进去,如果我们直接修改这个方法添加一个email参数

    class Demo{
          public void insertUserInfo(String name,int age,String tel,String email){
              ....
          }
    
    }
    

    这样的修改就有违背开闭原则,代码上所有调用这个方法的地方都需要修改。

    假如是这样的代码

    class Demo{
          public void insert(User user){
              ....
          }
    
    }
    
    class User{
        String name;
        int age;
        String tel;
    }
    

    我们为了满足业务的需求,需要在插入用户信息的时候把email也插入进去,那么我们直接在User类中添加一个email属性,这样的修改就没有违背开闭原则。

    其实也可以这样理解,开闭原则说的是软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。我们可以看出来,开闭原则可以应用在不同粒度的代码中,可以是模块,也可以类,还可以是方法(及其属性)。同样一个代码改动,在粗代码粒度下,被认定为“修改”,在细代码粒度下,又可以被认定为“扩展”。比如,添加属性和方法相当于修改类,在类这个层面,这个代码改动可以被认定为“修改”;但这个代码改动并没有修改已有的属性和方法,在方法(及其属性)这一层面,它又可以被认定为“扩展”。

    再则我们通过上面满足同一需求的方法代码来看,下面那个传User对象的方法比上面的方法更方便扩展一些,这也就是为什么要遵循开闭原则来写代码的原因。

    1.3 里式替换原则

    里式替换原则的英文翻译是:Liskov Substitution Principle,缩写为 LSP。这个原则最早是在 1986 年由 Barbara Liskov 提出,他是这么描述这条原则的:

    If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。

    在 1996 年,Robert Martin 在他的 SOLID 原则中,重新描述了这个原则,英文原话是这样的:

    Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。

    综合两者的描述,将这条原则用中文描述出来,是这样的:子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

    原则里说“子类替换父类出现的地方”咋一看有点像面向对象里多态的意思,其实不是。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。直接来讲就是按照协议来设计。

    举个例子

    class SecurityService{
    
        public boolean isRightfulPassword(String password){
              if(password==null || password.length < 6){
                    return false;
              }
              return true;
        }
    }
    
    class SonSecurityService extends SecurityService{
         @Override
         public boolean isRightfulPassword(String password){
              if(password==null){
                   throw new ParameterNullRunTimeException();
              }
              return super.isRightfulPassword(password);
        }
    }
    

    在父类SecurityService中isRightfulPassword参数password为空的时候返回的结果是false,而子类SonSecurityService在替换父类后,当password为空时却抛了一个异常,这就违背了里式替换原则。

    里式替换原则是用来指导继承关系中子类该如何设计的一个原则。理解里式替换原则,最核心的就是理解“按照协议来设计(design by contract)”。父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;业务上的特殊限制;

    有一个比较简单的方法来判断是否违背了里式替换原则。那就是拿父类的单元测试去验证子类的代码。如果某些单元测试运行失败,就有可能说明,子类有可能违背了里式替换原则。

    1.4 接口隔离原则

    接口隔离原则的英文翻译是“ Interface Segregation Principle”,缩写为 ISP。Robert Martin 在 SOLID 原则中是这样定义它的:“Clients should not be forced to depend upon interfaces that they do not use。”直译成中文的话就是:客户端不应该强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。

    举个例子:

    public interface UserService {
          //注册
          boolean register(String username, String password);
          //登录
          boolean login(String username, String password);
          //查询用户
          User getUserById(int id);
         //删除用户
          boolean deleteUserById(int id);
    }
    

    有一个用户服务的接口,里面有注册,登录,查询,删除几个函数暴露给客户端,但是很明显删除函数只有后台管理模块可以用到,而且其他模块如果都可以使用的话就有可能照成误删用户。这就违背了接口隔离原则,我们需要把删除函数单独抽离出来给后台管理模块使用。

    public interface UserService {
          //注册
          boolean register(String username, String password);
          //登录
          boolean login(String username, String password);
          //查询用户
          User getUserById(int id);
          
    }
    
    public interface AdminUserService {
            //删除用户
          boolean deleteUserById(int id);
    }
    

    其中“接口”也可以有不同的含义:

    • 可以把它理解为微服务的一组接口,如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口;
    • 可以把它理解成api接口或者函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。
    • 可以把它理解成面向对象编程中定义的接口语法,那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。

    说起接口隔离其实和单一原则比较类似,都是指在代码设计上尽量单一。不同的是单一原则针对的是模块,类,接口的设计。接口隔离原则不光说的是接口的设计,还提供了一种判断接口职责是否单一的标准:如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

    1.5 依赖倒置原则(Dependence Inversion Principle ,DIP)

    参考:https://www.jianshu.com/p/377830cb1112

    定义:
    ①高级模块不应该依赖于低级模块,两者都应该依赖于抽象。
    ②抽象不应该依赖于细节。
    ③细节应该依赖于抽象。

    那么高级模块、低级模块,抽象,细节各指的是什么呢?
    每一个逻辑的实现都是由原子逻辑组成,不可分割的原子逻辑就是低级模块。而低级模块组装就是高级模块。
    在Java中,抽象就是指接口或抽象类,两者并不能直接被实例化。
    细节就是实现类,继承抽象类或实现接口的类就是细节。特点是可以被直接实例化。

    那么依赖倒置原则定义在Java语言中可以表示为:
    ①模块间的依赖关系、实现类间的依赖关系都是通过接口或抽象类产生。
    ②接口与抽象类不依赖实现类。
    ③实现类依赖接口或抽象。
    从上述定义我们可以看出,依赖倒置原则的核心思想就是:面向接口编程

    代码重现:

    司机类

    public class Driver {
       public void drive(Benz benz){
          benz.run();
       }
    }
    

    奔驰类

    public class Benz {
       public void run(){
          System.out.println("开奔驰车上路");
       }
    }
    

    测试类

    public class Demo_01 {
       public static void main(String[] args) {
             Driver sanmao = new Driver();
             sanmao.drive(new Benz());
       }
    }
    --------------------output---------------------------
    开奔驰车上路
    

    通过以上代码完成了司机类开奔驰车的场景。
    但是随着业务需求的变更,现在要求这个司机还能开宝马车,那么上面代码必须要重写(耦合性太强),才能满足业务需求,显然上面代码在设计上有错误。
    那么我们依据依赖倒置原则重构以上代码:

    司机接口类

    public interface IDriver {
        void drive(ICar iCar);
    }
    

    司机实现类

    public class Driver implements IDriver{
       @Override
       public void drive(ICar iCar) {
          iCar.run();
       }
    }
    

    汽车接口类

    public interface ICar {
        void run();
    }
    

    汽车实现类

    public class Benz implements ICar{
       @Override
       public void run() {
          System.out.println("开奔驰上路");
       }
    }
    public class BMW implements ICar {
       @Override
       public void run() {
          System.out.println("开宝马上路");
       }
    }
    

    测试类

    public class Demo_01 {
       public static void main(String[] args) {
        IDriver sanmao = new Driver();
        ICar benz = new Benz();
        ICar BMW = new BMW();
        sanmao.drive(benz);
        sanmao.drive(BMW);
       }
    }
    --------------------output---------------------------
    开奔驰上路
    开宝马上路
    

    Demo_01类就是高级模块,他对所有低级模块的依赖都建立在抽象类或接口上

    在IDriver接口中传入ICar接口,实现模块间的依赖关系是通过接口或抽象类产生。在Driver实现类中也传入了ICar接口,究竟使用Car的哪个子类还得在测试类中声明。
    在测试类中,我们贯彻“②接口与抽象类不依赖实现类。”,所以在测试类Demo_01中,我们都是声明了各类的抽象。

    注意:在Java中,只要定义变量就必然要有类型。一个变量可以有两种类型:表面类型与实际类型。表面类型是在定义时赋予的类型,实际类型是对象的类型,如sanmao的表现类型是IDriver,实际类型为Driver。

    依赖的三种写法
    依赖是可以传递的。A对象依赖B对象,B依赖C,C依赖D...,
    只要做到抽象依赖,即使是多层的依赖传递也无所畏惧。
    对象的依赖关系有以下三种方式传递,可以参考Spring的依赖注入

    ①构造函数传递依赖关系

    public interface IDriver {
        void drive();
    }
    
    public class Driver implements IDriver{
       private ICar iCar;
       public Driver(ICar _iCar){
          this.iCar = _iCar;
       }
       
       public void drive() {
          this.iCar.run();
       }
    }
    

    ②Setter方法传递依赖关系

    public interface IDriver {
        void drive();
        void setCar(ICar _iCar);
    }
    
    public class Driver implements IDriver{
       private ICar iCar;
       @Override
       public void drive() {
          this.iCar.run();
       }
       @Override
       public void setCar(ICar _iCar) {
          this.iCar = _iCar;
       }
    }
    

    ③接口声明依赖关系

    public interface IDriver {
        void drive(ICar iCar);
    }
    
    public class Driver implements IDriver{
       @Override
       public void drive(ICar iCar) {
          iCar.run();
       }
    }
    

    依赖倒置原则的本质就是通过接口或抽象类使得各模块间相互独立,互不影响,实现松耦合。在使用这个原则时,我们需要注意以下几个原则:

    ①每个类都尽可能的都有接口或抽象类,或者抽象类和接口两者都具备。(依赖倒置原则的基本要求)
    ②变量的表面类型尽量是接口或者抽象类。(工具类不需要接口或者抽象类,使用类的Clone方法,必须使用实现类,这是JDK的规范)
    ③任何类都不应该从具体类派生。
    ④尽量不要覆写基类的方法。(若基类是一个抽象类,此方法已经实现,那么子类就不应该覆写)
    ⑤结合里氏替换原则使用
    接口负责public属性与方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。

    上面讲完了依赖关系与原则,那么什么叫倒置呢?
    要理解倒置,首先我们得知道什么叫“正置”。依赖正置就是类之间的依赖是实现类间的依赖,也就是面向实现编程。但是编写程序需要的是对现实世界的事物进行抽象,转化为我们熟知的抽象类或接口,这样我们就可以将模块间的依赖关系、实现类间的依赖关系都是通过接口或抽象类产生。这就是我们所说的“倒置”。

    相关文章

      网友评论

          本文标题:java核心技术笔记

          本文链接:https://www.haomeiwen.com/subject/lcpbwrtx.html