UNREMITTINGLY

V1

2022/03/23阅读:47主题:雁栖湖

高质量代码-面向对象

在日常的开发中能够写出高质量的代码,不仅仅可以在日常工作中负责审视同事代码,能够提出宝贵意见,而且还可以在面试的时候游刃有余。

写出高质量的代码,不仅仅是要求写出规范的代码,还要求写出高性能的代码,高质量的代码往往具备这些特点:可扩展性、可维护性、可读性、可复用、高内聚低耦合、高性能、安全性、整洁、简单、分层清晰、健壮性,那么这样的代码就要依托java自身的面向对象思想以及各种设计原则,设计模式,编程规范,以及不断的重构来实现。

面向对象编程因为其具有丰富的特性(封装、抽象、继承、多态),可以实现很多复杂的设计思路,是很多设计原则、设计模式等编码实现的基础。

设计原则是指导我们代码设计的一些经验总结,对于某些场景下,是否应该应用某种设计模式,具有指导意义。比如,“开闭原则”是很多设计模式(策略、模板等)的指导原则。

设计模式是针对软件开发中经常遇到的一些设计问题,总结出来的一套解决方案或者设计思路。应用设计模式的主要目的是提高代码的可扩展性。从抽象程度上来讲,设计原则比设计模式更抽象。设计模式更加具体、更加可执行。

编码规范相对于设计原则、设计模式,更加具体、更加偏重代码细节、更加能落地。但是我在这里所说的编程规范不仅仅是指规范的书写提高可读性,最重要的还要应用虚拟机和硬件底层的规则写出高性能的代码,持续的小重构依赖的理论基础主要就是编程规范。

重构作为保持代码质量不下降的有效手段,利用的就是面向对象、设计原则、设计模式、编码规范这些理论。


今天我们来探讨面向对象


1面向对象

面向对象编程要求必须以类和对象作为组织代码的基本单位,同时要支持封装,继承,多肽三大特性,也有人把抽象作为面向对象的第四特性,而我习惯把抽象归到继承这个范畴,反正不管怎样,这四个特性共同组成了java代码实现和设计的基石,这几个特性看起来很简单,但是却是java语言的基本功。各大框架源码中比比皆是。

封装

封装也叫作信息隐藏或者数据访问保护,我们用一个非常通俗的现实例子来说明下:

public class bank {
    private String id;
    private long createTime;
    private BigDecimal balance;
    private long balanceLastModifiedTime;
}

这是一个银行存款的例子,在存款的时候有存款时间,存款后的余额,余额更改时间,如果这几个属性是public修饰,那么其他类都可以访问这几个属性,并且对其任意修改,这是不安全的,封装的目的把属性的修改权限私有化,比如时间只有内部可以改动,不需要对外开放,对于余额的改动银行依赖于存款人存多少钱,所以可以提供一个带入参的函数入口,类似于存款人把钱给到银行,由银行人员进行余额的变动,这是把权限最小化,尽可能增强安全性,通过有限的方法暴露必要的操作,我们代码中的get set方法就是封装的最简体现。

继承

继承是指子类继承父类,语法机制:extends

public class A extends D
  1. 子类拥有父类非 private 的属性、方法。
  2. 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
  3. 子类可以用自己的方式实现父类的方法。
  4. 一个类可以被多个子类继承,但是一个类不能继承多个父类
  5. 继承让代码可以复用,把子类共有的方法抽离到父类中实现,由子类来继承父类,从而达到代码精简。

关于继承还有两种特殊的继承:实现接口和继承抽象类、

我们先来了解下接口和抽象类

抽象类

public abstract class A

一个普通的类如果被abstract修饰,就是一个抽象类,抽象类相对于普通类的特点如下:

  1. 可以存在abstract修饰的方法,即抽象方法,被abstract修饰的方法不能用private、static、synchronized、native等访问修饰符修饰,并且没有实现体

  2. 不能实例化,只能被子类继承

  3. 如果子类继承了抽象类,并且实现了抽象类的所有抽象方法,那么子类是一个具体的类,如果没有全部实现其抽象方法,则子类也是一个抽象类

  4. 抽象类有构造方法,目的是让子类来调用构造方法初始化

