顯示具有 OOAD 標籤的文章。 顯示所有文章
顯示具有 OOAD 標籤的文章。 顯示所有文章

2009年4月17日 星期五

Design Pattern: Singleton 的型式

Singleton 是非常經典的 Design Pattern。最簡單,也是最具實用性的 pattern。
我也最常拿來考面試者。一個有經驗的 programmer,怎麼可能沒碰過 Singleton?

目的

全域只有一個實例(instance)

舉例

一家銀行的分行,通常會有多個櫃台(窗口),以加快處理速度。但只能有一個取號機,避免號碼牌重覆。

因此,該取號機只能有一台。如果有兩台,天下就大亂了。

程式在實作時,也常常有類似的需求。如何才能確保一個 instance呢?大家經驗累積的結果,都有類似的樣式(pattern)。這樣的 pattern 後來被取名為 singleton,以方便大家的溝通。

策略

  1. 將預設建構子封閉起來,不讓外界直接建立 instance.
  2. 使用靜態屬性(static property),讓外界取的唯一的 instance

最常見的實作如下。

實作1
class MySingleton
{
private static MySingleton _singleton = null;
private int currentNumber = 0;
private MySingleton()
{
}

public static MySingleton Current
{
get
{
if (_singleton == null)//注意這一行
_singleton = new MySingleton();
return _singleton;
}
}

public void WriteNumber()
{
currentNumber++;
Console.WriteLine(currentNumber);
}
}


Client


而Client code 的使用方式如下
MySingleton.Current.WriteNumber();
MySingleton.Current.WriteNumber();
MySingleton.Current.WriteNumber();

呼叫的結果,就是 1, 2, 3了。

實作1的方法並不完美,因為不是 thread-safe。換句話說,使用在 multi-thread 的環境下,是有可能還是建立了多個 instance。可能發生問題的程式碼,就是實作1內註解的「注意這一行」。

因此,有人改寫如下


實作2: thread-safe


class MySingleton
{
private static MySingleton _singleton = null;
private int currentNumber = 0;
private MySingleton()
{
}

public static MySingleton Current
{
get
{
if (_singleton == null)
{
lock(typeof(MySingleton))
_singleton = new MySingleton();
}
return _singleton;
}
}

public void WriteNumber()
{
currentNumber++;
Console.WriteLine(currentNumber);
}
}

實作2的方法,是使用了 lock 的 c# 語法,讓進入 lock區段的執行緒只能有一個。

實作2其實已經相當完美了。唯一美中不足的是,簡單的singleton,竟然要這麼複雜?因此,又有人改寫如下

實作3: thread-safe, eager singleton


class MySingleton
{
private static MySingleton _singleton = new MySingleton();
private int currentNumber = 0;
private MySingleton()
{
}

public static MySingleton Current
{
get
{
return _singleton;
}
}

public void WriteNumber()
{
currentNumber++;
Console.WriteLine(currentNumber);
}
}

實作3是使用了 static 的 instance,並在一開始就建立了 instance。即使用了private static MySingleton _singleton = new MySingleton();這一行。


這樣的好處,是大幅減少了程式碼的複雜度,相當容易維護。我相當推薦這一型。

實作3有一個壞處,當 Client 尚未需要 singleton的 instance時,程式就先建立了 instance。而實作2時會先判斷是否尚未建立實例。

if (_singleton == null)

因此,實作2是lazy singleton「要用時才建立instance」, 但not thread-safe。而實作3是eager singleton,「未用時已建立 instance」, thread-safe。

有沒有既是 thread-safe, 又是 lazy singleton 的寫法呢?因此產生 實作4

實作4: thread-safe, lazy singleton



class MySingleton
{
private int currentNumber = 0;
private MySingleton() { }

public static MySingleton Current
{
get {
return Inner.Current;
}
}

public void WriteNumber() {
currentNumber++;
Console.WriteLine(currentNumber);
}

class Inner {
static Inner() { }
internal static readonly MySingleton Current = new MySingleton();
}
}

實作4非常巧妙地使用 inner class 並結合 static instance 的用法,完美地完成了任務,是我目前看過最棒的 singleton 解法。

2008年11月12日 星期三

Framework Design(2) : Property

Property 一般用來封裝 private member field。

  1. Property  應該愈簡單愈好,且不該 throw exception
  2. Property 不該與其他 Property 相關。設定一個 Property 時不該影響到其他的 Property
  3. Property 可以以任意順序來設定

