C#面向对象 之 封装

R | | 访问(21)

  案例代码:

/// <summary>
/// ATM类
/// </summary>
public class ATM
{
	#region 定义私有方法,隐藏具体实现         
	private Client GetUser(long userID)
	{
		Client client = new Client();
		client.Name = "小吴";
		client.Age = 18;
		client.Password = "123456";
		return client;
	}

	private bool IsValidUser(Client user)
	{
		return true;
	}

	private int GetCash(int money)
	{
		return 0;
	}
	#endregion

	#region 定义公有方法,提供对外接口     
	public void CashProcess(long userID, int money)
	{
		Client tmpUser = GetUser(userID);
		if (IsValidUser(tmpUser))
		{
			GetCash(money);
		}
		else
		{
			Console.Write("你不是合法用户,是不是想被发配南极?");
		}
	}
	#endregion
}

/// <summary>
/// 用户类
/// get和set对属性的读写控制,是通过实现get和set的组合来实现的,
/// 如果属性为只读,则只实现get访问器即可;
/// 如果属性为可写,则实现set访问器即可。
/// </summary>
public class Client
{

	private string name;// 用户姓名      
	public string Name
	{
		get { return name; }
		set
		{
			name = value == null ? String.Empty : value;
		}
	}

	private int age;// 用户年龄    
	public int Age
	{
		get { return age; }
		set
		{
			// 这样的方式来实现对年龄进行写控制时,自然会弹出异常提示,从而达到了保护数据完整性的目的。
			if ((value > 0) && (value < 150))
			{
				age = value;
			}
			else
			{
				throw new ArgumentOutOfRangeException("年龄信息不正确。");
			}
		}
	}

	private string password;// 密码      
	public string Password
	{
		get { return password; }
		set
		{
			if (value.Length < 6)
				password = value;
		}
	}
}

  创建一个类需要思考的问题:

  (1)类的功能是什么?

  (2)哪些是字段,哪些是属性,哪些是方法?

  (3)对外提供的公有方法有哪些,对内隐藏的私有变量有哪些?

  (4)类与类之间的关系是继承还是聚合?

  1、封装的第一个原则就是:将字段定义为private。

  字段(field)通常定义为private,表示类的状态信息。CLR支持只读和读写字段。值得注意的是,大部分情况下字段都是可读可写的,只读字段只能在构造函数中被赋值,其他方法不能改变只读字段。

public class Client { 
 private string name;// 用户姓名     
 private int age;// 用户年龄    
 private string password;// 用户密码 
}

  类的字段信息最好以私有方式提供给类的外部,而不是以公有方式来实现,否则不适当的操作将造成不必要的错误方式,破坏对象的状态信息,数据安全性和可靠性无法保证。

  2、属性(property)通常定义为public,表示类的对外成员。

  属性具有可读、可写属性,通过get和set访问器来实现其读写控制。

  get和set对属性的读写控制,是通过实现get和set的组合来实现的,如果属性为只读,则只实现get访问器即可;如果属性为可写,则实现set访问器即可。

  通过对公共属性的访问来实现对类状态信息的读写控制,主要有两点好处:一是避免了对数据安全的访问限制,包含内部数据的可靠性;二是避免了类扩展或者修改带来的变量连锁反应。

  至于修改变量带来的连锁反应,表现在对类的状态信息的需求信息发生变化时,如何来减少代码重构基础上,实现最小的损失和最大的补救。例如,如果对client的用户姓名由原来的简单name来标识,换成以firstName和secondName来实现,如果不是属性封装了字段而带来的隐藏内部细节的特点,那么我们在代码中就要拼命地替换原来xiaoWang.name这样的实现了。例如:

private string firstName; 
private string secondName; 
public string Name 
{
 get 
 { 
  return firstName + secondName; 
 } 
}

  这样带来的好处是,我们只需要更改属性定义中的实现细节,而原来程序xiaoWang.name这样的实现就不需要做任何修改即可适应新的需求。你看,这就是封装的强大力量使然。

  3、方法(method)封装了类的行为,提供了类的对外表现。

  用于将封装的内部细节以公有方法提供对外接口,从而实现与外部的交互与响应。例如,从上面属性的分析我们可知,实际上对属性的读写就是通过方法来实现的。因此,对外交互的方法,通常实现为public。

  通常将在内部的操作全部以private方式来实现,而将需要与外部交互的方法实现为public,这样既保证了对内部数据的隐藏与保护,又实现了类的对外交互。

  谁为公有、谁为私有,取决于需求和设计双重因素,在职责单一原则下为类型设计方法,应该广泛考虑的是类本身的功能性,从开发者与设计者两个角度出发,分清访问权限就会水到渠成。

  4、封装小结:

  (1)字段通常定义为private,属性通常实现为public,而方法在内部实现为private,对外部实现为public,从而保证对内部数据的可靠性读写控制,保护了数据的安全和可靠,同时又提供了与外部接口的有效交互。这是类得以有效封装的基础机制。

  (2)通常情况下的理解正如我们上面提到的规则,但是具体的操作还要根据实际的设计需求而定,例如有些时候将属性实现为private,也将方法实现为private是更好的选择。例如在ATM类中,可能需要提供计数器来记录更新或者选择的次数,而该次数对用户而言是不必要的状态信息,因此只需在ATM类内部实现为private即可;同理,类型中的某些方法是对内部数据的操作,因此也以private方式来提供,从而达到数据安全的目的。

  (3)从内存和数据持久性角度上来看,有一个很重要但常常被忽视的事实是,封装属性提供了数据持久化的有效手段。因为,对象的属性和对象一样在内存期间是常驻的,只要对象不被垃圾回收,其属性值也将一直存在,并且记录最近一次对其更改的数据。

  (4)在面向对象中,封装的意义还远不止类设计层面对字段、属性和方法的控制,更重要的是其广义层面。我们理解的封装,应该是以实现UI分离为目的的软件设计方法,一个系统或者软件开发之后,从维护和升级的目的考虑,一定要保证对外接口部分的绝对稳定。不管系统内部的功能性实现如何多变,保证接口稳定是保证软件兼容、稳定、健壮的根本。

  所以OO智慧中的封装性旨在保证:

  1)隐藏系统实现的细节,保证系统的安全性和可靠性。

  2)提供稳定不变的对外接口。因此,系统中相对稳定部分常被抽象为接口。

  3)封装保证了代码模块化,提高了软件的复用和功能分离。

  5、封装规则

  (1)尽可能地调用类的访问器,而不是成员,即使在类的内部。其目的在我们的示例中已有说明,例如Client类中的Name属性就可以避免由于需求变化带来的代码更改问题。

  (2)内部私有部分可以任意更改,但是一定要在保证外部接口稳定的前提下。

  (3)将对字段的读写控制实现为属性,而不是方法,否则舍近而求远,非明智之选。

  (4)类封装是由访问权限来保证的,对内实现为private,对外实现为public。再结合继承特性,还要对protected,inter-nal有较深的理解。

  (5)封装的精华是封装变化。张逸在《软件设计精要与模式》一书中指出,封装变化是面向对象思想的核心,他提到开发者应从设计角度和使用角度两方面来分析封装。因此,我们将系统中变化频繁的部分封装为独立的部分,这种隔离选择有利于充分的软件复用和系统柔性。

  结论:

  封装是什么?封装就是一个包装,将包装的内外分为两个空间,对内实现数据私有,对外实现方法调用,保证了数据的完整性和安全性。