这是这个系列的第二篇。在第二篇里,我决定讲一讲封装。 程序的不同部分应该用封装去互相隔离,模块之间应该不应该产生很随意的关联。
可能有的人觉得不解,又或觉得是有道理的废话,不急,先一步一步来。
我们先来看看面向对象的三个基本特征是什么?
如果你是科班毕业,这 6 个字应该是你第一次学到类(class)的时候就听老师说了。 我们老师的话大概是这样的:
在类里面,封装就是通过一些手段来限制类外部的访问,依此隔离出类相对封闭的区域。
也就是说,如果有人想要操作类里面的成员(field),不应该让它直接进行这样操作。而应该通过良好定义的函数(或属性的 Setter)来完成。除非你有不得不如此的理由,否则就不应该让人家直接访问你的私有成员。
下面的代码通常是 bad practice。 任意的类均能任意的修改 Person 内的 Name 和 Age,即便 Name 写成乱码或将 Age 设成负数,都是可以做到的,Person 类自己是控制不住的。
- public class Person
- {
- public int Age;
- public string Name;
- }
下面的代码则是演示的使用 Setter 或函数来控制 name 和 age 的值,防止错误的值被录入。
- 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 对外的封装。
同样道理,程序的模块之间也是如此,模块之间不应该任意的暴露内容出来,而应该通过良好定义的接口来实现模块之间的协作。
这种封装的设计方式隔离了模块内部的设计,只要模块的对外接口不产生变化,模块内部的任意变化都不会对模块间协作造成影响,从而实现子系统间的隔离。
假如我们现在要设计一个系统,你可以简单的理解成某个在线商城的发货的或物流子系统。 这个子系统需要完成几个基本的功能:
我们首先分析一下,应该如何来完成这个功能。
首先,我们需要一个运输中心(ShippingCenter),使用这个运输中心,可以寄送本公司任意的产品(Product)。 我们需要返回运输船的信息,为了更好的表达我的想法,这里理解成要返回运输船的实例。 我们的运输船能直接告诉我们当前运输的状态,如果在运输前需要召回任何产品,我们的运输船会安排人自动的处理。
所以伪代码大概就长这个样子,ShippingCenter 能运输货物,并返回运送这个货物的运输船实例,然后运输船本身有函数和属性来查看状态和召回货物。
也就是说,这是对外的最基本的接口:
- class ShippingCenter {
- Steamship Ship(Product p);
- }
- class Steamship {
- ShipingProgress DeliveryProgress {
- get;
- }
- void Recall(Product p);
- }
可是,以后业务的扩张,增加了空运,那怎么办呢? 假如 ShippingCenter 不要返回 SteamShip 的具体类就好了。
那么,不如我们增加一个接口吧,比如,就叫 IVehicle 好了,其实 vehicle 也是个抽象的概念,我在 iciba 上查到 vehicle 的含义是『交通工具』。
慢…… 这里再引申出一个知识点:抽象类。
抽象类到底是个啥?
我在面试的时候有时候会问到这个问题,好多同学给出的答案就是 abstract,里面不能写具体的实现。
我得补充一下,如果将抽象类跟现实的生活联系起来,这样就可以更好的理解。
抽象类就是包含的信息还不够具体化,于是就只能是抽象类。
举个例子,刚刚说的 vehicle:交通工具。
如果你要写个类,叫做交通工具,这个就应该写成抽象类。 为什么呢?
因为交通工具这个概念本来就是抽象的概念,我们常常接触到的交通工具就有:
所以说,今天早上小王问你今天怎么来上班的,你的回答是,我开车来的,或者是我坐公交、地铁来的。
你肯定不会回答说:『我是坐交通工具来的。』,如果你这样回答,小王会很懵逼的。
与此类似的概念比如哺乳动物、形状、玩具、机械装置等等,当有人跟你提到这些词时,你的脑海里是无法浮现出它的具体形象的,你只知道,他们属于某个类别。
扯了一段闲话,转回正题。
我们的运输中心,需要一个抽象的概念:那么,用接口还是抽象类呢?
这里暂时不扯这个问题了,不然离题越来越远。
今天在这里就选用接口好了(好像没有按常理出牌啊~~~)。
所以,这个系统大致变成了这个样子:
- class ShippingCenter {
- IVehicle Ship(Product p);
- }
- class Steamship: IVehicle {
- ShipingProgress DeliveryProgress {
- get;
- }
- void Recall(Product p);
- }
可能这里还是有点不够形象,我先放出整段的代码。
- 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.
- }
- }
外部调用时,就像这样:
- 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);
其他的子系统只需要调用 ShippingCenter.Ship() 函数,然后返回一个 IVehicle 的接口,这个接口上可以调用 DeliveryProgess 的属性来获取当前的运输状态,也可以调用 vehicle 上的 Recall() 函数来召回产品。 所以,该系统的外部接口是
这就回到本文最初的话题:子系统内部的封装。对外只有这三个接口。只要我的对外接口没变,其他的都不是问题。
所以,我要加空运、陆运、太空运都是系统内部的事情,我们内部的事情我们自己处理,你们统统不要管,你管的太多,你的脑子会乱掉的。所以,安安心心的交给我们物流子系统处理就好了。
所以,再进行分层和架构的时候,妥善的考虑对外的接口是很有必要的。如果你的代码位于不同的程序集,考虑更多的使用 internal 关键字,切勿动不动就是 public。
如果你的类用上了 public,那么其他开发者自然就可能调用到你的 public 函数,如果他们调用这些函数很多,你的子系统就不够独立,更像是一种千丝万缕的复杂网状关系了。
由于代码很简单,也不需要做过多的解释,但也稍微提一下:
至此,封装部分到此为止,记住,勿滥用 public 关键字哦。
小春 原创内容 本文地址:http://www.cnblogs.com/asis/p/architecture-encapsulation.html https://1few.com/architecture-encapsulation/ |
---|
来源: http://www.cnblogs.com/asis/p/architecture-encapsulation.html