[書摘] 深入淺出物件導向分析與設計 OOA & OOD

記錄深入淺出物件導向分析與設計筆記,內容包含:
1. 使用像是封裝與委派的OO原則,建立有彈性的應用程式。
2. 使用開閉原則(Open-Closed Principle)與單一責任原則(Single-Responsibility Principle),提升程式的重利用性。
3. 學習如何將 OO 原則,設計模式,各種開發方法,通通整合到 OOA&D 專案的生命週期中。
4. 運用 UML,使用案例及使用案例圖確保所有利害關係人都能清楚地溝通,協助你交付正確的軟體,符合每個人的需求。



偉大的程式碼使用千錘百鍊的設計模式和原則。盡量保持物件的鬆耦合,禁止修改而關閉,允許擴展而開放。

大綱:
1. 偉大軟體由此開始:良好應用程式之基石
偉大軟體三步驟:
(1) 讓客戶滿意,確認你的軟體做客戶要它做的事情

(2) 應用基本的OO原則,增加軟體彈性
(3) 努力達成設計良好,編程良好,易於維護、擴展、重利用

測試驅動(Test Drive)

物件型別:
(1) 物件應該做其名稱所指的事
(2) 每個物件應該代表單一概念
(3) 未使用的特性是無用的贈品

2. 收集需求
使用者案例的三部分:
(1) 清楚的價值
(2) 起點與終點 
(3) 外部啟用者

術語:
External Initiator 啟動使用案例所描述的步驟清單,沒有它使用案例不會開始
UserCase 幫助蒐集好需求,並詳述系統功能
Start Condition 案例第一步
Requirement 系統要成功必須要做到的事
Clear Value 沒有它,使用案例就沒有價值
Stop Condition 讓你知道案例何時結束
Main Path 當一切正常運作時,系統正在做的事

要點:
需求是系統為了正確運作所要做的事:
(1) 好的需求確保你的需求如客戶所欲其那樣運作
(2) 確認需求涵蓋系統的所有案例
(3) 運用使用案例找出客戶忘了告訴你的事
(4) 使用案例將揭露任何不完整、闕漏的需求,你可能必須將他們加到你的系統裡

最初的需求通常來自客戶
為了確保你有一組好需求,要發展出系統的使用案例
使用案例詳述系統確切應該做什麼
一個使用案例具有單一目標,但含多重路徑,到達此目標
好的使用案例具有起始與終止條件、外部啟動者,並且對使用者具有清楚的價值
一個使用案例就是一則系統如何運作的故事
對系統要達成的每一個目標,你至少會有一個使用案例
在使用案例完成後可藉由精鍊並增加需求
確保所有使用案例接可行的需求清單是一組好需求
系統必須運作在真實世界裡,而不只是在你預期的狀態中
當事情有錯時,你必須有替代路徑,到達目標

3. 需求改變
需求總是在改變。然而若是有很好的使用案例,通常可以快速的改變你的軟體,適應那些需求

需求:
(1) 好的需求確保你的系統如客戶所預期地那樣運作
(2) 確認需求涵蓋系統所有的使用案例
(3) 運用使用案例找出客戶忘了告訴你的事
(4) 使用案例將揭露任何不完整、闕漏的需求,你可能必須將他們加入系統中
(5) 需求總是隨時間做改變

要點:
(1) 需求將總是隨專案進行而改變
(2) 當需求變更時,你的系統必須隨之演進,處理新需求
(3) 當你的系統需要以新的或不同方式運作時,就從更新你的使用案例開始
(4) 一個使用情節是通過使用案例的單一路徑,從開始到結束
(5) 單一使用案例可以有多重使用情節,只要每個使用情節都具有相同的客戶目標
(6) 替代路徑可以是只發生在某些情況下,或者是提供完全不同路徑通過使用案例的一部分步驟
(7) 你應該總是嘗試著避免重複程式碼,那是維護工作的夢魘,並且是程式設計的問題點

4. 分析
分析與使用案例,讓你對客戶、經理、和其他開發者展示系統在真實世界的情境裡如何展示

