实战解析

开篇

在思想上兜了一圈,我决定要还是落地走走,否则就可能变成一张大嘴而已。

接下来要给大家讲解的是我的第一个大项目—X市动力机房监控,在这个项目中我从一个毕业生成长为单位的开发骨干。由于不记得当初的技术保密协议到底是5年还是10年,所以出于职业精神,我还是不能把所有项目背景和设计进行介绍,抽取我认为没有机密性但是比较有意思的部分来解析一番,所以肯定有人会说,你说的复杂项目是否太简单了,应该也不足为奇。但是事实上我们当初是三个单位联合开发,验收时花费7人*6个月,事情总是比我们想象的复杂。

当初的项目使用C,C++开发了底层,PowerBuilder开发了上层应用。这么多年,我当然记不得那些程序,但是C#还是足够强大,我将尽力使用1~2周把一个框架给再现出来,这下不是因为公司业务而加班了,而是”自讨苦吃、苦中作乐”。为了避免今后的官司麻烦,请理解我不能把全部代码放到网上,不过关键部分我还是会在文章里使用代码来辅助叙述我的思路。

以下是我的大致目标和提交顺序:

  1. 项目的目的和需求概括,明确要讲解分析的范围;
  2. 我想到的设计
  3. 关键技术储备
  4. 主体实现和界面示意
  5. 点评与总结

今后有兴趣的朋友可以联系我们团队进行实战开发,我这里有几个不错的项目机会。

其实现在我进行开发的速度肯定比网上很多朋友要慢的,但是我的开发将遵循一些原则,了解这些原则有助于你理解我的思路。本次解析可能没有涉及到一些原则,毕竟还是个提取后的小项目而已。

我目前总结的八大原则如下:

  1. 想方设法把客户拉进项目组

    这个大家都明白,可是大家都很难做到!CMMI、XP也解决不了,靠自己的智慧吧,要知道,是人就有爱好,有爱好就有机可乘:是财是色,自己看着办吧!

  2. 建设自适应的团队

    你的团队是否严重依赖某个人呢?是否每个人都关注项目的苦难?遇到问题大家都认真提供对策。面对责任是否敢于承担,还是推给他人?

  3. 始终保持目标

    项目经理是否独自关起门填写项目计划并且分配活动(其他人都网上冲浪去了)?是否每个人只知道要干什么而不理解为什么要干?

  4. 风险驱动、迭代开发、组件开发、测试驱动

    这个不用说,大把资料,参考RUP就很多了。

  5. 简单的角色分工

    蜜蜂蚂蚁应该远远不及人的聪明,但是他们可以出色完成一次次的”项目”?他们有一个超级PM吗?我看没有!一个人能指挥社会如何运作吗?不能!但是这社会不会因为某个人而停止运转。

  6. 打开黑盒,保证信息一致

    你是如何确保你提交的代码是有质量的呢?拍胸脯吗?你的证明材料在哪里?

    你如何让领导和客户相信你的工作量真的大于30人月?如何让他们认同你现在确实完成了90%,并且进展良好?否则和你纠缠的不光是客户,你还要花很多时间对付公司内那些疑惑的目光(来自老总、财务、其他项目经理等)!

  7. 一有机会就重构

    这个不用说,参见那本好书

  8. 为提速做好准备

    你能保证在同一个问题上你或者你今后的同事不要重复第二次吗?

项目目的和范围

项目的目的(动机)

任何有明确时间约束和结果要求的事情我们都可以称之为项目,项目培训上老师会给大家讲所谓的QRT三重限制:产出、资源、时间,要想产出又多有好,那势必投入更好的资源或者延长时间,这应该不太难理解。但是根据我的真实感受,还应该在再强调:目标(或者说动机)!很多项目组做完了还不知道项目的动机。我举个例子:某市消防局投资几个亿上一套指挥调度系统的目的很明确:火灾发生后3分钟可以到现场进行救援!

我们示范的项目名称是:X市的电信动力监控系统
目的:当异常情况发生后2秒内把信息传送到指挥中心,并产生报警。

项目背景知识

我们每人都应该用过电信的固定电话,但是可能大家没有注意过,当停电的时候,我们的电话系统却还能正常地工作。通常在一个市里有多个电信动力局房,通常有几组交流变压器,在正常供电时给电信设备供电,当市电停止供应时自动转为电池供电,而电池组通常只能支持个把小时,如果遇上台风等恶劣天气,那就只能靠柴油发电机来为大家保证通讯的动力了。所以通常每个局房都配有专员检查维护这些设备,检查人员通常要查看温度,电压等参数来确定机器是否正常运作。

