我們知道因為編程語言的限制,歷史遺留下來的系統總是有很多的毛病,不夠面向對象,尤其是很多系統濫用if else.我曾經見過一個項目,大家基本上就是寫一個方法,然後在裡面if else套if esle得嵌套了好幾層,難看就不必說了,這種代碼根本就沒法維護。
今天我就使用從實際項目中提煉出來的例子來講解一下如何將這類代碼變得更加面向對象 - 重構成模式並且添加測試代碼,
先來看一個丑陋的類:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->package de.jingge.refactoring;
public class SystemManager {
public static final int LOGGEDIN = 0;
public static final int LOGGEDOUT = 1;
public static final int IDLE = 2;
int state;
public void login() {
// call service#login()
updateState(LOGGEDIN);
}
public void logout() {
// call service#logout()
updateState(LOGGEDOUT);
}
public void idle() {
// call some other services
updateState(IDLE);
}
public void updateState(int state) {
if (state == LOGGEDIN) {
// do something after logging in is successful,
// for example: show welcome dialog, open the last edit document, etc.
} else if (state == LOGGEDOUT) {
// do something after logging out is successful,
// for example: free used resource, dispose GUI components, etc.
} else if (state == IDLE) {
// do something after the user is idle,
// for example: save the application state temporarily, lock the application, etc.
} else {
throw new IllegalArgumentException("unknown state");
}
this.state = state;
}
}
這裡我們展示了一個 SystemManager,它負責處理用戶在系統中的狀態:登入(logged in),登出(logged out),以及空閒(idle)。從代碼中可以看到,這個類用了int來定義狀態並且因此導致了updatteState()方法裡面出現大量if else.從目前看來這些if else是無法避免的,應為這個類需要針對不同的狀態作出反應。隨著狀態的增加,if else的數量也會繼續增加。這個解決方案顯然很差。
那麼怎麼樣才能讓這個類更加地面向對象呢?
在處理面向對象之前,我們首先要編寫一個測試類,這也是處理這類歷史遺留下來代碼所必需做的第一步,只有在測試代碼的保護下,我們才能放心大膽地進行重構。
初步的測試代碼如下:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->package de.jingge.refactoring;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;
public class SystemManagerTest {
private static SystemManager manager;
@BeforeClass
public static void setUpClass() throws Exception {
manager = new SystemManager();
// add some service mock objects
}
@AfterClass
public static void tearDownClass() throws Exception {
}
@Test
public void login() {
manager.login();
assertEquals(manager.state, SystemManager.LOGGEDIN);
}
@Test
public void logout() {
manager.logout();
assertEquals(manager.state, SystemManager.LOGGEDOUT);
}
@Test
public void idle() {
manager.idle();
assertEquals(manager.state, SystemManager.IDLE);
}
}
運行測試代碼->通過。
使用Enum替換int常量
這一步比較簡單,先創建一個enum類:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->package de.jingge.refactoring;
public enum SystemState {
LOGGEDIN,
LOGGEDOUT,
IDLE;
}
然後開始重構SystemManager, 使用SystemState代替SystemManager裡的int狀態:
1. 添加 import static de.jingge.refactoring.SystemState.*;
2. 刪除所有的integer常量
3. 將變量state的類型改為SystemState.
代碼如下:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->package de.jingge.refactoring;
import static de.jingge.refactoring.SystemState.*;
public class SystemManager {
SystemState state;
public void login() {
// call service#login()
updateState(LOGGEDIN);
}
public void logout() {
// call service#logout()
updateState(LOGGEDOUT);
}
public void idle() {
// call some other services
updateState(IDLE);
}
public void updateState(SystemState state) {
if (state == LOGGEDIN) {
// do something after logging in is successful,
// for example: show welcome dialog, open the last edit document, etc.
} else if (state == LOGGEDOUT) {
// do something after logging out is successful,
// for example: free used resource, dispose GUI components, etc.
} else if (state == IDLE) {
// do something after the user is idle,
// for example: save the application state temporarily, lock the application, etc.
} else {
throw new IllegalArgumentException("unknown state");
}
this.state = state;
}
}
然後重構測試類:
1. 添加import static de.jingge.refactoring.SystemState.*;
2. 刪除所有常量前引用的SystemManager.
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->package de.jingge.refactoring;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;
import static de.jingge.refactoring.SystemState.*;
public class SystemManagerTest {
private static SystemManager manager;
@BeforeClass
public static void setUpClass() throws Exception {
manager = new SystemManager();
// add some service mock objects
}
@AfterClass
public static void tearDownClass() throws Exception {
}
@Test
public void login() {
manager.login();
assertEquals(manager.state, LOGGEDIN);
}
@Test
public void logout() {
manager.logout();
assertEquals(manager.state, LOGGEDOUT);
}
@Test
public void idle() {
manager.idle();
assertEquals(manager.state, IDLE);
}
}
運行這個測試類->通過
移除if else
首先仔細觀察一下updateState()方法,我們會發現,導致該方法內存在大量if else的原因是它的參數僅僅是一個enum.由於enum本身並不含有任何邏輯代碼,因此導致處理enum的方法需要使用if else來分析enum然後調用相應的邏輯。明白了這個道理之後,重構的方向就明了了。簡單的說,我們需要要將方法參數由enum替換成一個更加強壯的抽象類,每一個繼承該類的子類將具體負責處理一個enum實例,之後再將updateState()方法中相應的邏輯代碼轉移到這些子類中。這樣處理之後,令人討厭的if else就會消失了。
我們將這個替換enum的抽象類命名為SystemStatePerformer,代碼如下:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->package de.jingge.refactoring;
import Java.awt.Image;
public abstract class SystemStatePerformer {
private final SystemState state;
private Image image;
public SystemStatePerformer(SystemState state, Image image) {
this.state = state;
this.image = image;
}
public SystemState getState() {
return state;
}
public Image getImage() {
return image;
}
public abstract void perform();
}
從代碼中可以看出,每一個performer都含義有一個SystemState,這個SystemState屬性,將只能通過構建器映射方式射入一個performer的對象實例。換句話說SystemState只是一個只讀屬性,而且每一個performer實體類都只負責處理一個enum的實例(下面馬上會解釋如何實現的)。這裡使用的Image作為一個例子,它表示用戶的每一個狀態都可以使用一個圖標來表示。performer()方法將負責處理具體的邏輯。這個 SystemStatePerformer的實體子類可以引用任何類型的對象,然後在perform()方法裡面進行調用。
下一步就是編寫SystemStatePerformer的實體子類。我首先想到的是為每一個enum實例編寫一個實際的子類,理論上來說是沒問題的,但是這樣做必須編寫一大堆的子類,不便於管理。所以我決定使用Factory + annonymous classes來構建具體的實體子類,讓Factory來管理所有的實體子類。 代碼如下:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->package de.jingge.refactoring;
import static de.jingge.refactoring.SystemState.*;
import Java.awt.Image;
import Java.awt.image.BufferedImage;
public class SystemStatePerformerFactory {
private static SystemStatePerformerFactory INSTANCE = new SystemStatePerformerFactory();
private SystemStatePerformerFactory() {
}
public static SystemStatePerformer getSystemStatePerformer(SystemState state) {
switch (state) {
case LOGGEDIN:
return createLoggedInPerformer();
case IDLE:
return createIdlePerformer();
case LOGGEDOUT:
return createLoggedOutPerformer();
default:
throw new IllegalAccessError("Unkonw status");
}
}
private static SystemStatePerformer createLoggedInPerformer() {
return new SystemStatePerformer(LOGGEDIN, getImage("loggedin.gif")) {
@Override
public void perform() {
// do something after logging in is successful,
// for example: show welcome dialog, open the last edit document, etc.
}
};
}
private static SystemStatePerformer createLoggedOutPerformer() {
return new SystemStatePerformer(LOGGEDOUT, getImage("loggedout.gif")) {
@Override
public void perform() {
// do something after logging out is successful,
// for example: free used resource, dispose GUI components, etc. }
}
};
}
private static SystemStatePerformer createIdlePerformer() {
return new SystemStatePerformer(IDLE, getImage("idle.gif")) {
@Override
public void perform() {
// do something after the user is idle,
// for example: save the application state temporarily, lock the application, etc.
}
};
}
private static Image getImage(String string) {
return new BufferedImage(10, 10, BufferedImage.TYPE_4BYTE_ABGR);
}
}
從代碼中可以看到,針對每一個enum狀態都有一個創建performer的方法,該方法返回一個匿名類。邏輯代碼將會被轉移至個匿名類的 perform()方法之內。整個Factory只有一個公開的方法:getSystemStatePerformer(SystemState),SystemManager可以調用這個方法來獲得相應的 Performer實例。
在這篇文章中,我希望專屬於if else的問題。對於其他設計方面的問題,我采取的態度是能省略就省略。實際開發中,還有有很多問題需要處理,例如,使用static方法會導致系統的可測試性下降,在實際開發中應該盡量避免,解決這類問題的方法之一是使用DI框架,例如Google Guice.
OK, 到目前為止,所有的邏輯代碼已經從SystemManager重構到了SystemStatePerformer.下一步應該繼續重構SystemManager, 將SystemState替換為performer:
1, 使用IDE的重構功能,將變量SystemState改為SystemStatePerformer
2. 在updateState()方法中調用SystemStatePerformerFactory
3. 在測試代碼裡面,調用manager.statePerformer.getState()
重構後的代碼如下:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->package de.jingge.refactoring;
import static de.jingge.refactoring.SystemState.*;
public class SystemManager {
SystemStatePerformer statePerformer;
public void login() {
// call service#login()
updateState(LOGGEDIN);
}
public void logout() {
// call service#logout()
updateState(LOGGEDOUT);
}
public void idle() {
// call some other services
updateState(IDLE);
}
public void updateState(SystemState state) {
this.statePerformer = SystemStatePerformerFactory.getInstance()
getSystemStatePerformer(state);
statePerformer.perform();
}
}
可以看到if else已經消失了。
測試代碼也要做相應修改:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->package de.jingge.refactoring;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;
import static de.jingge.refactoring.SystemState.*;
public class SystemManagerTest {
private static SystemManager manager;
@BeforeClass
public static void setUpClass() throws Exception {
manager = new SystemManager();
// add some service mock objects
}
@AfterClass
public static void tearDownClass() throws Exception {
}
@Test
public void login() {
manager.login();
assertEquals(manager.statePerformer.getState(), LOGGEDIN);
}
@Test
public void logout() {
manager.logout();
assertEquals(manager.statePerformer.getState(), LOGGEDOUT);
}
@Test
public void idle() {
manager.idle();
assertEquals(manager.statePerformer.getState(), IDLE);
}
}
到這裡重構已經差不多完成了,代碼已經更加面向對象了。這裡還有一個小問題,在factory裡面還有一個switch,這個和if else其實是沒有本質區別的,也就是說if else並沒有被完全移除掉。
那麼如何能夠徹底把這個switch也移除掉呢?很簡單,我們只需要在getSystemStatePerformer()方法被調用之前先創建所有 performer匿名類的實例,然後在該方法被調用時直接返回對應的實力。
上面談到如何能夠徹底把這個switch也移除掉呢?很簡單,我們只需要在getSystemStatePerformer()方法被調用之前先創建所有 performer匿名類的實例,然後在該方法被調用時直接返回對應的實力。 如何具體實現呢? 用Map, 請看代碼:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->package de.jingge.refactoring;
import static de.jingge.refactoring.SystemState.*;
import Java.awt.Image;
import Java.awt.image.BufferedImage;
import Java.lang.reflect.Method;
import Java.util.Collections;
import Java.util.HashMap;
import Java.util.Map;
/**
*
* @author [email protected]
*/
public class SystemStatePerformerFactory {
private static SystemStatePerformerFactory INSTANCE = new SystemStatePerformerFactory();
private Map
private SystemStatePerformerFactory() {
}
public static SystemStatePerformerFactory getInstance() {
return INSTANCE;
}
private synchronized Map
throws Exception {
if (performers == null) {
performers = new HashMap
// call all @FactoryMethod using reflection
for (Method m : getClass().getDeclaredMethods()) {
if (m.getAnnotation(FactoryMethod.class) != null) {
SystemStatePerformer p = (SystemStatePerformer) m.invoke(
this, new Object[]{});
performers.put(p.getState(), p);
}
}
// make it readonly
performers = Collections.unmodifiableMap(performers);
}
return performers;
}
public SystemStatePerformer getSystemStatePerformer(SystemState state) throws Exception{
return getPerformers().get(state);
}
@FactoryMethod
private SystemStatePerformer createLoggedInPerformer() {
return new SystemStatePerformer(LOGGEDIN, getImage("loggedin.gif")) {
@Override
public void perform() {
// do something after logging in is successful,
// for example: show welcome dialog, open the last edit document, etc.
}
};
}
@FactoryMethod
private SystemStatePerformer createLoggedOutPerformer() {
return new SystemStatePerformer(LOGGEDOUT, getImage("loggedout.gif")) {
@Override
public void perform() {
// do something after logging out is successful,
// for example: free used resource, dispose GUI components, etc. }
}
};
}
@FactoryMethod
private SystemStatePerformer createIdlePerformer() {
return new SystemStatePerformer(IDLE, getImage("idle.gif")) {
@Override
public void perform() {
// do something after the user is idle,
// for example: save the application state temporarily, lock the application, etc.
}
};
}
private Image getImage(String string) {
return new BufferedImage(10, 10, BufferedImage.TYPE_4BYTE_ABGR);
}
}
從代碼中可以看出,當getPerformers()方法被第一次調用時,我們會為每一個performer匿名類創建一個實例,並且將它們納入Map的管理之中,以後每次調用的時候,直接從Map裡面提取對應某個狀態的performer就可以了, switch可以捨棄了。 @FactoryMethod這個注釋是我自己寫的,使用它主要是為了避免每次新增加一個create***Performer()方法後,都必須修改 getSystemStatePerformer()。
@FactoryMethod的代碼如下:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->package de.jingge.refactoring;
import Java.lang.annotation.ElementType;
import Java.lang.annotation.Retention;
import Java.lang.annotation.RetentionPolicy;
import Java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface FactoryMethod {
}
到這裡整個重構已經結束了, 我們已經將if else, switch完全從代碼裡剔除了。
讀過Refactoring to Patterns這本書的朋友可能會覺得,這裡所作的一些和書中第七章最後一節Replace Conditional Dispatcher with Command完全一樣。 Well,第一眼看上去確實很像,但是看完我寫的所有代碼後,再仔細想一想,兩者還是有區別的(Refactoring to Patterns這本書寫的非常好,對此書,我可以說是愛不釋手,還曾經寫過一篇書評。事實上,我這篇文章正式基於這本書的):
1. Factory + annonymous類而不是每一個狀態一個具體的實體類。
這樣處理問題, 類的數量大大減少,類關聯的復雜程度也大大減少,維護起來很方便。
2. performer並不單單是一個command,它擁有狀態,並且可以處理更多的邏輯。