深入分析面向对象中的封装作用

简介:

之前我(梦在旅途)发表过一篇名为《深入分析面向对象中的对象概念》的文章,里面主要讲解了类与对象的关系以及对象的概念、状态、行为、角色几个知识点,让大家对对象有了一些深入的了解,而本文则再来谈谈面向对象的三大特性之一:封装,封装是实现面向对象的基础,深入的理解封装的特性,有利于我们能更好的领悟面向对象的思想以及实现面向对象编程。以下均为本人凭借多年开发经验总结的个人观点,大家若发现有误或不足的地方,欢迎指正和交流,谢谢!

一、什么是封装?

顾名思义,封:封闭,装:装起来,将具体的实现细节装到一个容器中,并封闭起来,防止容器外部直接访问容器内部的实现细节,仅公开暴露指定的访问路径;生活中的例子:到饭馆吃饭,你只需要跟服务员讲你要吃什么饭菜,服务员就会跟后台厨房的厨师说明一下,等待一段时间后,饭菜就被服务员直接递送到你面前,作为客户的你是不知道也无需知道饭菜的具体做法,后台厨房的厨师及做饭菜的过程就是被封装的细节,而服务员则是公开暴露的访问路径,你只能通过服务员点餐然后获得饭菜,而不能直接去到后台厨房要求厨师如何做饭菜;

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
     /// <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.封装成常量/变量:
如:计算圆周长度,未封装前的代码如下:

1
2
3
//封装前:
decimal  result = 2 * 3.141592653M * 10.8M;
Console.WriteLine( "圆周长度是:{0}" , result);

封装后的代码如下:

1
2
3
4
5
6
7
//封装后:
const  decimal  PI = 3.141592653M;
decimal  radius = 10.8M;
 
decimal  circumference = 2 * PI * radius;
 
Console.WriteLine( "圆周长度是:{0}" , circumference);

你觉得哪种可读性更高一些呢?从我看来,很显然封装后的代码更易被他人所理解,因为圆周长的计算公式就是:C=2πr;从circumference就知道是圆周长的结果,而等号右边刚好符合圆周长计算公式,所以非常的直观,可读性由此体现出来;
2.封装成方法/函数/属性:

1
2
3
4
5
6
7
8
9
10
//计算圆周长
static  decimal  ComputeCircumference( decimal  radius)
         {
             const  decimal  PI = 3.141592653M;
             return  2 * PI * radius;
         }
 
 
//用法:
Console.WriteLine( "圆周长度是:{0}" , ComputeCircumference(10.8M));

通过封装成方法后,我们看到ComputeCircumference方法,就知道是计算圆周长,同时我可以用此方法来计算所有的不同半径的圆的周长,可读性、复用性由此体现出来;
3.封装成类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
     /// <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,意为数学.图形,从名字就知道这个程序集或命名空间下都是用来处理数学与图形相关的。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
namespace  Math.Shape
{
      public  class  Circle
      {
          //省略,同上
       }
 
}
 
//用法:
Math.Shape.Circle circle =  new  Math.Shape.Circle(10, 10, 10.8M);
Console.WriteLine( "圆周长度是:{0}" , circle.GetCircumference());

四、封装的禁忌

1. 忌封装过度

如:

1
2
3
4
5
6
7
string  a =  "a" ;
string  b =  "b" ;
string  c =  "c" ;
string  d =  "d" ;
string  joinString = a + b + c + d;
 
Console.WriteLine(joinString);

改进后的代码:

1
2
string  joinString =  "{0}{1}{2}{3}" ;
joinString =  string .Format(joinString,  "a" "b" "c" "d" );

这是典型的封装过度,太过原子化,为每一个字符串都定义一个变量,代码量增加,且效率也不高,而改进后代码精简且效率高。

当然还有一些封装过度,比如:一个方法或一个类的代码量非常多,假设有一个数学计算类,可以计算所有的数字类型和所有的数学计算方法,想象一下它的代码量会有多少,这个时候就应该考虑进行适当的拆分封装,至少可以拆成数学类型类及数学计算类。
2. 忌不恰当的封装

如:

1
2
3
4
5
6
7
8
9
static  bool  IsNullOrEmpty( string  str)
{
     return  string .IsNullOrEmpty(str);
}
 
static  bool  IsNotNullOrEmpty( string  str)
{
     return  ! string .IsNullOrEmpty(str);
}

从上述代码可以看出,String的IsNullOrEmpty已经可以满足需求,但有些人可能还会画蛇添足,增加这么两个类,而即使是为了想不写string.这样的,那也没有必需写两个方法,一个方法可以了,因为这两个方法本身就是对立的,只可能同时存在一种情况,可以进行如下改进:

1
2
3
4
5
6
7
8
9
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等,只要明白其原理,举一反三就能达到“无招胜有招”的最高境界。

本文转自 梦在旅途 博客园博客,原文链接:http://www.cnblogs.com/zuowj/p/5427291.html  ,如需转载请自行联系原作者

相关文章
|
6月前
|
安全 Java 数据安全/隐私保护
|
6月前
|
Java 数据安全/隐私保护
Java面向对象编程:封装技术详解
Java面向对象编程:封装技术详解
72 0
|
6月前
|
C++
C++ 数据封装的方法,重点是其编程思想
在C++中,数据封装一般指的是将数据和操作这些数据的函数绑定在一起的程序设计方式。通常使用C++的类来实现
62 7
|
6月前
|
Serverless PHP
当谈论面向对象编程时,这四个概念是非常重要的。以下是对接口、继承、封装和多态的简要说明
本文介绍了面向对象编程的四个核心概念:接口、继承、封装和多态。接口定义对象的行为规范,类通过实现接口确保符合所需行为。继承允许在已有类基础上创建新类,实现代码重用。封装是将数据和操作捆绑并隐藏内部细节,提高安全性和可维护性。多态使对象在运行时能表现出不同行为,增加代码灵活性和可扩展性。文中还提供了代码示例来说明这些概念。
38 0
|
Java
Java面向对象基础2——封装
简单来说,封装就是正确地设计对象的属性。要注意的是,对象代表什么,就封装对应的数据,并提供数据的对应行为
105 0
Java面向对象基础2——封装
|
程序员 数据安全/隐私保护 C++
C++面向对象封装特性的实例分析与应用扩展(一)
生活中充满复杂性,处理复杂性的方法之一就是简化和抽象。在计算中,为了根据信息与用户之间的接口来表示它,抽象是至关重要的。将问题的本质特征抽象出来,并根据特征来描述解决方案。抽象往往是用户定义类型的捷径,在C++中用户定义类型指的就是实现抽象接口的类设计。
127 1
C++面向对象封装特性的实例分析与应用扩展(一)
|
安全 Java
java面向对象三大特性,封装篇
1.封装的概念 在面向对象程式设计方法中,封装是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。 封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。
103 0
|
编译器 C语言 C++
C++面向对象封装特性的实例分析与应用扩展(二)
封装意义一: 在设计类的时候,属性和行为写在一起,表现事物 语法: class 类名{ 访问权限: 属性 / 行为 }; 示例1:设计一个圆类,求圆的周长
148 0
C++面向对象封装特性的实例分析与应用扩展(二)
|
设计模式
【设计模式】装饰者模式 ( 概念 | 适用场景 | 优缺点 | 与继承对比 | 定义流程 | 运行机制 | 案例分析 )
【设计模式】装饰者模式 ( 概念 | 适用场景 | 优缺点 | 与继承对比 | 定义流程 | 运行机制 | 案例分析 )
288 0
下一篇
无影云桌面