所以这个项目就是要给X市的电信动力保证系统上一个台阶,为此需要增加一些模数传感器安放在局房动力设备上用于采集各项参数指标,在每个局站还要增加相应的工控机器用来收集和处理这些信号,当出现告警时本地必须进行声光报警,有条件的局站还可以安装简易的监控软件对本地的设备进行监视和控制。与此同时,所有的数据也必须同步送网市电信总监控站,在那里可用统一查看全市的动力保证系统的状态,还可以对这些情况进行分析,发现一些隐患。在X市成功运用后,还可能在本省其他市推广,在省局建立本省的集中监控台。

以上是典型的传统需求概述(当然我已经把上百页的SRS简化得不能再简化了),使用我们的方法,可以把它改成图形化表述如下:

mgexp4_1

为了方便后面讨论核心要点,我们将约定如下:

a) 不讨论标有打勾的需求,你就当它可用得了; b) 标有美元符号的需求我会再总结时讲一些心得; c) 标有钥匙符号的需求很明显,是这个项目关键的需求(其实这也是我后来感觉到的);

虽然R21需求本次不涉及,但是他是客户体验的一个关键需求,给大家一个感性认识如下图:

mgexp4_2

R23很重要,它是项目提升商业价值的一个好包装,如下:

mgexp4_3

OK,我又要做其他工作了,有疑问的朋友可以跟帖,我将一边准备设计部分的资料一边给需求答复。

alex 11-21

非主流之设计

找出系统的关键问题

我们已经在项目的背景中了解到,该项目成功的两个必要条件:

  1. 异常信号采集并送到指挥中心的时间不能超过2秒;
  2. 今后会有其他类型的信号类型,要保证能够支持这种扩展;

[这就是开篇中强调的:3--- 始终保持目标]
所以这些关键的技术问题应该是首次迭代要解决的。

再深入调研

通过和客户的深入调研:我们发现电信的单位组织架构通常是:基层动力局房 -->县局-->市局-->省局。
为了上这套系统,我们就必须在基层动力局房安装数据采集器把模拟信号转换为数据信号,从而计算出真实的物理数据,判断信号是否正常,再向上级汇报数据。