接口是一种特殊的抽象类,语法机制:interface

public interface B

接口其实就是一种特殊的抽象类,相对于抽象类不同的地方是:

  1. 只能被实现,其实现也是一种特殊的继承,这里可以看做是区别,也可以不当做区别。

  2. 接口中默认所有的方法都是抽象方法

  3. 接口中定义的成员变量默认是public static final,即只能够有静态的不能被修改的数据成员,而且,必须赋初值

  4. 一个类可以实现多个接口,一定程度上弥补了不能多继承的问题

  5. 接口与接口之间是可以多继承的,即一个接口可以用关键字extends继承多个接口

对于接口和抽象类的应用如何理解:

结合面向对象思想来理解,抽象类是类的溯源产物,比如:幼年猫-猫-猫科-动物,是一个类群里不断向上溯源就是抽象;而接口是对行为的分组,比如吃饭,睡觉,玩游戏。

那么在日常应用中,应该怎么应用呢,接口是设计的结果,抽象类是重构的结果,如何理解呢,日常开发中我们设计一个功能,往往是以接口的形式对外暴露,不暴露实现,这便是设计,为了解决解耦问题,隔离接口和具体的实现,提高代码的扩展性。;抽象类往往是在对已有代码进行重构的时候,把公共的行为抽离到抽象类中实现,达到代码复用目的,所以说抽象类其实更倾向于充当公共类角色

多态

多态是指子类可以替换父类,父类的引用可以指向子类,我们直接用代码来说明:

public class DynamicArray {
  
  private static final int DEFAULT_CAPACITY = 10;
  protected int size = 0;
  protected Integer[] elements = new Integer[DEFAULT_CAPACITY];

  public int size() {
   return this.size;
  }

  public Integer get(int index) {
   return elements[index];
  }

  public void add(Integer e) {
   elements[size++] = e;
  }

 }

 public class SortedDynamicArray extends DynamicArray {
  @Override
  public void add (Integer e) {
   int i;
   for (i = size - 1; i >= 0; --i) {
    if (elements[i] > e) {
     elements[i + 1] = elements[i];
    } else {
     break;
    }
   }
   elements[i + 1] = e;
   ++size;
  }
 }

 public class Example {
  public static void test(DynamicArray dynamicArray) {
   dynamicArray.add(5);
   dynamicArray.add(1);
   dynamicArray.add(3);
   for (int i = 0; i < dynamicArray.size(); ++i) {
    System.out.println(dynamicArray.get(i));
   }
  }

  public static void main(String args[]) {
   DynamicArray dynamicArray = new SortedDynamicArray();
   test(dynamicArray);
  }
 }

public interface Iterator {

  String hasNext();

  String next();

  String remove();

 }

 public class Array implements Iterator {
  
  private String[] data;

  public String hasNext() { ...}

  public String next() { ...}

  public String remove() { ...}

 }



 public class LinkedList implements Iterator {

  private LinkedListNode head;

  public String hasNext() { ...}

  public String next() { ...}

  public String remove() { ...}

 }

 public class Demo {
  
  private static void print(Iterator iterator) {
   while (iterator.hasNext()) {
    System.out.println(iterator.next());
   }
  }

  public static void main(String[] args) {
   Iterator arrayIterator = new Array();
   print(arrayIterator);
   Iterator linkedListIterator = new LinkedList();
   print(linkedListIterator);
  }
 }

上面两个案例中最关键的代码:

DynamicArray dynamicArray = new SortedDynamicArray();

Iterator arrayIterator = new Array();

这两行代码都是用父类来接收子类,其实这就是多肽,但是要达到这样的目的是要依赖于“继承”或者“实现”,正如上面的代码一样。

多态特性能提高代码的可扩展性和复用性

在那个例子中,我们利用多态的特性,仅用一个print()函数就可以实现遍历打印不同类型(Array、LinkedList)集合的数据。当再增加一种要遍历打印的类型的时候,比如HashMap,我们只需让HashMap实现Iterator接口,重新实现自己的hasNext()、next()等方法就可以了,完全不需要改动print()函数的代码。所以说,多态提高了代码的可扩展性。

