up's profiledupPhotosBlogListsMore ![]() | Help |
|
dup
March 03 关于模块化模块化是迄今为止对付复杂软件系统的最好的办法。 问题并不在于上面这句话。问题在于怎么模块化。这儿的“怎么模块化”并不是说不知道模块化是把系统分拆成几个模块,组合而成系统。而是说不知道以什么规则指导我们进行模块分拆。 当然,关于模块化,有很多非常著名的指导原则,最著名的莫过于“高内聚,低耦合”。可是这个原则有点太过空泛。即便如此,我们也很有必要研究一下这个原则。高内聚的好处是什么?为什么要高内聚?我个人的感觉是,高内聚降低了模块的理解成本,同时潜在的降低了模块组合的困难程度,因为高内聚比较倾向于产生一致性好的接口。低耦合呢?低耦合有一个非常直接的目的,就是为了降低模块的相互依赖程度,以便于:一、更容易替换模块和重用模块;二、强迫模块的接口简单直白。 有了这些铺垫,我准备暂时岔开话题,说说分层体系。 分层逻辑上是把同类【注意:这里是同类而不是同种,以后这儿的差别会被区分的非常清楚】的模块放置到一个叫做层的逻辑模块容器中的一种分拆方式。举例来说:把所有数据库访问类的模块放入到DAL,所有的业务逻辑放入Logical,所有的用户交互放入UI,就是一个经典的分层模式。 对于分层体系,我最大的质疑就是它简直就是低耦合,高内聚的完美反例。首先,它是低内聚的,同一层的子模块,除了逻辑上的同类性,没有别的任何需要聚合在一起的理由。其次,它是高耦合的,层与层之间的耦合关系显然典型的比层内子模块的内聚程度还要高。 那为什么分层模式是如此的流行呢?理由如下: 一、分层给了老板一个完美的项目延期的理由。也因而受到广大的受剥削受压迫的程序的大力吹捧。 二、分层流行的,所以采用分层的风险是小的,所以一定要采用分层,否则自己得全部承担反抗分层的后果,有可能是失败的后果。 三、分层在一定程度上确实解决了某些复杂性问题。 在我看来,第三个理由才是真正的好理由,虽然第一和第二个理由才是最强有力的分层理由。我说说第三个。 我要举ISO的OSI模型为例。OSI是开放系统互联的意思。先假设我们没有任何相关的背景。我们需要制作出一个可扩展的机器系统互相通信的系统。我们可以参考人类已经有的通信系统,当时已经有了语言、电话、电报、IP等等系统可供参考。通信方式从线路交换到包交换都有。问题很快区分出来了。有定址问题,有路由问题,有会话的建立和拆除等控制问题,传输问题,流量控制问题,差错控制问题,进一步还有编码问题,表示问题等等。 所有的这些问题纠缠在一块,并不很容易解决,其复杂度也超出了人类处理复杂度的极限。这时候模块化就来救驾了。由于所有的这些模块是为了一个共同的问题,所以区分出来的模块难免会有很强的耦合。而为了能够控制耦合的强度,采用了分层这种模式,用于限制模块之间任意的耦合,只允许部分模块与另部分模块耦合,也就是所谓的分层。分层就是限制了耦合关系的模块系统,只允许模块跟其上层模块和下层模块有耦合。 所以,分层,模式是为了控制耦合度的一种模块化系统。因此,它确实拥有模块化应该拥有的好处,同时还强迫系统中的模块耦合关系不能是任意的,只能是规定的。 但是,任何把分层模式推往极限的做法都不一定再是对的了。真理往前再走一小步就是谬误了。现在好多系统都是为了分层而分层,结果导致本来该存在于一个模块中的东西却被硬生生的拆分成多个模块,分布在多个层中。形成一种很强的耦合关系,而为了减轻或者消弭这种人为造成的耦合,不断的加入更多的层次,比如我们经常见到的关于VO和DAL,贫血和充血之类的争论就是这方面的争论,这不仅是搬石头上山,而且严重的降低了开发和运行效率。 再岔开一步,我们说说OO。 OO有很多有点,也有一些缺点,当年OO很火很拉风的时候,人人以OO为尊为荣,现在OO成了主流,在理论家或者先行者眼中就成了落魄者了,所以开始很批OO,以OO为卑为耻。其实无论是崇尚还是鄙夷,OO还是OO,它的优点依然存在,跟刚刚被提出来一样有效。而其缺点也依然存在,也跟其刚刚被提出来一样有害。 OO是一种模块化方式。其基本思路就是希望尽可能自然的表达模块,对象作为一个有状态有功能的模块,确实比以前更容易处理模块化这个高深的技术活了。 上面关于OO的讨论,其实可以套用到OB上【或者也叫做ADT】,OO还有个核心的思想是“分类”。其实对于OO的批判主要集中在这个分类上。由于人类思维的飘忽性,昨天认可的分类方式今天却发现不适合,所以OO的分类提供了“继承”来完成这个所谓的思维进化。可是问题是继承只是子类化,只是在类【或者在数学上叫做集合】分拆出一部分作为子类【或者在数学上叫做子集合】。由于人人都有自己的观点,这样就难免会有见仁见智的问题。而且最大的问题就是其实这个所谓的继承根本没有办法完成思维进化,只能在一定程度上完成概念精化的模拟。后来就引入了接口【或者叫做契约、概念】之类的方式{《这儿我并不准备区分编译时还是运行时等等细节问题,所以Cpper也不要吹毛求疵了》},允许多继承,也就是允许了从多个维度进行分类。比如从大小这个维度进行分类,分出:大、中、小三个类别,从颜色这个维度进行分类,分出:红、绿、蓝三个类别,组合这两个维度【多继承】就能得到一个新的类别出来。有了多继承的引入,OO已经比较完善了。 OO就是上面说的那样。其实OO还有一个比较强的隐含假设,那就是,作为一个Object,它是自足的。也就是说,它要能自我存在而不需要强依赖于别的实体。或者换一句话说:它要是完备的。 那么,我们结合OO和分层就能看出,现在UI、Logical、Data这样的分层机制跟OO机制是冲突的。从OO角度来说,订单就是订单,产生订单,收集订单信息,检验订单,签署订单,订单相关交易【现金和商品】,订单完成等等都是订单的功能,订单有其自身的状态,比如:货物的数量、单价、约定时间等等。从分层角度来说,订单的填写属于UI层,订单的处理属于Logical层,订单的永久化属于Data层。 现在,Java界的业务对象慢慢的获得重视,就是要把各个层次的东西组合起来形成一个完备的自足的对象。而Ruby这类语言由于类的动态可扩充性,自然而然的更能采用OO的思路来思考问题。C#语言也提供了扩展类和扩展方法以便于支持LINQ,同时也自然的提供了类Ruby的解决思路。Java界有一个项目叫做Qi4j,就完全是基于组合开发软件的思路,提供的是纯Java接口,而不是JRuby,Jython,Groovy之类新语言【它们都算是Java界的】,对于坚持咬定Java不放松的那些Programmer来说,应该有些吸引力吧。 November 13 TimerAgent一个定时器队列 一个简单的目录遍历器只适合在Windows系统下[包括WinCE]使用。主要是因为依赖于Windows平台的CString和FindFirstFile、FindNextFile以及FindClose系列函数。其实对于CString的依赖并不是必要的,只是方便了Find*系列函数的调用而已。 当然,我知道,boost实现了一个更漂亮的目录访问器,并且显然功能更强,适应性更好,但是由于我想到我仍然不得不在自己的平台上实现一个目录遍历器,因此我觉得它可能仍然是有用的。我就没有办法把boost的实现在WinCE上编译通过,它的代码如同天书般晦涩,我知道这是由于要适应多个平台导致的,可是这仍然阻止不了我不喜欢读它。希望你能喜欢这个简单的实现,虽然有很多限制,但是在相应平台上立即可用的一个小巧实现。 November 09 一点实用代码有限状态机 HTML2TEXT 实现的基本描述: 其中比较差的一点是我对Parameter结构的使用,我是特意这样演示的,主要是想展现对于FSM灵活的使用方式,其实最常见也最实用的方式是只使用this作为参数。 显然,HTML2TEXT可以非常容易得扩展成一个完整的HTML Parse【可以加上DOM支持】,再加上HTML Render【包括CSS的支持】,Javascript支持和HTTP支持,就是一个Web Browser了。 February 16 依赖顺序是罪恶C和C++语言是流行和常用的依赖于顺序的语言。C++略为有点改进,在类范围内部不依赖于顺序。依赖于顺序是一种惩罚,是一种不重视程序员感受的表现,并且在深层次要求采用采用声明和实现分开,因而也就不能维持DRY原则。违背DRY会导致维护复杂。
我详细说明一下为什么依赖顺序是罪恶的。 大家都知道抽象是一种强大的能力,但是,依赖于顺序强迫我们只能从低层次的细节开始,这跟现如今流行的IDE的智能完成一样。我们不得不把所有的细节描述完备,才能构造更大的构件。 我还是用一个实例来说明吧。 我为了练习的目的,试图写一个HTTP Client,自然,我是从阅读RFC规范开始的。通过扫描RFC的目录和快速掠过其内容,我们发现HTTP是一个请求响应模式的简单协议,其请求和响应都是所谓的Message,显然,只要我们能解析和构造这个Message,我们就大致完成任务。 于是就有了: namespace Http
{ class Message { public: MessageHeader headers; CRLF crlf; MessageBody body; std::string toString(void) { return headers.toString() + crlf.toString() + body.toString(); } }; class MessageHeader
{ public: std::vector<Field> fields; std::string toString(void) { std::string result; for (int i = 0; i < fields.size(); ++i) { result += fields[i].toString(); } return result; } }; class Field
{ public: FieldName name; Colon colon; FieldValue value; CRLF crlf; std::string toString(void) { return name.toString() + colon.toString() + value.toString() + crlf.toString(); } }; class FieldName
{ public: FieldNameValue name; std::string toString(void) { std::string result; switch (name) { case CacheControl: result = "Cache-Control"; break; ...... } return result; } FieldNameValue fromString(std::string fieldName)
{ FieldNameValue result; if (fieldName == "Cache-Control") result = CacheControl; else if (...) ... return result; } }; enum FieldNameValue
{ //generic headers CacheControl, Connection, Date, Pragma, ...... } 我想,上面的代码非常的直接了当。几乎不需要解释,类似于递归下降法制作解析器,而事实上,确实是下降的,但是递归没有,因为HTTP的Message格式还没有那么强大和灵活。稍微有点奇特的是FieldName和FieldNameValue这一对,它们合作构成了我设想的枚举,这也说明了C/C++语言的枚举并不强大到可以作为一个抽象机制使用,当然,这不是我们焦点。暂且不论。
由于其中的toString都是很简单的,所以我准备把它们做成内联函数,这个无可厚非,我们看看就是这个要求是怎么导致我们需要扭曲我们的代码的。
首先需要说明的是:上面的代码不能通过C++编译器的编译。原因很简单:编译器看到MessageHeader被使用的时候还没有发现其定义,会给出一大堆抱怨。怎么办?我们给出一个前向声明吧。也很简单,在使用之前声明一下就行了:class MessageHeader。把它加在使用之前的任意地方,编译器就应该不说话了吧。
哦,还是不行,原因是:对了,它要求必须有完整的类定义才能定义该类的对象,别忘了,C++是值语义的语言。好吧,我们改造成指针……,类型后面加上*,一连串的.变成->,应该没有问题了吧(如果你做过实际的项目,你就知道:这个修改的代价是非常高的,不过区分指针和原始对象不是我们批判的焦点,这里不再深入)。啊……还是不行,原因仍然很简单,就算可以定义指针而不是真正的对象,我们仍然不不能够调用该指针对象的方法,原因是:我(编译器)没有看到类型的定义,怎么知道有没有该方法可以调用呢?
现在我们的选择有三个,但是都不够优雅。 一、我们调整类型定义的顺序,把低层次的构件往前挪。 二、我们把成员函数的实现挪到最后,这时候它已经看到所有的定义了。 三、这是二的彻底化,干脆移动其实现到另一个文件。 方法一的缺陷就是,我们不再能够自然的从上而下看出我们设计的脉络。这跟我看HTTP的RFC一个缺陷是一样的,HTTP的RFC就不是采用自上而下描述的,而是先描述了一些基础的构件,在描述了整体结构,看得人晕头转向,另外,协议中有大量的用途和功能分析,我不是说这不好,但是作为协议这让人分神。可以把功能或者需求放在开始诱导大家构思HTTP应该怎样设计。而不是放在HTTP协议都描述了以后才描述功能。 方法二和方法三本质上是一样的,但是方法三的可维护性就更差一些了,因为所有的修改都得涉及到两个文件,而且,方法三不再能够是inline的,这个代价让我觉得非常不快。而方法二确实仍保持inline,但是,一旦修改我还是得从头到尾的查找。让我写两遍相同的函数头也让我不爽。 而最本质的原因就是:我没有办法让一个决策(函数签名)在一个地方说明,必须出现在两个地方,这是对DRY或者叫做SPOT原则的公然违反,它们会导致大量的微妙的问题和产生难以维护的代码。 C++在很大程度上鼓励我们采用第三种方案,这也是C++中的正统方案,只要我们坚持顺序依赖,我们就不得不倾向于这种方案。顺序依赖把维持依赖关系的责任推给了程序员而不是编译器自己去解决,这当然简化了编译器,但是却使得程序员的日子更艰难。那么,顺序依赖究竟应该是谁的责任呢?是该由程序员处理的呢,还是该由编译器处理的? 我们先大而化之描述一下依赖。依赖表示一个构件(这儿的构件包括函数,类,全局变量,源文件等等任意可以用来搭建整个系统的部分)对别的构件的使用。我们很自然很直观的能感受到,只要我们使用了别的构件,编译器显然知道我们的构件依赖于那些使用的构件,所以,让程序员维持依赖是不必要的。 但是还有一个问题:构件A依赖于构件B,这个编译器知道,但是构件B在哪儿?这个编译器不知道。显然,我们需要而且也只需要在某个地方描述一下这个构件跟位置的映射,我们的编译器就可以工作了。如果我们把这种映射放在任何构件里面,这样势必会导致这种映射关系描述上的重复,导致大量的维护问题。显然,最适合描述构件跟位置的映射关系的地方是工程的配置文件。它是整个工程级别的属性。 那么顺序依赖是什么呢?顺序依赖本质上是希望把构件和位置的映射关系消除。它要求所有依赖的东西必须在它之前出现,这样,被依赖方也就自然不需要指定位置了,它就在前面。这个有利于分块编译(不知道大家注意没有,分块编译的用处似乎并不是很大,无论是大工程还是小工程,因为:毕竟编译只是一个阶段,而且维持构件是否是最新编译版本似乎也一点都不难),但是实现分块编译导致需要大量的重复编译,毕竟任何被多次依赖的组件都必须多次被编译。并且分块编译要求类似于Makefile之类的支持环境,现代的语言及其开发环境都完全不支持分块编译了,它们都采用运行时动态装载这个更灵活的模式。 December 04 软件开发的本质关于这个话题,我似乎说过好多次了。软件系统其实就是现实系统的抽象,就是现实系统的模型。最近,看到一个论点,是这样说的:软件建模(架构)的过程其实就是一个定理证明的过程。我得说,在我看来,这有一定的真理性。但是事实有一定的差异。基本原因是:我们碰到问题的时候,并不总是对问题的解决方案一无所知的,或者更确切的说,对问题的解决方案几乎总是有一定的了解得。这时候,证明的过程其实并不是很明显,反而是表达的过程成了重头戏。 这儿,我在阐述一遍我的观点,软件就是模型,就是抽象。基于上面的描述,软件开发过程也就是构思模型和表达模型的过程。由于表达和构思互相影响,密不可分(大家可以参照语言和思维的关系来理解这一点),所以我只说表达这个方面。我们的模型如何表达现实? 必须解释一下数据的概念了。数据就是表达关系的,关系紧密到一定程度就叫做属性了。一个客体,或者常常被称作对象,物件,就是用其属性描述的。我们描述了客体,也就反映了现实。而客体是数据的集合,所以我们操作数据也就是反映了现实的行为了。对于数据的操作我们有CRUD(Create Read Update Destory)四个,对应于数据库系统的INSERT、SELECT、UPDATE、DELETE四个基本的数据操纵原语。所有的客体的行为,最终都是用CRUD来表达的。所以,只要我们正确地表达了数据,我们就能够正确地表达软件系统。 September 28 Event系统相关接口interface EventTarget {
void addEventListener(in DOMString type, in EventListener listener, in boolean useCapture);
void removeEventListener(in DOMString type, in EventListener listener, in boolean useCapture);
boolean dispatchEvent(in Event evt) raises(EventException);
};
interface EventListener {
void handleEvent(in Event evt);
};
interface Event {
// PhaseType
const unsigned short CAPTURING_PHASE = 1;
const unsigned short AT_TARGET = 2;
const unsigned short BUBBLING_PHASE = 3;
readonly attribute DOMString type;
readonly attribute EventTarget target;
readonly attribute EventTarget currentTarget;
readonly attribute unsigned short eventPhase;
readonly attribute boolean bubbles;
readonly attribute boolean cancelable;
readonly attribute DOMTimeStamp timeStamp;
void stopPropagation();
void preventDefault();
void initEvent(in DOMString eventTypeArg, in boolean canBubbleArg, in boolean cancelableArg);
};
exception EventException {
unsigned short code;
};
// EventExceptionCode
const unsigned short UNSPECIFIED_EVENT_TYPE_ERR = 0;
interface DocumentEvent {
Event createEvent(in DOMString eventType) raises(DOMException);
};
interface UIEvent : Event {
readonly attribute views::AbstractView view;
readonly attribute long detail;
void initUIEvent(in DOMString typeArg, in boolean canBubbleArg, in boolean cancelableArg, in views::AbstractView viewArg, in long detailArg);
};
interface MouseEvent : UIEvent {
readonly attribute long screenX;
readonly attribute long screenY;
readonly attribute long clientX;
readonly attribute long clientY;
readonly attribute boolean ctrlKey;
readonly attribute boolean shiftKey;
readonly attribute boolean altKey;
readonly attribute boolean metaKey;
readonly attribute unsigned short button;
readonly attribute EventTarget relatedTarget;
void initMouseEvent(in DOMString typeArg, in boolean canBubbleArg, in boolean cancelableArg, in views::AbstractView viewArg, in long detailArg, in long screenXArg, in long screenYArg, in long clientXArg, in long clientYArg, in boolean ctrlKeyArg, in boolean altKeyArg, in boolean shiftKeyArg, in boolean metaKeyArg, in unsigned short buttonArg, in EventTarget relatedTargetArg);
};
interface MutationEvent : Event {
// attrChangeType
const unsigned short MODIFICATION = 1;
const unsigned short ADDITION = 2;
const unsigned short REMOVAL = 3;
readonly attribute Node relatedNode;
readonly attribute DOMString prevValue;
readonly attribute DOMString newValue;
readonly attribute DOMString attrName;
readonly attribute unsigned short attrChange;
void initMutationEvent(in DOMString typeArg, in boolean canBubbleArg, in boolean cancelableArg, in Node relatedNodeArg, in DOMString prevValueArg, in DOMString newValueArg, in DOMString attrNameArg, in unsigned short attrChangeArg);
}; 代码位置的说明
|
|
||||||
|
|