Property 與 Method 有一定的相似度。何時應該用 Method 呢?

  1. 代表一種轉換時。例如 .ToString()
  2. 使用 Property 會導致 side effect 時
  3. 須要較長時間運算時
  4. 回傳陣列時
舉例來說,下面範例是不好的程式碼。因為會讓 calling method 誤認為 Roles 這個 property 取得的成本很低。

 public class User
  {
    public Role[] Roles
    {
      get { return roles_from_database; }
    }
  }

  //calling method
    User user = new User();
    for (int i = 0; i < user.Roles.Length; i++)
    {
      Role role = user.Roles[i];
      //more
    }
此時應改成 method, 就不會讓人誤會了
 public class User
  {
    public Role[] GetRoles()
    {
      get { return roles_from_database; }
    }
  }

  //calling method
    User user = new User();
    Role[] userRoles = user.GetRoles();
    for (int i = 0; i < userRoles .Length; i++)
    {
      Role role = userRoles[i];
      //more
    }

Framework Design(1) : 建構子應該只傳參數

Constructors are lazy

建構子應該只傳參數。不要在建構時就做了一堆的事。下面的code 就不好。

  public class XmlFile
  {
    string data;
    public XmlFile(string filename)
    {
      data = File.ReadAllText(filename);
    }
  }

該改成下面的事即可

  public class XmlFile
  {
    string filename;
    public XmlFile(string filename)
    {
      this.filename = filename;
    }

    public void DoWork()
    {
      string data = File.ReadAllText(filename);
      //do more
    }

2008年10月30日 星期四

OOAD(14): OOAD Project Life Cycle

  1. Feature List
  2. Use Case Diagrams
  3. Break Up the Problem
  4. Requirements
  5. Domain Analysis
  6. Preliminary Design
  7. Imlementation
  8. Delivery

2008年10月29日 星期三

OOAD(13): 程式開發方式

所有的程式寫作的開發方式,其實都是下面三個的混合

Use case driven development

一次針對一個 Use case 下一個 scenario 進行開發。再逐步完成該Use case 下所有的scenario。完成該Use case 下所有的scenario後才對下一個 Use case 開發

Feature driven development

一次對針一個系統行為的特色來開發。

Test driven development

針點系統的一個功能寫出 test cases。寫程式的目的就是通過所有的 test cases

2008年10月23日 星期四

OOAD(12): 三種不使用繼承的好方法

如果不透過繼承,又希望從別的類別得到好處。可使用

  1. Delegation (委派)
  2. Composition
  3. Aggreation

Delegation (委派)

一台轎車,如果想要可以欣賞音樂的能力,不是透過繼承(畢竟車子不是音響),而是在車子內加裝汽車音響。
因此,轎車內需要放音樂的能力時,只需要打開汽車音響即可。
class 轎車
{
  private 汽車音響 a汽車音響 = new 汽車音響();
  public void 欣賞音樂()
  {
    a汽車音響.打開();
  }
}

或者,隨時找到一台音響再打開

class 轎車

  public void 欣賞音樂()
  {

    汽車音響 a汽車音響 = new 汽車音響(); //隨時想有就可以有?這大概是軟體世界才有的能力
    a汽車音響.打開();
  }
}

Composition

一個士兵,需要有攻擊的能力,但士兵畢竟不是武器,因此士兵需要有武器。到目前為止就像委派。
但是,武器有許多種,如飛彈、槍、劍,每一種的攻擊方式都不同。

注意到 weapon 是可替換的,只要有實作 IWeapon 的武器就可以。換句話說,一整族IWeapon的武器都可以替換。
另外,weapon 是天生就存在於 Soldier 內的。一旦 Soldier 被 destoryed, weapon 就不存在了。

class Soldier
{
    private IWeapon weapon = new Sword();
    public void Attack()
    {
      weapon.Attack();
    }
  }

  interface IWeapon
  {
    void Attack();
  }

  class Sword : IWeapon
  {
    public void Attack() { }
  }

Aggregation

aggregation 與 Composition 類似,但結構更鬆散,更容易 resuse。上述的例子,weapon 只能生存在 Soldier 的生命週期內,並不合理。如果改成下面的例子,就是 aggregation 了。注意到 weapon 是由建構子傳進去的。當soldier1 被destoryed 後,sword 並不隨之消減。也就是說,可以被 soldier2 再拿去用。

