随笔 - 66, 评论 - 226, 引用 - 9

导航

关于

《软件设计精要与模式》已经出版,敬请关注!
From 03-03-2006
Counter: hit counter script

online

贴子以"现状"提供且没有任何担保也没有授予任何权利。

标签

每月存档

最新留言

广告

北京的秋

    终日在技术海洋里倘佯,总不免会疲倦,来点文学小品,既能解乏,又能消遣,闲适写意,不亦乐乎?在北京度过两个夏天,体味北京的秋还是第一次。

    初次体味北京的秋,实在是还不知道如何体味,秋已经来了。北方的秋与南方的秋是大不相同的,而北京的秋与重庆的秋比较起来,则是异乎寻常的迥异了。重庆的初秋,与夏天并无二至,更准确的说,“秋老虎”的狠劲儿,令人感觉比在大暑的时候还要难受。热,是真正的闷热,是没有空调风扇就活不下去的炎热。室外是明晃晃的亮,白的一片糊涂,与天混沌在一起,近乎于没有界限。都说“秋风习习,夜凉如水”,重庆的秋天即时是在中秋月明之际,还是被夏日所侵占,久久不肯归还。渐渐的,落叶下来了,等一阵秋雨下来了,这天儿才日复一日的下凉,单衣则渐渐地不能敌过秋风的凉意了。
    从前读郁达夫的《故都的秋》,总是无法体味那种“特别来得清,来得静,来得悲凉”的北国之秋。由于总是被故乡的秋所牵绕着,所误解着,因此在北京度过夏天后,不觉之间,秋天已经来了,果然那么地清,那么地静,那么地悲凉。悲凉只是一种暗暗的思绪,而秋日的清与静,却是特别的明朗,我以为,这确乎可以贴切的形容这秋日中的北京城吧。
    说是北京城,其实应该是在北京的城郊。因为少了车水马龙的喧嚣,所以那份静谧,就愈发让人回味隽永了。偶尔还会在树间草丛响起鸟儿的啁啾,鸣声婉转,在寂寞的天地里,仿若流动的行云,顿时使得一幅洗墨的画卷,多了几分卷舒自如的生趣和自在。可惜,少了古雅的四合院,也少见北国的槐树,至乎槐树叶间撒落的轻柔的落蕊,北京此时的秋味亦然减色不少,幸而在晚间,时而能清晰的听到秋虫的吟唱,在夜色之中传来,那么的渺茫而寂寥,在异乡人的心头,确实有几分悲秋的意味了。
    北京的秋,让我最不可想象的还是天的高与远,还有那浸入得很深的,好似带着水气一般的蕴蓝的颜色。在这样的秋空下,景色即使有几分破败,即使有几分衰落,如此的秋也大可赞美了。事实上,处在这北京的城郊,放眼望去,确实能见到大片大片衰草萋萋的荒野,然而在这样高的天空下,反而有种凄然的美丽;尤其是瓦蓝的天下,更有鸟雀翻飞其间或者鹰隼高傲的盘旋,为这秋色增加了旷远的妙趣,实在是秋意“盎然”。假如趁着秋高气爽,到故宫、前门或者北海一带,看黄瓦红墙在如此高远的天地间存在,委实能感受到一种皇家气派。倘若是在四盒院里,在槐树的叶间缝隙仰望青天,对于北京的秋的理解,又是另一番滋味吧。听说香山的红叶已经红了,在这样的秋空之间,又会是何种颜色?陶然亭不曾去过,不过在颐和园的荡漾的湖水里,芦花凋残,破败在水里,站在桥上,看着那一片碧水,映照着蓝天,必然是格外的美丽吧。
    所以说,北京的秋,所有的清,所有的静,乃至于悲凉与寂寥,全是因为这格外高、格外远的天空使然吧。如果将北京的景儿,诸如“陶然亭的芦花,钓鱼台的柳影,西山的虫唱,玉泉的夜月,潭柘寺的钟声”,搬到重庆那低沉压抑如锅炉罩子的秋空下,也体现不出北国这种特别的秋的意味吧。这么说,重庆的山水在秋日里,总是少了几分颜色啊。
    北京的秋天,雨似乎格外的少,秋风却不曾断过。所以说,北京秋天的凉意,一大半是这秋风带来的吧。虽则这风并不算过分的肆虐,但有时候在夜间,也能听到窗外风声的呜呜。秋天顶适意的是秋日,因为云少,阳光没有遮拦的落下来,洒在身上格外的温暖。即使有秋风吹过,也是一种凉爽的感觉。一旦夜幕降临,太阳失去踪迹,月亮睁着冷眼,气温陡然的降下来,“夜凉如水”的形容就再贴切不过了。重庆则不然,虽然阳光的散去,稍稍消去几分热气,但夜晚的凉意与白天相比,却也多不了几分。如果是初秋与中秋之间,重庆夜晚的暑热依然是难以抵挡的,待得秋雨下来,方才会暑气褪去。由于天地混沌,云层密集而厚,在夏日里猖狂的艳阳,此时却暴露出色厉内荏的本色来,龟缩在云层里。此时重庆的晚秋,固然是秋风秋雨,但秋的意味却已经被冬日的严寒所替代了。因此,重庆的夏秋之间,总是那么严厉的热,然后是严厉的寒,却少了几分默默的温情,好叫人在心底把秋天留恋。
    已近秋末了,不知道北京的寒冬会是怎么模样?想着这样的秋季就这样逝去了,不免有几分留恋与惆怅。如我这样的漂泊者,在下一个秋天,又会在哪一个城市的秋空下,体味这秋的况味?

