架構(gòu)漫談系列(2) 封裝(Encapsulation)

這是這個(gè)系列的第二篇。在第二篇里,我決定講一講封裝。
程序的不同部分應(yīng)該用封裝去互相隔離,模塊之間應(yīng)該不應(yīng)該產(chǎn)生很隨意的關(guān)聯(lián)。

可能有的人覺得不解,又或覺得是有道理的廢話,不急,先一步一步來。

我們先來看看面向?qū)ο蟮娜齻€(gè)基本特征是什么?

  • 繼承
  • 多態(tài)
  • 封裝

如果你是科班畢業(yè),這6個(gè)字應(yīng)該是你第一次學(xué)到類(class)的時(shí)候就聽老師說了。
我們老師的話大概是這樣的:

在類里面,封裝就是通過一些手段來限制類外部的訪問,依此隔離出類相對(duì)封閉的區(qū)域。

也就是說,如果有人想要操作類里面的成員(field),不應(yīng)該讓它直接進(jìn)行這樣操作。而應(yīng)該通過良好定義的函數(shù)(或?qū)傩缘腟etter)來完成。除非你有不得不如此的理由,否則就不應(yīng)該讓人家直接訪問你的私有成員。

下面的代碼通常是bad practice。
任意的類均能任意的修改Person內(nèi)的Name和Age,即便Name寫成亂碼或?qū)ge設(shè)成負(fù)數(shù),都是可以做到的,Person類自己是控制不住的。

public class Person
{
    public int Age;
    public string Name;
}

下面的代碼則是演示的使用Setter或函數(shù)來控制name和age的值,防止錯(cuò)誤的值被錄入。

public class Person
{
    private int _age;
    private string _name;

    public int Age
    {
        get => _age;
        set
        {
            if (value >= 0 && value <= 200 && value != _age)
                _age = value;
        }
    }

    public void SetName(string newName)
    {
        if (!string.IsNullOrWhiteSpace(newName))
            _name = newName;
    }
}

上面的例子描述了class對(duì)外的封裝。

同樣道理,程序的模塊之間也是如此,模塊之間不應(yīng)該任意的暴露內(nèi)容出來,而應(yīng)該通過良好定義的接口來實(shí)現(xiàn)模塊之間的協(xié)作。

這種封裝的設(shè)計(jì)方式隔離了模塊內(nèi)部的設(shè)計(jì),只要模塊的對(duì)外接口不產(chǎn)生變化,模塊內(nèi)部的任意變化都不會(huì)對(duì)模塊間協(xié)作造成影響,從而實(shí)現(xiàn)子系統(tǒng)間的隔離。

實(shí)例

假如我們現(xiàn)在要設(shè)計(jì)一個(gè)系統(tǒng),你可以簡單的理解成某個(gè)在線商城的發(fā)貨的或物流子系統(tǒng)。
這個(gè)子系統(tǒng)需要完成幾個(gè)基本的功能:

  • 將貨物寄送出去(發(fā)貨),這是最基本的功能
  • 其他的子系統(tǒng)需要一個(gè)API來查詢當(dāng)前的寄送的進(jìn)度
  • 發(fā)貨后,需要得到發(fā)貨的運(yùn)輸船的信息
  • 如果客戶臨時(shí)又取消了訂單,則依據(jù)具體的寄送進(jìn)度通知對(duì)應(yīng)的運(yùn)輸船取消訂單
  • 后來業(yè)務(wù)擴(kuò)大了,我們可能需要考慮增加其他的運(yùn)輸方式,例如空運(yùn)。

我們首先分析一下,應(yīng)該如何來完成這個(gè)功能。

首先,我們需要一個(gè)運(yùn)輸中心(ShippingCenter),使用這個(gè)運(yùn)輸中心,可以寄送本公司任意的產(chǎn)品(Product)。
我們需要返回運(yùn)輸船的信息,為了更好的表達(dá)我的想法,這里理解成要返回運(yùn)輸船的實(shí)例。
我們的運(yùn)輸船能直接告訴我們當(dāng)前運(yùn)輸?shù)臓顟B(tài),如果在運(yùn)輸前需要召回任何產(chǎn)品,我們的運(yùn)輸船會(huì)安排人自動(dòng)的處理。

