连载05:软件体系设计新方向:数学抽象、设计模式、系统架构与方案设计(简化版)(袁晓河著)

置换的特征

现在我们分析,使用“置换”这样的操作对于软件设计有什么样的好处?为什么采用这样的置换手法,就能够化腐朽为神奇呢?

面向对象的置换特征

上面的方式中,我们将继承表示一种置换过程,抽象表示一种与继承相反的置换过程,这两个过程虽然都可以在逻辑上划归为置换,但是对于一个类来说,其实包含对一个实体进行不同的划分,什么意思呢?就是说将某些属性和能够完成的操作聚集在一起,这个是表示对整个世界的一种划分方式,不同的类就有不同的划分,这些划分包含了具有相同置换的基本元素,也包含了具有不同置换的基本元素,在继承类这种置换中,在语义上是子类对父类的置换,在实际效用中,是部分的置换过程,但是只要具有这样的替换方式,我们都认为继承也是一种置换的过程,这种置换过程也是上面提到的部分置换过程。
同时,另一个容易被忽略的情况就是组合方式的使用,其实也是保留一个对另一个体系的“置换”过程,而对于组合方式的指针或引用的替换过程,也应该归属于部分置换的过程。
我们将一个可置换的变量和一个可置换的函数(动作)作为一个整体,其表达了一个对象,而且其置换的函数与之变量之间有相关的关系,而在继承关系中,为什么多态的置换(即函数的置换)是最有效的呢?那是因为函数多态的置换是具有双向的特点。
父类函数《--》子类函数
而变量的置换具有单一方向性
变量《-值
所以在继承体系中,如果使用值替换以后,其在抽象意义上是一个不可逆的过程,这破坏了抽象能力的实现价值。
另外,函数这种双向置换也不是一定有效的,如果在一个继承体系中,其父类与之类函数所表达的意义不一致或相反,则其抽象的作用也就会被破坏。
一个经典的例子就是如果正方形继承与长方形,其设置长方形的长和宽的长度函数则不能为正方形的多态置换,因为正方形的边是相等的,设置正方形的长和宽相等情况下才是正方形的边长,因此是在父类函数和子类函数只有一个有条件下的双向置换,所以其双向置换不一定有效。
使用指针或者引用的置换,让其值置换具有了方向性,其间接地址传递这种方向性,所以这种置换是一种横向的置换,与继承相垂直交叉的置换,其作用域超越了这个对象,通过其他对象或对象的部分来置换当前的操作或值。
因此继承和组合都具有相似的“置换”分类和方式,所以其在效果方面是等价的,两种方式都可以进行相互转换。也就是说可以推广到只要有地址置换就具有等效的抽象能力。后面我们可以看到,这两种方式在置换操作的效果是一致的。而为什么使用组合比使用继承更好?
刚才谈到,继承关系中,其目前的实现都是通过虚函数的双向置换进行的,但是继承也可能存在对具体类的继承,那么就存在单向的置换方式的值或者非虚函数,这导致其父类的某些特性无法通过继承方式得到置换,而对于组合来说,其对某个类的使用就是其引用(指针也属于引用的一种),此时完全达到对其的置换,所以使用组合比使用继承这种方式对可扩展性能够得到更好的体现。
但是,如果继承接口的方式,由于其所有的函数都是双向置换的,所以继承于接口,比继承于具体类来说,能够获得更大的扩展性,其耦合程度就更小。所以依赖接口比依赖于具体类更好。对于不管是继承接口还是继承于具体类的方式,在置换过程中都具有双向特性,但是对于继承于具体类,由于两个方向上可能存在非等价的双向置换,而继承接口则可以在双向置换上具有等价的效果,这样这两个在置换方向上具有对称性,大家可以从数学中几何学知道,一个对称性就代表了其中的置换操作所形成的集合,往往具有“群”的特性。这种具有“群”特性的置换具有很多很强的特殊性,而就是这种更为特殊的特性,才让其所进行的操作具有更多的特性,具有更加广泛的应用。所以继承接口比继承具体类将具有更加灵活的变换和更加优美的特征。
这里有一些需要提及的是,继承方式的置换是表达虚函数是否能够被子类的虚函数所覆盖,而目前所规定的能力中,成员变量是不能被子类所覆盖,也就不具有置换的能力。
也许上面的推论有点让人不太清楚和明了,也许用爱因斯坦的一句话来表达“Everything should be made as simple as possible, but not simpler”,我们可以通过一些数学实例来说明:比如正方形是矩形中最特殊的一种矩形,一般的矩形其对称线有两个,而正方形的对称线就可以达到四个,而在图形中,四个对称线的图形就比两个对称线的图形变化方式要多,其数学特性也会更多,例如正方形可以作为一种菱形的,其图形也会更加的优美。
回过头来,我们看看下图的继承关系:


 
图1-5


 
图1-6


  
图1-7
图1-5中A可以置换到C,也可以置换到D,而图1-6中A可能置换到C,但不能置换到D。那么为什么图1-7的方式也不是很好的方式呢?
这主要是因为图1-7的这种置换是全量置换,即A的全部信息都必须能够让C和D置换,而图1-5中只要A的部分让C和D进行置换,这样也是增加了相关性的灵活度,所以可以看出继承是一个可全量置换的处理方式,虽然使用private的方式进行隔离这种置换,但是其只是对全量置换的一种限制。
所以,在判断是否用继承还是用组合的方式,需要根据其置换是否是全量的还是部分的概念。
然而,接口也是多个虚函数的分组,虽然在函数声明中,其具有双向置换,但是接口的定义是静态的置换过程,而要在运行的时候更改这些置换的函数的定义,或者在这个虚函数分组中新增或删除虚函数,都变得非常困难,所以对于语言来说,为了达到更好的可扩展性需要新增动态接口的特性,以此支持完全的接口双向置换。
由于接口的这种双向性,所以在《java与模式》一书中提到一个显著的例子,就是在接口中定义常量,这种方式为什么不好呢?从实用的角度来看,主要是这些常量会影响所有接口的继承者和接口的调用者,由于需求的变化,这样的常量可能被废弃不用,也可能需要发生更改,此时对其的操作都会导致接口发生变化而影响继承和调用,从置换的角度我们也可以知道,常量本身体现的是一个代码级的置换,是符号的置换,是单向的置换,我们无法通过改变继承者的行为来改变其常量的定义,通俗的说,就连赋值一样的操作我们都无法运用,所以其耦合程度肯定比使用双向的接口的要强的多,因此我们需要尽量避免在接口中定义产品。