posted on 2005-11-22 16:34:00 by wayfarer  评论(23) 阅读(6321)

基于消息与.Net Remoting的分布式处理架构

 分布式处理在大型企业应用系统中,最大的优势是将负载分布。通过多台服务器处理多个任务,以优化整个系统的处理能力和运行效率。分布式处理的技术核心是完成服务与服务之间、服务端与客户端之间的通信。在.Net 1.1中,可以利用Web Service或者.Net Remoting来实现服务进程之间的通信。本文将介绍一种基于消息的分布式处理架构,利用了.Net Remoting技术,并参考了CORBA Naming Service的处理方式,且定义了一套消息体制,来实现分布式处理。 

 一、消息的定义

       要实现进程间的通信,则通信内容的载体——消息,就必须在服务两端具有统一的消息标准定义。从通信的角度来看,消息可以分为两类:Request Messge和Reply Message。为简便起见,这两类消息可以采用同样的结构。

       消息的主体包括ID,Name和Body,我们可以定义如下的接口方法,来获得消息主体的相关属性:

public interface IMessage:ICloneable { IMessageItemSequence GetMessageBody(); string GetMessageID(); string GetMessageName(); void SetMessageBody(IMessageItemSequence aMessageBody); void SetMessageID(string aID); void SetMessageName(string aName); }

    消息主体类Message实现了IMessage接口。在该类中,消息体Body为IMessageItemSequence类型。这个类型用于Get和Set消息的内容:Value和Item:

public interface IMessageItemSequence:ICloneable { IMessageItem GetItem(string aName); void SetItem(string aName,IMessageItem aMessageItem); string GetValue(string aName); void SetValue(string aName,string aValue); }

          Value为string类型,并利用HashTable来存储Key和Value的键值对。而Item则为IMessageItem类型,同样的在IMessageItemSequence的实现类中,利用HashTable存储了Key和Item的键值对。

       IMessageItem支持了消息体的嵌套。它包含了两部分:SubValue和SubItem。实现的方式和IMessageItemSequence相似。定义这样的嵌套结构,使得消息的扩展成为可能。一般的结构如下:

       IMessage——Name
                     ——ID
                     ——Body(IMessageItemSequence)
                            ——Value
                            ——Item(IMessageItem)
                                   ——SubValue
                                   ——SubItem(IMessageItem)
                                          ——……

       各个消息对象之间的关系如下:

Distribute1.gif
       在实现服务进程通信之前,我们必须定义好各个服务或各个业务的消息格式。通过消息体的方法在服务的一端设置消息的值,然后发送,并在服务的另一端获得这些值。例如发送消息端定义如下的消息体:

IMessageFactory factory = new MessageFactory(); IMessageItemSequence body = factory.CreateMessageItemSequence(); body.SetValue("name1","value1"); body.SetValue("name2","value2"); IMessageItem item = factory.CreateMessageItem(); item.SetSubValue("subname1","subvalue1"); item.SetSubValue("subname2","subvalue2"); IMessageItem subItem1 = factory.CreateMessageItem(); subItem1.SetSubValue("subsubname11","subsubvalue11"); subItem1.SetSubValue("subsubname12","subsubvalue12"); IMessageItem subItem2 = factory.CreateMessageItem(); subItem1.SetSubValue("subsubname21","subsubvalue21"); subItem1.SetSubValue("subsubname22","subsubvalue22"); item.SetSubItem("subitem1",subItem1); item.SetSubItem("subitem2",subItem2); body.SetItem("item",item); //Send Request Message MyServiceClient service = new MyServiceClient("Client"); IMessageItemSequence reply = service.SendRequest("TestService","Test1",body);

       在接收消息端就可以通过获得body的消息体内容,进行相关业务的处理。

  二、.Net Remoting服务

       在.Net中要实现进程间的通信,主要是应用Remoting技术。根据前面对消息的定义可知,实际上服务的实现,可以认为是对消息的处理。因此,我们可以对服务进行抽象,定义接口IService:

public interface IService { IMessage Execute(IMessage aMessage); }

       Execute()方法接受一条Request Message,对其进行处理后,返回一条Reply Message。在整个分布式处理架构中,可以认为所有的服务均实现该接口。但受到Remoting技术的限制,如果要实现服务,则该服务类必须继承自MarshalByRefObject,同时必须在服务端被Marshal。随着服务类的增多,必然要在服务两端都要对这些服务的信息进行管理,这加大了系统实现的难度与管理的开销。如果我们从另外一个角度来分析服务的性质,基于消息处理而言,所有服务均是对Request Message的处理。我们完全可以定义一个Request服务负责此消息的处理。

       然而,Request服务处理消息的方式虽然一致,但毕竟服务实现的业务,即对消息处理的具体实现,却是不相同的。对我们要实现的服务,可以分为两大类:业务服务与Request服务。实现的过程为:首先,具体的业务服务向Request服务发出Request请求,Request服务侦听到该请求,然后交由其侦听的服务来具体处理。

       业务服务均具有发出Request请求的能力,且这些服务均被Request服务所侦听,因此我们可以为业务服务抽象出接口IListenService:

public interface IListenService { IMessage OnRequest(IMessage aMessage); }

       Request服务实现了IService接口,并包含IListenService类型对象的委派,以执行OnRequest()方法:

public class RequestListener:MarshalByRefObject,IService { public RequestListener(IListenService listenService) { m_ListenService = listenService; } private IListenService m_ListenService; IService Members public override object InitializeLifetimeService() { return null; } }

       在RequestListener服务中,继承了MarshalByRefObject类,同时实现了IService接口。通过该类的构造函数,接收IListService对象。

       由于Request消息均由Request服务即RequestListener处理,因此,业务服务的类均应包含一个RequestListener的委派,唯一的区别是其服务名不相同。业务服务类实现IListenService接口,但不需要继承MarshalByRefObject,因为被Marshal的是该业务服务内部的RequestListener对象,而非业务服务本身:

public abstract class Service:IListenService { public Service(string serviceName) { m_ServiceName = serviceName; m_RequestListener = new RequestListener(this); } IListenService Members private string m_ServiceName; private RequestListener m_RequestListener; }

       Service类是一个抽象类,所有的业务服务均继承自该类。最后的服务架构如下:

Distribute2.gif


       我们还需要在Service类中定义发送Request消息的行为,通过它,才能使业务服务被RequestListener所侦听。

public IMessageItemSequence SendRequest(string aServiceName,
string aMessageName,IMessageItemSequence aMessageBody) { IMessage message = m_Factory.CreateMessage(); message.SetMessageName(aMessageName); message.SetMessageID(""); message.SetMessageBody(aMessageBody); IService service = FindService(aServiceName); IMessageItemSequence replyBody = m_Factory.CreateMessageItemSequence(); if (service != null) { IMessage replyMessage = service.Execute(message); replyBody = replyMessage.GetMessageBody(); } else { replyBody.SetValue("result","Failure"); } return replyBody; }

       注意SendRequest()方法的定义,其参数包括服务名,消息名和被发送的消息主体。而在实现中最关键的一点是FindService()方法。我们要查找的服务正是与之对应的RequestListener服务。不过,在此之前,我们还需要先将服务Marshal:

