2018年9月29日 星期六

違反 ISP Interface Segregation Principle 介面隔離原則

違反 ISP Interface Segregation Principle 介面隔離原則

ISP Interface Segregation Principle 介面隔離原則 也是對於很多想要了解 SOLID 物件導向程式設計觀念與技能的人,相當困惑的。有人會說,要是我,才不會設計出這樣的程式碼,但是,現實情況是,產生這樣的問題,真的很常見,而且,會造成開發者的困擾,這怎麼說呢?
羅馬不是一天造成的,程式不是很快就可以寫出來的,當相同的程式經過多人的手,並且有著修改時間壓力下,往往會造成很多不可思議的程式碼,畢竟,這些程式碼都是人寫出來的。在底下的範例中,開發人員想要開發出個辦公室事務機器,因此,定義了許多功能,也造就了 IBusinessPrinters 這個介面,緊接著實作出 AllInOnePrinter 這個具體類別;可是,客戶想要開發出更具有價格競爭力的商品,因此,就想到只留下列印功能,把其他功能都移除,設計出一個超時尚的 SimplePrinter 印表機,另外,又想要移除傳真功能而已,造就出讓中小企業有能夠購買的 CopyPrinter 拷貝列印機。從底下的類別宣告,您看出了甚麼問題嗎?
public interface IBusinessPrinters
{
    void Scan();
    void Print();
    void Copy();
    void FaxSending();
    void FaxReceiving();
}
public class AllInOnePrinter : IBusinessPrinters
{
    public void Copy() { Console.WriteLine("拷貝中"); }
    public void FaxReceiving() { Console.WriteLine("傳真接收中"); }
    public void FaxSending() { Console.WriteLine("傳真發送中"); }
    public void Print() { Console.WriteLine("列印中"); }
    public void Scan() { Console.WriteLine("掃描中"); }
}
public class SimplePrinter : IBusinessPrinters
{
    public void Copy() { Console.WriteLine("錯誤!! 無此功能"); }
    public void FaxReceiving() { Console.WriteLine("錯誤!! 無此功能"); }
    public void FaxSending() { Console.WriteLine("錯誤!! 無此功能"); }
    public void Print() { Console.WriteLine("列印中"); }
    public void Scan() { Console.WriteLine("錯誤!! 無此功能"); }
}
public class CopyPrinter : IBusinessPrinters
{
    public void Copy() { Console.WriteLine("拷貝中"); }
    public void FaxReceiving() { Console.WriteLine("錯誤!! 無此功能"); }
    public void FaxSending() { Console.WriteLine("錯誤!! 無此功能"); }
    public void Print() { Console.WriteLine("列印中"); }
    public void Scan() { Console.WriteLine("掃描中"); }
}

違反 LSP Liskov Substitution Principle 里氏替換原則

違反 LSP Liskov Substitution Principle 里氏替換原則

