前言:首先要思考一个问题,什么是好代码?好代码和差代码的区别是什么?
有的人写的代码一眼看上去,就感觉很好。有的人写的代码一看上去就感觉很烂。即便如此,对于计算机来说,不管好的代码还是烂的代码,计算机可能都会执行的结果都是一样的没有区别。
所以我在这里大胆的定义,好的代码应该是易于人理解的,烂的代码是只有计算机能读的懂。
下面用一个案例来感受一下把一段平庸的代码,重构成相对好的代码。
案例:
这个案例是一个闹钟service,它会早上、中午、晚上三个时间定时播放指定的音乐,比如中午休息时,会播放60秒,音量持续1分钟;晚上的时候会播放1个小时,音量是15。
看完这个案例,你也要思考一下,如果是你,你会如何重构优化它?
package service.alarmClock;
import javax.sound.sampled.*;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
public class AlarmClock {
/**
* 闹钟提醒-功能介绍:
* 每当早上、中午、晚上时,分别有对应类型的播放音乐,分别对应不同的音量、不同的曲子、不同的播放时长
* 1、如果是周末就不要响
* 2、如果是早上7点,播放起床闹钟, 持续1小时,音量3
* 3、如果是中午12点,播放中午休息闹钟,持续1分钟,音量80
* 4、如果是晚上12点,播放睡觉催眠曲, 持续30分钟,音量15
*/
public void run() throws IOException, UnsupportedAudioFileException, LineUnavailableException, InterruptedException {
String soundFileSrc = ""; //定义闹钟铃声的声音文件地址
int continueSeconds = 0; //闹钟持续的时间(单位是秒)
int soundVolume = 0; //闹钟的音量
while (true) {
// 每秒循环一次
Thread.sleep(1000);
Calendar calendar = Calendar.getInstance();
SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
// 获取当前时分秒格式的时间
String currentTime = formatter.format(calendar.getTime());
// 周末不提醒,获取今天是星期几,西方周日是第1天,周六是第7天
int week = calendar.get(Calendar.DAY_OF_WEEK);
if (week == 1 || week == 7) {
continue;
}
switch (currentTime) {
case "07:00:00":
soundFileSrc = "./wake.mp3";
continueSeconds = 60 * 60;
soundVolume = 3; //微弱的音量
break;
case "12:00:00":
soundFileSrc = "./rest.wav";
continueSeconds = 60;
soundVolume = 80;
break;
case "00:00:00":
soundFileSrc = "./sleep.mp3";
continueSeconds = 60 * 30;
soundVolume = 15;
break;
}
System.out.println("当前时间是: " + currentTime);
if (!soundFileSrc.equals("")) {
AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(new File(soundFileSrc));
Clip clip = AudioSystem.getClip();
clip.open(audioInputStream);
FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.VOLUME);
gainControl.setValue(soundVolume);
clip.start();
Thread.sleep(continueSeconds * 1000);
soundFileSrc = "";
clip.stop();
}
}
}
}
开始重构:
1、对当前时间进行抽象:
上面的案例,当我看到这段代码的时候,让我心理感到特别的不舒服,我的内心迫不及待的想立即先对获取当前时间currentTime和判断是不是周末week这两个地方下手改造一下,把这两个抽出来看看。
这里可能有的人想不开,没有重构之前,Calendar calendar = Calendar.getInstance(); 这一句代码一次执行的结果,获取时间和获取周几两个地方可以复用,但是抽象出两个方法以后,两个地方都要各自执行一次,这不就影响性能了吗。这种思想要批评一下,试想一下,如果所有的地方都要按照这种小的复用而影响大局,使得大局上看着乱的话,当代码行数非常多的时候就会越来越乱,就会牺牲更多的时间和精力去梳理代码逻辑,得不偿失。好代码的标准一定是清晰易读,耦合度低,每个方法、模块单独是一个功能互不影响。像这种对性能的影响微乎其微,如果真的有很大的性能影响的话,再另当别论。
public String currentTime() {
Calendar calendar = Calendar.getInstance();
SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
// 获取当前时分秒格式的时间
return formatter.format(calendar.getTime());
}
// 是否是周末
public Boolean isWeekend() {
Calendar calendar = Calendar.getInstance();
int week = calendar.get(Calendar.DAY_OF_WEEK);
if (week == 1 || week == 7) {
return true;
}
return false;
}
这时候核心层代码变成了这样:
public void run() {
String soundFileSrc = ""; //定义闹钟铃声的声音文件地址
int continueSeconds = 0; //闹钟持续的时间(单位是秒)
int soundVolume = 0; //闹钟的音量
while (true) {
Thread.sleep(1000);
String currentTime = this.currentTime();
if (this.isWeekend()) {
continue;
}
switch (currentTime) {
case "07:00:00":
soundFileSrc = "./wake.mp3";
continueSeconds = 60 * 60;
soundVolume = 3; //微弱的音量
break;
case "12:00:00":
soundFileSrc = "./rest.wav";
continueSeconds = 60;
soundVolume = 80;
break;
case "00:00:00":
soundFileSrc = "./sleep.mp3";
continueSeconds = 60 * 30;
soundVolume = 15;
break;
}
System.out.println("当前时间是: " + currentTime);
if (!soundFileSrc.equals("")) {
AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(new File(soundFileSrc));
Clip clip = AudioSystem.getClip();
clip.open(audioInputStream);
FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.VOLUME);
gainControl.setValue(soundVolume);
clip.start();
Thread.sleep(continueSeconds * 1000);
soundFileSrc = "";
clip.stop();
}
}
}
2、对音乐播放功能进行抽象
看到播放音乐那一坨,也让人感到头疼,但那个地方的定义是执行音乐进行播放这样一个功能,所以也应该进行抽象出来。由于播放一个音乐后,需要把音乐路径初始化,所以顺便也把音乐路径、播放时间、播放音量作为对象的属性提取出来,方便播放后进行初始化。
// 闹钟对象的属性
private String soundFileSrc;
private int continueSeconds;
private int soundVolume;
播放功能抽象
public void playSound() {
if (!this.soundFileSrc.equals("")) {
AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(new File(this.soundFileSrc));
Clip clip = AudioSystem.getClip();
clip.open(audioInputStream);
FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.VOLUME);
gainControl.setValue(this.soundVolume);
this.initSoundFileSrc();
clip.start();
Thread.sleep(this.continueSeconds * 1000);
clip.stop();
}
}
public void initSoundFileSrc() {
this.soundFileSrc = "";
}
再来看一看全景:
package service.alarmClock;
import javax.sound.sampled.*;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
public class AlarmClock {
/**
* 闹钟提醒-功能介绍:
* 每当早上、中午、晚上时,分别有对应类型的播放音乐,分别对应不同的音量、不同的曲子、不同的播放时长
* 1、如果是周末就不要响
* 2、如果是早上7点,播放起床闹钟, 持续1小时,音量3
* 3、如果是中午12点,播放中午休息闹钟,持续1分钟,音量80
* 4、如果是晚上12点,播放睡觉催眠曲, 持续30分钟,音量15
*/
private String soundFileSrc;
private int continueSeconds;
private int soundVolume;
public void run() {
while (true) {
// 每秒循环一次
Thread.sleep(1000);
String currentTime = this.currentTime();
if (this.isWeekend()) {
continue;
}
switch (currentTime) {
case "07:00:00":
this.soundFileSrc = "./wake.mp3";
this.continueSeconds = 60 * 60;
this.soundVolume = 3; //微弱的音量
break;
case "12:00:00":
this.soundFileSrc = "./rest.wav";
this.continueSeconds = 60;
this.soundVolume = 80;
break;
case "00:00:00":
this.soundFileSrc = "./sleep.mp3";
this.continueSeconds = 60 * 30;
this.soundVolume = 15;
break;
}
System.out.println("当前时间是: " + currentTime);
this.playSound();
}
}
public String currentTime() {
Calendar calendar = Calendar.getInstance();
SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
// 获取当前时分秒格式的时间
return formatter.format(calendar.getTime());
}
// 是否是周末
public Boolean isWeekend() {
Calendar calendar = Calendar.getInstance();
int week = calendar.get(Calendar.DAY_OF_WEEK);
if (week == 1 || week == 7) {
return true;
}
return false;
}
public void playSound() {
if (!this.soundFileSrc.equals("")) {
AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(new File(this.soundFileSrc));
Clip clip = AudioSystem.getClip();
clip.open(audioInputStream);
FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.VOLUME);
gainControl.setValue(this.soundVolume);
this.initSoundFileSrc();
clip.start();
Thread.sleep(this.continueSeconds * 1000);
clip.stop();
}
}
public void initSoundFileSrc() {
this.soundFileSrc = "";
}
}
3、对switch部分进行抽象。
在看到switch的时候,思考一下,这个功能是在做什么,经过自习阅读,可以发现,这里是为了获取根据当前的时间应该播放哪一个音频,播放多少时间,播放的音量有多大等等。
3.1、复杂的情况的重构:这时候需要考虑,如果把这个播放看做一个播放器对象,用多态的形式给播放器对象下面增加三个子对象也是可以的。但很明显仅仅这三个属性还不足以费劲巴拉的去抽象出来三个对象,所以暂不考虑了。
3.2、简单的情况的重构: 与上面相反,如果switch只是简单的功能,按照简单的方法进行重构。
3.2的实现:
直接看代码,代码中每一步都有注释
这样有一个好处,就是当我们对代码进行增加或删除修改等等操作时,不用再去switch一个一个的去看逻辑,扩展性维护性更强更清晰。
// 第一步,定义一个结构体,用于存储音频的三个属性
public class SoundEntity {
public SoundEntity(String soundFileSrc, int continueSeconds, int soundVolume) {
this.soundFileSrc = soundFileSrc;
this.continueSeconds = continueSeconds;
this.soundVolume = soundVolume;
}
public String soundFileSrc;
public int continueSeconds;
public int soundVolume;
}
// 第二步,把每个时间对应的音频信息塞进一个map中
Map<String, SoundEntity> map = new HashMap<String, SoundEntity>();
map.put("07:00:00", new SoundEntity("./wake.mp3", 60 * 60, 3));
map.put("12:00:00", new SoundEntity("./rest.wav", 60, 80));
map.put("00:00:00", new SoundEntity("./sleep.mp3", 60 * 30, 15));
// 第三步,在读秒的while循环中,把时间当做key去map中比对,从而拿到对应的音频信息
if (!map.containsKey(currentTime)) {
continue;
}
this.soundFileSrc = map.get("currentTime").soundFileSrc;
this.continueSeconds = map.get("currentTime").continueSeconds;
this.soundVolume = map.get("currentTime").soundVolume;
再看一下3.2版修改的代码全貌:
package service.alarmClock;
import javax.sound.sampled.*;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
public class AlarmClock {
/**
* 闹钟提醒-功能介绍:
* 每当早上、中午、晚上时,分别有对应类型的播放音乐,分别对应不同的音量、不同的曲子、不同的播放时长
* 1、如果是周末就不要响
* 2、如果是早上7点,播放起床闹钟, 持续1小时,音量3
* 3、如果是中午12点,播放中午休息闹钟,持续1分钟,音量80
* 4、如果是晚上12点,播放睡觉催眠曲, 持续30分钟,音量15
*/
private String soundFileSrc;
private int continueSeconds;
private int soundVolume;
public void run() throws IOException, UnsupportedAudioFileException, LineUnavailableException, InterruptedException {
Map<String, SoundEntity> map = new HashMap<String, SoundEntity>();
map.put("07:00:00", new SoundEntity("./wake.mp3", 60 * 60, 3));
map.put("12:00:00", new SoundEntity("./rest.wav", 60, 80));
map.put("00:00:00", new SoundEntity("./sleep.mp3", 60 * 30, 15));
while (true) {
// 每秒循环一次
Thread.sleep(1000);
String currentTime = this.currentTime();
System.out.println("当前时间是: " + currentTime);
if (this.isWeekend()) {
continue;
}
if (!map.containsKey(currentTime)) {
continue;
}
this.soundFileSrc = map.get("currentTime").soundFileSrc;
this.continueSeconds = map.get("currentTime").continueSeconds;
this.soundVolume = map.get("currentTime").soundVolume;
this.playSound();
}
}
public String currentTime() {
Calendar calendar = Calendar.getInstance();
SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
// 获取当前时分秒格式的时间
return formatter.format(calendar.getTime());
}
// 是否是周末
public Boolean isWeekend() {
Calendar calendar = Calendar.getInstance();
int week = calendar.get(Calendar.DAY_OF_WEEK);
if (week == 1 || week == 7) {
return true;
}
return false;
}
public void playSound() throws IOException, UnsupportedAudioFileException, LineUnavailableException, InterruptedException {
if (!this.soundFileSrc.equals("")) {
AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(new File(this.soundFileSrc));
Clip clip = AudioSystem.getClip();
clip.open(audioInputStream);
FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.VOLUME);
gainControl.setValue(this.soundVolume);
this.initSoundFileSrc();
clip.start();
Thread.sleep(this.continueSeconds * 1000);
clip.stop();
}
}
public void initSoundFileSrc() {
this.soundFileSrc = "";
}
public class SoundEntity {
public SoundEntity(String soundFileSrc, int continueSeconds, int soundVolume) {
this.soundFileSrc = soundFileSrc;
this.continueSeconds = continueSeconds;
this.soundVolume = soundVolume;
}
public String soundFileSrc;
public int continueSeconds;
public int soundVolume;
}
}
本文完。
用一个我自己编造的小案例,来重现了一次重构的思路,要记住好代码和怀代码的区别是是否容易被人类理解,是否容易扩展,是否容易维护等等。
当我们对臃肿的代码进行抽象的时候,一定要以"它的意义"为单位,每一个被抽象出来的方法都应该有它单独的、明确的意义,意义也可以被再细分,方法中抽象方法也是没有问题的。