public void Initialize() { RemotingServices.Marshal(this.m_RequestListener,
this.m_ServiceName +".RequestListener"); }

       我们Marshal的对象,是业务服务中的Request服务对象m_RequestListener,这个对象在Service的构造函数中被实例化:

       m_RequestListener = new RequestListener(this); 

       注意,在实例化的时候是将this作为IListenService对象传递给RequestListener。因此,此时被Marshal的服务对象,保留了业务服务本身即Service的指引。可以看出,在Service和RequestListener之间,采用了“双重委派”的机制。

       通过调用Initialize()方法,初始化了一个服务对象,其类型为RequestListener(或IService),其服务名为:Service的服务名 + ".RequestListener"。而该服务正是我们在SendRequest()方法中要查找的Service:

       IService service = FindService(aServiceName);

       下面我们来看看FindService()方法的实现:

protected IService FindService(string aServiceName) { lock (this.m_Services) { IService service = (IService)m_Services[aServiceName]; if (service != null) { return service; } else { IService tmpService = GetService(aServiceName); AddService(aServiceName,tmpService); return tmpService; } } }

       可以看到,这个服务是被添加到m_Service对象中,该对象为SortedList类型,服务名为Key,IService对象为Value。如果没有找到,则通过私有方法GetService()来获得:

private IService GetService(string aServiceName) { IService service = (IService)Activator.GetObject(typeof(RequestListener), "tcp://localhost:9090/" + aServiceName + ".RequestListener"); return service; }

       在这里,Channel、IP、Port应该从配置文件中获取,为简便起见,这里直接赋为常量。

       再分析SendRequest方法,在找到对应的服务后,执行了IService的Execute()方法。此时的IService为RequestListener,而从前面对RequestListener的定义可知,Execute()方法执行的其实是其侦听的业务服务的OnRequest()方法。

       我们可以定义一个具体的业务服务类,来分析整个消息传递的过程。该类继承于Service抽象类:

public class MyService:Service { public MyService(string aServiceName):base(aServiceName) { } }

       假设把进程分为服务端和客户端,那么对消息处理的步骤如下:

 1、 在客户端调用MyService的SendRequest()方法发送Request消息;
 2、 查找被Marshal的服务,即RequestListener对象,此时该对象应包含对应的业务服务对象MyService;
 3、 在服务端调用RequestListener的Execute()方法。该方法则调用业务服务MyService的OnRequest()方法。

在这些步骤中,除了第一步在客户端执行外,其他的步骤均是在服务端进行。
 

 三、业务服务对于消息的处理

       前面实现的服务架构,已经较为完整地实现了分布式的服务处理。但目前的实现,并未体现对消息的处理。我认为,对消息的处理,等价与具体的业务处理。这些业务逻辑必然是在服务端完成。每个服务可能会处理单个业务,也可能会处理多个业务。并且,服务与服务之间仍然存在通信,某个服务在处理业务时,可能需要另一个服务的业务行为。也就是说,每一种类的消息,处理的方式均有所不同,而这些消息的唯一标识,则是在SendRequest()方法已经有所体现的aMessageName。

       虽然,处理的消息不同,所需要的服务不同,但是根据我们对消息的定义,我们仍然可以将这些消息处理机制抽象为一个统一的格式;在.Net中,体现这种机制的莫过于委托delegate。我们可以定义这样的一个委托:

public delegate void RequestHandler(string aMessageName,IMessageItemSequence aMessageBody,
   ref IMessageItemSequence aReplyMessageBody);

       在RequestHandler委托中,它代表了这样一族方法:接收三个入参,aMessageName,aMessageBody,aReplyMessageBody,返回值为void。其中,aMessageName代表了消息名,它是消息的唯一标识;aMessageBody是待处理消息的主体,业务所需要的所有数据都存储在aMessageBody对象中。aReplyMessageBody是一个引用对象,它存储了消息处理后的返回结果,通常情况下,我们可以用<"result","Success">或<"result", "Failure">来代表处理的结果是成功还是失败。

       这些委托均在服务初始化时被添加到服务类的SortedList对象中,键值为aMessageName。所以我们可以在抽象类中定义如下方法:     

