這是這個(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)鍵字哦。