class Soldier
  {
    private IWeapon weapon;
    public Soldier(IWeapon weapon)
    {
      this.weapon = weapon;
    }
    public void Attack()
    {
      weapon.Attack();
    }
  }

  interface IWeapon
  {
    void Attack();
  }

  class Sword : IWeapon
  {
    public void Attack() { }
  }

class Program
  {
    static void Main(string[] args)
    {
      Sword sword = new Sword();
      Soldier soldier1 = new Soldier(sword);
      soldier1.Attack();
    }
  }


在日常生活上,Aggreation 遠較 Composition 多見,這同時也反應到軟體工程上。畢竟物件導向工程是模擬真實的世界。

OOAD(11):Liskov Substitution Principle(LSP)

LSP (替換原則) 是談何謂「良好的繼承」。

  • 子類別應該可以使用其基礎類別替代,而不會發生的誤。如果會發生錯誤,就代表使用繼承失敗。
  • Liskov代換原則是繼承之所以能重複使用的基石,只有當衍生類別可以替換掉基礎類別,且軟體的功能不受影響時,其類別才算真正的被重複使用,而衍生類別也才能夠在基礎類別的基礎上增加新的行為。
  • Liskov代換原則要求凡是基礎類別使用的地方,衍生類別一定適用,故衍生類別必須包含全部基礎類別的介面
  • 針對違反LSP設計時可行的重構(Refactoring)方式
    • 當類別A錯誤的繼承類別B時,可建構一個新的抽象類別C,作為2個具體類別A,B的父類別
    • 當類別A錯誤的繼承類別B時,可重構為類別B委派(Delegate)類別A

 

from http://irw.ncut.edu.tw/peterju/se.html

OOAD(10):Open-Closed Principle(OCP)

  • 模組應當敞開擴充大門,但關閉修改之窗。
  • 如何達成開閉原則,關鍵在抽象化。
  • 不允許更改的是系統的抽象層,允許擴充的是系統的實作層。
  • OCP的另一個角度是EVP對可變性的封裝原則(Principle of Encapsulation of Variation)即找到一個系統的可變因素,並將之封裝起來。
  • 可變性必須被封裝,那不同的可變性呢?應用繼承來處理,因此繼承應被視為封裝變化的方法,但繼承的層數避免超過2層以免不同的可變性混和。
  • 應避免將單純的流程控制轉移語句改寫成多型,除非內含了某種商務邏輯。
  • 所有的設計樣式(Design Pattern)都是針對不同的可變性封裝,使系統在不同的角度上達到開閉原則。

from http://irw.ncut.edu.tw/peterju/se.html