[依据我的5--简单的角色分工原则,我很容易就设想有本系统软件几个重要部分,分别是数据采集器、数据处理枢纽、监控台,他们的关系参见下图:

mgexp4_4

系统建成后可能的部署:

mgexp4_5

非常规的概要设计

基于那三个软件单元的角色,我们容易推导出他们应该具备以下功能:

mgexp4_6

如果你是个心细的读者,一定会记得上篇中关于需求的整理,这里在设计时进行了跟踪,我们很明显发现有些需求虽然客户没有提到,但是还是要做的,这就是那些隐含的需求。

总结业务模型如下:

mgexp4_7

说了半天,有些读者还是心里没有答案,你一开始提到的那两个必要条件在哪里设计呢?由于怕篇幅过长,我把它放到下篇,关键技术里讲吧。

项目的主要技术储备

1--通讯技术

在上篇了展望了本项目的一个框架,我们也知道:如果这个项目的处理时间不能达到要求的话,则花哨的功能也不能逃过失败的定论。

而通讯的关键在于数据处理枢纽的层数和处理中转的时间,数据枢纽下联数据采集器、上联高层数据枢纽或者监控台。数据的最长路经是:局房数据采集器-->县局数据枢纽-->市局数据枢纽-->省局数据枢纽-->监控台。则总共经历四次传送,则每次中转和处理的时间应该<2/4=500ms。

目前.Net提供比较安全的远程处理机制和Web Service,我编写一个测试程序,发现在我配置比较高的机器上一次传送100条记录的数据包的处理时间平均在400ms,看来就比较危险了。

以前我们的数据采集器连接的有智能设备;A/D单片机等,而上层的数据处理部分有跑在UNIX,所以稳妥地方式还是使用原生的socket保险些,但是这里就只能忽略了安全保证(考虑是在电信内网运行),当然传送时可以简单加密一下数据。

下面是我定义的接口:

using System;

namespace QPG.Net
{
    public delegate void StateChangedCallback(string text);
    public delegate void DataReceivedCallback(string text,int curSocketIndex);

    public interface IMonitorChannel{
        event StateChangedCallback OnConnected;
        event DataReceivedCallback OnReceived;
        event StateChangedCallback OnDisconnect;

        void open();
        void close();

        bool Opened {get;}
    }

    public interface IClientChannel:IMonitorChannel{        
        void sendMsg(string msg);        
    }

    public interface IServerChannel:IClientChannel {
        int Port{get;}
    
        void sendMsgToAll(string msg);
        void sendMsgToClient(string msg, int clientNumber);
    }
}

IMonitorChannel---提供给观看者,只能收取数据,不能发送命令;
IClientChannel---主要用于监控台;
IServerChannel---主要用于数据枢纽,可以用来接收采集器的数据或者发布数据到数据订阅者

实现就很简单了,网上很多开源的Socket C#版。我就不贴了,有需要的可以邮件给我。

2--扩展技术

既然castle提供了那么强大的容器,那我就直接使用它得了。下图是传统的功能调用和基于数据队列处理的思路图,后者我已经在好几个大型项目中使用,比较容易维护和调试。

以下是所有规则处理器的祖先类:

namespace OpenMonitor.Services {
    using System;
    using System.Collections;
    using OpenMonitor.Data;
    using OpenMonitor.Utility;
    using OpenMonitor;
    using Castle.Model;

    public class BaseService:IStartable {
        protected string _serviceName="monitor.base_service";
        protected  SiteConfigPacket _cfg;            
        protected IMessageQueue _mq;
        private bool _Started = false;
        private System.Timers.Timer _timer;
        protected bool _Stopped=false;
        private const int TIMER_INTRVAL=50;
        
        public BaseService(string ServiceName,IMessageQueue mq,SiteConfigPacket cfg) {
            _serviceName=ServiceName;
            _mq=mq;
            _cfg=cfg;
            _timer=new System.Timers.Timer(TIMER_INTRVAL);
            _timer.Elapsed+=new System.Timers.ElapsedEventHandler(timer_Elapsed);
        }
        
        public virtual string ServiceName {
            get{return _serviceName;}
        }

        public virtual void Start() {
            if(_Started==true) return;
            DefaultLogger.INSTANCE.DEBUG(ServiceName+" Started");
            _timer.Enabled=true;
            _Started=true;
        }

        public  virtual void Stop() {
            _Stopped =  true;
            _timer.Enabled=false;
            DefaultLogger.INSTANCE.DEBUG(ServiceName+" Stoped");
        }

        protected void ToNextRule(InfoData data,InfoConfig infocfg) {
            string next=infocfg.getNextRule(_serviceName).Name;
            if(next=="") next=ResultService.ServiceName;
            this.addDataToQueue(next,data);
        }

        public virtual void handle(object data) {
        }

        protected void addDataToQueue(string sn,object data) {
            _mq.addDataToQueue(sn,data);
        }

        protected void checkQueue() {
            object obj=_mq.getDataFromQueue(_serviceName) ;
            if(obj==null) return ;
            try{
                handle(obj);
            }
            catch(Exception e) {
                DefaultLogger.INSTANCE.ERROR(this._serviceName+"("+obj.ToString()+")",e);
            }
            checkQueue();
        }
        
        private void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) {
            checkQueue();
        }        
    }
}

这里使用了castle的自动服务启动功能,所以容器创建后,内部的所有服务都会自动启动,并且每隔50ms对它负责的队列进行一次集中的扫荡处理。如果是多个处理器实例,那么处理的效果更佳。那也很简单,在配置文件里类似如下配置:

<component id="linearity"
            type="OpenMonitor.Services.LinearityFormula,ExtendRule"
            lifestyle="Pooled" initialPoolSize="2" maxPoolSize="5" >
</component>

但是实际上多个和一个处理效果没有区别,第二个总会扑空的,你看出来了吗?且听我下回分解吧。

alex 11-24

主体实现和界面示意

在上篇中细心的读者会发现以下两句是有问题的:

_timer=new System.Timers.Timer(TIMER_INTRVAL);
_timer.Elapsed+=new System.Timers.ElapsedEventHandler(timer_Elapsed);

就是因为这一句,我们的规则处理器的实例[比如数据线性转换f(x)=a.x+c ]哪怕启动两个也没有用,因为我们的容器是在单线程中执行,所以这两个规则处理实例的时间触发时刻非常接近。所以当第一个的时间点到时,第一个实例将会处理完队列中的所有待处理数据。等到第二个实例的处理代码得以执行时,队列可能已经是空的了。