protected abstract void AddRequestHandlers(); protected void AddRequestHandler(string aMessageName,RequestHandler handler) { lock (this.m_EventHandlers) { if (!this.m_EventHandlers.Contains(aMessageName)) { this.m_EventHandlers.Add(aMessageName,handler); } } } protected RequestHandler FindRequestHandler(string aMessageName) { lock (this.m_EventHandlers) { RequestHandler handler = (RequestHandler)m_EventHandlers[aMessageName]; return handler; } }

       AddRequestHandler()用于添加委托对象与aMessageName的键值对,而FindRequestHandler()方法用于查找该委托对象。而抽象方法AddRequestHandlers()则留给Service的子类实现,简单的实现如MyService的AddRequestHandlers()方法:

public class MyService:Service
{ public MyService(string aServiceName):base(aServiceName) {} protected override void AddRequestHandlers() { this.AddRequestHandler("Test1",new RequestHandler(Test1)); this.AddRequestHandler("Test2",new RequestHandler(Test2)); } private void Test1(string aMessageName,IMessageItemSequence aMessageBody,
ref IMessageItemSequence aReplyMessageBody) { Console.WriteLine("MessageName:{0}\n",aMessageName); Console.WriteLine("MessageBody:{0}\n",aMessageBody); aReplyMessageBody.SetValue("result","Success"); } private void Test2(string aMessageName,IMessageItemSequence aMessageBody,
ref IMessageItemSequence aReplyMessageBody) { Console.WriteLine("Test2" + aMessageBody.ToString()); } }

       Test1和Test2方法均为匹配RequestHandler委托签名的方法,然后在AddRequestHandlers()方法中,通过调用AddRequestHandler()方法将这些方法与MessageName对应起来,添加到m_EventHandlers中。

       需要注意的是,本文为了简要的说明这种处理方式,所以简化了Test1和Test2方法的实现。而在实际开发中,它们才是实现具体业务的重要方法。而利用这种方式,则解除了服务之间依赖的耦合度,我们随时可以为服务添加新的业务逻辑,也可以方便的增加服务。

       通过这样的设计,Service的OnRequest()方法的最终实现如下所示:

public IMessage OnRequest(IMessage aMessage) { string messageName = aMessage.GetMessageName(); string messageID = aMessage.GetMessageID(); IMessage message = m_Factory.CreateMessage(); IMessageItemSequence replyMessage = m_Factory.CreateMessageItemSequence(); RequestHandler handler = FindRequestHandler(messageName); handler(messageName,aMessage.GetMessageBody(),ref replyMessage); message.SetMessageName(messageName); message.SetMessageID(messageID); message.SetMessageBody(replyMessage); return message; }

       利用这种方式,我们可以非常方便的实现服务间通信,以及客户端与服务端间的通信。例如,我们分别在服务端定义MyService(如前所示)和TestService:

public class TestService:Service { public TestService(string aServiceName):base(aServiceName) {} protected override void AddRequestHandlers() { this.AddRequestHandler("Test1",new RequestHandler(Test1)); } private void Test1(string aMessageName,IMessageItemSequence aMessageBody,
ref IMessageItemSequence aReplyMessageBody) { aReplyMessageBody = SendRequest("MyService",aMessageName,aMessageBody); aReplyMessageBody.SetValue("result2","Success"); } }

       注意在TestService中的Test1方法,它并未直接处理消息aMessageBody,而是通过调用SendRequest()方法,将其传递到MyService中。

       对于客户端而言,情况比较特殊。根据前面的分析,我们知道除了发送消息的操作是在客户端完成外,其他的具体执行都在服务端实现。所以诸如MyService和TestService等服务类,只需要部署在服务端即可。而客户端则只需要定义一个实现Service的空类即可:

public class MyServiceClient:Service { public MyServiceClient(string aServiceName):base(aServiceName) {} protected override void AddRequestHandlers() {} }

       MyServiceClient类即为客户端定义的服务类,在AddRequestHandlers()方法中并不需要实现任何代码。如果我们在Service抽象类中,将AddRequestHandlers()方法定义为virtual而非abstract方法,则这段代码在客户端服务中也可以省去。另外,客户端服务类中的aServiceName可以任意赋值,它与服务端的服务名并无实际联系。至于客户端具体会调用哪个服务,则由SendRequest()方法中的aServiceName决定:

IMessageFactory factory = new MessageFactory(); IMessageItemSequence body = factory.CreateMessageItemSequence(); //…… MyServiceClient service = new MyServiceClient("Client"); IMessageItemSequence reply = service.SendRequest("TestService","Test1",body);

       对于service.SendRequest()的执行而言,会先调用TestService的Test1方法;然后再通过该方法向MyService发送,最终调用MyService的Test1方法。

       我们还需要另外定义一个类,负责添加服务,并初始化这些服务:

public class Server { public Server() { m_Services = new ArrayList(); } private ArrayList m_Services; public void AddService(IListenService service) { this.m_Services.Add(service); } public void Initialize() { IDictionary tcpProp = new Hashtable(); tcpProp["name"] = "tcp9090"; tcpProp["port"] = 9090; TcpChannel channel = new TcpChannel(tcpProp,
new BinaryClientFormatterSinkProvider(),
new BinaryServerFormatterSinkProvider()); ChannelServices.RegisterChannel(channel); foreach (Service service in m_Services) { service.Initialize(); } }
}

       同样的,这里的Channel,IP和Port均应通过配置文件读取。最终的类图应如下所示:

Distribute3.gif


       在服务端,可以调用Server类来初始化这些服务:

static void Main(string[] args) { MyService service = new MyService("MyService"); TestService service1 = new TestService("TestService"); Server server = new Server(); server.AddService(service); server.AddService(service1); server.Initialize(); Console.ReadLine(); }

 四、结论

       利用这个基于消息与.Net Remoting技术的分布式架构,可以将企业的业务逻辑转换为对消息的定义和处理。要增加和修改业务,就体现在对消息的修改上。服务间的通信机制则完全交给整个架构来处理。如果我们将每一个委托所实现的业务(或者消息)理解为Contract,则该结构已经具备了SOA的雏形。当然,该架构仅仅处理了消息的传递,而忽略了对底层事件的处理(类似于Corba的Event Service),这个功能我想留待后面实现。

       唯一遗憾的是,我缺乏验证这个架构稳定性和效率的环境。应该说,这个架构是我们在企业项目解决方案中的一个实践。但是解决方案则是利用了CORBA中间件,在Unix环境下实现并运行。本架构仅仅是借鉴了核心的实现思想和设计理念,从而完成的在.Net平台下的移植。由于Unix与Windows Server的区别,其实际的优势还有待验证。

posted on 2005-11-22 15:56:00 by wayfarer  评论(7) 阅读(9500)