有很多人對於 LSP Liskov Substitution Principle 里氏替換原則 所講述的內容,看得很模糊,有些時候是學習者對於物件導向程式設計觀念不慎清楚、開發經歷中,也甚少經常設計類別繼承的程式碼,不過,也有人說,我都有設計類別繼承,要是我,才不會有這樣的問題;可是,這是個原則,講的是當您遵從這個原則之後,就不會設計出有行為異常的子類別,有些時候,您也可能需要維護別人開發的舊專案,這個專案也許不知道經過幾個人來維護過了,往往有需求變更的時候,為了客戶時效要求,手頭上有很多專案要開發,就直接繼承類別,產生新的子類別來設計出滿足變更需求的程式,當然,經過一段時間之後,這個專案上就會產生違反 LSP 的問題;不過,說實在的,若這個專案從頭到尾都是您自己開發,也是有可能產生違反 LSP 的問題。一旦程式碼出現違反 LSP 的原則,就會產生出許多意想不到的問題,而這些問題,很多時候是在設計與除錯時候,無法立即發現到的 (更多關於 LSP 的介紹,網路上已經有海量的文章,請各位自行去搜尋、研究)
現在,讓我們來看看底下的範例,您能夠看出它存在著甚麼問題嗎?
另外,他是否有遵從 LSP 原則嗎?
不論答案是肯定或者是否定,請您要提出的論述觀點,這樣,才能夠知道,您是否真正明瞭甚麼是 LSP Liskov Substitution Principle 里氏替換原則
class 鳥
{
    public virtual void 飛() { Console.WriteLine("鳥在飛"); }
    public virtual void 吃() { Console.WriteLine("鳥在吃"); }
}
class 老鷹 : 鳥
{
    public override void 飛() { Console.WriteLine("老鷹在飛"); }
    public override void 吃() { Console.WriteLine("老鷹在吃"); }
}
class 鴕鳥 : 鳥
{
    public override void 飛() { throw new NotSupportedException("鴕鳥不能飛"); }
    public override void 吃() { Console.WriteLine("老鷹在吃"); }
}
底下是使用上面類別的範例程式碼
class Program
{
    static void Main(string[] args)
    {
        List<鳥> birds = new List<鳥>();
        birds.Add(new 鳥());
        birds.Add(new 老鷹());
        birds.Add(new 鴕鳥());
        BirdsFly(birds);
    }
    static void BirdsFly(List<鳥> birdList)
    {
        foreach (var item in birdList)
        {
            item.飛();
        }
    }
}

『SOLID 之會員管理重構挑戰練習』

『SOLID 之會員管理重構挑戰練習』

我們要設計一個會員管理服務 MemberManager,其可以接受會員 Member 進行註冊功能,如底下程式碼。
public enum MemberType { Personal, Education, Organization }
public class Member
{
    public MemberType MemberType { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
    public string LineID { get; set; }
    public bool Binding { get; set; }
}
public class MemberManager
{
    public void Register(Member member)
    {
        if (string.IsNullOrEmpty(member.Name))
        {
            throw new ArgumentException("姓名不可為空白", nameof(member.Name));
        }
        if (string.IsNullOrEmpty(member.Phone))
        {
            throw new ArgumentException("電話不可為空白", nameof(member.Phone));
        }
        if (string.IsNullOrEmpty(member.Email))
        {
            throw new ArgumentException("Email不可為空白", nameof(member.Email));
        }
        if (string.IsNullOrEmpty(member.LineID))
        {
            throw new ArgumentException("LineID不可為空白", nameof(member.LineID));
        }
        Console.WriteLine($"發送簡訊驗證通知給 {member.Phone}");
        if (member.Binding == false)
        {
            Console.WriteLine("進行 Line ID 資料綁定");
        }
        Console.WriteLine("儲存會員資料");
    }
 
}
底下為使用這個類別的用法
Console.WriteLine("原始程式碼");
OriginalSituation.MemberManager member = new OriginalSituation.MemberManager();
member.Register(new OriginalSituation.Member()
{
    MemberType = OriginalSituation.MemberType.Personal,
    Name = "Vulcan",
    Email = "vulcan@doggy.com.tw",
    Phone = "0987654321",
    Binding = true,
    LineID = "vulcan",
});
 
Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
Console.ReadKey();

變更需求說明

由於現在這個 會員管理服務 MemberManager 在進行會員註冊過程中,將會透過手機簡訊發會給會員一個驗證通知訊息,不過,客戶覺得這樣並不足夠,希望能夠加入發送電子郵件的驗證訊息到會員的信箱中。

練習目的說明

當您面對這樣的變更需求的時候,請依據 SOLID 個原則,重構這個類別,使他可以符合 SOLID 原則(也許,並不是每個原則都可以套用進去),並且重構後程式碼可以滿足客戶的變更需求。

練習提示

