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

277 阅读4分钟
mark

泄露的封装

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

为什么不能泄露封装?

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

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

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

##泄露的封装的潜在原因

不知道该隐藏哪些东西

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

使用细粒度接口

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

示例分析一

我们用程序来维护一个待办事项列表。在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. 抽象泄露了内部数据结构时,抽象的完整性遭到了破坏。增加了代码运行阶段发生问题的可能性。

参考:《软件设计重构》


                                                    -----END-----


                  喜欢本文的朋友们,欢迎扫一扫下图关注公众号撸码那些事,收看更多精彩内容