当然可以在new System.Timers.Timer(TIMER_INTRVAL)时加上一个随机数来让这两个实例的触发时刻分开;或者使用每个实例运行在单独的线程上真正互不干扰;或者使用事件通知而取消时间轮询。总之,技术上可以有很多办法达到一个目标。

言归正传。我们的监控系统核心稳定的数据协议可以定义类似下面:

namespace OpenMonitor.Data {
    using System;
    using System.Collections;
    using System.Text;
    using System.Collections.Specialized;
    using System.Xml.Serialization;

    [Serializable]
    [XmlRoot("DATA")]
    public class InfoData:EventArgs{
        [XmlAttribute("ID")]
        public string ID;

        [XmlAttribute("Time")]
        public string Time;//HH:MM
        
        [XmlAttribute("Value")]
        public string Value;
    
        [XmlAttribute("State")]
        public string State;

        public InfoData() {
        }
        public InfoData(string id,object val) {
            ID=id;
            Value=val.ToString();
            DateTime dt=DateTime.Now;
            Time=dt.Hour.ToString("D2")+":"+dt.Minute.ToString("D2")+":"+dt.Second.ToString("D2");
            State="?";
        }
        
        public InfoData(InfoData data) {
            this.ID=data.ID;
            this.Time=data.Time;
            this.Value=data.Value;
            this.State=data.State;
        }

        public override string ToString() {
            return "["+ID+"]("+this.State+")===>"+Value;
        }
    }
        
    [Serializable]
    [XmlRoot("DATA")]
    public class DataPacket {
        [NonSerialized()]
        private IDictionary ht;
            
        public  DataPacket() {
            ht=new System.Collections.Specialized.HybridDictionary();
        }
    
        [XmlArray(ElementName="InfoDatas")]
        [XmlArrayItem(ElementName="InfoData", Type=typeof(InfoData))]
        public InfoData[] Infos {
            get{
                InfoData[] rt=new InfoData[ht.Count];
                int i=0;
                foreach(InfoData p in ht.Values) rt[i++]=p;
                return rt;
            }
            set {
                ht.Clear();
                foreach(InfoData d in value) add(d);
            }
        }

        public string this[string Key] {
            get{
                foreach(InfoData s in ht.Values) if(s.ID==Key) return s.Value;
                return String.Empty;
            }
        }
        public void add(InfoData info) {
            if(ht.Contains(info.ID)) ht[info.ID]=info;
            else ht.Add(info.ID,info);
        }
        public int Count {
            get {return ht.Count;}
        }
        public void clear() {
            ht.Clear();
        }
    }
}

这是这个系统比较稳定的地方,大量的通讯数据就是这样定义的,很简单吧。这些数据应该是从底往上走,我们再来看看从上往下发的控制命令的协议定义:

namespace OpenMonitor.Data {
    using System;
    using System.Collections;
    using System.Text;
    using System.Collections.Specialized;
    using System.Xml.Serialization;

    [Serializable]
    [XmlRoot("DATA")]
    public class CmdData:EventArgs{
        public string Operator;
        public string Time;
        public string CommandName;
        public string Command;
    
        public  CmdData() {
        }

        public  CmdData(string cmdName,string cmd,string op) {
            DateTime dt=DateTime.Now;
            Time=dt.Hour.ToString("D2")+":"+dt.Minute.ToString("D2")+":"+dt.Second.ToString("D2");
            CommandName=cmdName;
            Operator=op;
            Command=cmd;
        }
        
        public  CmdData(CmdData data) {
            this.Operator=data.Operator;
            this.CommandName=data.CommandName;
            this.Command=data.Command;
            this.Time=data.Time;
        }

        public override string ToString() {
            return "from "+Operator+"["+Time+"]===>"+Command;
        }
    }
}

这些原始的数据和加工后的数据将要送到数据队列中,对于商业应用。我们可以选择IBM,MS等的产品。在此我们可以用一个简化的实现来看看核心的部分:

namespace OpenMonitor.Utility {
    using System;
    using System.Collections;
    using OpenMonitor;

    public class SimpleQueue :IMessageQueue {
        private IDictionary _catalogs = Hashtable.Synchronized(new Hashtable());
        private ILogService log;

        private void init() {
            log=DefaultLogger.INSTANCE;           
        }

        public SimpleQueue() {
            init();
            DefaultLogger.INSTANCE.DEBUG("SimpleQueue created!");
        }

        private Queue getQueue(string catalog) {
            if (!_catalogs.Contains(catalog))
                _catalogs.Add(catalog,Queue.Synchronized(new Queue()));
            return _catalogs[catalog] as Queue;
        }