OOAD(9):物件導向類別設計的法則

  • 開閉原則(Open-Closed Principle ; OCP)
  • Liskov代換原則(Liskov Substitution Principle ; LSP)
  • 依賴倒轉原則(Dependency Inversion Principle ; DIP)
  • 介面隔離原則(Interface Segregation Principle ; ISP)
  • 組合/聚合重複使用原則(Composition / Aggregation Principle ; CARP)
  • Demeter原則(Law of Demeter; LoD

2008年10月19日 星期日

OOAD(8): 不確定的事即是風險

當得到需求後,發現有些事情是「不確定」的。這些不確定的事,就是未來的風險。
在專案的過程中,最重要的就是想辦法降低風險。
如果這些風險,仍然是需求不夠清楚時,那還是回頭問客戶吧。

問完客戶後,知道了需求是什麼,記得要技術評估一下,要了解大概該需求未來該如何實作。否則容易引發「技術風險」。
但在技術評估階段時有個重點需要注意,「不要埋頭開始寫程式」。此時的最重要的目的仍是「降低風險」。
過早實作,容易模糊了此時此刻的重點,反而因為尚未了解所有的需求,不但可能做錯了方向,還容易增加專案時程或成本的風險呢。

2008年10月7日 星期二

OOAD(7): 常見的 check

繼承是因為行為發生變化

如果不是行為發生變化,就不值得繼承

善用適合的類別作為屬性

如果子類別只是較父類別多了一些屬性,而非行為發生變化,則可考慮使用Dictionay 作為屬性,而不要子類別了。

舉例來說,下面的程式是不好的。

class Parent
{
   public string Name;
   public void Fly();
}

class Child1 : Parent
{
   public string Color;
   public string AliasName;
}

class Child2 : Parent
{
   public string BackColor;
}

class Child3 : Parent
{
   public string BorderColor;
}

原因是 Child1, Child2, Child3 在行為上根本不會發生變化。還是只有 Parent 的Fly。因此,只需要修改成下面的程式,就不需要子類別了。

class Parent
{
   public string Name;
   public void Fly();
   public Dictionary  OtherProperties {get; set;}
}

2008年10月1日 星期三

OOAD(6): textual analysis

Textual analysis

  1. 在use case 中尋找名詞與動詞,可用來尋找 class 及其 method
  2. 一個好的 use case 應該精準並清楚地描述系統行為,並容易了解。因此,進行 textual analysis 是快速而容易的。
  3. 當一個 use case 無法或不容易進行 textual analysis 時,就應該進行重寫來重新描述。
  4. 注意到名詞與動詞並非一定是 class 與 method,常常需要逐字檢查。例如「飛機進行轟炸」,不注意的話會以為進行是動詞,而把轟炸當名詞。與客戶重新確認後,發現是客戶描述不正確。經修改後的use case 就會改成「飛機轟炸都市」,就會產生 飛機.轟炸(都市) 的結果。
  5. 即使名詞經分析後並未成為我們需要的 class ,也需要注意。因為這個名詞通常也可能是 actor。

Textual analysis 完畢後,接著可進行更新類別圖 (class diagram)

OOAD(5): Design Principles

Avoid duplicate code

當發現重複的程式碼片斷時,通常代表「設計」有問題。例如責任放錯了類別,或者是否應該delegate到別的類別等等。

Encapsulate what varies

Code to an interface rather than an implementation

Each class in your should have only one reason to change

Classes are about behavior and fuctionlity

在系統中的每個物件只有單一的責任

2008年9月30日 星期二

OOAD(4): 需求清單 (Requirement lists)與 use case 的發展過程

 

  1. 由客戶取得原始需求。如下之A.B.C
    • A
    • B
    • C
  2. 由原始需求發展 use case。如下之 a, b, c , d
    • a
    • b
    • c
    • d
  3. 再發展 alternate path。如下之 b.1, b.2
    • a
    • b
      • b.1
      • b.2
    • c
    • d
  4. 根據上一步的 use case,與原始需求比對,看看需求是否完整。如果不完整,就向客戶確認並取得更完整的需求
  5. 重複2到4,直到需求完整為止。

OOAD(3): Use Case 的三個部份

一個好的 Use Case,必須有三個部份

Clear Value

  1. 每個 use case 必須對於系統有清楚且單一的價值。
  2. 如果該 use case對於系統沒有價值,就沒有什麼用處。
  3. 一個 use case只能有一個目的(goal)。一個目的(goal)可以有多個 use case,原因是Alternate path。

Start and Stop

每個 use case必須有非常明顯的起點與終點。某個東西(Actor)必須開始這個 use case,而在某個狀況下結束這個流程。

External Initiator

每個 use case必須被系統外的「外來初始者」來起動。通常是個「人」。

2008年9月29日 星期一

OOAD(2): 檢查物件的責任

在OOAD中,物件的應當負什麼責任是相當重要的。
因此,在寫完的程式中,要如何一個object中找出不適當的責任呢?

  1. 由物件方法(method),屬性(property)來找。例如飛機.起飛(plane.TakeOff),飛機.降落(plane.Land)是正確的。但飛機.驗票(plane.VerifyTacket)明顯的是放錯了責任。
  2. 無用的屬性(property)。如果一個物件的某個屬性常常是 null,這很可能代表該屬性不應屬於此物件。

雖然是老掉牙的原則,但要在每個物件中遵守卻也是相當難的了。

OOAD(1):物件方法的命名

物件(object, an instance of class)方法(method)的命名,應該是

  1. 動詞:如飛機.起飛 (plane.TakeOff)
  2. 動詞+受詞:如飛機.加油 (plane.AddOil)

OOAD(0): 建立好的軟體之三步驟

最近在讀一本書。
以此當作筆記

建立好的軟體之三步驟

  1. 確定軟體如客戶所期望
  2. 使用物件導向基本原則來增加彈性
  3. 可維護、可重用的設計

第一步驟即規格,或 functionality。在軟體建置的過程中,這是最重要,也是我們的目的。即使其他兩個步驟寫的再好,不是客戶要的,也都沒有用了。所以規格必須放在第一步。

Share with Facebook