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