        public string[] getQueueNames() {
            string[] rt=new string[_catalogs.Count];
            int i=0;
            foreach(string key in _catalogs.Keys) 
                rt[i++]=key;
            return rt;
        }

        public string[] getQueueInfo(string QueueName) {
            Queue q=getQueue(QueueName);
            object[] objs= q.ToArray();
            string[] rt=new string[objs.Length];
            for(int i=0;i<objs.Length;i++)
                rt[i]=objs[i].ToString();
            return rt;
        }

        public void addDataToQueue(string Catalog,object XmlData) {
            getQueue(Catalog).Enqueue(XmlData);
            //log.DEBUG(string.Format("addDataToQueue({0})",Catalog));
        }

        public object getDataFromQueue(string Catalog) {
            object rt = null;
            Queue q=getQueue(Catalog);
            if (q.Count>0) {
                rt=q.Dequeue();
            }
            return rt ;
        }
    }
}

这个系统的主体模块是DataHub,以下是主要片断:

namespace OpenMonitor {
    using System;
    using System.Collections;
    using System.Text;
    using QPG.Net;
    using OpenMonitor.Utility;
    using OpenMonitor.Data;
    using OpenMonitor.Services;

    public class DataHub {
        protected    SiteConfigPacket _cfg;//保存本地的采集点的配置信息
        private IServerChannel _server;//接收数据采集器的通道    
        private QPG.Net.IClientChannel _client;//作为客户端报送数据的通道        
        private IMessageQueue _queue;
        private ILogService log=DefaultLogger.INSTANCE;        
        private DefaultDataTransform dt;
        protected System.Collections.IDictionary _map;//用于索引信息点所在的通道号
        private lookForHandler _cmdFactory;

        public event StateChangedCallback OnServerStateChanged;
        public event DataReceivedCallback OnServerReceivedData;

        public virtual lookForHandler CommandFactoryMethod{
            get{return _cmdFactory;}
            set{ _cmdFactory=value;}
        }

        public virtual IServerChannel ServerChannel{
            get{return _server;}
        }
        public virtual IClientChannel ClientChannel{
            get{return _client;}
            set{_client=value;}
        }
        
        public virtual IMessageQueue MQ{
            get{return _queue ;}
        }

        public virtual string HubCode{
            get{return _cfg.SiteCode ;}
        }
        public string HubName{
            get{return _cfg.SiteName ;} 
        }

        public DataHub(IServerChannel collect_svr,IMessageQueue queue,    SiteConfigPacket cfg) {
            _server=collect_svr;        
            _queue=queue;            
            _cfg=cfg;
            init();
            _map=new Hashtable();        
            log.DEBUG("DataHub "+HubName+" created!");
        }

        public virtual void  sendCmd(string id,CmdData cmd) {
            string  cmdtext=dt.toXml(cmd);
            int cnum=getChannelNum(id);
            if(cnum>0) _server.sendMsgToClient(cmdtext,cnum);
            else _server.sendMsgToAll(cmdtext);
        }

        public virtual void addMapItem(string id,int ChannelNum) {
            if(_map.Contains(id)) _map[id]=ChannelNum;
            else _map.Add(id,ChannelNum);
        }

        public virtual int getChannelNum(string id) {
            if(_map.Contains(id)) return (int)_map[id];
            return 0;//不存在!
        }

        private void init() {
            //log.DEBUG("Memory Message Queue Started (SocketServerPort:"+_server.Port+")");
            _server.OnConnected+=new StateChangedCallback(OnConnected);
            _server.OnDisconnect+=new StateChangedCallback(OnDisconnect);
            _server.OnReceived+=new DataReceivedCallback(OnReceived);
            dt=new DefaultDataTransform();
            _server.open();
        }

        private void OnConnected(string text) {
            if(OnServerStateChanged!=null) OnServerStateChanged(text);
        }

        private void OnDisconnect(string text) {
            if(OnServerStateChanged!=null) OnServerStateChanged(text);
        }
//        private void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) {
//            string[] ServiceNames=_queue.getQueueNames();
//            for(int i=0;i<ServiceNames.Length;i++) {
//                Console.WriteLine(ServiceNames[i]+":");
//                string[] val=_queue.getQueueInfo(ServiceNames[i]);
//                for(int j=0;j<val.Length;j++) Console.WriteLine(val[j]);
//            }
//        }