面向方面的置换特征


如今在网络上正炙手可热的就是面向方面编程,先来让我们理解一下什么是面向方面,在网络上是这样描述的:面向方面可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。面向方面实际是延续设计模式中为了调用者和被调用者之间的解除耦合。
其思想就是通过在一个系统中设置不同的关注点,这些关注点能够独立出这个系统,即不耦合在这个系统中,在运行时候,将这些独立的关注点织入系统中运行,在源代码层面上来看,是不需要任何的代码更新的。
面向对象编程是对业务处理过程的对象的属性和行为进行抽象封装,然后通过继承体系来达到引用效果一致的置换过程。
而面向方面则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。这有点类似于面向对象中的组合引用,但是面向方面是将组合引用中的对这个整体进行置换,而不是组合引用中对某个行为操作进行置换,这两种设计思想在目标上有着本质的差异。
就我个人的浅见:面向对象是模块化继承体系,是从纵向进行置换的过程,而面向方面是模块化横切体系,是从横向置换的过程。所以面向方面也是置换的不同表现方式。而这些关注点,在一个架构中是横向进行挖取,所以又称为横切点。这些各种各样的横切点也可以从继承体系中具有继承关系的置换方式,所以面向方面的过程也是一种置换的操作过程。


泛型编程的置换特征


泛型编程的置换特征就明显的多,其主要特点就是共同体和具体之间的置换过程。通过某个特征对这一族进行置换,这种置换也不是抽象的过程,而是体现的一般性和特殊性不同层次上的置换过程。在泛型编程中有一个术语叫做refinement过程,更能够说明这个过程可以通过不断的共同化以后不断的归一化。
首先,让我们看看typename的定义,这里可以通过置换的方式,将一个类或者类型通过符号的置换,可以具体化到某一个类或者类型。例如下面对strack的定义可以看出:
template <typename T>
class Stack
{
private:
std::vector<T> elems;
public:
void push(T const &);
void pop();
T top() const ;
bool empty() const;
}
在这个Stack中,所包含的类型可以通过一个具有空间实体的类或者类型来进行置换,如果用搞一个int置换完成以后,在运行态下,其Stack就会变成一个int类型大小为实体的一个栈的操作。
然后,让我来看看迭代器的应用。对于迭代器来说,其定义为指针的一般化过程。但是迭代器是具有不同层次的置换情况构成的:
在《泛型编程与STL》中对迭代器的层次高度概括为下面的方式(图1-8):


