現在的ATM只能存款,一進入程式就是存款介面,而且目前也沒有新的部分需要測試
所以我們開始著手開發新功能
首先是加入選擇介面層
System.out.println("請選擇功能,\n[1] 存款");
int choose = sc.nextInt();
這裡你會發現
sc居然不能用?事實上,這非常合理,因為
main裡可沒有
sc的參考,所以讓我們把它移出來
public class Main {
static Scanner sc = new Scanner(System.in);
//...
ps.我不會解釋什麼是static,這是JAVA的基礎,請自己去找說明
但光是這樣沒有用,我們必須讓deposit方法由選擇結果決定是否要執行,所以
if (choose == 1) {
deposit();
}
這裡我們用1代表執行deposit,可以看出我們打算如何組織選擇介面了吧
我們決議再來先實作提款功能,對應到2
else if (choose == 2) {
withdraw();
}
再一次,編譯器又說出錯了,因為withdraw並不存在,所以我們再來要實作一個
private static void withdraw() {
System.out.println("請輸入提款金額:");
int amountOfWithdraw = sc.nextInt();
TransactionLogger.AddWithdrawLog(amountOfWithdraw);
}
還有它調用的紀錄器函式
public static void AddWithdrawLog(int amountOfWithdraw) {
TransactionLogger.save.add(-amountOfWithdraw);
}
可以看出來這跟存款相關函式近乎沒有區別
不過我改了幾個小地方,以區辨兩者的使用者介面還有一些不同的行為(像是一個紀錄正,另一個則是負值)
@Test
public void AddWithdrawLog_should_add_log_into_save() {
TransactionLogger.AddWithdrawLog(1);
List<Integer> expected = new ArrayList<>();
expected.add(-1);
Assert.assertEquals(expected, TransactionLogger.save);
}
這是測試,看起來也沒什麼不同,但結果可能大出你的預期,它失敗了
但其實是合理的,我們用static修飾紀錄List,這使得它連結在class本身身上,所以上一個測試所造成的副作用仍在,因此我們也要跟上如此狀況,所以讓我們修改一下測試程式
public class TransactionLoggerTest {
static List<Integer> expected = new ArrayList<>();
// ...
方法非常簡單,把expected也變成static就好了,原因你可以自己慢慢深入了解,我不會在這裡解釋,因為這與主旨關聯不大
ps. 這裡的測試事實上是先寫的,也就是說我採用了TDD
現在測試都通過了,讓我們前往下一站
我們需要區分用戶,否則大家都共用一個帳戶,這樣的銀行大概沒有人要來存錢(雖然我能保證來申請的客戶可不會少)
所以我先建立一個登入介面
System.out.println("請輸入帳號");
String userID = sc.nextLine();
System.out.println("請輸入密碼");
String userPassword = sc.nextLine();
現在這個登入介面一點用也沒有,所以我們要提供處理輸入的邏輯程式,能夠根據輸入看出是否有這個使用者,並進入他的介面
為了看出是否有這個使用者,我們必須儲存使用者的資料,讓我們先用簡單的方式達成
static Map<String, String> users = new HashMap();
static {
users.put("1", "1");
}
我建立了一個雜湊表來存放帳號與它所對應的密碼,表示使用者集群
讓我們看看它如何被使用
if (userPassword.equals(users.get(userID))) {
// ...
}
中間是選擇功能的部分,為什麼是用equals方法請自己去查JAVA compare string
我們馬上就找到了一個有趣的問題,我們偉大的(?)
TransactionLogger出了點差錯,一開始我們忽略了有數個使用者的情況,這使得我們必須面對它只為一人服務的窘境,讓我們解決這個問題
首先我們得讓
TransactionLogger能擁有數個實體,所以static必須拿掉
public class TransactionLogger {
// 紀錄存款交易的列表
public List<Integer> save = new ArrayList<>();
public void AddDepositLog(int amountOfDeposit) {
save.add(amountOfDeposit);
}
public void AddWithdrawLog(int amountOfWithdraw) {
save.add(-amountOfWithdraw);
}
}
這是它現在的樣子,任何改動都必須通過測試,毫不易外的,我們失敗了,因為如今的
TransactionLogger必須建立實體才能使用,讓我們解決它
static TransactionLogger testLogger = new TransactionLogger();
接著把參考都改為指向實體,測試現在通過了,舉手歡呼吧!
接著棘手的問題來了,即便測試剛剛證明
TransactionLogger可以使用,設計並無錯誤,然而主程式依然未按預期動作,因為我們並沒有連結帳號與其紀錄器的手段,我們只有一個紀錄器實體,所有帳號還是共用它,我們發現實作一個帳號型別是必要的了
public class User {
TransactionLogger log = new TransactionLogger();
String ID;
String password;
User(String ID, String password) {
this.ID = ID;
this.password = password;
}
}
因為我們並不想改動原本的使用方式,所以保留了原先的作法
static List<User> Users = new ArrayList<>();
static Map<String, String> users = new HashMap<>();
static {
Users.add(new User("1", "1"));
for (User u: Users) {
users.put(u.ID, u.password);
}
}
現在的問題在於,存款與提款函式,都沒辦法存取到User實體的紀錄器,這清楚的顯示了他們宣告錯了地方,因為他們依賴著User的紀錄器運作,所以我們把他們移過去,而且要改變他們的控制域
void deposit() {
System.out.println("請輸入存款金額:");
int amountOfDeposit = Main.sc.nextInt();
log.AddDepositLog(amountOfDeposit);
}
void withdraw() {
System.out.println("請輸入提款金額:");
int amountOfWithdraw = Main.sc.nextInt();
log.AddWithdrawLog(amountOfWithdraw);
}
接著我們遇到的是,我們沒辦法說明,我們要使用哪一個User實體,因為我們沒辦法指定要參考到哪一個
Users.get(Users.indexOf(new User(userID, userPassword))).deposit();
我們用上超級複雜的寫法,試圖連結到我們想要的那個帳號身上
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1
at java.util.ArrayList.elementData(ArrayList.java:418)
at java.util.ArrayList.get(ArrayList.java:431)
結果我們拿到這個,一個錯誤,喔!又來了,問題在於,new出來的兩個物件即便值是一樣的,也不等於對方,因此我們找不到這個帳號,天啊!真讓人絕望,真的嗎?
for (User u:Users) {
if (u.ID.equals(userID) && u.password.equals(userPassword)) {
System.out.println("請選擇功能,\n[1] 存款\n[2] 提款");
int choose = sc.nextInt();
if (choose == 1) {
u.deposit();
} else if (choose == 2) {
u.withdraw();
}
break;
}
}
我們把事情變成這樣,執行符合我們的猜想,現在我們不滿意的是choose造成的重複性,為什麼我們同一件事得寫這麼多次?
因此我們得想出怎麼不重複程式碼,包進另一層邏輯讓程式碼外觀不重覆是一種作法,但我並不打算那麼做,我想真正的解決重複問題
@IfChoose(value = 1)
void deposit() {
// ...
}
@IfChoose(value = 2)
void withdraw() {
// ...
}
所以我採用了annotation來解決問題
IfChoose是什麼?
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface IfChoose {
int value();
}
這就是註記IfChoose的定義,上面兩樣註記定義的意思是
- @Retention(RetentionPolicy.RUNTIME)
It means that the annotation can be accessed via reflection at runtime. If you do not set this directive, the annotation will not be preserved at runtime, and thus not available via reflection.
- @Target(ElementType.TYPE)
It means that the annotation can only be used ontop of types (classes and interfaces typically). You can also specify METHOD or FIELD, or you can leave the target out alltogether so the annotation can be used for both classes, methods and fields.
我限制只能給Method使用
當我們宣告註記在方法上時
for (Method m : u.getClass().getDeclaredMethods()) {
IfChoose c = m.getAnnotation(IfChoose.class);
if (choose == c.value()) {
m.invoke(u, null);
break;
}
}
就能像這樣使用(這是一個選擇器)
我還是不會去解釋這是怎麼運作的,因為重點是如何利用annotation減少程式重複
為什麼要這樣?因為接下來帳號還有好幾種功能要實現,我相信沒有人會喜歡
if (choose == 1) {
// ...
} else if (choose == 2) {
// ...
} else if (choose == 3) {
// ...
} else if (choose == 4) {
// ...
} else if (choose == 5) {
// ...
} // ...
勝過簡單的定義
@IfChoose(value = 1)
void deposit() {
// ...
}
@IfChoose(value = 2)
void withdraw() {
// ...
}
@IfChoose(value = 3)
void unknowYet() {
// ...
}
// ...
一方面上面的選擇器不需要重新實作,沒錯,一個字都不用改
而另一方面,也是最重要的事就是,你並不需要擔心對應錯誤的問題,例如提款與存款的代碼寫反,那可是大麻煩,因為程式沒錯卻是錯誤的(即語意錯誤)
這篇就在這裡告一段落,下一篇我們要嘗試?算了,隨便啦 w
NEXT: ATM 04
沒有留言:
張貼留言