【Java】《面向对象程序设计——Java语言》Castle代码修改整理
前言
最近閑來無事刷刷MOOC,找到以前看的浙大翁凱老師的《面向對象程序設計——Java語言》課程,重新過一遍仍覺受益頗深。
其中有一個Castle的例子,思路很Nice但代碼很爛,翁凱老師在后面幾章不斷地帶領觀看者修改這個代碼,那我也大概整理一下這部分的內容吧。
原版代碼
即使類的設計很糟糕,也還是有可能實現一個應用程序,使之運行并完成所需的工作。一個已完成的應用程序能夠運行,但并不能表明程序內部的結構是否良好。當維護程序員想要對一個已有的軟件做修改的時候,問題才會浮現出來。比如,程序員試圖糾正已有軟件的缺陷,或者為其增加一些新的功能。顯然,如果類的設計良好,這個任務就可能很輕松;而如果類的設計很差,那就會變得很困難,要牽扯大量的工作。在大的應用軟件中,這樣的情形在最初的實現中就會發生了。如果以不好的結構來實現軟件,那么后面的工作可能變得很復雜,整個程序可能根本無法完成,或者充滿缺陷,或者花費比實際需要多得多的時間才能完成。在現實中,一個公司通常要維護、擴展和銷售一個軟件很多年,很可能今天在商店買到的軟件,其最初的版本是在十多年前就開始了的。在這種情形下,任何軟件公司都不能忍受不良結構的代碼。既然很多不良設計的效果會在試圖調整或擴展軟件時明顯地展現出來,那么就應該以調整或擴展軟件來鑒別和發現這樣的不良設計。
這里將使用一個叫作城堡游戲的例子,這個例子很簡單,基本實現了一個基于字符的探險游戲。起初這個游戲并不十分強大,因為還沒全部完成,你可以運用你的想像力來設計和實現這個的游戲,讓它更有趣更好玩……
那么,首先,從下邊這個糟糕的代碼開始吧!
Room類
package castle;public class Room {public String description;public Room northExit;public Room southExit;public Room eastExit;public Room westExit;public Room(String description) {this.description = description;}public void setExits(Room north, Room east, Room south, Room west) {if(north != null)northExit = north;if(east != null)eastExit = east;if(south != null)southExit = south;if(west != null)westExit = west;}@Overridepublic String toString(){return description;} }Game類
import java.util.Scanner;public class Game {private Room currentRoom;public Game() {createRooms();}private void createRooms(){Room outside, lobby, pub, study, bedroom;// 制造房間outside = new Room("城堡外");lobby = new Room("大堂");pub = new Room("小酒吧");study = new Room("書房");bedroom = new Room("臥室");// 初始化房間的出口outside.setExits(null, lobby, study, pub);lobby.setExits(null, null, null, outside);pub.setExits(null, outside, null, null);study.setExits(outside, bedroom, null, null);bedroom.setExits(null, null, null, study);currentRoom = outside; // 從城堡門外開始}private void printWelcome() {System.out.println();System.out.println("歡迎來到城堡!");System.out.println("這是一個超級無聊的游戲。");System.out.println("如果需要幫助,請輸入 'help' 。");System.out.println();System.out.println("現在你在" + currentRoom);System.out.print("出口有:");if(currentRoom.northExit != null)System.out.print("north ");if(currentRoom.eastExit != null)System.out.print("east ");if(currentRoom.southExit != null)System.out.print("south ");if(currentRoom.westExit != null)System.out.print("west ");System.out.println();}// 以下為用戶命令private void printHelp() {System.out.print("迷路了嗎?你可以做的命令有:go bye help");System.out.println("如:\tgo east");}private void goRoom(String direction) {Room nextRoom = null;if(direction.equals("north")) {nextRoom = currentRoom.northExit;}if(direction.equals("east")) {nextRoom = currentRoom.eastExit;}if(direction.equals("south")) {nextRoom = currentRoom.southExit;}if(direction.equals("west")) {nextRoom = currentRoom.westExit;}if (nextRoom == null) {System.out.println("那里沒有門!");}else {currentRoom = nextRoom;System.out.println("你在" + currentRoom);System.out.print("出口有: ");if(currentRoom.northExit != null)System.out.print("north ");if(currentRoom.eastExit != null)System.out.print("east ");if(currentRoom.southExit != null)System.out.print("south ");if(currentRoom.westExit != null)System.out.print("west ");System.out.println();}}public static void main(String[] args) {Scanner in = new Scanner(System.in);Game game = new Game();game.printWelcome();while ( true ) {String line = in.nextLine();String[] words = line.split(" ");if ( words[0].equals("help") ) {game.printHelp();} else if (words[0].equals("go") ) {game.goRoom(words[1]);} else if ( words[0].equals("bye") ) {break;}}System.out.println("感謝您的光臨。再見!");in.close();}}熟悉代碼
說說代碼的問題
沒法說,太多了……看后面咋改吧。。。
消除代碼重復
程序中存在相似甚至相同的代碼塊,是非常低級的代碼質量問題。
代碼復制存在的問題是,如果需要修改一個副本,那么就必須同時修改所有其他的副本,否則就存在不一致的問題。這增加了維護程序員的工作量,而且存在造成錯誤的潛在危險。很可能發生的一種情況是,維護程序員看到一個副本被修改好了,就以為所有要修改的地方都已經改好了。因為沒有任何明顯跡象可以表明另外還有一份一樣的副本代碼存在,所以很可能會遺漏還沒被修改的地方。
我們從消除代碼復制開始。消除代碼復制的兩個基本手段,就是函數和父類。
代碼復制是不良設計的一種表現,而上面的代碼中并不少見,比如:
System.out.println("現在你在" + currentRoom); System.out.print("出口有:"); if(currentRoom.northExit != null)System.out.print("north "); if(currentRoom.eastExit != null)System.out.print("east "); if(currentRoom.southExit != null)System.out.print("south "); if(currentRoom.westExit != null)System.out.print("west "); System.out.println();處理方式就是單獨封裝成一個函數:
public void showPrompt() {System.out.println("現在你在" + currentRoom);System.out.print("出口有:");if(currentRoom.northExit != null)System.out.print("north ");if(currentRoom.eastExit != null)System.out.print("east ");if(currentRoom.southExit != null)System.out.print("south ");if(currentRoom.westExit != null)System.out.print("west ");System.out.println(); }注意可擴展性
可擴展性也是必須注意的事情,簡單的講就是“面對未來未知的變化,能夠以不變應萬變,以最小的代價和最小的影響來擁抱變化”(非官方的說法,個人覺得更容易理解)。
可運行的代碼≠良好的代碼,雖代碼做維護的時候更能看出代碼的質量(無論是自己維護還是交給他人維護)。
比如說上面的代碼,我們在Room類中用的是north、south、east、west,如果要加入up、down,則不僅要改Room,還要改Game,而且是大改,這樣影響程序的可擴展性、可維護性。
做好封裝
要評判某些設計比其他的設計優秀,就得定義一些在類的設計中重要的術語,以用來討論 設計的優劣。對于類的設計來說,有兩個核心術語:耦合和聚合。耦合這個詞指的是類和類之間的聯系。之前的章節中提到過,程序設計的目標是一系列通過定義明確的接口通信來協同工作的類。耦合度反映了這些類聯系的緊密度。我們努力要獲得低的耦合度,或者叫作松耦合(loose coupling)。
耦合度決定修改應用程序的容易程度。在一個緊耦合的結構中,對一個類的修改也會導致對其他一些類的修改。這是要努力避免的,否則,一點小小的改變就可能使整個應用程序發生改變。另外,要想找到所有需要修改的地方,并一一加以修改,卻是一件既困難又費時的事情。另一方面,在一個松耦合的系統中,常常可以修改一個類,但同時不會修改其他類,而且整個程序還可以正常運作。
聚合與程序中一個單獨的單元所承擔的任務的數量和種類相對應有關,它是針對類或方法這樣大小的程序單元而言的。理想情況下,一個代碼單元應該負責一個聚合的任務(也就是說,一個任務可以被看作是一個邏輯單元)。一個方法應該實現一個邏輯操作,而一個類應該代表一定類型的實體。聚合理論背后的要點是重用:如果一個方法或類是只負責一件定義明確的事情,那么就很有可能在另外不同的上下文環境中使用。遵循這個理論的一個額外的好處是,當程序某部分的代碼需要改變時,在某個代碼單元中很可能會找到所有需要改變的相關代碼段。
當然,以上面的代碼為例,其余細節暫且不論,把屬性設置成public,直接訪問,這完全不符合封裝的原則。
再細說一下這里的封裝問題:Room和Game都有大量代碼和出口相關,尤其是Room的四大屬性,這樣的設計大大加強了耦合度,不利于維護。
那是不是用“初學OOP經典大法”——[private]屬性+[public]getter方法?
比如說public Room northExit;改成:
其實真不是,這真的是很多人的一個誤區。
誠然,寫setter/getter比起public的屬性已經好了很多,但你細品,持有引用的類還是需要知道被引用的類的細節,二者還是緊緊耦合在一起的。
那我們需要什么呢?
我們需要這樣一個函數:
在此基礎上,我們也知道之前為了避免代碼重復而寫了這樣一個方法:
public void showPrompt() {System.out.println("現在你在" + currentRoom);System.out.print("出口有:");if(currentRoom.northExit != null)System.out.print("north ");if(currentRoom.eastExit != null)System.out.print("east ");if(currentRoom.southExit != null)System.out.print("south ");if(currentRoom.westExit != null)System.out.print("west ");System.out.println(); }我們為了降低耦合度,需要將其改為:
public void showPrompt() {System.out.println("現在你在" + currentRoom);System.out.print("出口有:");System.out.println(currentRoom.getExitDesc());System.out.println(); }接著看,之前由于去重代碼,goRoom()已經是這個樣子了:
private void goRoom(String direction) {Room nextRoom = null;if(direction.equals("north")) {nextRoom = currentRoom.northExit;}if(direction.equals("east")) {nextRoom = currentRoom.eastExit;}if(direction.equals("south")) {nextRoom = currentRoom.southExit;}if(direction.equals("west")) {nextRoom = currentRoom.westExit;}if (nextRoom == null) {System.out.println("那里沒有門!");} else {currentRoom = nextRoom;showPrompt();} }但其實這里還是重度耦合,我們要將這個事交還給Room來做:
public Room getExit(String direction) {Room nextRoom = null;if(direction.equals("north")) {nextRoom = this.northExit;}if(direction.equals("east")) {nextRoom = this.eastExit;}if(direction.equals("south")) {nextRoom = this.southExit;}if(direction.equals("west")) {nextRoom = this.westExit;}return nextRoom; }而goRoom()則變成了:
private void goRoom(String direction) {Room nextRoom = currentRoom.getExit(direction);if (nextRoom == null) {System.out.println("那里沒有門!");} else {currentRoom = nextRoom;showPrompt();} }至此,Room和Game之間的耦合度大大降低了,至少沒了直接的屬性調用,Game不必完全知道Room的細節了。
使用接口增強可擴展性
上面的修改完成之后還有哪些不足呢?
上面的代碼修改針對Room類實現的新方法,雖說把方向的細節正是隱藏在Room內部了,今后方向如何實現也與外部無關了,但還是一種“硬編碼”的方式。
Game與Room松耦合,但Room本身還是“硬編碼”,一旦方向變化,則需要大量的重寫代碼,可擴展性還是不好。
那怎么處理呢?
答案是:使用集合容器,比如HashMap。
修改方法就是刪去所有的屬性,轉而換成一個Map屬性:
private Map<String , Room> exits = new HashMap<>();這么改可還行,問題是之前的全被推翻了,那就重寫唄!
比如說這個方法:
肯定是不能要了,那就重寫一個getExit():
public void setExit(String dir, Room room) {exits.put(dir, room); }同樣地,之前有一個修改后加進去的方法:
public String getExitDesc() {StringBuilder sb = new StringBuilder();if (this.northExit != null) {sb.append("north ");}if (this.southExit != null) {sb.append("south ");}if (this.eastExit != null) {sb.append("east ");}if (this.westExit != null) {sb.append("west ");}return sb.toString(); }也是涉及方向細節,要改:
public String getExitDesc() {StringBuilder sb = new StringBuilder();for (Entry entry : exists.entrySet()) {sb.append(entry.getKey()).append(' ');}return sb.toString(); }上一次重寫的getExit()也要改:
public Room getExit(String direction) {return exits.get(direction); }需要說明的是,永遠不要認為這樣一行代碼的方法沒有存在的意義,因為這最關鍵的是表示一個接口,提供這種服務,如果以后不這么寫了呢?對吧,大家都是聰明人,不必多言。
Game類也受到點“波及”:
private void createRooms() {Room outside, lobby, pub, study, bedroom;// 制造房間outside = new Room("城堡外");lobby = new Room("大堂");pub = new Room("小酒吧");study = new Room("書房");bedroom = new Room("臥室");// 初始化房間的出口outside.setExits(null, lobby, study, pub);lobby.setExits(null, null, null, outside);pub.setExits(null, outside, null, null);study.setExits(outside, bedroom, null, null);bedroom.setExits(null, null, null, study);currentRoom = outside; // 從城堡門外開始 }這里要改,但很簡單,反正不過是初始化而已,調用getExit()改一改就行了。
而此時我們發現其他部分不需要改,這就是松耦合的好處啊!
框架+數據
從程序中識別出框架和數據,以代碼實現框架,將部分功能以數據的方式加載,這樣能在很大程度上實現可擴展性。
這個框架不是我們說的“Spring”、"MyBatis"那種。我們不想“if-else-”泛濫,就可以使用Handler,再使用Map來保存命令和Handler之間的關系,進而破除“if-else-”硬編碼。
我們使用Map是一個很秀的想法,但是函數不是對象,而Map的value必須是對象,所以我們才用的Handler。
Handler被定義為一個類,這樣會很好:
public class Handler {public void doCmd(String message){//TODO something} }而Game需要一個Map:
private Map<String, Handler> handlers = new HashMap<>();那么在初始化Game的時候,在構造器中直接使用put()初始化必要的命令:
public Game() {handlers.put("go", new HandlerGo());handlers.put("help", new HandlerHelp());handlers.put("bye", new HandlerBye());createRooms(); }還需要一個play()方法,把main()的死循環扔進去:
public void play() {while (true) {String line = in.nextLine();String[] words = line.split(" ");if (words[0].equals("help")) {game.printHelp();} else if (words[0].equals("go")) {game.goRoom(words[1]);} else if (words[0].equals("bye")) {break;}} }這個方法需要改一改:
public void play() {while (true) {String line = in.nextLine();String[] words = line.split(" ");Handler handler = handlers.get(words[0]);if (handler != null) {handler.doCmd(words[1]);}} }這個沒改好,因為沒考慮退出的問題,但你要是考慮退出的問題,就需要if特判,就又繞回去了,所以需要再考慮:
if (handler != null) {handler.doCmd(words[1]);if (handler.isBye()) {break;} }對應的,Handler也要完善一下:
public class Handler {protected Game game;public Handler(Game game) {this.game = game;}public void doCmd(String message){}public boolean isBye() {return false;} }之前我們也發現了HandlerGo、HandlerHelp、HandlerBye還沒出現,自然是都要extends類Handler,把板子做出來:
public class HandlerGo extends Handler {public HandlerGo(Game game) {super(game);}@Overridepublic void doCmd(String message) {game.goRoom(message);} } public class HandlerHelp extends Handler {public HandlerHelp(Game game) {super(game);}@Overridepublic void doCmd(String message) {System.out.print("迷路了嗎?你可以做的命令有:go bye help");System.out.println("如:\tgo east");} } public class HandlerBye extends Handler {public HandlerBye(Game game) {super(game);}@Overridepublic boolean isBye() {return true;} }而goRoom()要改成public:
public void goRoom(String direction) {Room nextRoom = currentRoom.getExit(direction);if (nextRoom == null) {System.out.println("那里沒有門!");} else {currentRoom = nextRoom;showPrompt();} }再就是,構造Game對象的時候要傳this:
public Game() {handlers.put("go", new HandlerGo(this));handlers.put("help", new HandlerHelp(this));handlers.put("bye", new HandlerBye(this));createRooms(); }這樣就完成了基本的修改,只需微調即可完成系統修改。
這里直接使用了普通類來表示Handler,其實也可以考慮接口與抽象類,這里點到為止。
匿名內部類讓代碼更優雅
在評論區看到下面的代碼(僅限于Game類的構造器),寫的很不錯,還做了擴展:
public Game() {// 匿名類handlers.put("go", new Handler() {@Overridepublic void doCmd(String word) {goRoom(word);}});handlers.put("bye", new Handler() {@Overridepublic boolean isBye() {return true;}});handlers.put("help", new Handler() {@Overridepublic void doCmd(String word) {System.out.print("迷路了嗎?你可以做的命令有:");System.out.print(getHandlers());System.out.println(".");System.out.println("如: go east");}});handlers.put("gorandom", new Handler() {@Overridepublic void doCmd(String word) {goRandom();}});rooms = createRooms(); }是不是更秀了呢?哈哈,根本不再需要每一個具體的Handler類,也不需要this傳參,Nice!
總結
本文總結了一下如何修改給出的Castle代碼,使之基本做到高內聚、低耦合和具備可擴展性,也說明了很多編程的注意事項。
原版代碼和課程講評來自浙江大學翁凱老師,感興趣的讀者可以去查看相關的資源!
總結
以上是生活随笔為你收集整理的【Java】《面向对象程序设计——Java语言》Castle代码修改整理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【C语言】三种方式不使用分号输出Hell
- 下一篇: 【Java】Java数据库访问体系重点总