之前我(梦在旅途)发表过一篇名为《深入分析面向对象中的对象概念》的文章,里面主要讲解了类与对象的关系以及对象的概念、状态、行为、角色几个知识点,让大家对对象有了一些深入的了解,而本文则再来谈谈面向对象的三大特性之一:封装,封装是实现面向对象的基础,深入的理解封装的特性,有利于我们能更好的领悟面向对象的思想以及实现面向对象编程。以下均为本人凭借多年开发经验总结的个人观点,大家若发现有误或不足的地方,欢迎指正和交流,谢谢!
一、什么是封装?
顾名思义,封:封闭,装:装起来,将具体的实现细节装到一个容器中,并封闭起来,防止容器外部直接访问容器内部的实现细节,仅公开暴露指定的访问路径;生活中的例子:到饭馆吃饭,你只需要跟服务员讲你要吃什么饭菜,服务员就会跟后台厨房的厨师说明一下,等待一段时间后,饭菜就被服务员直接递送到你面前,作为客户的你是不知道也无需知道饭菜的具体做法,后台厨房的厨师及做饭菜的过程就是被封装的细节,而服务员则是公开暴露的访问路径,你只能通过服务员点餐然后获得饭菜,而不能直接去到后台厨房要求厨师如何做饭菜;
示例代码如下:
/// <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等,只要明白其原理,举一反三就能达到“无招胜有招”的最高境界。