盲人摸象之于软件编程

2016-11-07  本文已影响61人  going_hlf
盲人摸象

盲人摸象这个成语故事,大家一定不陌生。它比喻对事物只凭片面的了解或局部的经验,就乱加猜测,想做出全面的判断。用来讽刺人的目光短浅,片面。多少年来,这个成语一直作为一个贬义词沿用至今。

而在最近一些年,很多软件大师包括一些国外的大师都借用盲人摸象这个典故作为软件编程的一个隐喻,形象地说明了很多软件开发的原则和理念。在软件编程的世界里,盲人摸象的故事成了一个褒义词。

可能大家已经能够想到了,盲人摸象跟软件设计中的封装、隐藏的概念是如此的契合。封装和隐藏的概念在面向对象(C++、JAVA等)的世界里是核心内容,即便是在面向过程的C语言中,如今的编程理念也处处体现了封装的味道。之所以如此强调封装和隐藏,是因为软件设计中解耦的需要。我们常常挂在嘴边的高内聚低耦合,就是要我们做到很好的封装和隐藏。

下面举几个封装和隐藏的例子,一起体会一下:

C语言程序中,数据结构+算法的一段程序可能是这个样子。
//data.h

void processDataA();
void processDataB();

//data.c

DATA gDataA = 0;
DATA gDataB = 0;

void processDataA()
{
    // call gDataA
}

void processDataB()
{
    // call gDataB
}

下面对gDataAgDataB的使用可能是你不期望的,但是你无法阻拦,原因是你把数据暴露给了所有人。
//other.c

extern DATA gDataA;
extern DATA gDataB;

void otherFunc()
{
    // call gDataA
    // call gDataB
}

为了阻止这种随意访问,我们可以用类对数据和方法做个封装,实现数据的私有化,像下面这个样子。
//data.h

class DataAProcessor
{
public:
    void process();

private:
    DATA dataA;
};

class DataBProcessor
{
public:
    void process();

private:
    DATA dataB;
};

//data.cpp

void DataAProcessor::process()
{
    //call dataA
}

void DataBProcessor::process()
{
    //call dataB
}

这样,再企图去滥用数据dataAdataB的函数,将会被拒之门外。

#include "data.h"

void otherFunc()
{
    // call DataAProcessor::dataA, error
    // call DataBProcessor::dataB, error
}

其实类除了限制了数据的访问权限,更重要的是,它把数据和操作数据的方法作为一个完整的概念聚合了在了一起。

在高内聚、低耦合的概念如此深入人心的今天,封装和隐藏技术已经不仅仅体现在类上,它无处不在。在编程的世界里,人们应该是自私的,在没有足够的理由暴露某个数据之前,它应该是被隐藏的。

让我们继续探讨信息隐藏。
假设我们需要新增DataAProcessorprint方法。
//data.h

class DataAProcessor
{
public:
    void process();
    void print();

private:
    DATA dataA;
};

class DataBProcessor
{
public:
    void process();

private:
    DATA dataB;
};

这个操作会导致所有包含了data.h的文件都要重新编译。而对于下面的dataBUser.cpp,实在是编译的很冤枉。因为它并没有使用DataAProcessor中的任何数据和成员,由于DataAProcessor中新增了一个方法,却导致了自己的重新编译。
//dataBUser.cpp

#include "data.h"

void handleDataB()
{
    DataBProcessor::process();
}

这种情况在我们的项目工程中十分常见,不合理的头文件依赖,牵一发而动全身,导致了编译时间加长,令人难以忍受。

仍然是使用隔离和隐藏技术,我们把文件拆开,从这个意义上讲,文件也是一种封装技术。
//dataA.h

class DataAProcessor
{
public:
    void process();

private:
    DATA dataA;
};

//dataB.h

class DataBProcessor
{
public:
    void process();

private:
    DATA dataB;
};

//dataA.cpp

void DataAProcessor::process()
{
    //call dataA
}

//dataB.cpp

void DataBProcessor::process()
{
    //call dataB
}

这样分开后,变得清爽了很多,各自互不影响。一个文件对应一个类,同时对应一个概念。

再进一步,假设DataBProcessor需要新增一个方法,该方法仅仅类内部使用。但凡有隐藏意识的人,都很容易想到使用private。可能像这样:
//dataB.h

class DataBProcessor
{
public:
    void process();

private:
    bool isDataAvailable();

private:
    DATA dataB;
};

//dataB.cpp

void DataBProcessor::isDataAvailable()
{
    return dataB != 0;
}

void DataBProcessor::process()
{
    if(isDataAvailable())
    {
        //call dataB
    }
}

能够想到把方法isDataAvailable private起来,已经对信息隐藏有了很好的意识。但是,在dataB.h中让用户看到这个方法,仍然显得有些碍眼,从某种意义上讲,我们把自己的实现细节暴露给了用户。而原本我们只应该暴露用户接口。

通常,不需要给用户使用的方法,不要放在头文件中(这个对于C语言同样适用),应该把它完全隐藏在实现文件中。为了防止跟别的文件中的函数重名,可以使用匿名命名空间namespace,像这样:

//dataB.cpp

namespace
{
    void isDataAvailable(DATA data)
    {
        return data != 0;
    }
}

void DataBProcessor::process()
{
    if(isDataAvailable(dataB))
    {
        //call dataB
    }
}

我们称这种文件规划和信息隐藏技术为物理设计。物理设计已经成为一个衡量软件设计好坏的重要指标。

上面说了一大堆面向对象的信息隐藏技术,有人可能要说,我们现在用的是C语言,怎么用的你方法做信息隐藏?其实上面已经说到了,信息隐藏是一种编程理念,并非针对某种编程语言的特有的技术。

对于C语言,用static修饰函数,不把函数声明暴露在头文件中(前提是这个函数没有外部用户使用),就是一项很好的封装和信息隐藏技术,其实static等同于C++的namespace

但是大多数传统的C程序员,一个固定的思维是,在.c里面实现的函数,一定要在.h中声明。甚至有人危言耸听说不这样做,编译不过。

对于C语言,把一堆相互关联的数据和方法归类集中在同一个文件中,就是一种概念的抽象和封装。这就是为什么有面向对象思想的程序员,写出的C程序,也处处透出面向对象的味道。

这篇文章就到这里,欢迎大家一起探讨。

上一篇 下一篇

猜你喜欢

热点阅读