要點:
(1) 分析幫你確保軟體在真實環境的情境裡運作,而不只是在完美環境下
(2) 使用案例意圖被你自己、你的經理、你的客戶、和其他程式設計師所了解
(3) 你應該以任何對你與其他觀看者最有用的格式撰寫使用案例
(4) 好的使用案例準確地描述系統做的事,但並未指出系統怎樣完成這些工作
(5) 每個使用案例應該只聚焦在一個客戶目標。假設有多個客戶目標,應該撰寫多個使用案例
(6) 類別圖以一萬呎,給你一個簡單方式,顯示你的系統以及它的程式構想
(7) 類別圖裡的屬性通常對映到類別的成員函數 
(8) 類別圖裡的操作通常代表類別方法
(9) 類別圖漏掉許多細節,如建構子,每些型別資料,以及類別裡操作目的
(10) 文本分析幫你將使用案例轉換成程式碼層次的類別、屬性與操作
(11) 使用案例裡的名詞是系統的類別候選人,而動詞是系統的類別上的方法候選人

5. 良好的設計

 // analytics.java
public class Inventory {
 private List inventory;

 public Inventory() {
  inventory = new LinkedList();
 }

 public void addInstrument(String serialNumber, double price, InstrumentSpec spec) {
  Instrument instrument = null;
  if(spec instanceof GuitarSpec) {
   instrument = new Guitar(serialNumber, price, (GuitarSpec) spec);
  } else if (spec instanceof MandolinSpec){
   instrument = new Mandolin(serialNumber, price, (MandolinSpec)spec);
  }
  inventory.add(instrument);
 }

 public Instrument get(String serialNumber) {
  for(Iterator i = inventory.iterator(); i.hasNext();) {
   Instrument instrument = (Instrument)i.next();
   if(instrument.getSerialNumber().equals(serialNumber)) {
    return instrument;
   }
   return null;
  }
 }

 public List search(InstrumentSpec searchSpec) {
  List matchingInstruments = new LinkedList();
  for(Iterator i = inventory.iterator(); i.hasNext();) {
   Instrument instrument = (Instrument)i.next();
   if(instrument.getSpec().matches(searchSpec))
    matchingInstruments.add(instrument);
  }
  return matchingInstruments;
 }
}

分析與設計
(1) 設計良好的軟體容易改變與擴展
(2) 使用像是封裝與繼承的基本OO原則,確保你的軟體有彈性
(3) 如果設計沒有彈性,就改變它,別跟壞設計妥協
(4) 確認每個類別,都具有內聚性,每個類別都應該聚焦在一件事情上做的很好
(5) 隨著軟體的設計生命週期進行,要持續努力提升內聚力

內聚力(cohesion):內聚力量度個別模組、類別ㄩ

6. 彈性的軟體
解決大問題:
聆聽客戶,找出他們的需求
用客戶了解的語言組合功能清單
確認你的功能是客戶真正想要的
運用案例圖及使用案例建立系統藍圖
將大系統分解成許多較小部分
應用設計模式到系統裡面較小的部分
運用基本的OOA&OOD原則為較小部分設計程式

看待大問題的方式,是將它視為一組較小問題集合
就像在較小的專案,從收集功能和需求開始進行大專案
功能通常是系統做的大事,而且能夠與需求一詞互相交換使用
共通性與變化性給予你在新系統與已知事物之間相互比較的觀點
使用案例是細節導向,使用案例圖則是比較聚焦在整體概廓上
案例圖應顧及系統所有功能
領域分析以

若是一個使用案例包含另一個使用案例,或是一個使用案例擴充另一個使用案例,那就是<<include>>、<<extend>>關鍵字的意思

7. 架構
(1) 架構幫你將所有的圖型、計畫、及功能清單,轉化成井然有序的應用程式
(2) 系統中對專案最重要的功能就是在架構上重要的
(3) 聚焦在這些功能上:系統本質、你不確定其意義為何,以及不清楚一開始要怎麼實作的
(4) 在專案架構階段中,你所做的每一件事,都應該減少專案的風險
(5) 假如你不需要使用案例中的所有細節,專寫詳述軟體能如何被運用的使用情節,可以幫你快速蒐集好需求
(6) 當你不確定某項功能是什麼時,你應該詢問客戶,然後,試著把答案歸納出來會對該功能取得良好的理解
(7) 運用共通性分析,建造具有彈性的軟體解法
(8) 客戶對做他們想要之事,準時交付的軟體,遠比你認為程式碼寫的很酷的軟體更有興趣