  • 在進行重構實作之前,請大家先提出您的意見,這個 MemberManager 類別,違反了那些 SOLID 原則,並且,請說明,您會準備如何進行重構這件事情或者預計執行想法?
    各位可以參考 如何進行您的專案程式的 SOLID Principle 原則之評估方法 文章中的說明,套用 分析理由 的制式描述方式,闡明您對於重構前、後,對於 SOLID 驗證的論述
    若該程式碼沒有符合 SOLID 其中一個原則,也請您說明您的理由與看法
  • 之後,在嘗試逐步修正與重構這個類別,您可以將套用個原則的結果,將原始碼提出來與大家一起分享,並且各位學員一同來檢視這個重構後的程式碼,是否有符合 SOLID 個原則,如果沒有,那麼,在重構後的程式碼中,您覺得是那些地方沒有修正好,而這些地方一樣違反了 SOLID 的哪個原則,並且提出您的解釋理由。
這個練習並沒有一定標準的作法(標準答案),您需要參考 SOLID 的個原則說明,進行重構。經過這樣的練習,相信您已經開始進入到 SOLID 精通領域中了。*



『SOLID 重構挑戰練習』1. 需求說明

『SOLID 重構挑戰練習』 1. 需求說明

我們要設計一個將字串文字轉變成為顏色的支援類別,這裡有個類別 StringToColor,裡面有個 Transfer ,負責接收一個字串參數,依據字串參數來進行顏色的轉換,一般來說,你可能會設計成如下的程式碼 (在這裡,我們先假設 Color 這個物件,僅支援傳入 ARGB 四個數值,才能夠建立起一個 Color 物件,不支援傳入顏色字串的功能)
public class StringToColor
{
    public Color Transfer(string name)
    {
        Color transferResult;

        switch (name.ToLower())
        {
            case "red":
                transferResult = Color.FromArgb(0xFF, 0xFF, 0x00, 0x00);
                break;
            case "green":
                transferResult = Color.FromArgb(0xFF, 0x80, 0x80, 0x80);
                break;
            case "blue":
                transferResult = Color.FromArgb(0xFF, 0x00, 0x00, 0xFF);
                break;
            default:
                throw new ArgumentException("不正確的顏色名稱");
        }
        return transferResult;
    }
}

變更需求說明

現在,需要能夠將 Red, Greed, Blue 這三個文字轉換成為相對應的 Color 物件,但是,之後會經常會有變更需求產生,例如:今天有一個需求變更,需要加入一個 Pink 文字顏色,下個星期又要加入 Yellow 文字顏色,總之,這個 StringToColor 類別會經常得變更(不是錯誤與Bug修正喔),您會如何因應呢?繼續使用 Switch、猜成多個 if

練習目的說明

請依據 SOLID 個原則,重構這個類別,使他可以符合 SOLID 原則(也許,並不是每個原則都可以套用進去)

練習提示