所以偽代碼大概就長這個(gè)樣子,ShippingCenter能運(yùn)輸貨物,并返回運(yùn)送這個(gè)貨物的運(yùn)輸船實(shí)例,然后運(yùn)輸船本身有函數(shù)和屬性來查看狀態(tài)和召回貨物。

也就是說,這是對(duì)外的最基本的接口:

class ShippingCenter{
    Steamship Ship(Product p);
}
class Steamship{
    ShipingProgress DeliveryProgress{get;}
    void Recall(Product p);
}

可是,以后業(yè)務(wù)的擴(kuò)張,增加了空運(yùn),那怎么辦呢?
假如ShippingCenter不要返回SteamShip的具體類就好了。

那么,不如我們?cè)黾右粋€(gè)接口吧,比如,就叫IVehicle好了,其實(shí)vehicle也是個(gè)抽象的概念,我在iciba上查到vehicle的含義是『交通工具』。

慢……這里再引申出一個(gè)知識(shí)點(diǎn):抽象類。

抽象類到底是個(gè)啥?

我在面試的時(shí)候有時(shí)候會(huì)問到這個(gè)問題,好多同學(xué)給出的答案就是abstract,里面不能寫具體的實(shí)現(xiàn)。

我得補(bǔ)充一下,如果將抽象類跟現(xiàn)實(shí)的生活聯(lián)系起來,這樣就可以更好的理解。

抽象類就是包含的信息還不夠具體化,于是就只能是抽象類。

舉個(gè)例子,剛剛說的vehicle:交通工具。

如果你要寫個(gè)類,叫做交通工具,這個(gè)就應(yīng)該寫成抽象類。
為什么呢?

因?yàn)榻煌üぞ哌@個(gè)概念本來就是抽象的概念,我們常常接觸到的交通工具就有:

  • 自行車、摩托車、三輪車

  • 小轎車、公共汽車、卡車

  • 地鐵

  • 飛機(jī)

  • 輪船

    ?

    所以說,今天早上小王問你今天怎么來上班的,你的回答是,我開車來的,或者是我坐公交、地鐵來的。

你肯定不會(huì)回答說:『我是坐交通工具來的?!?,如果你這樣回答,小王會(huì)很懵逼的。

與此類似的概念比如哺乳動(dòng)物、形狀、玩具、機(jī)械裝置等等,當(dāng)有人跟你提到這些詞時(shí),你的腦海里是無法浮現(xiàn)出它的具體形象的,你只知道,他們屬于某個(gè)類別。

扯了一段閑話,轉(zhuǎn)回正題。

我們的運(yùn)輸中心,需要一個(gè)抽象的概念:那么,用接口還是抽象類呢?

這里暫時(shí)不扯這個(gè)問題了,不然離題越來越遠(yuǎn)。

今天在這里就選用接口好了(好像沒有按常理出牌啊~~~)。

所以,這個(gè)系統(tǒng)大致變成了這個(gè)樣子:

class ShippingCenter{
    IVehicle Ship(Product p);
}
class Steamship : IVehicle{
    ShipingProgress DeliveryProgress{get;}
    void Recall(Product p);
}

可能這里還是有點(diǎn)不夠形象,我先放出整段的代碼。

public class ShippingCenter
{
    public static IVehicle Ship(Product productToDeliver)
    {
        var vehicle = GetIVehicle();
        Internalivery(vehicle, productToDeliver);
        return vehicle;
    }

    private static IVehicle GetIVehicle()
    {
        if (DateTime.Now.Millisecond % 2 == 1)
        {
            return new AirliftPlane();
        }

        return new Steamship();
    }

    private static void Internalivery(IVehicle vehicle, Product delivery)
    {
        // some logic to deliver the Product.
    }
}

public enum ShippingProgress
{
    Preparing,
    Shipping,
    Deliveryed,
}
public interface IVehicle
{
    ShippingProgress DeliveryProgress { get; }
    void Recall(Product delivery);
}

public class Product
{
    public Product(string name, int weight)
    {
        Name = name;
        Weight = weight;
    }

    public string Name { get; }
    public int Weight { get; }
}

internal class AirliftPlane : IVehicle
{
    public ShippingProgress DeliveryProgress => ShippingProgress.Preparing;

    public void Recall(Product delivery)
    {
        // recall product if the Shipping Progress is still ShippingProgress.Preparing
    }
}