8. 設計原則
使用業經證實的OO設計原則,形成更好的維護、更具彈性、以及更易擴展的軟體

#1  開閉原則(OCP,Open-Closed Principle)
類別應該允許擴展而開放,禁止修改而關閉
(OCP全然關乎允許改變,然而這是以不需要修改既有的程式碼的方式進行,它是封裝和抽象化的結合)

可以讓他人透過覆寫的方式擴充你的類別,而不會修改到你原有的類別

#2 不自我重複原則(DRY, Don't Repeat Yourself Principle)
透過將共同之物抽取出來,置於單一地方,避免重複的程式碼。亦即關於讓系統裡給一個資訊與行為的片段都存在於單一、合理的地方
(不只是把出現一次以上的程式碼丟進單一類別裡,你必須確保系統中每一個行為與資訊片段,只存在於單一、清楚的地方)

#3 單一責任原則(SRP,Single Responsibility Principle)
系統裡的每一個物件應該具有單一責任,所有物件服務都應該聚焦在實現單一責任上

* 當你的每一個物件都只有一個理由改變時,你已經正確地實作單一責任原則

SRP
是關於確認一個類別只做一件事,而且把一件事情做好(又稱為內聚力)

DRY
是關於把一個功能性片段放在單一地方,像一個類別


#4 Liskov 替代原則(LSP,Liskov Substitution Principle)
子型別(Sub type)必須能夠替代其基礎類別(base type)

LSP 全然關乎設計良好的繼承。當你從一個基礎類別繼承下來,你必須能用你的子類別替代該基礎類別,而不會把事情弄糟,否則你已經錯誤地使用繼承

* Liskov  所提出關於繼承的原則『繼承必須確保父型別所擁有的特質(特性與方法)對子型別仍然成立』,也就是說,當子型別能替代其父型別時,才算具有正確合理的繼承關係

將功能委派給其他類別:
委派(delegation)是一個類別將做某是的任務委交給另一個類別或方法。它是繼承的幾個替代方法之一。

*  假如你需要使用另一個類別的功能性,但不想要改變該功能性,考慮以委派代替繼承

使用合成(composition)將來自其他多個類別的行為組合起來
合成是讓你使用來自『一組其他類別』的行為,並且可以在執行期間切換該行為

在合成中,由其他行為所組成的物件擁有那些行為。當物件被摧毀時,其所有行為也是。在合成裡的行為不存在於合成本身之外

使用聚合(aggregation)是當一個類別是另一個類別的一部分,但仍然可以存在於該類別之外

繼承之外的選項(當子類別不可替代基礎類別時):
(1) 委派(Delegation)
當你不想要改變某個行為,而實作該行為不是此物件本身的責任時,將行為委託給另一個類別

(2) 合成(Composition)
使用合成,你可以重利用來自『一或多個類別』的行為,特別是來自一個群族的類別。你的主要物件完全擁有被組成的物件,而且被組成物件不存在於主要物件之外

(3) 聚合(Aggregation)
當你想要合成的好處,也想要在主要物件之外,使用被組成物件的行為時,就使用聚合

* 假如你偏好委派、合成、與聚合,勝過繼承,你的軟體通常會較有彈性、較易維護、擴展、與重利用

OO原則:
將變化之物件封裝起來
對介面編碼,而不是對實作
應用程式的每一類別只有一個理由改變
類別是關於行為和功能
類別應該允許擴充而開放,禁止修改而關閉(OCP)
透過抽取出共通之事,並將它們放在單一位置,避免重複程式碼(DRY)
系統裡的每一個物件都應該具有單一責任,所有的物件服務都應該聚焦在實現單一責任上(SRP)
子類別應該能替代其基礎類別(LSP)