        private void handleInfoData(string text,int curSocketIndex) {
            DataPacket packet=dt.toObject(text,typeof(DataPacket)) as DataPacket;
            InfoData[] data=packet.Infos;
                
            foreach(InfoData info in data) {
                addMapItem(info.ID,curSocketIndex);
                if(_cfg[info.ID]==null||_cfg[info.ID].RuleConfigs.Length<1) _queue.addDataToQueue(ResultService.ServiceName,info);
                else _queue.addDataToQueue(_cfg[info.ID].RuleConfigs[0].Name,info);
            }
        }

        private void handleData(string text, int curSocketIndex) {
            if(text.IndexOf("InfoDatas")>=0) handleInfoData(text,curSocketIndex);
            else if(text.IndexOf("CommandName")>=0) {
                CmdData cmd=dt.toObject(text,typeof(CmdData)) as CmdData;
                IDictionary dic=InfoConfig.getParameterDictionary(cmd.Command);
                dic.Add("CurSocketIndex",curSocketIndex);
                try{
                    IRule  hander=CommandFactoryMethod(cmd.CommandName);
                    if(hander!=null) hander.handle(this,dic);
                }
                catch{
                    log.ERROR(text,new Exception("找不到命令处理程序"));
                }
            }
        }
        
        private void OnReceived(string text, int curSocketIndex) {
            try{
                string msg=string.Format("数据采集器({0})发来数据",curSocketIndex)+System.Environment.NewLine;
                if(OnServerReceivedData!=null) OnServerReceivedData(msg+text,curSocketIndex);
                handleData(text,  curSocketIndex);
            }
            catch{
                log.ERROR(text,new Exception("不是完整XML"));
            }
        }
    }
}

我们把DataHub加载到容器中,再给一个控件显示其最新收到的数据,效果如下:

mgexp4_8

mgexp4_9

由于我们采用的是开放的XML+socket.所以任何符合上面格式的数据都可以把数据汇总到DataHub.
DataHub收到数据后,就可以把数据进行加工处理,必要时要过滤,否则越到上面数据量就越多,对于没有变化的数据,我们为什么要耗费资源呢?

订阅者收到数据后,可以发挥你的想象力了,下面是一个展示:

mgexp4_10

今后的维护工作有两部分:一是增加已有类型的信息点,这很简单,修改配置文件如下:

<?xml version="1.0" encoding="utf-16"?>
<DATA xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <SiteCode>HN</SiteCode>
  <SiteName>湖南</SiteName>
  <InfoConfigs>
    <InfoConfig ID="01" Code="A01">
      <Name>信号1</Name>
      <Rules>
        <Rule Name="linearity">
          <Parameters>a=30;k=0</Parameters>
        </Rule>
        <Rule Name="userscope">
          <Parameters>NormalLow=100;NormalHigh=200</Parameters>
        </Rule>
      </Rules>
    </InfoConfig>
    <InfoConfig ID="02" Code="A02">
      <Name>信号2</Name>
      <Rules>
        <Rule Name="linearity">
          <Parameters>a=30;k=0</Parameters>
        </Rule>
        <Rule Name="sysscope">
          <Parameters></Parameters>
        </Rule>
      </Rules>
    </InfoConfig>
  </InfoConfigs>
</DATA>

但是如果增加了新的数据类型或者规则,那就要改程序了,也不难,看看我的线形处理器代码,保证你会写了:

namespace OpenMonitor.Services {
    using System;
    using System.Collections;
    using OpenMonitor.Data;
    using OpenMonitor.Utility;

    public class LinearityFormula:BaseService {
        public LinearityFormula(IMessageQueue mq,SiteConfigPacket cfg):base("linearity",mq,cfg) {  
        }
        
        public override void handle(object data) {
            InfoData info=new InfoData(data as InfoData);
            string old=info.Value;
            InfoConfig cfg=_cfg[info.ID];
            
            IDictionary d=InfoConfig.getParameterDictionary(cfg.getCurRule(this.ServiceName).Parameters);
            double v=double.Parse(d["a"].ToString())*double.Parse(info.Value)+double.Parse(d["k"].ToString());
            info.Value=v.ToString();
            ToNextRule(info,cfg);
            DefaultLogger.INSTANCE.DEBUG(this.GetType().Name+":"+d["a"].ToString()+"×"+old+"+"+d["k"].ToString()+"-->"+info);
        }
    }
}

好了,经过一周的努力,终于兑现了我给各位读者的承诺,请关注我的最后一篇<<总结>>吧。

总结

摘要

