架构漫谈系列(2) 封装(Encapsulation)

这是这个系列的第二篇。在第二篇里,我决定讲一讲封装。
程序的不同部分应该用封装去互相隔离,模块之间应该不应该产生很随意的关联。

可能有的人觉得不解,又或觉得是有道理的废话,不急,先一步一步来。

我们先来看看面向对象的三个基本特征是什么?

  • 继承
  • 多态
  • 封装

如果你是科班毕业,这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对外的封装。

同样道理,程序的模块之间也是如此,模块之间不应该任意的暴露内容出来,而应该通过良好定义的接口来实现模块之间的协作。

这种封装的设计方式隔离了模块内部的设计,只要模块的对外接口不产生变化,模块内部的任意变化都不会对模块间协作造成影响,从而实现子系统间的隔离。

实例

假如我们现在要设计一个系统,你可以简单的理解成某个在线商城的发货的或物流子系统。
这个子系统需要完成几个基本的功能:

  • 将货物寄送出去(发货),这是最基本的功能
  • 其他的子系统需要一个API来查询当前的寄送的进度
  • 发货后,需要得到发货的运输船的信息
  • 如果客户临时又取消了订单,则依据具体的寄送进度通知对应的运输船取消订单
  • 后来业务扩大了,我们可能需要考虑增加其他的运输方式,例如空运。

我们首先分析一下,应该如何来完成这个功能。

首先,我们需要一个运输中心(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()函数来召回产品。
所以,该系统的外部接口是

  • ShippingCenter.Ship()

  • IVehicle.DeliveryProgress

  • IVehicle.Recall()

    这就回到本文最初的话题:子系统内部的封装。对外只有这三个接口。只要我的对外接口没变,其他的都不是问题。

所以,我要加空运、陆运、太空运都是系统内部的事情,我们内部的事情我们自己处理,你们统统不要管,你管的太多,你的脑子会乱掉的。所以,安安心心的交给我们物流子系统处理就好了。

所以,再进行分层和架构的时候,妥善的考虑对外的接口是很有必要的。如果你的代码位于不同的程序集,考虑更多的使用internal关键字,切勿动不动就是public。

如果你的类用上了public,那么其他开发者自然就可能调用到你的public函数,如果他们调用这些函数很多,你的子系统就不够独立,更像是一种千丝万缕的复杂网状关系了。

由于代码很简单,也不需要做过多的解释,但也稍微提一下:

  • ShippingProgress是个枚举,表示可能的运输状态
  • Product表示我们要运输的产品
  • AirliftPlane表示我们以后的空运方式
  • Steamship就是我们的水运方式
  • ShippingCenter就是我们的运输中心,它可以Ship产品,也能以接口方式返回当前运输方式的那个实例。GetIVechicle()模拟一种运输方式的选择,比如这个产品中含有电池,可能不能空运;这个是加急件,首先空运等等,但是为了简化动作,我只是取模而已,这不是本文的重点。
  • IVehicle上能返回ShippingProgress状态,也能召回产品。

至此,封装部分到此为止,记住,勿滥用public关键字哦。

小春微信

引用地址:https://1few.com/architecture-encapsulation/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,790评论 25 707
  • (一)Java部分 1、列举出JAVA中6个比较常用的包【天威诚信面试题】 【参考答案】 java.lang;ja...
    独云阅读 7,083评论 0 62
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,598评论 18 399
  • 时间:20160913 地点:微信群(妈妈微课) 分享人:周之尧 主题:如何掌握夫妻相处秘籍,成就幸福美满婚姻 听...
    summerlight阅读 539评论 0 0
  • 教练是什么?_? 教练是长期的伙伴关系,以成果为导向,通过聆听、发问让客户找到自己的资源,发掘自身潜力,解决问题。...
    张昭奕阅读 735评论 0 0