  • 在進行重構實作之前,請大家先提出您的意見,這個 StringToColor 類別,違反了那些 SOLID 原則,並且,請說明,您會準備如何進行重構這件事情或者預計執行想法?
    各位可以參考 如何進行您的專案程式的 SOLID Principle 原則之評估方法 文章中的說明,套用 分析理由 的制式描述方式,闡明您對於重構前、後,對於 SOLID 驗證的論述
  • 之後,在嘗試逐步修正與重構這個類別,您可以將套用個原則的結果,將原始碼提出來與大家一起分享,並且各位學員一同來檢視這個重構後的程式碼,是否有符合 SOLID 個原則,如果沒有,那麼,在重構後的程式碼中,您覺得是那些地方沒有修正好,而這些地方一樣違反了 SOLID 的哪個原則,並且提出您的解釋理由。
這個練習並沒有一定標準的作法(標準答案),您需要參考 SOLID 的個原則說明,進行重構。經過這樣的練習,相信您已經開始進入到 SOLID 精通領域中了。*

『延伸探討』

在這個範例中,每個 Switch 內的 case 若需要執行超過一個以上的表示式,並且每個 case 內的這些敘述都會不進相同,例如,該類別為產生報表的類別,他會接收一個來自於資料庫內取得的資料,並且產生各式報表:Excel 檔案報表、Word 檔案報表、圖片檔案報表等等。
像是這樣的情況,我們要如何套用 SOLID 各原則,使我們的程式碼具有高可讀性、容易維護性、高內聚、低耦合、可以用於單元測試等特性呢?

如何進行您的專案程式的 SOLID Principle 原則之評估方法

如何進行您的專案程式的 SOLID Principle 原則之評估方法


在這篇文章中,我們將分別針對 SOLID 的五個原則,說明如何檢驗您的程式碼,是否有符合這些原則的檢驗論述指引。也就是說,當您判斷一個程式,是否有符合 SOLID 各個原則或者其中一個原則,您可以使用這篇文章提出的指引,描述出您的看法。

Single Responsibility Principle SRP

在 Wiki 定義 Single Responsibility Principle 如下
every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.
而在 Robert C. Martin 對於這個原則的描述為
A class should have only one reason to change.

驗證步驟

在這裡,我們將需要進行 分析對象 與 分析理由 的論述

分析對象

我們在這裡採用 Robert C. Martin 的描述,因此,請您要指出要分析的類別是哪一個,也就是: 一個類別應該只有一個改變的理由!
類別 C

分析理由

因為 responsibility as a reason to change 責任將會定義為一個改變的理由,而要改變這個需求,有些時候會提早知道,而大部分的時候,您幾乎無法做到預測,這些變更的理由可能會來自於客戶的要求、PM的修正、當時環境的變更、公司的政策等等。
所以,請在這裡描述您認為這個類別究竟存在哪些責任,為什麼會有這樣的責任存在呢?
在類別 C 中,存在著 R 責任,會有 R 責任存在,是因為存在著 RC 變更理由
經過重構之後,在類別 C 中,僅存在著一個變更理由 (RC'),也就是具有一個責任 (R')
請依序把上述的 C, R , RC , RC' , R' 替換您的分析理由,若有多個責任,請分別描述出來

Open Closed Principle OCP

在 Wiki 定義 Open Closed Principle 如下
software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification, that is, such an entity can allow its behaviour to be extended without modifying its source code.
在 Uncle Bob 的書中,對於 OCP ,定義 OCP 如下
Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
在最初的 OCP 提出者 Bertrand Meyer,定義 OCP 如下
A module will be said to be open if it is still available for extension. For example, it should be possible to add fields to the data structures it contains, or new elements to the set of functions it performs.
A module will be said to be closed if [it] is available for use by other modules. This assumes that the module has been given a well-defined, stable description (the interface in the sense of information hiding).[3]
Meyer's proposed solution to this dilemma relied on the notion of object-oriented inheritance (specifically implementation inheritance):
A class is closed, since it may be compiled, stored in a library, baselined, and used by client classes. But it is also open, since any new class may use it as parent, adding new features. When a descendant class is defined, there is no need to change the original or to disturb its clients.

驗證步驟

在這裡,我們將需要進行 分析對象 與 分析理由 的論述

分析對象

根據該原則的定義,我們分析的對象會是 software entities (classes, modules, functions, etc.),因此,請您要指出要分析的類別是哪一個
在軟體項目 E (可能是 類別、模組、函數)、開放 O 理由、封閉 C 理由

分析理由

請在這裡分別描述您認為存在著 開放 Open 與 封閉 Closed 的狀況
先描述為重構前的情況
在 軟體項目 E
開放 : 為什麼在軟體項目 E 中,為什麼不可以擴展,其 開放 O 理由
封閉 : 為什麼在軟體項目 E 中,為什麼不可以封閉修改,其 封閉 C 理由
經過重構之後,
軟體項目 E
開放 : 為什麼在軟體項目 E 中,為什麼可以擴展,其 開放 O 理由
封閉 : 為什麼在軟體項目 E 中,為什麼可以封閉修改,其 封閉 C 理由
請依序把上述的 E, O, C 替換您的分析結果

Liskov Substitution Principle LSP

在 Wiki 定義 Liskov Substitution Principle 如下
the Liskov substitution principle (LSP) is a particular definition of a subtyping relation, called (strong) behavioral subtyping, that was initially introduced by Barbara Liskov in a 1987 conference keynote address titled Data abstraction and hierarchy. It is a semantic rather than merely syntactic relation, because it intends to guarantee semantic interoperability of types in a hierarchy, object types in particular. Barbara Liskov and Jeannette Wing formulated the principle succinctly in a 1994 paper as follows:
在 Uncle Bob 的書中,對於 LSP ,定義 LSP 如下
Subtypes must be substitutable for their base types.
在最初的 LSP 提出者 Liskov ,定義 LSP 如下
if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program.
What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

驗證步驟

在這裡,我們將需要進行 分析對象 與 分析理由 的論述

分析對象

根據該原則的定義,我們分析的對象會是 subtype 子型別 與 base type 基底型別 和 有用到基底型別的程式碼,因此,請您要指出這兩個型別與其相關程式碼的那些地方與行為,是否違反了 LSP 的要求
程式碼 P , 子型別 S , 基底型別 T

分析理由

請將您的分析結果,在這裡描述
符合 : 在程式碼 P ,對於 子型別 S ,是 可以替換 基底型別 T
違反 : 在程式碼 P ,對於 子型別 S ,無法 可以替換 基底型別 T,理由是 R
請依序把上述的 P, S, T, R 替換您的分析結果

Interface Segregation Principle ISP

在 Wiki 定義 Interface Segregation Principle 如下
no client should be forced to depend on methods it does not use.[1] ISP splits interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them.
在 Uncle Bob 的書中,對於 ISP ,定義 ISP 如下
Classes whose interfaces are not cohesive have "fat" interface.
也可以說是
Clients should not be forced to depend upon interfaces that they do not use.
在 Uncle Bob 的網頁中,對於 ISP ,定義 ISP 如下
Make fine grained interfaces that are client specific.
在最初的 ISP 提出者 Robert C. Martin 的 Object Mentor SOLID Design Papers Series

驗證步驟

在這裡,我們將需要進行 分析對象 與 分析理由 的論述

分析對象

根據該原則的定義,我們分析的對象會 Client 用戶端,也就是要使用這個介面的用戶端與 Interface 介面,也就是抽象化的介面
用戶端 C , 介面 I , 方法 M

分析理由

請將您的分析結果,在這裡描述
對於 用戶端 C , 相依於介面 I ,將不會用到該 介面 I 的甚麼 方法 M
請依序把上述的 C , I, M 替換您的分析結果

Dependency Inversion Principle DIP

在 Wiki 定義 Dependency Inversion Principle 如下
refers to a specific form of decoupling software modules. When following this principle, the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are reversed, thus rendering high-level modules independent of the low-level module implementation details.
在 Uncle Bob 的書中,對於 ISP ,定義 ISP 如下
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.

驗證步驟

在這裡,我們將需要進行 分析對象 與 分析理由 的論述

分析對象

根據該原則的定義,我們分析出 高階模組 High-Level Module , 低階模組 Low-Level Module , 抽象 Abstraction , 細節 Detail
高階模組 H , 低階模組 L , 抽象 A , 細節 D

分析理由

請將您的分析結果,在這裡描述
對於 高階模組 H 原先有相依於 低階模組 L ,經過重構之後 高階模組 H 相依於 抽象 A
對於 抽象 A 原本相依於 細節 D ,經過重構之後, 細節 D 相依於 抽象 B
請依序把上述的 H , L , A , D , B 替換您的分析結果