温伯格大师曾说:“技术(本身)毫无价值”----转送给迷惑的程序员
艾森豪威尔上将曾说:"计划本身什么都不是,而编制计划的过程就是一切"----转送给彷徨的项目经理
齐白石大师曾说:“学我者生;似我者死”----转送给委屈的过程改进者

====================================================================================

我忽发齐想,大致看了cnblogs的访问量分布(我可不想要高手、专家之帽,免得太累),看看我们的读者群的焦点在哪里,结果如下:

最受欢迎的是一些防流氓软件的共享软件的下载;其次是一些国内高手的开源的框架;还有就是一些设计模式、技术技巧(尤其是.Net2.0)的;关于思想和过程的很少很少,这多少也是社会和市场的一个快照吧。书店里堆满了太多XX21天从入门到精通的书,网上的很多文章也是天下文章一大抄。为什么中国就没有大师呢?我们的一些IT业的教授站得太高,你们了解我们在实际开发中经受怎样的煎熬吗?我们多么希望感受到你们的关怀和指点。你们在忙什么:让博士们发表一篇又一篇的论文吗,还是正在参加什么评审?

又扯远了,回到总结的话题:

一、与本项目有关的总结

1.1 技术总结

a) 这个项目在底层的数据采集作业调度和采集控制上没有涉及,但那是本系统的基础,还有上层的统计报表也没有说,那个地球人都知道是什么回事,这些是花费了大量的时间进行开发的; b) 收到数据后应该还有数据库操作,前几篇文中没有提起,您可以看出,数据保存可以作为一个数据订阅者和监控台同时并行作业; c) 本文没有采用最新的技术,其实,NET1.1都已经很强大了,试问有几人敢说”精通”。本文中的这些例子除了使用delegate这个特性外,其他都可以在其他技术中轻易实现,所以您可以很方便地移植到其他平台。 d) 真正的工业级别产品都会使用“看门狗”技术,确保永不停机。实际上是用汇编写一个刷新主板CMOS的“心跳”控制,主机可以设定几秒钟没有刷新就会重新启动机器,不得以为之的办法!

1.2 非技术的总结

a) 关于团队

还记得我开篇中八个原则中有建设自适应的团队这一条?

本项目由于是多方参与,所以主承包商为了降低自己的工作量,把大量工作推给了底层,所以最新的数据采集器就集中了DadaHub的功能,还有声光报警,而这块是没有界面的工控机,十分难以调试,后来的维护着实让大家头痛;主承包商也是挨了两次臭骂次才通过省局的验收!(我却因此游览了很多地方,今后没有这样的机会了)

b) 不是每个需求具有同等价值

事实上在验收上专家们才不管开发的技术路线,首先看的是性能,接着看的是功能。而我们花费了80%的精力做报表和自定义界面,在验收时只是一笔带过,而那20%的工作却是客户关注的,当然客户不会告诉你,回去看看我的那篇范围,注意那些有$符号的是吃力不讨好的功能;注意那些有钥匙符号的是验收很重要的功能

二、与本项目无关的总结

2.1 我不是技术”追新族”

技术用来解决问题,从来没有最好的技术,你找到了最适合的技术吗?当然要考虑成本!我们如果不明白周围的环境,如果没有一个清晰的目标,背负一堆工具是难道不是傻吗?

“技术毫无价值”!虽然温大师说的大家不爱听,但是想想。你要买技术、学习技术这些都是别人获得了价值,对自己来说,经验和思想的提升才是今后价值的体现啊。可能大家会说,dudu发了那么多文章,你不是在贬他?非也,我猜dudu的志向一定不会局限于第一博客,难道大家还没有感受到cnblogs本身具有的价值!

我是单位里唯一使用.NET的人,我主要用来验证我对一些解决方案的体会,学习一些开源项目解决问题的思路而已。

2.2 再谈J2EE vs .NET

如果你刚毕业,从我的感觉来说,很多企业都是选择要J2EE的学生,这并不说明.NET不好。我非常喜欢这个一个真正技术天才和商业巨子合力打造的强大工具,尽管这后面有一统天下的野心。

我看过的有些项目很搞笑,客户坚持就是要J2EE,就是要跨平台,结果搞得开发商在windows上运行Web Logic(上有政策,下有对策),无奈,无奈……

再看看微软吧,为了自己的windows一统天下的目的,就是不把.NET这么强大的框架扩展到UNIX\Linux ,苦了千万的程序员,可能微软就是毫赌.NET成功—〉windows成功的。但是我想应该在5年内可能是并存吧,大家要学习更多的东西。如果.NET支持linux,很可能没有人用windows,看来windows还不是真金,否则为何要这样呢?