在不同的层次上都可以置换上一个具体的类型,而这些层次上的置换之间就具有一个refinement的过程,这个过程说明反方向上,其置换的过程是可以达到。
而泛型中的trait技术,也同样可以说明置换所起到的重要作用。trait所使用的最根本的置换方式是使用typedef,这种直接性的置换定义,将不同类型名称关联在一起,就像一个链,可以通过这个链达到对某个类型的使用或者限制。
例如在《C++ Templates》书中详细描述了trait技术:
template<typename T>
class AccumulationTraits;


template<>
class AccumulationTraits<char>
{
public:
typedef  int  AccT;
}


template<>
class AccumulationTraits<short>
{
public:
typedef  int  AccT;
}


template<>
class AccumulationTraits<int>
{
public:
typedef  long  AccT;
}


template<>
class AccumulationTraits<unsigned int>
{
public:
typedef  long  AccT;
}


template<>
class AccumulationTraits<float>
{
public:
typedef  double  AccT;
}
在使用的时候,可以根据所传入类型进行自动匹配。
template<typename T>
inline
typename AccumulationTraits<T>::AccT accum(T const * beg,
                                       T const *end)
{
typedef  typename AccumlationTraits<T>::AccT AccT;
    … … …
}


在trait技术中,其typedef作为最为基本的置换方式,而trait主要是将这样的一个或者多个typedef进行封装,封装以后再进行置换,从而达到对整体的置换。所以在STL中将之描述为:
template<class C, class T, class Dist = ptrdiff_t>
struct iterator
 {
    typedef C iterator_category;
    typedef T value_type;
    typedef Dist distance_type;
 };
同理,对于函数对象、偏特化等等特性都是由置换形成的因素,这里就不一一列举了。置换在泛型中作为一种基本的符号置换,其起到了最为基本的因素。


设计原则体现的置换特性


首先我们来看看哪些设计原则与置换的联系呢?

开放-封闭原则


定义:一个软件实体如类、模块和函数应该对扩展开放,对修改封闭。
1、“对于扩展是开发的”,这意味模块的行为是可以扩展的。当应用的需求改变时,可以对模块进行扩展,是其具有满足那些改变的新行为,换句话说,我们可以改变模块的功能。
2、“对于更改是封闭的”,对模块行为进行扩展时,不必改动模块的源代码或者二进制代码,模块的二进制可执行版本,无论是可链接的库、DLL或者Java的.jar文件,都无需要改动。
开放-封闭原则是所有面向对象原则的核心,软件设计本身所追求的目标就是封装变化、降低耦合,而开放封闭原则正是对这一目标的最直接体现。从上面的两方面的描述则说明,开放-封闭原则关键要求模块具有“抽象和多态”的能力,而这种能力就是“置换”的能力,能够在业务逻辑变化的点上具有通过更好的置换法则进行更好的替换。置换的不同点之间尽量小的相互影响,从置换层次来说,可以做到物理层次上的可链接的库、DLL或者Java的.jar文件的置换,也可以做到在逻辑层次上具有抽象和多态的处理。
在开放-封闭原则中,能够开放的就是这些能够“置换”的基本结构,例如虚函数、指针、引用等等,而且要让开放以后系统的相互影响最小,则就需要能够进行完全置换的结构对象,例如接口。

实现开放封闭的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。让类依赖于固定的抽象,所以对修改就是封闭的。从中可以看出,依赖的方向是向上“置换”的抽象,而向上“置换”就是只保留了具有“双向”置换能力的特性,而丢弃特殊化的非“双向”置换的特征。

Liskov替换原则

子类型必须能够替换掉它们的基类型。
继承复用的基石,只有当子类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而子类也能够在基类的基础上增加新的行为。所有引用基类的地方必须能完整置换其子类的对象。如果不能进行正确的置换,那么会导致行为上的矛盾。所以通过置换的思想也可以看出,不仅仅在继承体系中,在所有纵向和横向的扩展过程中,只有具有“置换”能力才不会产生矛盾的行为。

依赖倒置原则

高层模块不应该依赖于底层模块,二者都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。
说明部分置换和扩展置换应该遵循的原则,部分倒置和扩展倒置在依赖上是不一样的,部分倒置不需要根据被置换的具体细节所影响,比如B和C都继承于A,那么A对B的置换和A对C的置换都是等价的,在形式上都是一致的,不会因为B和C的不一样而存在差异。同理,扩展置换要求具体细节能够覆盖抽象体,还是以B和C都继承于A为例,B扩展置换A或者C扩展置换A,表示B或者C能够置换A,如果存在不一致,那么这种置换过程就是一个违背替换原则的置换,就不是真正意义的扩展置换。


猜你喜欢

转载自blog.51cto.com/13832308/2132739
今日推荐