DDD 领域驱动模型

贫血模型开发模式

java是一个面向对象的编程语言,但是我们日常开发却不一定完全符合面向对象风格,比如我们熟知的MVC开发模型将数据和业务逻辑完全分离,正是违背了面向对象的封装特性,封装要求的是信息隐藏,仅仅暴露给外部必要的操作接口,而MVC模型中数据的操作全部暴露给了service层,是一种彻彻底底的面向过程的编程风格。这种被称为反模式,这种模型中只包含数据,不包含业务逻辑的形式称为基于贫血模型开发模式。

充血模型的DDD开发模式

与之相反的就是基于充血模型的DDD开发模式,也就是领域驱动设计,模型中既包含数据,又包含业务逻辑,真正做到数据和业务不分离,这种模式就是基于充血模型的DDD开发模式。

实际上,基于充血模型的DDD开发模式实现的代码,也是按照MVC三层架构分层的。Controller层还是负责暴露接口,Repository层还是负责数据存取,Service层负责核心业务逻辑。它跟基于贫血模型的传统开发模式的区别主要在Service层,在基于贫血模型的传统开发模式中,Service层包含Service类和BO类两部分,BO是贫血模型,只包含数据,不包含具体的业务逻辑。业务逻辑集中在Service类中。在基于充血模型的DDD开发模式中,Service层包含Service类和Domain类两部分。Domain就相当于贫血模型中的BO。不过,Domain与BO的区别在于它是基于充血模型开发的,既包含数据,也包含业务逻辑。而Service类变得非常单薄。总结一下的话就是,基于贫血模型的传统的开发模式,重Service轻BO;基于充血模型的DDD开发模式,轻Service重Domain。

为什么基于贫血模型的传统开发模式如此受欢迎?

第一点原因是,系统业务可能都比较简单,简单到就是基于SQL的CRUD操作,所以,不需要动脑子精心设计充血模型。

第二点原因是,充血模型的设计要比贫血模型更加有难度。因为充血模型是一种面向对象的编程风格。我们从一开始就要设计好针对数据要暴露哪些操作,定义哪些业务逻辑。

第三点原因是,思维已固化,转型有成本。

什么项目应该考虑使用基于充血模型的DDD开发模式?

基于充血模型的DDD开发模式,更适合业务复杂的系统开发。比如,包含各种利息计算模型、还款模型等复杂业务的金融系统。而对于业务比较简单的,就没有必要去设计充血模型,即便是设计了,其模型也会很单薄,没有什么意义。

充血模型的DDD开发模式的优势在哪里?

不夸张地讲,我们平时的开发,大部分都是SQL驱动的开发模式。我们接到一个后端接口的开发需求的时候,就去看接口需要的数据对应到数据库中,需要哪张表或者哪几张表,然后思考如何编写SQL语句来获取数据。之后就是定义Entity、BO、VO,然后模板式地往对应的Repository、Service、Controller类中添加代码。

业务逻辑包裹在一个大的SQL语句中,而Service层可以做的事情很少。SQL都是针对特定的业务功能编写的,复用性差。当要开发另一个业务功能的时候,只能重新写个满足新需求的SQL语句,这就可能导致各种长得差不多、区别很小的SQL语句满天飞。

如果我们在项目中,应用基于充血模型的DDD的开发模式,那对应的开发流程就完全不一样了。在这种开发模式下,我们需要事先理清楚所有的业务,定义领域模型所包含的属性和方法。领域模型相当于可复用的业务中间层。新功能需求的开发,都基于之前定义好的这些领域模型来完成。

我们知道,越复杂的系统,对代码的复用性、易维护性要求就越高,我们就越应该花更多的时间和精力在前期设计上。而基于充血模型的DDD开发模式,正好需要我们前期做大量的业务调研、领域模型设计,所以它更加适合这种复杂系统的开发。

2总结

面向对象是基本思想,封装,继承,多肽,抽象,接口是开发的基石,要想开发出高质量的代码,首先基本功要牢固,但是至于是否要采用DDD开发模式,可以自行根据业务斟酌。

分类:

后端

标签:

Java

作者介绍

UNREMITTINGLY
V1