2.3 再谈国内高手的框架

我想说,中国最不缺的就是人才,为何这些年来国内少有大师产生?这是教育的问题?是社会的问题?

我的感觉是:“在项目的时间压力下,很多程序员的青春和梦想被吞噬了,他们疲惫地离开了他们原本热爱的行业”,一个好的框架需要一批像linux社区那样有时间保证、愿意奉献的人们去产生。而国内的优秀苗子就是恰恰缺少时间。

国产的共享软件中就不缺FoxMail,NetAnt这样的好作品,但是这些作品往往体现不了软件项目的管理重要特性。反倒是又激发了一些人朝孤胆英雄的方向前进。

难道国内的高手们就不能联手做一点什么产品或者应用吗?非要“白花齐谢”吗?

2.4 再谈软件的盗版

做软件开发的都不买必要的正版,又如何希望别人给你的产品付款呢?中国的IT就是这么怪圈吗?我知道有几家日子好的企业,干脆不接国内的单了,陪不起这种玩法(客户没有人敢在需求上签字,谁都可以提出“仅作参考”的意见,但是验收时又一个都不能少.....合同呢?难道你还真跟客户闹翻,通常非得老总出马摆平)

我觉得学生应该可以使用学生版的软件,对产品也是推广,我支持大厂商和政府加大力度打击盗版。但是有些国外大公司倒是不紧不慢地等待时机,要等到中国的民族软件企业无法站起来时再秋后清算,好狠啊……

好了,一些想法突然就冒出来,我也不想太修饰什么,就直接打出来,供大家借鉴而已,如有冲突和冒犯,还请大家不要计较,毕竟天下的程序员都不坏。

如果你要了解我如何产生以上的牢骚,请看: 让我欢喜让我忧

Feedback

2005-11-30 18:44 by Teddy's Knowledge Base

呵呵其实我也不觉得自己是技术追新族我觉得国内少的是真正有大局观的程序员,太多人专在技术细节中了,而很少去思考很高一个抽象层面的为什么。

我想技术创新肯定不等于技术追新。如果一有新技术不管它的由来,它的原理,它的优缺点,就忙目的追逐,或盲目的抵制,那叫技术追新或者技术抵新。但是,新技术的出现,总是有一定的原因,并能解决一定的问题的,去了解其背后的为什么,总是需要的,否则,又怎么来判断新的技术的意义以及何时该如何正确使用呢?同样的道理,对现在已有的技术,也不应该因为它应用广泛,而不去了解其背后的价值和局限。

qpg的系列文章初看都是很简单的论点,用到很简单的技术,但是称得上恰到好处的从头到尾解决了一个实际的问题,我觉得这就是开发人员对待项目应有的态度和行为方式。

至于最后的呼吁,我对牢骚部分表示理解。但是,很多深层的问题没有到国外的程度,个人觉得,其实主要还是经济基础和社会基础使然,急也急不得。一个人,几个人能做到的,总是非常有限的~~ 但是能一点点一点的唤起更多人的觉悟和正确的思考方式,终究是有意义的~~

2005-12-01 09:35 by y_eric

看过之后让我想起了很多事情,曾经看到一朋友说过的一些话:
[
...
也许过了一段时间,我们又会发现跟在别人屁股后面,最后没跟上,又被人甩了,没有基础工业的支撑,it终将是泡沫,把中国的经济吊在所谓的第三产业,个人认为是很危险的,看看现在的高校,为了it而it,物理,化学,数学,生物,机械还剩下多少?

这其实是利用贸易全球化的新形式剥削,比以前政治经济学里写的资本家还狠多了。
只不过以前资本家就在身边,现在隔了国家,看不到剥削你的资本家。
中国工人累死累获活,没有福利和劳保,生活还远远不如一天到晚游手好闲,靠领救济的的美国黑人。

IT也一样,美国的商业公司先让中国盗版,以后可以强迫中国打盗版,花钱买软件。
美国靠救济金生活和公司赞助作开源软件的,可以象全世界输出开源,总之你自己的软件行业肯定发展不了的,因为中高端中竞争不过美国的商业软件,低端又有免费的开源,这些不计研发成本的,因为公司赞助的钱和国家的救济金是第三世界国家剥削来的。
...]

套用鲁迅说的,Ada 我的编译器,Ada 我的操作系统...

--------------------------------------------------------------

It is a mistake to think you can solve any major problems just with potatoes.

Contributors: FHL