*
子類別可以替代基礎類別
委派類別、合成類別讓其他人為我做事
我的行為被使用當做另一個類別的行為一部分:被聚合類別
我改變另一個類別的行為:子類別
我不改變另一個類別的行為:被委派類別、被聚合類別、委派類別、合成類別

我能將其他幾個類別的行為結合起來:合成類別、委派類別
即使其他相關的類別消失,我也不會消失:被聚合類類別、被委派類別
我從我的基礎型別獲得行為與功能性:子類別

9. 反覆與測試
偉大軟體的撰寫是反覆進行打造而成的,通常會針對整體輪廓作業,接著,反覆進行應用程式的每個片段直到完成

開發方式:
1. 功能驅動開發(細粒化 granular)
挑出應用程式的特定功能,並且規劃、分析、及開發特定功能,直到完成

2. 使用案例驅動開發(整體觀點 big picture)
挑出通過使用案例的情節,並且撰寫程式碼支援通過該使用案例的完整情節

3. 測試驅動開發
測試驅動開發聚焦在讓類別的行為正確
ID、測試什麼、輸入、預期輸入、起始狀態

(1) 撰寫良好軟體的第一步,是確保你的應用程式像客戶預期的或想要的那樣運作
(2) 客戶通常不在乎圖表和清單;他們想要看你的軟體實際在做什麼
(3) 使用案例驅動開發一次把焦點放在應用程式之使用案例的一個情節
(4) 在使用案例驅動開發裡,你一次把焦點放在單一情節上,然而,在進行其他使用案例的其他情節之前,你通常為單一案例中的所有情節編寫程式
(5) 功能驅動開發裡,你所進行的功能可大可小,只要你一次處理一個功能
(6) 軟體開發總是反覆性的,你看見整體輪廓,接著反覆進行深入較小的功能性片段
(7) 在開發循環裡的每一個階段,你必須進行分析與設計,包括當你開始進行新功能或使用案例時
(8) 測試讓你明確軟體沒有臭蟲,讓你像客戶證明軟體能運作
(9) 好的測試案例只測試一個特定功能片段
(10) 測試案例可能只牽涉到單一類別裡的一個或一些方法,或者牽涉到多個類別
(11) 測試驅動開發的基本想法是:先撰寫你的測試,再開發你的軟體,通過那些測試,解果是功能完整、有效運作的軟體
(12) 按契約編程假定協議雙方了解什麼動作會產生什麼行為,並且遵守該契約
(13) 當錯誤發生在按契約編程的環境時,方法通常會傳回null或非檢查的例外(unchecked exception)
(14) 防禦性編程會尋找會出錯的事,廣泛的測試,避免出錯的狀況
(15) 在防禦性編程的環境裡,方法通常會回傳空(empty)的物件,或者丟出檢查的例外(unchecked exception)

10. OOA & D 生命週期
物件導向分析設計的生命週期:
(1) 功能清單
從高階觀點,找出應用程式應該做什麼
(2) 使用案例圖
確認應用程式要執行的大流程
(3) 分解問題
將應用程式分解成功能性模組(modules of functionality)
(4) 需求
為每個功能性模組想出個別的需求(requirement),並且確定他們符合整體的概廓
(5) 領域分析
想出你的使用案例如何對應到應用程式裡的物件,並確認客戶和你有相同的認知
(6) 初步設計
加入物件的細節,定義物件之間的關係,並且應用原則與設計模式
(7) 實作
撰寫程式碼,進行測試,確認他有效運作。為每個行為、每項功能、每個使用案例、每個問題,做這些事,直到你完成
(8) 交付
完成,上線,開發票,收工!

11. 本書遺珠
(1) IS-A 與 HAS-A 問題
*IS-A 涉及繼承
IS-A關係到繼承,因此,Sword是武器
*HAS-A 涉及合成或聚集
Unit戰鬥單位有武器