internal class Steamship : IVehicle
{
    public ShippingProgress DeliveryProgress => ShippingProgress.Deliveryed;
    public void Recall(Product delivery)
    {
        // if it's already devivered, maybe recalling is not allowed anymore.
        // or some other business logic.
    }
}

外部調(diào)用時(shí),就像這樣:

var p1 = new Product("iPad", 15);
var vehicle = ShippingCenter.Ship(p1);
Console.WriteLine($"current ship status {vehicle.DeliveryProgress}");
Console.WriteLine("now I want to cancel it.");
vehicle.Recall(p1);
var p2 = new Product("Books", 123);
vehicle = ShippingCenter.Ship(p2);
vehicle.Recall(p2);

其他的子系統(tǒng)只需要調(diào)用ShippingCenter.Ship()函數(shù),然后返回一個(gè)IVehicle的接口,這個(gè)接口上可以調(diào)用DeliveryProgess的屬性來獲取當(dāng)前的運(yùn)輸狀態(tài),也可以調(diào)用vehicle上的Recall()函數(shù)來召回產(chǎn)品。
所以,該系統(tǒng)的外部接口是

  • ShippingCenter.Ship()

  • IVehicle.DeliveryProgress

  • IVehicle.Recall()

    這就回到本文最初的話題:子系統(tǒng)內(nèi)部的封裝。對(duì)外只有這三個(gè)接口。只要我的對(duì)外接口沒變,其他的都不是問題。

所以,我要加空運(yùn)、陸運(yùn)、太空運(yùn)都是系統(tǒng)內(nèi)部的事情,我們內(nèi)部的事情我們自己處理,你們統(tǒng)統(tǒng)不要管,你管的太多,你的腦子會(huì)亂掉的。所以,安安心心的交給我們物流子系統(tǒng)處理就好了。

所以,再進(jìn)行分層和架構(gòu)的時(shí)候,妥善的考慮對(duì)外的接口是很有必要的。如果你的代碼位于不同的程序集,考慮更多的使用internal關(guān)鍵字,切勿動(dòng)不動(dòng)就是public。

如果你的類用上了public,那么其他開發(fā)者自然就可能調(diào)用到你的public函數(shù),如果他們調(diào)用這些函數(shù)很多,你的子系統(tǒng)就不夠獨(dú)立,更像是一種千絲萬縷的復(fù)雜網(wǎng)狀關(guān)系了。

由于代碼很簡單,也不需要做過多的解釋,但也稍微提一下:

  • ShippingProgress是個(gè)枚舉,表示可能的運(yùn)輸狀態(tài)
  • Product表示我們要運(yùn)輸?shù)漠a(chǎn)品
  • AirliftPlane表示我們以后的空運(yùn)方式
  • Steamship就是我們的水運(yùn)方式
  • ShippingCenter就是我們的運(yùn)輸中心,它可以Ship產(chǎn)品,也能以接口方式返回當(dāng)前運(yùn)輸方式的那個(gè)實(shí)例。GetIVechicle()模擬一種運(yùn)輸方式的選擇,比如這個(gè)產(chǎn)品中含有電池,可能不能空運(yùn);這個(gè)是加急件,首先空運(yùn)等等,但是為了簡化動(dòng)作,我只是取模而已,這不是本文的重點(diǎn)。
  • IVehicle上能返回ShippingProgress狀態(tài),也能召回產(chǎn)品。

至此,封裝部分到此為止,記住,勿濫用public關(guān)鍵字哦。

小春微信

引用地址:https://1few.com/architecture-encapsulation/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,189評(píng)論 25 708
  • (一)Java部分 1、列舉出JAVA中6個(gè)比較常用的包【天威誠信面試題】 【參考答案】 java.lang;ja...
    獨(dú)云閱讀 7,265評(píng)論 0 62
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,741評(píng)論 18 399
  • 時(shí)間:20160913 地點(diǎn):微信群(媽媽微課) 分享人:周之堯 主題:如何掌握夫妻相處秘籍,成就幸福美滿婚姻 聽...
    summerlight閱讀 609評(píng)論 0 0
  • 教練是什么?_? 教練是長期的伙伴關(guān)系,以成果為導(dǎo)向,通過聆聽、發(fā)問讓客戶找到自己的資源,發(fā)掘自身潛力,解決問題。...
    張昭奕閱讀 1,016評(píng)論 0 0

友情鏈接更多精彩內(nèi)容