设计,看上去很美

    设计没有标准,模式充满变化,我们对设计与模式的探讨,就是希望能从没有标准的设计中体验设计的乐趣,从充满变化的模式中寻求问题的解决之道。
    我这里所谓“设计没有标准”,其实并非没有标准,现实是设计的标准实在太多了。我们都希望找到最好的设计方案,然而什么是最好,每个人都有自己的“哈姆雷特”。满足客户需求的设计就是最好的,这个结论我想不会有人反对,前提是,怎样通过设计来满足客户需求?

    计划的设计和演进的设计

    通常来说,软件设计不外乎两种方式:计划的设计和演进的设计。很多人看来,计划的设计更符合工程学的理念。如果你要建一间茅屋,那么你只需夯好土墙,再胡乱堆放一些茅草置于屋顶之上就可以了。然而,如果要你建一座苏州的拙政园,就必须事先有计划的设计了。哪里应该堆放假山,哪里应该开辟池塘,亭子的形状,院落的分布,乃至于园内的一花一木,无不需要独具匠心。软件设计也是如此,且过之而无不及。接手项目的时候,首先考虑的不是编码,而是考虑整个系统的架构,根据需求考虑系统中的重大问题。模块的功能,模块间的关系和系统分布的层次,都需要匠心独运,从一个抽象的层面来考虑。
    演进的设计恰好与之相反,它是一种渐进的过程。它并不要求前期的设计有多么的完美,实现的需求有多么的完整,你只需要把现阶段考虑的问题编码实现就可以了,随着演进的深入,编码也会随之而修正,最后设计会逐渐丰满起来,经过一系列的方法,最后的设计也渐趋完美。
    你也许会认为“演进的设计”如此的简陋与平庸。没有计划,只会令设计一团遭。但我需要提醒你的是,虽然都是工程学,软件的设计并没有建筑设计那么简单。因为,你很难在设计之初,考虑到客户的全部需求,甚至于实现未来的扩展。在设计一开始,你能确信:
    你对客户的需求都理解了吗?
    你能确定客户的需求不再变化吗?
    你设计的软件架构真的能满足需求吗?

    是的,你无法给出肯定的回答。总之我在这里不是想说服每个人,要采取哪一种设计方式。事实上,我也面临抉择的困难。

    过度设计,还是简单设计?

    Kent在《解析极限编程——拥抱变化》中为简单系统制定了四个评价标准,依次为(最重要的排在最前面):
    通过所有测试;
    体现所有意图;
    避免重复;
    类或者方法数量最少;

    这些标准写出来简单,要根据这个标准来实现,就不是那么容易的事了。我相信,软件设计人员都希望自己的设计尽可能简单。然而,在设计时,我们不仅仅要考虑软件的功能,我们还要考虑软件的性能、扩展性,模块间的耦合关系,系统的稳定、部署和更新,版本的管理,系统的安全,界面的友好程度。要想简单,何其之难!
    Do the simplest thing that could possibly work! 这是XP人士大声疾呼的口号,我也举双手赞成。问题是,我们需要让简单的事情,同时又有效。很多人在设计时,并不满足于实现眼前的功能。看到加法,他们可能还会想到乘法;虽然目前的需求是整数,他们可能想到今后可能会扩展到实数,甚至于复数。他们希望能利用某种设计,使其具有更好的扩展性。从眼前的需求来看,可能是过度设计;然后对于未来,这个设计才是最完美的方案。
    问题不在于设计是否过度,关键还是在于设计的理念。是只做目前需要的事,还是未雨绸缪,想好今后的功能扩展?这个问题的答案还需要实际的项目开发来检验,根据不同的需求,答案会因此而异。

    需要设计模式吗?

    答案是肯定的,但你需要确定的是模式的应用是否过度?我得承认,世界上有很多天才的程序员,他可以在一段代码中包含6种设计模式,也可以不用模式而把设计做得很好。但我们的目标是追求有效的设计,而设计模式可以为这个目标提供某种参考模型、设计方法。我们不需要奉GOF的设计模式为圭臬,但合理的运用设计模式,才是正确的抉择。
    很多人看过GOF的《Design Patterns》,对这23种模式也背得滚瓜烂熟。但重要的不是你熟记了多少个模式的名称,关键还在于付诸实践的运用。为了有效地设计,而去熟悉某种模式所花费的代价是值得的,因为很快你会在设计中发现这种模式真的很好,很多时候它令得你的设计更加简单了。
    其实在软件设计人员中,唾弃设计模式的可能很少,盲目夸大设计模式功用的反而更多。言必谈“模式”,并不能使你成为优秀的架构师。真正出色的设计师,懂得判断运用模式的时机。
    还有一个问题是,很多才踏入软件设计领域的人员,往往对设计模式很困惑。对于他们来说,由于没有项目的实际经验,OO的思想也还未曾建立,设计模式未免过于高深了。其实,即使是非常有经验的程序员,也不敢夸口对各种模式都能合理应用。

    重构是必然的!

    既然我们无法给出一个完美的设计方案,因为客户的需求总是变化的,重构也就成为必然。问题是,这样没有添加任何功能的重构,你是否愿意为此付出精力、时间去完成。当客户要求的Deadline将要到来的时候,你还认为你的重构工作是必要的吗?
    有时候,软件设计常常身不由己。然而,纯从技术的角度来看,重构非但必然,而且重要。既然我们都明白,复杂的未尝就是好的,简单的也不一定是容易的。要保持你的设计尽可能的简单,可能你还需要时时借助重构的利器,来“改善你既有代码的设计”。
    对于重构,Martin Fowler给出了很多条款。这些条款并不是政治课本的教条,也不是“日月神教”的神奇咒语,念着它们就可以防身。这些条款确实很重要,但你需要的是学会他后,然后忘记他,就象张无忌学太极拳那样。我不是故弄玄虚,事实上只有这样,重构的精神才能完全融入到你的设计中。

    UML重要吗?

    我现在看一个设计方案的时候,更希望先看看UML图,然后再看文档的实际描述。如果让我读一段代码,我希望能先看看类图,或许更容易理解代码的含义。UML在OO世界里像是世界语,它便于程序员间的交流,让别人更容易理解你的意图。同时,在设计UML图的过程中,也是一种对思路的清理,对客户需求的把握,设计思想的跟踪。
    UML是一种基于对象的统一建模语言,它能够为系统设计提供清晰直观的设计。在面向对象世界里,UML的地位弥足轻重,甚至被称为是软件设计的一场革命。对于有计划的设计,UML的价值就体现得淋漓尽致。如果我们要清晰地表现模块的功能,模块间的关系和系统分布的层次,使用UML可以使设计师减少很多麻烦,同时降低了语义描述的二义性。然而,如果我们在做演进的设计时,UML还有那么重要吗?我们只需要对眼前的需求进行编码、测试,然后重构。可能我们只需要在Pair Team中讨论设计方案,在预定技术框架内探讨实现的可能和细节。我们完全可以抛开UML繁琐而死板的设计,毕竟最能忠实体现设计思想的,不是文档,不是用例图或是什么类图,而是代码。
    那么,有多少人是这样想的?

    TDD、单元测试和其他

    软件的生命是什么,是质量!而保证质量的唯一方法,就是测试。传统的软件开发过程,强调首先进行需求分析,再从需求分析中抽象出概要设计,进而作出详细设计,然后编码,最后才是测试以验证代码的正确性。而测试驱动开发(TDD)改变了编码的过程,开发仅仅包括三方面的活动:编写测试用例,编码并进行测试,重构代码以消除重复代码使其更简单、更灵活、更容易理解。通过测试来驱动开发,听起来是那么的离经叛道,然而实施起来,又是那么合理、正确和简单,前提是:我们不能在一开始就获得正确的设计!TDD避免了对不完整需求造成的不成熟的设计。通过单元测试,保证了代码的正确性与高质量;通过重构,使设计更加简单、灵活。

