逻辑实体:诸如类和函数,为“皮肉”;
物理结构:“骨架”;
逻辑实体分布于许多物理实体,诸如文件和目录中。
对于逻辑关系,一般有以下三种:
1.组件是物理设计的最小单位
类、函数、枚举等都是组成这些组件的逻辑实体。特别的是,每个类定义都刚好只驻留在一个组件中。
1) 一个组件刚好由一个头文件(.h) 和 一个实现文件(.c) 构成。
一个组件通常定义一个或者多个密切相关的类并被认为适合所支持抽象的任何自由运算符,如下图每一个组件都既有物理试图也有逻辑视图。物理视图由.h
文件和.c
文件组成。
2) 组件是合适的逻辑设计和物理设计的基本单位,这至少有三个理由:
1) 一个组件常常把跨越许多逻辑实体(例如,类与自由运算符)的大量易管理的内聚功能打包到单一的物理单元。
2) 一个组件不仅作为单一的实体捕获整个抽象,而且允许考虑不通过类级别设计来解决物理问题。
3)一个适当设计的组件(作为一个物体实体不像类那样)可以作为单一的单位从系统中提出来,不必重写就可以在另一个系统中有效地重用。
3) 一个组件的逻辑接口是以编程方式可访问或可被用户检测到的。
组件的逻辑接口是定义在头文件中的类型和功能的集合,可以被该组件的用户以编程方式访问。由于组织原因,存在于.h 文件中的私有细节将被封装,并不属于逻辑接口。
一个类的公共接口由该类公共成员的接口的联合体组成,同样,一个组件的“公共”接口由该组件.h文件中所有公共成员函数、typedef、枚举和自由(运算符)函数的集合组成。
2.物理设计规则
1) 在一个组件内部声明的逻辑实体,不应该在该组件的外部定义。
以下是不合理的组件逻辑规则,其中 “stack.h” 中的push(int i) 在intset.c 定义,”stack.h” 中的 pop() 在main.c 定义。
组成一个组件的.c文件和.h 文件的根名称应该完全匹配。
2) 每个组件的.c 文件都应该将包含它自己的.h 文件作为第一行独立的代码。
//wildthing.h
#ifndef INCLUDE_WILDTHING
#define INCLUDE_WILDTHING
(这里其实有点错误,少声明了什么)
class WildThing{
public:
WildThing();
// ...
};
ostream& operator<<(const ostream& o,const WildThing& thing);
#endif
//wildthing.c
#include <iostream.h>
#include "Wildthing.h"
int main()
{
WildThing wild;
//...
cout<<wild<<endl;
return 0;
}
这个wildthing.c 编译完全没有问题,因为编译器首先递归的把
//wildthing.c
#include "Wildthing.h"
#include <iostream.h>
int main()
{
WildThing wild;
//...
cout<<wild<<endl;
return 0;
}
原因是编译器在递归调用”Wildthing.h”的时候,发现并没有找到ostream这个类,故会报错,应该换成下面的程序:
//wildthing.h
#ifndef INCLUDE_WILDTHING
#define INCLUDE_WILDTHING
class ostream; (需要先声明ostream类)
class WildThing{
public:
WildThing();
// ...
};
ostream& operator<<(const ostream& o,const WildThing& thing);
#endif
通过这种方法,每个组件都能确保自己的头文件对于编译来说都是能够自给自足的。
3) 客户端程序应该包含直接提供所需类型定义的头文件;除了非私有集成,应避免依靠一个头文件去包含另一个头文件。
一个头文件是否应该包含另一个头文件是一个物理问题,而不是一个逻辑问题。
4) 避免这样的定义: 该定义在一个组件的.c文件中带有外部链接,而在相应的.h文件中没有显示的声明。
上图中,我们可以清楚的看到,bar.c 直接调用了 foo.c 里面的函数,而在foo.h 中声明。这样子在查看 foo 组件的时候,就会忽略掉void f(int x, int y)这个接口。
因此,在.c 程序中定义自由函数和全局变量是违反设计规则的。
5) 避免通过局部声明访问另一个组件中带有外部链接的定义,正确的做法是包含该组件的.h文件。
//foo.c
#include "foo.h"
extern "C" double pow(double, int);
double Foo::func(double x, double y)
{
return pow(x,y)+pow(x,y) //这里会报错,因为局部extern中为pow(double,int),而传入的参数为(double, double)
}
我们可以用.h文件来代替,如下所示:
//foo.c
#include "foo.h"
#include <math.h>
double Foo::func(double x, double y)
{
return pow(x,y)+pow(x,y)
}
3.依赖关系
1) 如果为了编译或者链接组件y,需要组件x,则组件y依赖于(DependsOn) 组件x。
在上面说到的 IsA (继承) 和 Uses (在接口或者实现中实现)。
逻辑实体用椭圆形表示, 而物理实体用长方形表示,从上图可以知道,是Plane组件依赖于组件Wing,而不是 Plane 类依赖于 Wing 类。
//str.h
#ifndef INCLUDE_STR
#define INCLUDE_STR
#ifndef INCLUDE_CHARARRAY
#include "chararray.h" //
#endif
class String{
}
2) 如果编译y.c 时需要x.h, 那么组件y呈现了对组件x的编译时依赖。
A. 编译时依赖
如下图所示,组件str依赖于组件chararray,由于组件str中的str.c在编译的时候依赖于str.h, 且str.h 依赖于 chararray.h, 则我们有str.c 间接依赖于chararray.h
B.链接时依赖
//word.h
#ifndef INCLUDE_WORD
#define INCLUDE_WORD
#ifndef INCLUDE_STR
#include "str.h"
#endif
class Word{
String d_string; //HasA
//...
public:
Word();
//...
}
//str.h
#ifndef INCLUDE_STR
#define INCLUDE_STR
#include "chararray.h"
class String
{
CharArray *d_array_p;
//...
public:
String();
//...
};
#endif
//word.c
#include "word.h"
//...
//str.c
#include "str.h"
//...
编译 chararray.c 时需要 chararray.h。编译str.c 时需要str.h 和 chararray.h。 最后,编译word.c 时需要word.h 和 str.h。注意,编译word.c 不需要chararray.h, 编译时没有依赖,但是word仍呈现出对 chararray 的物理依赖。
3) 如果目标文件y.o (由编译y.c 生成)包含未定义的符号,目标文件y.o在链接时可能直接或者间接地调用x.o 来帮助解析这些符号,那么就说明组件y呈现了对组件x的一种链接时的依赖。
在C++中,除了内联函数,C++中所有的成员函数和具有外部链接。
如果一个组件为了编译需要包含另一个组件,那么它将在链接时依赖该组件,在目标代码层次上解析未定义的符号。
4) 一个编译时依赖几乎总是隐含一个链接时依赖。
如上图所示,word.o 依赖定义在 str.o 中的外部名称。间接依赖于chararray.o. 需要用来解析未定义的符号。
4.隐含依赖
1) 定义了某个函数的组件,通常会物理依赖于定义了某个类型(组件定义的函数所需要的类型)的任意组件。
//two.c
#include "two.h"
#include "one.h"
//...
int Two::getInfo(const One& one)
{
return one.info();
}
//...
//one.h
#ifndef INCLUDE_ONE
#define INCLUDE_ONE
class One{
//...
int info() const;
//...
};
//...
#endif
如以上程序所示,Two::getInfo 调用了类 One 的 const 成员函数 info,这就要求two.c 要看到 One的定义了。
2) 如果一个组件定义了IsA 或 HasA 用户自定义类型的一个类,那么该组件总会编译时依赖于定义了该类型的组件。
从一个类型 (IsA) 派生或将实例嵌入一个类型(HasA)
Word 类继承了 String类,同时 类String 在实现中使用了 CharArray类,因此 Word 类间接依赖于 CharArray 类。
5.提取实际的依赖
1) 仅凭C++ 预处理器#include 指令所产生的包含图,足以推断出一个系统内提供系统编译的所有物理依赖。
2) 只有当组件x实质性地直接使用了一个类或定义在y中的自由运算符函数(自由运算符函数可以在.h文件中被定义)时,组件x 才应该包含y.h。
6. 友元关系
1) 避免将(远距离的)友元关系授权给在另一个组件中定义的逻辑实体。
如果一个类进行友元关系的一个声明,那么,按照封装的定义,该声明就不是这个类的一个封装细节。friend 声明本身是类的接口的一部分——实际的函数定义却不是如此。
2) 一个组件内的友元关系是该组件的一个实现细节。
只要局部授权友元关系(即只要它只授权给在相同组件内定义的逻辑实体),那么这些友元实际上就与授权友元的对象不可分离地结合在一起了。
3) 若组件无法通过一个组件的逻辑接口,以编程方式访问或检测一个所包含的实现细节(类型、数据或函数),则称该组件封装了该实现细节。
4) 为相同组件内定义的类授权(局部的)友元关系不会违反封装。
友元关系虽然表面上是一个逻辑关系,却会影响物理设计。在一个组件内部,(局部的)友元关系是该组件的一个封装的实现细节。为了改进可用性和用户拓展性,一个容器常常会把同一组件的一个迭代器视作友元,这不会破坏封装。
参考:大规模C++ 程序设计/(美) 洛科什 (Lakos. J.)著; 刘冰,张林译. —北京: 机械工业出版社,2014.8