(2) IS-A 與 HAS-A 問題
使用繼承的時機是當一個物件的行為類似於另一個物件的行為時,而不只是因為IS-A關係成立
(3) 使用案例格式
(4) 反設計模式
設計模式幫你識別並實作常見問題的好解法
反設計模式幫助你識別並避免常見問題的壞解法
(5) CRC卡
類別(Class)、責任(Responsibility)、合作(Collaborator
(6) 統計數據
缺陷密度 = 程式碼所發現的缺陷 / (程式碼總行數  / 1000)
(7) 循序圖
(8) 狀態圖
描述狀態以及造成狀態改變的動作,描述系統的一部分
(實心圓:起始狀態、內含實心圓:最終狀態、圓角矩陣:狀態、箭頭:轉換、轉換名稱:觸發狀態改變的東西) (9) 撰寫標準與可讀的程式碼
(10) 單元測試
以一組特定輸入,或一組特定順序的方法呼叫,測試每個類別

(11) 重構
重構改變程式碼內部結構,而不影響程式的行為

12. 歡迎光臨物件村
(1) UML 統一塑模語言 Unified Modeling Language

類別圖只是一種傳達類別變數與方法之基本細節方式
class digram, member variables, type, method, type
public, protected, private
 // class.java
public class Airplane {
 private int speed;

 public Airplane() {

 }

 public void setSpeed(int speed) {
  this.speed = speed;
 }

 public int getSpeed() {
  return speed;
 }
}
constructor, deconstructor



(2) 繼承(inheritance)
繼承來自另一個類別的行為,避免重複程式碼
// inheritance.java
public class Jet extends Airplane {
 private static final int MULTIPLIER = 2;
 public Jet() {
  super();
 }
 public void setSpeed(int speed) {
  super.setSpeed(speed * MULTIPLIER);
 }

 public void accelerate() {
  super.setSpeed(getSpeed() * 2);
 }
}

(3) 多型(Polymorphism)
多型與繼承密切相關,當一個類別繼承另外一個類別多型讓子類別能代替父類別。


Airplane plane = new Airplane()
Airplane plane = new Jet()
Airplane plane = new Rocket() 
覆寫 override
多載 overloading

(4) 封裝(Encapsulation)
封裝是將部分資料對應用程式的其餘部分隱藏,並且限制程式碼其餘部分存取該資料的能力。封裝是將類別的實作隱藏起來,好讓它容易使用與改變。封裝讓類別以黑箱的方式提供服務給它的使用者,但不開放該程式碼讓其他人改變或者以錯誤的方式使用它。封裝是遵循開閉原則(OCP)的關鍵技術。將編程元素包裝在較大抽象之實體內的過程,也被稱為資訊隱藏,或關注點分離(separation of concerns)。
// encapsulation.java
public class Airplane {
 public int speed;

 public Airplane() {

 }

 public void setSpeed(int speed) {
  this.speed = speed;
 }

 public int getSpeed() {
  return speed;
 }
}
現在任何人都可以直接設定speed了!

// setGet.java
public class FlyTest {
 public static void main (String[] args) {
  Airplane biplane = new Airplane();
  biplane.setSpeed(212);
  System.out.println(baplane.getSpeed());
  Jet boeing = new Jet();
  boeing.setSpeed(422);
  System.out.println(boeing.getSpeed());
  int x = 0;
  while(x < 4) {
   boeing.accelerate();
   System.out.println(boeing.getSpeed());
   if(boeing.getSpeed() > 5000) {
    biplane.setSpeed(biplane.getSpeed() * 2);
   } else {
    boeing.accelerate();
   }
   x++;
  }
  System.out.println(biplane.getSpeed());
 }
}

關鍵字:
1. Feature List
2. User Case Diagrams
3. Break Up the Problem
4. Requirements
5. Domain Analysis
6. Preliminary Design
7. Implementation 
8. Deliver

Talk to the customer

Iteration
Encapsulation
Alternate Path
Textual Analysis
Feature List
OO Principles
Cohesion
Requirement List
Variability
Feature Driven Development
Design Principles
External Initiator
Analysis
Class Diagram
Delegation
Key Feature List
Commonality 
Scenario
Test Scenario 
Test Driven Development
Architecture
Design Pattern
Commonality

參考文件:
1. 深入淺出物件導向分析與設計

圖片來源:
1. Digital Simulation & Gaming


贊助本站 (Donate)