posted on 2005-11-17 09:36:00 by wayfarer  评论(11) 阅读(9797)

面向对象思想

面向对象思想为软件设计与开发赋予了哲学的意义。在哲学的世界里,小至沙粒微尘,大至日月星辰乃至宇宙,均可视为单独的个体对象而存在。如果以哲学的目光凝视程序的世界,又何尝不是如此?一个用户,一种销售策略,一条消息,或是某种算法,一个Web的网页,面向对象思想均将其看作为一种对象。而每一种对象,都有其单独的生命周期,谁来创建它,谁来销毁它,它的内在属性,表现行为,以及它与外界之间的关系和集合,无不具有某种哲学的意味。我们在定义对象时,就好比是在描述一个活生生的事物,需要定义该对象的自然属性和社会属性,限定它的内涵与外延,勾勒出该对象的社会关系。而对于抽象、多态与封装,则是一种形而上学的概念,它将面向对象技术推向为思想的境界。

因此,我们在运用面向对象思想来定义对象时,就必须从思想的高度上俯瞰它,同时又必须结合现实来描述它。两者之间并没有绝对的矛盾。

所谓思想的高度,就要求我们必须理解所谓面向对象思想的精髓,并通过运用诸如设计模式、对象法则等知识,并从软件架构的角度出发,高屋建瓴地勾勒出整个软件结构的全貌。说得玄一些,就颇有几分“超然物外”的感觉。

所谓结合现实,也即是说对象离不开其依存的环境,毕竟软件设计不可能达到完全抽象的境界。从软件工程的角度来看,就是在设计时,需结合客户的需求、具体的业务来综合考虑。怎么界定对象的边界?对象的属性和行为是什么?哪些需要封装,而哪一些又需要暴露接口?有时候,业务才能决定设计的一切,如果纯为设计而设计,只能是空中楼阁,并不能搭建出扎实的建筑来。

posted on 2005-11-16 15:57:00 by wayfarer  评论(16) 阅读(9669)

Powered by: Joycode.MVC引擎 0.5.2.0