之前我(夢在旅途)發表過一篇名為《深入分析面向對象中的對象概念》的文章,裡面主要講解了類與對象的關系以及對象的概念、狀态、行為、角色幾個知識點,讓大家對對象有了一些深入的了解,而本文則再來談談面向對象的三大特性之一:封裝,封裝是實作面向對象的基礎,深入的了解封裝的特性,有利于我們能更好的領悟面向對象的思想以及實作面向對象程式設計。以下均為本人憑借多年開發經驗總結的個人觀點,大家若發現有誤或不足的地方,歡迎指正和交流,謝謝!
一、什麼是封裝?
顧名思義,封:封閉,裝:裝起來,将具體的實作細節裝到一個容器中,并封閉起來,防止容器外部直接通路容器内部的實作細節,僅公開暴露指定的通路路徑;生活中的例子:到飯館吃飯,你隻需要跟服務員講你要吃什麼飯菜,服務員就會跟背景廚房的廚師說明一下,等待一段時間後,飯菜就被服務員直接遞送到你面前,作為客戶的你是不知道也無需知道飯菜的具體做法,背景廚房的廚師及做飯菜的過程就是被封裝的細節,而服務員則是公開暴露的通路路徑,你隻能通過服務員點餐然後獲得飯菜,而不能直接去到背景廚房要求廚師如何做飯菜;
示例代碼如下:
/// <summary>
/// 飯館
/// </summary>
class FanGuan
{
private string doFoodName;
/// <summary>
/// 點餐
/// </summary>
/// <param name="foodName"></param>
public void ChooseFood(string customer, string foodName)
{
doFoodName = foodName;
Console.WriteLine("顧客:{0},點餐:{1}", customer, foodName);
}
/// <summary>
/// 獲得飯菜
/// </summary>
/// <returns></returns>
public string GetFood()
{
string cookeResult = CookeFood("廚師甲");
return string.Format("{0},請您用餐,謝謝!", cookeResult);
}
/// <summary>
/// 廚師做菜,私有方法,外部不可通路
/// </summary>
/// <param name="cooker"></param>
/// <returns></returns>
private string CookeFood(string cooker)
{
Console.WriteLine("廚師:{0}開始做菜:{1}>>>>", cooker, doFoodName);
Console.WriteLine("開火");
Console.WriteLine("放油");
Console.WriteLine("放食材,翻炒");
Console.WriteLine("加入佐料");
Console.WriteLine("菜熟起鍋,盛到盤子遞給服務員");
Console.WriteLine("結束<<<<");
return string.Format("菜:{0}已做好", doFoodName);
}
}
//實際用法:
static void Main(string[] args)
{
FanGuan fanGuan = new FanGuan();
fanGuan.ChooseFood("夢在旅途", "紅燒茄子");
string food = fanGuan.GetFood();
Console.WriteLine(food);
Console.WriteLine("用餐");
Console.ReadKey();
}
該示例非常簡單,示範結果就不再截圖出來了。
二、封裝的作用是什麼?
1.隔離性:
被封裝後的對象(這裡的對象是泛指代碼的程式設計單元,一般指:程式集,命名空間,類,方法,屬性,變量等)其外部對象是無法直接通路對象的内部實作細節,内部實作細節的的改動不會影響到外部對象的通路原則(即:對象内部修改後,在公開暴露指定的通路路徑不變的情況下,外部通路它的對象是無需修改的),這是隔離性的展現,同時也是實作高内聚,低耦合的最根本的思想之一;
2.可複用性:
被封裝後的對象可以被外部多個對象通路,而無需為每個外部對象去指定不同的服務對象;如:所有的對象的基類都是object類,object類裡面的公共成員可以被其所有子類使用,Ado.Net相關的資料通路類及其公共成員均可被其它所有的對象使用等。
3.可讀性:
被封裝後的對象的名稱(如:程式集名,類名,方法名)如果命名恰當,那麼就能在不看裡面的實作細節的前提下,了解該對象的作用;如:DataTable就是用來裝表格資料的;ToString就是轉換為字元串,Length就是指長度等。
三、封裝的範圍有哪些?
1.封裝成常量/變量:
如:計算圓周長度,未封裝前的代碼如下:
//封裝前:
decimal result = 2 * 3.141592653M * 10.8M;
Console.WriteLine("圓周長度是:{0}", result);
封裝後的代碼如下:
//封裝後:
const decimal PI = 3.141592653M;
decimal radius = 10.8M;
decimal circumference = 2 * PI * radius;
Console.WriteLine("圓周長度是:{0}", circumference);
你覺得哪種可讀性更高一些呢?從我看來,很顯然封裝後的代碼更易被他人所了解,因為圓周長的計算公式就是:C=2πr;從circumference就知道是圓周長的結果,而等号右邊剛好符合圓周長計算公式,是以非常的直覺,可讀性由此展現出來;
2.封裝成方法/函數/屬性:
//計算圓周長
static decimal ComputeCircumference(decimal radius)
{
const decimal PI = 3.141592653M;
return 2 * PI * radius;
}
//用法:
Console.WriteLine("圓周長度是:{0}", ComputeCircumference(10.8M));
通過封裝成方法後,我們看到ComputeCircumference方法,就知道是計算圓周長,同時我可以用此方法來計算所有的不同半徑的圓的周長,可讀性、複用性由此展現出來;
3.封裝成類:
/// <summary>
/// 圓類
/// </summary>
class Circle
{
//原點X坐标
public int OriginX
{ get; set; }
//原點Y坐标
public int OriginY
{ get; set; }
//半徑
public decimal Radius
{ get; set; }
public Circle(int originX, int originY, decimal radius)
{
this.OriginX = originX;
this.OriginY = OriginY;
this.Radius = radius;
}
/// <summary>
/// 擷取圓周長度
/// </summary>
/// <returns></returns>
public decimal GetCircumference()
{
const decimal PI = 3.141592653M;
return 2 * PI * this.Radius;
}
}
//用法:
Circle circle = new Circle(10,10,10.8M);
Console.WriteLine("圓周長度是:{0}", circle.GetCircumference());
從上述示例代碼可以看出,我定義(封裝)了一個圓類,圓類有原點及半徑,同時有一個擷取圓周長度的方法,該圓類可以用來表示多個不周大小不同位置的圓,而且都能獲得圓的圓周長,至于圓周長是如何計算的,PI的精度是多少,我們無需知道也無法直接更改,故隔離性、可讀性、複用性都展現出來了;
4.封裝成層/包/程式集:
有的時候因系統架構的需要,我們可能需要将描述各種圖形類資訊的代碼單獨封裝成一個程式集、包、命名空間,以便于代碼的管理,于是我們可以将上述Circle類放到一個單獨的程式集中,同時程式集及命名空間名稱定為:Math.Shape,意為數學.圖形,從名字就知道這個程式集或命名空間下都是用來處理數學與圖形相關的。
namespace Math.Shape
{
public class Circle
{
//省略,同上
}
}
//用法:
Math.Shape.Circle circle = new Math.Shape.Circle(10, 10, 10.8M);
Console.WriteLine("圓周長度是:{0}", circle.GetCircumference());
四、封裝的禁忌
1. 忌封裝過度
如:
string a = "a";
string b = "b";
string c = "c";
string d = "d";
string joinString = a + b + c + d;
Console.WriteLine(joinString);
改進後的代碼:
string joinString = "{0}{1}{2}{3}";
joinString = string.Format(joinString, "a", "b", "c", "d");
這是典型的封裝過度,太過原子化,為每一個字元串都定義一個變量,代碼量增加,且效率也不高,而改進後代碼精簡且效率高。
當然還有一些封裝過度,比如:一個方法或一個類的代碼量非常多,假設有一個數學計算類,可以計算所有的數字類型和所有的數學計算方法,想象一下它的代碼量會有多少,這個時候就應該考慮進行适當的拆分封裝,至少可以拆成數學類型類及數學計算類。
2. 忌不恰當的封裝
static bool IsNullOrEmpty(string str)
{
return string.IsNullOrEmpty(str);
}
static bool IsNotNullOrEmpty(string str)
{
return !string.IsNullOrEmpty(str);
}
從上述代碼可以看出,String的IsNullOrEmpty已經可以滿足需求,但有些人可能還會畫蛇添足,增加這麼兩個類,而即使是為了想不寫string.這樣的,那也沒有必需寫兩個方法,一個方法可以了,因為這兩個方法本身就是對立的,隻可能同時存在一種情況,可以進行如下改進:
static bool IsNullOrEmpty(object obj)
{
if (obj == null)
{
return true;
}
return string.IsNullOrEmpty(obj.ToString());
}
這樣改進後,明顯的IsNullOrEmpty可以用來判斷所有的類型是否為Null 或者 Empty,如果需要判斷不需要為Null 或者 Empty,隻需調用該方法并取反即可,如:!IsNullOrEmpty("zuowenjun")
五、結尾
這篇文章本來打算自去年釋出了《深入分析面向對象中的對象概念》後就立即寫這篇,我一般寫一些總結性很強的文章都是先在WORD中寫好後再COPY過來的,這篇文章同樣也是,但由于之前工作原因一直是隻寫了一個提綱,故今天看到了這篇博文躺在我的檔案夾中,同時又聯想最近我當面試官及新進人員的狀況(我發表過一篇文章《由面試引發的思考:B/S與C/S究竟是何物》),于是果斷在今天花了一個下午的時間,邊想邊寫,終于給寫完了,也希望大家能夠從中受益,這兩篇文章本身沒有很深很新的技術,但作為一個程式員,若想技術上有所造詣,必需先學好基本功,我再重複一下我的觀點:
技術就如同國術,基本功很重要,隻要基本功紮實了,再去學習架構、設計模式,就會比較容易,同時這些看似高大上的東西,如:AOP,SOA,DI,IOC,DDD,CQRS等,隻要明白其原理,舉一反三就能達到“無招勝有招”的最高境界。