【封装那些事】 泄露的封装

泄露的封装

抽象通过公有接口(方法)暴露或泄露实现细节时,将导致这种坏味。需要注意的是,即使抽象不存在“不充分的封装”坏味,其公有接口也有可能泄露实现细节。

为什么不能泄露封装?

为实现有效封装,必须将抽象的接口(即抽象的内容)和实现(即抽象的方式)分离。为遵循隐藏原则,必须对客户程序隐藏抽象的实现方面。

如果通过公有接口暴露了实现细节(违反了隐藏原则)可能会造成:

  • 对实现进行修改时,可能会影响客户程序
  • 暴露的实现细节可能会让客户程序能够通过公有接口访问内部数据结构,进而有意或无意地损坏抽象的内部状态。

泄露的封装的潜在原因

不知道该隐藏哪些东西

开发人员通常会在无意之间泄露实现细节。

使用细粒度接口

类的公有接口直接提供了细粒度的方法,这些细粒度的方法通常会向客户程序暴露不必要的实现细节。更好的做法是在类的公有接口提供粗粒度的方法,在粗粒度方法内部使用细粒度的私有方法。

示例分析一

我们用程序来维护一个待办事项列表。在ToDoList类中,公有方法GetListEntries()返回对象存储的待办事项列表。

public class ToDoList
{
    private List<string> listEntries = new List<string>();

    public List<string> GetListEntries()
    {
        return listEntries;
    }

    public void AddListEntry(string entry)
    {

    }
}

问题出在方法的返回类型上,它暴露了一个内部细节,ToDoList内部使用List来存储待办事项列表。

现在问题来了,如果待办事项列表程序主要执行插入和删除操作,那么选择使用List没啥问题;但是后来发现查找频率比修改频率高,那么使用HashTable可能更合适。然而GetListEntries()的返回类型是List,如果修改这个方法的返回类型,可能破坏依赖于这个方法的客户程序。如果要支持未来数据结构的变更,方法返回类型可以使用IEnumerable(C#中的集合类型都实现的接口类型),这样可以做到在不改变方法签名的条件下(里氏替换原则),替换存储待办事项列表的数据结构。

重构后的代码实现:

使用List数据结构:

public class ToDoList
{
    private List<string> listEntries = new List<string>();

    public IEnumerable GetListEntries()
    {
        return listEntries;
    }

    public void AddListEntry(string entry)
    {

    }
}

使用Hashtable数据结构:

public class ToDoList
{
    private Hashtable listEntries = new Hashtable();

    public IEnumerable GetListEntries()
    {
        return listEntries;
    }

    public void AddListEntry(string entry)
    {

    }
}

方法GetListEntries()存在另一个严重的问题是,它返回一个指向内部数据结构的引用,通过这个引用,客户程序可以绕过AddListEntry()方法直接修改数据结构。当然如果使用IEnumerable这个问题也就迎刃而解了,因为IEnumerable接口没有相应的针对于某一种数据集合的操作。

public interface IEnumerable
{
    //
    // 摘要:
    //     返回循环访问集合的枚举数。
    //
    // 返回结果:
    //     一个可用于循环访问集合的 System.Collections.IEnumerator 对象。
    [DispId(-4)]
    IEnumerator GetEnumerator();
}

示例分析二

假设显式图像包含4个步骤,这些步骤必须按照特定顺序执行,图形才可以正常显式。

现在在Image类中提供4个公有方法Load(),Process(),Validate(),Show()供客户程序使用,但是这样有一个很麻烦的问题是写客户程序的开发人员不一定会按照正确顺序调用方法使用(永远不要给客户选择的权利)。而且客户程序只是想要显式图像,我们为什么要向它们暴露4个内部步骤呢?这就是泄露的封装的潜在原因——使用细粒度接口。

public class Image
{
    public void Load()
    {
    }
    public void Process()
    {
    }
    public void Validate()
    {
    }
    public void Show()
    {
    }
}

要解决这个问题,可以让Image类只向客户程序暴露一个方法Display(),然后在这个方法内部按照特定顺序调用4个步骤方法。

public class Image
{
    private void Load()
    {
    }
    private void Process()
    {
    }
    private void Validate()
    {
    }
    private void Show()
    {
    }

    public void Display()
    {
        Load();
        Process();
        Validate();
        Show();
    }
}

总结

  1. 抽象通过公有接口暴露或泄露了实现细节时,客户程序可能直接依赖于实现细节吗,这种直接依赖性使得难以在不破坏既有客户代码的情况下对设计进行修改或扩展。
  2. 抽象泄露了内部数据结构时,抽象的完整性遭到了破坏。增加了代码运行阶段发生问题的可能性。

参考:《软件设计重构》

原文发布于微信公众号 - 撸码那些事(lumanxs)

原文发表时间:2018-05-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏信安之路

利用Java反射和类加载机制绕过JSP后门检测

JSP 后门,一般是指文件名以 .jsp 等后缀结尾的,可运行于 Java servlet 及相关容器和组件内的通用 JSP 脚本。

1530
来自专栏北京马哥教育

做到这二十条,Python程序性能轻松翻倍!

1.优化算法时间复杂度 算法的时间复杂度对程序的执行效率影响最大,在Python中可以通过选择合适的数据结构来优化时间复杂度,如list和set查找某一个元素的...

3397
来自专栏恰同学骚年

设计模式的征途—17.模板方法(Template Method)模式

在现实生活中,很多事情都需要经过几个步骤才能完成,例如请客吃饭,无论吃什么,一般都包含:点单、吃东西、买单等几个步骤,通常情况下这几个步骤的次序是:点单=>吃东...

563
来自专栏恰同学骚年

《你必须知道的.NET》读书笔记一:小OO有大智慧

此篇已收录至《你必须知道的.Net》读书笔记目录贴,点击访问该目录可以获取更多内容。

372
来自专栏一个会写诗的程序员的博客

ES6 极简教程(ES6 Tutorial) 文 / 东海陈光剑ES6 极简教程(ES6 Tutorial)Kotlin 开发者社区

JavaScript是ECMAScript的实现和扩展,由ECMA(一个类似W3C的标准组织)参与进行标准化。ECMAScript定义了:

643
来自专栏IT派

Python 性能优化的20条招数

算法的时间复杂度对程序的执行效率影响最大,在 Python 中可以通过选择合适的数据结构来优化时间复杂度,如 list 和 set 查找某一个元素的时间复杂度分...

813
来自专栏撸码那些事

【封装那些事】 泄露的封装

抽象通过公有接口(方法)暴露或泄露实现细节时,将导致这种坏味。需要注意的是,即使抽象不存在“不充分的封装”坏味,其公有接口也有可能泄露实现细节。

33615
来自专栏草根专栏

C# 7.0简而言之 -- 01. C#和.NET Framework简介

C#里面所有的类型都有一个共享的基类, 这也意味之C#里面所有的类型都具备一些相同的基本功能, 例如任何类型都可以通过调用ToString()方法来转化成字符串...

4699
来自专栏ZRJ的专栏

spark 写 gp/tpg 效率优化:写入 237w 行数据耗时从 77 分钟到 34 秒

写这个文章的点主要是分享一下spark 写 gp/tpg 效率优化 ,这个过程中的一些思路历程和细节。

1.2K1
来自专栏北京马哥教育

做到这二十条,Python程序性能轻松翻倍!

算法的时间复杂度对程序的执行效率影响最大,在Python中可以通过选择合适的数据结构来优化时间复杂度,如list和set查找某一个元素的时间复杂度分别是O(n)...

1015

扫码关注云+社区