鄭士康 國立台灣大學 電機工程學系/電信工程研究所/ 資訊網路與多媒體研究所 物件導向程式設計 鄭士康 國立台灣大學 電機工程學系/電信工程研究所/ 資訊網路與多媒體研究所 1
綱要 物件導向思維 CRC卡會議 UML類別圖 SRP:單一責任原理 物件導向二十一點模擬程式0.0.0版 物件導向二十一點模擬程式0.0.1版 物件導向二十一點模擬程式0.0.2版
綱要 物件導向二十一點模擬程式0.0.3版 物件導向二十一點模擬程式0.0.4版
綱要 物件導向思維 CRC卡會議 UML類別圖 SRP:單一責任原理 物件導向二十一點模擬程式0.0.0版 物件導向二十一點模擬程式0.0.1版 物件導向二十一點模擬程式0.0.2版
物件導向程式設計 利用逐步細分法的函式導向程式設計曾經風行一時 物件和類別的觀念及程式語言普及之後,有必要採用不同的方式思考,以便更直截了當的寫出物件和類別的程式,並且充份發揮物件導向封裝、繼承、多型的特點
程式規劃 版本規劃 (80-20定律、要事第一、工程方法) 測試規劃 良好程式設計習慣 測試驅動方法開發 使用研發日誌,與夥伴合作,漸增式與回合式發展程式,維持可持續的發展步調,維護原始碼、註解、重要程式文件 測試驅動方法開發
物件導向程式設計之引入 版本規劃及測試規劃完成,要開始以漸增及回合方式,進行各個版本的進一步設計之時
物件導向思維 什麼是程式要完成的功能? 需要那些物件/類別才能完成程式功能? 物件/類別需要那些行為才能合作完成程式功能? 例: Mission Impossible, RPG 8
綱要 物件導向思維 CRC卡會議 UML類別圖 SRP:單一責任原理 物件導向二十一點模擬程式0.0.0版 物件導向二十一點模擬程式0.0.1版 物件導向二十一點模擬程式0.0.2版
CRC卡 Class (類別)、Responsibilities (負責功能)、Collaborators (合作類別) 由Kent Beck與Ward Cunningham發明,用以教導其同儕程式設計師物件導向的觀念 利用CRC卡會議可使初學者更容易了解物件與類別的意義
參考書籍 N. M. Wilkinson, Using CRC Cards: An Informal Approach to Object-Oriented Development, New York: SIGS Books, 1995. 11
練習 實際演練二十一點遊戲模擬原型的CRC卡會議 12
CRC卡會議 定義問題 產生類別 演練場景 整理卡片 13
產生類別 腦力激盪 過濾類別 初步建立繼承關係 描述類別 寫出類別名稱及定義 初步寫出負責功能(Responsibilities) 初步寫出資料成員(Attributes) 14
產生類別 腦力激盪 過濾物件/類別 建立繼承關係 描述類別
物件/類別之識別 流程敘述中的名詞、代名詞、名詞片語可能需要轉換為物件/類別(CRC卡) 類別之間的關係可以畫UML類別圖表示 主程式及類別的方法可以用虛擬碼進一步描述 16
資料成員與 類別之區分 資料成員通常表示類別的狀態變數,而非單獨之類別或型別 撲克牌之花色 借書資料之到期日期 但資料成員如可操作,且可用藍圖描述,可能可以構成一個類別,而資料成員變成一個物件 日期 17
必也正名乎 物件、類別、狀態、功能之名稱力求精準 使用一致命名規範 物件、類別、狀態:名詞片語 功能:動詞片語 條件判斷:形容詞片語、判斷句 多字連用,除第一字另外考慮外,各字開頭字母大寫 物件、狀態:第一個字小寫起頭 humanPlayer 類別、功能:第一個字大寫起頭 HumanPlayer 18
演練場景 列出場景清單 逐一演練場景 寫下類別負責功能、合作類別、資料成員 先演練簡單場景 複雜場景可能包括其他簡單場景 例外場景押後演練 19
整理卡片 刪除沒用到的類別、方法成員、資料成員 發現繼承結構 繪出類別圖(Class Diagram) 20
CRC卡正面 類別名稱 父類別 負責功能 合作類別 21
CRC卡背面 描述 成員變數 22
類別Card之CRC卡正面 類別名稱: Card 合作類別 負責功能 知道花色 知道點數 Deck Hand 23
類別Card之CRC卡背面 描述:代表用到的52張撲克牌 成員變數 花色(suit) 點數(rank) 24
類別Deck之CRC卡正面 類別名稱: Deck 合作類別 負責功能 知道剩下的牌 發一張牌 Game Card 25
類別Deck之CRC卡背面 描述:代表牌疊 成員變數 牌(cards) 26
類別 HumanPlayer之CRC卡正面 合作類別 負責功能 存入一張牌 知道玩家狀態 決定是否再要一張牌 傳回手牌總點數 Game Hand 27
類別HumanPlayer之CRC卡背面 描述:代表人類玩家 成員變數 手牌(hand) 玩家狀態(status) 28
類別Hand之 CRC卡正面 類別名稱: Hand 合作類別 負責功能 Card 知道手中的牌 知道手牌總點數 HumanPlayer ComputerPlayer 29
類別Hand之 CRC卡背面 描述:代表人類玩家或電腦玩家手中的牌 . 成員變數 牌(cards) 30
類別ComputerPlayer之 CRC卡正面 合作類別 負責功能 存入一張牌 知道玩家狀態 決定是否再要一張牌 傳回手牌總點數 Game Hand 31
類別 CRC卡背面 描述:代表電腦玩家. 成員變數 手牌(hand) 玩家狀態(status) 32
類別Game之CRC卡正面 類別名稱: Game 合作類別 負責功能 Deck 控制遊戲流程 比較電腦與人類玩家點數 HumanPlayer 大小 Deck HumanPlayer ComputerPlayer 33
類別Game之CRC卡背面 描述:代表整個二十一點遊戲. 成員變數 牌疊(deck) 人類玩家(humanPlayer) 電腦玩家(computerPlayer) 34
類別關係圖 HumanPlayer Game Deck Hand ComputerPlayer Card
綱要 物件導向思維 CRC卡會議 UML類別圖 SRP:單一責任原理 物件導向二十一點模擬程式0.0.0版 物件導向二十一點模擬程式0.0.1版 物件導向二十一點模擬程式0.0.2版
UML Unified Modeling Language 由Booch、Rumbaugh、Jacobson三人所創的物件導向軟體圖形表示標準 做為軟體系統的設計文件,使系統維護者容易了解修改程式,另一方面則是軟體工程的工具 背後有一套軟體工程架構,但只適合大規模軟體開發 小範圍程式設計,以較簡易具彈性的Agile Software Development比較實際好用 初學者只須了解幾種簡單的圖形,用來了解或記錄程式架構
UML繪製工具 Rational Rose JUDE-Community 可由UML圖形直接產生程式框架,並能保持程式與UML圖形的一致性,但價格相當昂貴 JUDE-Community 免費UML圖形繪製軟體 自2009年年底,JUDE-Community軟體已不再提供下載,改提供較進階的astah-Community http://jude.change-vision.com/jude-web/index.html 38
類別UML符號 39
UML 類別圖( Class Diagram ) 40
不同類別間傳送資料 類別的成員變數通常設為private,不能任意擷取 有人因此乾脆將所有成員變數宣告為public,方便取得,卻失去了使用類別封裝的原意 這種狀況,通常可以畫出類別圖,觀察可能的資料傳送路徑,再於相關類別使用適當的暫存變數或成員變數和成員函式,進行接力傳送資料
練習 以大富翁遊戲模擬為目標,規畫版本,進行CRC卡會議,產生CRC卡,繪出UML類別圖 42
astah
用astah Community畫類別圖 File>New Diagram>Class Diagram 左下角>Base標籤>輸入類別名稱 左下角>Attribute標籤>Add按鈕>輸入資料狀態名稱、型別、可見度 左下角>Operation標籤>Add按鈕>輸入功能函式名稱、傳回值型別、可見度
用astah Community畫類別圖 工具列>Association符號旁箭頭>選擇所需Association符號 起點類別圖形按左鍵,轉折點按左鍵 終點類別圖形連按左鍵兩次 按起點、終點類別圖形調整位置 或按Shift鍵點選要對齊的圖形>Alignment>選擇調整方式 Edit>Copy to Clipboard>BMP, PNG File>Save As …
綱要 物件導向思維 CRC卡會議 UML類別圖 SRP:單一責任原理 物件導向二十一點模擬程式0.0.0版 物件導向二十一點模擬程式0.0.1版 物件導向二十一點模擬程式0.0.2版
單一責任原理 (SRP: Single-Responsibility Principle) A class should have only one reason to change 若某類別擔負多個責任,某一責任相關的改變會妨礙該類別其他責任的達成 何以不讓Game類別進行某一玩家是否為21點的計算? *Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices, Pearson Education, 2003 47
一個違反SRP的例子 *Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices, Pearson Education, 2003 48
符合SRP的設計 *Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices, Pearson Education, 2003 49
綱要 物件導向思維 CRC卡會議 UML類別圖 SRP:單一責任原理 物件導向二十一點模擬程式0.0.0版 物件導向二十一點模擬程式0.0.1版 物件導向二十一點模擬程式0.0.2版
BlackJack_0_0_0.Program using System; using System.Diagnostics; namespace BlackJack_0_0_0 { class Program static void Main(string[] args) Debug.Assert( BlackJackTest.Scenario_1_OK()); }
混充類別BlackJack_0_0_0.BlackJackTest using System; namespace BlackJack_0_0_0 { class BlackJackTest public static bool Scenario_1_OK() return true; }
綱要 物件導向思維 CRC卡會議 UML類別圖 SRP:單一責任原理 物件導向二十一點模擬程式0.0.0版 物件導向二十一點模擬程式0.0.1版 物件導向二十一點模擬程式0.0.2版
二十一點遊戲模擬v0.1流程 產生牌疊 電腦(莊家)向玩家(一人)及本身派發一張明牌 電腦向玩家及本身派發一張明牌 莊家詢問玩家是否加牌, 直至玩家不加牌或報到 莊家如不足 17點便需加牌直至超過或等於 17點 對未有爆煲或報到的玩家, 比點數大小, 大者勝, 如莊家爆煲, 玩家勝
二十一點遊戲模擬原型之測試規畫: 場景1 玩家 莊家 ♠A ♥J ♦10 勝
UML 類別圖( Class Diagram ) 56
Scenario 1: Sequence Diagram
BlackJack_0_0_1.BlackJackTest片段 public static bool Scenario_1_OK() { Deck deck = new Deck(); HumanPlayer player = new HumanPlayer(); ComputerPlayer computer = new ComputerPlayer(); player.SaveACard(deck.DealACard()); computer.SaveACard(deck.DealACard()); return (player.IsBlackJack()); }
BlackJack_0_0_1.Deck 片段 private Card[] cards; private int top; public Deck() { cards = new Card[3]; cards[0] = new Card(Suit.SPADE, 1); cards[1] = new Card(Suit.HEART, 11); cards[2] = new Card(Suit.DIAMOND, 10); top = 0; } public Card DealACard() { return cards[top++];
BlackJack_0_0_1.Card 片段 public enum Suit { CLUB = 0, DIAMOND = 1, HEART = 2, SPADE = 3 } public struct Card { public Suit suit; public int rank; public Card(Suit suit, int rank) { this.suit = suit; this.rank = rank;
BlackJack_0_0_1.HumanPlayer 片段(1/3) private Card[] hand = new Card[2]; private int nCards = 0; public HumanPlayer() {} public void SaveACard(Card card) { hand[nCards++] = card; }
BlackJack_0_0_1.HumanPlayer片段 (2/3) public bool IsBlackJack() { int point1 = Points( hand[0].rank ); int point2 = Points( hand[1].rank ); bool isBlackJack = (point1 + point2 == 21); if (!isBlackJack && point1 == 1) { point1 = 11; isBlackJack = (point1 + point2 == 21); }
BlackJack_0_0_1.HumanPlayer片段 (3/3) if (!isBlackJack && point2 == 1) { point2 = 11; isBlackJack = (point1 + point2 == 21); } return isBlackJack; private int Points(int rank) { int points = rank; if (rank > 10) points = 10; return points;
BlackJack_0_0_1.ComputerPlayer 片段 Card[] hand = new Card[1]; int nCards = 0; public ComputerPlayer() { } public void SaveACard(Card card) { hand[nCards++] = card; }
綱要 物件導向思維 CRC卡會議 UML類別圖 SRP:單一責任原理 物件導向二十一點模擬程式0.0.0版 物件導向二十一點模擬程式0.0.1版 物件導向二十一點模擬程式0.0.2版
UML 類別圖
BlackJack_0_0_2.BlackJackTest. Scenario_1_OK 片段 Card[] cards = { new Card(Suit.SPADE, 1), new Card(Suit.HEART, 11), new Card(Suit.DIAMOND, 10) }; Deck deck = new Deck(cards); HumanPlayer player = new HumanPlayer(); ComputerPlayer computer = new ComputerPlayer(); player.SaveACard(deck.DealACard()); computer.SaveACard(deck.DealACard()) return (player.GetStatus() == Status.BLACK_JACK && computer.GetStatus() == Status.PASS);
BlackJack_0_0_2.Deck 片段 private Card[] cards; private int top = 0; public Deck(Card[] card) { int nCards = card.Length; this.cards = new Card[nCards]; int i; for (i = 0; i < nCards; ++i) { this.cards[i] = card[i]; } public Card DealACard() { return cards[top++];
BlackJack_0_0_2.Status public enum Status { PASS = 0, BLACK_JACK = 1, BURST = 2 }
BlackJack_0_0_2.HumanPlayer片段 (1/6) private Card[] hand = new Card[3]; private int nCards; private Status status; private int totalPoints; public HumanPlayer() { nCards = 0; } public void SaveACard(Card card) { hand[nCards++] = card; SetStatus();
BlackJack_0_0_2.HumanPlayer片段 (2/6) public Status GetStatus() { return status; } public int GetTotalPoints() { return totalPoints; private int Points(int rank) { int points = rank; if (rank > 10) points = 10; return points;
BlackJack_0_0_2.HumanPlayer片段 (3/6) private void SetStatus() { int[] point = new int[nCards]; int i; int sum = 0; for (i = 0; i < nCards; ++i) { point[i] = Points(hand[i].rank); sum += point[i]; } status = JudgeStatus(sum); totalPoints = sum; if (status != Status.PASS) return; bool isWithAce = false;
BlackJack_0_0_2.HumanPlayer片段 (4/6) for (i = 0; i < nCards; ++i) { if (point[i] == 1) { isWithAce = true; break; } if (isWithAce) { sum += 10; if (sum == 21) { status = Status.BLACK_JACK;
BlackJack_0_0_2.HumanPlayer片段 (5/6) if (sum <= 21) { totalPoints = sum; }
BlackJack_0_0_2.HumanPlayer片段 (6/6) private Status JudgeStatus(int sum) { Status status; if (sum == 21) { status = Status.BLACK_JACK; } else if (sum > 21) { status = Status.BURST; } else { status = Status.PASS; } return status;
綱要 物件導向二十一點模擬程式0.0.3版 物件導向二十一點模擬程式0.0.4版
UML 類別圖
BlackJack_0_0_3.Program.Main 片段 Debug.Assert(BlackJackTest.DeckRandom_OK(), "Deck random mode test failed"); Debug.Assert(BlackJackTest.Game_OK(), "Game test failed"); Console.WriteLine(); Console.WriteLine("21點遊戲開始"); Game game = new Game(); game.Run();
BlackJack_0_0_3.BlackJackTest. DeckRandom_OK 片段 (1/3) Deck deck = new Deck(123); Card[] cards = new Card[52]; int i; for (i = 0; i < 52; ++i) { cards[i] = deck.DealACard(); } int[] nSuit = new int[4]; int s; for (s = 0; s < 4; ++s) { nSuit[s] = 0; int[] nRank = new int[13];
BlackJack_0_0_3.BlackJackTest. DeckRandom_OK 片段 (2/3) int r; for (r = 0; r < 13; ++r) { nRank[r] = 0; } for (i = 0; i < 52; ++i) { switch (cards[i].suit) { case Suit.CLUB: nSuit[0]++; break; . . . . . . nRank[cards[i].rank - 1]++;
BlackJack_0_0_3.BlackJackTest. DeckRandom_OK 片段 (3/3) bool suit_OK = true; for (s = 0; s < 4; s++) { if (nSuit[s] != 13) { suit_OK = false; break; } bool rank_OK = true; for (r = 0; r < 13; r++) { if (nRank[r] != 4) { rank_OK = false; return suit_OK && rank_OK;
BlackJack_0_0_3.BlackJackTest. Game_OK public static bool Game_OK() { Console.WriteLine("Game 測試"); Game game = new Game(); game.Run(123); return true; }
BlackJack_0_0_3.Game 片段 private HumanPlayer player = new HumanPlayer(); private ComputerPlayer computer = new ComputerPlayer(); public void Run() { deck = new Deck(); Play(); } public void Run(int seed) { deck = new Deck(seed);
BlackJack_0_0_3.Game.Play 片段 (1/3) player.SaveACard(deck.DealACard()); player.Dump(); computer.SaveACard(deck.DealACard()); computer.Dump(); if (IsBlackJackOrBurst()) return; while (player.GetStatus() == Status.PASS && player.WantOneMoreCard() && deck.HasMoreCard()) {
BlackJack_0_0_3.Game.Play 片段 (2/3) player.SaveACard(deck.DealACard()); player.Dump(); } if (IsBlackJackOrBurst()) return; while ( computer.GetStatus() == Status.PASS && computer.WantOneMoreCard() && deck.HasMoreCard()) { computer.SaveACard(deck.DealACard()); computer.Dump(); Console.WriteLine("比大小分勝負");
BlackJack_0_0_3.Game.Play 片段 (3/3) if (IsBlackJackOrBurst()) return; if (computer.GetTotalPoints() >= player.GetTotalPoints()) { Console.WriteLine("電腦勝"); } else { Console.WriteLine("玩家勝"); }
BlackJack_0_0_3.Game. IsBlackJackOrBurst 片段 (1/2) bool isBlackJack = false; if (player.GetStatus() == Status.BLACK_JACK) { isBlackJack = true; Console.WriteLine("玩家 BlackJack!!!"); } if(computer.GetStatus()== Status.BLACK_JACK) Console.WriteLine("電腦 BlackJack!!!"); bool isBurst = false;
BlackJack_0_0_3.Game. IsBlackJackOrBurst 片段 (2/2) if (player.GetStatus() == Status.BURST) { isBurst = true; Console.WriteLine("玩家爆!!!"); } if (computer.GetStatus() == Status.BURST) Console.WriteLine("電腦爆!!!"); return (isBlackJack || isBurst);
BlackJack_0_0_3.Deck 片段 private Card[] cards = new Card[52]; private int top = 0; public Deck() { Random rand = new Random(); PrepareDeck(rand); } public Deck(int seed) { Random rand = new Random(seed);
BlackJack_0_0_3.Deck. PrepareDeck 片段 (1/2) bool[] used = new bool[52]; for (i = 0; i < 52; ++i) { used[i] = false; } pos = rand.Next() % 52; while (used[pos]) { ++pos; pos = pos % 52; s = pos / 13;
BlackJack_0_0_3.Deck. PrepareDeck 片段 (2/2) switch (s) { case 0: cards[i].suit = Suit.CLUB; break; . . . . . . default: } cards[i].rank = pos % 13 + 1; used[pos] = true;
BlackJack_0_0_3.Deck. DealACard & HasMoreCard public Card DealACard() { return cards[top++]; } public bool HasMoreCard() { return (top < 52);
BlackJack_0_0_3.HumanPlayer. WantOneMoreCard public bool WantOneMoreCard() { Console.Write("要再一張牌嗎? (y/n) "); string answer = Console.ReadLine(); return (answer == "Y" || answer == "y"); }
BlackJack_0_0_3.HumanPlayer. Dump 片段 Console.Write("玩家牌: "); for (i = 0; i < nCards; ++i) { hand[i].Dump(); Console.Write("\t"); if ((i + 1) % 5 == 0) Console.WriteLine(); } Console.WriteLine(); Console.WriteLine("玩家總點數: " + totalPoints);
BlackJack_0_0_3.ComputerPlayer. WantOneMoreCard public bool WantOneMoreCard() { return (totalPoints < 17); }
綱要 物件導向二十一點模擬程式0.0.3版 物件導向二十一點模擬程式0.0.4版
重構(Refactoring) 不好的味道(Bad program flavor) 重構 重複出現的程式碼 不妥的名稱 擷取方法 重新命名 建立新類別 使用繼承
BlackJack_0_0_3重構: BlackJack_0_0_4 HumanPlayer與ComputerPlayer中有關判斷status的函式SetStaus、 JudgeStatus、Points完全相同 建立新類別StatusChecker,函式SetStaus, JudgeStatus, Points改成此一類別的靜態函式 SetStatus改名DetermineStatusAndTotalPoints,並修改參數 將列舉型別Status的宣告移到StatusChecker.cs
BlackJack_0_0_4.Status public enum Status { PASS = 0, BLACK_JACK = 1, BURST = 2 }
BlackJack_0_0_4.StatusChecker. DetermineStatusAndTotalPoints (1/2) public static void DetermineStatusAndTotalPoints( Card[] hand, int nCards, out Status status, out int totalPoints) { int[] point = new int[nCards]; int sum = 0; for (i = 0; i < nCards; ++i) { point[i] = Points(hand[i].rank); sum += point[i]; } status = JudgeStatus(sum); totalPoints = sum; if (status != Status.PASS) return; bool isWithAce = false;
BlackJack_0_0_4.StatusChecker. DetermineStatusAndTotalPoints (2/2) for (i = 0; i < nCards; ++i) { if (point[i] == 1) { isWithAce = true; break; } if (isWithAce) { sum += 10; if (sum == 21) { status = Status.BLACK_JACK; } if (sum <= 21) { totalPoints = sum; }
BlackJack_0_0_4.HumanPlayer. SaveACard public void SaveACard(Card card) { hand[nCards++] = card; StatusChecker. DetermineStatusAndTotalPoints( hand, nCards, out status, out totalPoints); }