0%

重构代码,从过程化到面向对象

许多人刚开始学习编程时,写的代码都是过程化的代码(procedural code),但是过程化的代码有许多缺点,这些缺点能利用面向对象的原则来解决。本文分享如何将过程化的代码重构为面向对象的代码,用 Java 语言举例。

什么是过程化的代码

没有了解过面向对象,或者各种编程范式,或者对面向对象理解不深入的老铁,写的代码往往都是过程化的。就算这些老铁知道类和对象的概念,但往往出现滥用类的情况,因此会误认为自己在面向对象,其实写的仍然是过程化的代码。具体表现在一个类往往包含了很多不相关的函数,例如在 Java 中,一个 Main 类里有很多函数,而这些函数你中有我,我中有你,互相调用来调用去,还常常传的是同一套参数,这就是过程化的代码的表现。

什么是面向对象的代码

首先看几个概念:封装(encapsulation),抽象(abstraction),耦合(coupling),继承(inheritance),多态(polymorphism):

封装

将状态与改变这些状态的方法封装为一个类,这可以解决过程化的代码多个函数互相调用,传一组参数的问题。即将这样的函数与他们需要的参数分别对应到类中的方法与域(fileds)上去。

抽象

隐藏类的实现细节,不暴露不必要的接口。

耦合

类与类之间的依赖关系,依赖关系越强,耦合程度越高,一般类的接口越多,这个类越容易与其他类高耦合,低耦合是我们的目标。

随着各个类的分工越来越明确,他们的接口也会越来越少,因为分工明确了,不需要通过一些接口来让其他类完成本该由它自己完成的工作,这降低了耦合程度,提高了抽象程度。

继承

利用继承可以实现代码的重用。

多态

利用多态可以让一个对象有多种多种表现形式,可以帮助我们构建可扩展的应用。

如何重构

首先进行封装,即构造类,如何知道我们需要什么类呢?

这取决于应用程序的目标或目的或责任。

如在蟹老板的汉堡店,我们有海绵宝宝负责做秘制小汉堡 🍔,章鱼哥 🐙 负责收银,蟹老板负责管理,每一个人都有自己明确被定义的责任,这些人就像不同的对象(Object)一样,它们之间共同协作来提供服务,就像各个类或对象协作完成应用程序的功能。

当你正确的给各个类分工后,给你的奖励是,这个类的接口会更少,更多的 member 会变成 private。

一个类只干一件事

Each Class Should Have a Single Responsibility.

比如让章鱼哥 🐙 既要收银也要做汉堡,这是不好的。

在这里,初学者容易犯的错误是,没有做到 “Presentation and calculation should be seperated”。

打印一个对象的相关信息与处理这个对象的属性应该分为两个类,因为他们需要各司其职,共同协作来提供服务。你并不知道负责 calculation 的类产出的数据是要打印在终端上还是在 UI 上显示,所以必须要分开,做到 “Each class has a single responsibility”。

static Filed

若一个方法可能经常被调用,而方法里还 new 了对象,那么多次的调用该方法就会多次 new,若这是一个适用于整个类的概念,可以考虑是否能把这个对象重构为静态域(static field),即 extract local variable to field,避免多次在 heap 上 new 同一个对象。

但我们要克制住自己用 static member 的欲望,只在该 static 的地方 static(即多个对象都只有这一个相同资源时),否则会造成很多问题,比如增加耦合的程度,降低抽象的程度,这都是不好的。

重复的表达式

对于在类中不同方法里重复出现的表达式,可以考虑两种方法重构,改成 field 或提取成新的 get 方法,根据不同情况选择。

避免太深的继承层次

Avoid deep inheritance hierarchies.

继承链太长会造成很多问题,举个例子,你的代码中有这样一个继承链:

            graph TD
            A[Entity] --> B[User]
A[Entity] --> C[Post]
B --> D[Admin]
B --> E[Visitor]
D --> F[NormalAdmin]
D --> G[AdvancedAdmin]
          

构造出这样的继承链的理由是:UserPost 都有 idAdminVisitor 都是 UserAdmin 又可以分为 NormalAdminAdvancedAdmin

这样继承确实理论上没有问题,但是太过复杂、太长的继承链会给维护和扩展代码带来问题,因为这些类之间都是高耦合的,例如:

  • 若修改 Entity 的实现,如修改构造函数的参数,那么直接或间接继承与 Entity 的类都需要修改构造函数的实现。
  • 当你想要给一个基类添加 filed 或 method 时,你却发现他们不应该被现有的子类继承,这会造成不必要的麻烦。

所以我们需要给这个继承链做一些修改,比如虽然 UserPost 都有 field id,但他们可以各自有一个 id field,虽然这带来了代码冗余,但这两个类不再继承于同一个父类了,之后他们可以各自进行扩展而不影响对方了;还可以删除 NormalAdminAdvancedAdmin 这两个 Admin 的子类,通过给 Admin 添加一个 field isAdvanced 来区分即可,不要滥用继承。

借助 IntelliJ IDEA 重构

多使用 IntelliJ IDEA 的重构功能,这样重构能保证代码依然能够执行,避免出现不安全的重构,自己改又要改半天,浪费时间。

但对于 IntelliJ IDEA 无法实现的一些重构,在自己手工改完后,记得用下 IDEA 提供的 Find Usages 功能来看下哪里用到了你刚刚重构的这个哥们,并做出必要的修改,避免出现不安全的重构。

Bonus! 使用 IntelliJ IDEA 重构时的快捷键

快捷键能让我们的生活更简单,下面介绍一些在重构代码时常用的 IntelliJ IDEA 快捷键,针对 Windows 操作系统。

To Shortcut
Find Usage cursor on -> menu or right click -> u
Generate Constructor alt + ins (generate code) -> choose constructor
Refactor cursor on -> menu or right click -> r
Move method to another class Refactor -> Move Member -> Specify “To” -> Specify “Visibility” -> Refactor
Create Getter & Setter cursor on -> alt + enter -> choose “Encapsulate field” -> do some setting
Extract Method ctrl + alt + m
奥里给,老铁们,干了!