论坛首页 综合技术论坛

[原创]定论——软件开发的评价标准(1)

浏览 39974 次
该帖已经被评为精华帖
作者 正文
   发表时间:2004-04-25  
先说两句题外话,o6z的话相当准确,我现在的确是处在思考尚未完整的阶段,需要大家多多批评,多多指正。目前的这篇文章,我的中心思想是有的,但是要写的很多部分,我还没有找出一个比较合理的顺序,因此拖了好几天。现在我决定先把已经想清楚的部分写出来。至于次序,会在以后再调整。

这也同时回答了无明的问题,这篇帖子,本来就没有定稿,等到定稿之后,我会整理出一个完整的,不间断的版本出来的。

三、注释与文档

前面说到,我们要计算的是“代码总数+文档总数”,而不是单纯的代码总数。o6z说的那个最佳demo奖,肯定比较的只是代码总数的问题。那么为什么要把文档总数也计算进去呢?对于计算机来说,不是只要代码就足够了吗?

因为注释是给人看的。我们常常会听到这样的忠告:“没有注释,人家就会面对一堆无法理解的代码,今后的维护将会异常困难。”,“程序员都不喜欢写注释,但是好的程序员会培养自己养成这种良好的习惯。”

在没有注释的时候,我们可以这样计算工作量:
首次开发工作量+第一次维护工作量+第二次维护工作量+...+第N次维护工作量

加入注释和文档之后,我们需要这样计算工作量:
首次开发工作量+首次注释文档工作量+第一次维护工作量+第一次修改注释文档工作量+......依次类推。

那么注释与文档的意义何在?就在于能够相对于没有注释与文档的情况下,减少每一次维护的工作量。这样的减少带来的效益,要大于写注释与文档投入的工作量。也就是磨刀不误砍材功。

我们常说“过犹不及”。用在这个方面,也是成立的。如果写注释与文档的工作量,超过了所能减少的维护工作量。那么这个注释与文档,就浪费了。如果因为文档太多,注释太多,导致我维护更加困难,那么这样的文档,就不但无益,而且有害了。

前面的这段话,其实隐含了一个判断,“文档是为了维护服务的”。但是很多人将这个判断,和另一句话混淆了:“文档是为了理解程序服务的”。但是,这两句话是有区别的。我认为,“文档不应该是为了让你理解程序而写的。”

举例说明:
int itemCount=15;//每页显示15条记录
//...
while(rs.next(););{
out.println(rs.getString("cTitle"););;
}
//...
//<a href="">上一页</a><a href="">下一页</a>


这是一个简单的分页显示查询结果的程序,这段程序只有一行注释:“//每页显示15条记录”,在我看来,如果这句话改成“//在此设定每页显示的记录数”。将会更好。

这两种注释,区别在哪里呢?前者是为了让你理解程序,而后者是为了方便你修改程序。

一般来说,理解的目的是为了修改。如果我能直接通过注释,就知道如何修改,那么这个注释,就节约了我的工作量。

再举例子,如果这里的15,不是写在程序里,而是从配置文件里取出来的。
int itemCount=SysInfo.getItemCount();;//取得每页显示记录数
//...
while(rs.next(););{
out.println(rs.getString("cTitle"););;
}
//...
//<a href="">上一页</a><a href="">下一页</a>

这段注释,同样是解释型的注释,如果能够改成(//在此设定每页显示的记录数;@see SysInfo.getItemCount();)
然后在SysInfo.getItemCount()函数的注释的第一行里,就清楚的说明。
//SysInfo.XML中的<ItemCount>15</ItemCount>标签
那么修改程序的人,就会在两到三次查看注释之后,就能修改原来的代码。

这也就是我说的“文档是为了维护服务的”的含义。如果注释者能够考虑别人可能对这个程序进行怎样的修改,并且给出简捷、明快的指点,就能大大减少今后维护的工作量。

但是有很多注释和文档,不是出于这样的目的而编写的。

举例说明:
<%
	String[] NDOCID	= new String[1000000];
	String[] NCURRENTWORKSORTID	= new String[1000000];
	String[] NCURRENTRSID =	new	String[1000000];
	String[] CTITLE	=new String[1000000];
	String TodayWeekFlg=null;
	String LaiFaFlg=null;
	String strToday=null;
	String strShow=null;
	String strSql =	"";
	String strSqlCount = "";
	//取当前页列出文件的SQL
	String strDqYeWjSql = "";
	String sPage=null;
	int	nPage=1;
	int	nCount=15;
	int	nPages=0;
	int	nStart=0;
	int	nEnd=0;
	Integer	val;
	String strURLParamList="";
	String strURLJsp="";
	String strURL="";
	//文件当前状态
	String strStatus="";
	PreparedStatement pstmt	= null;
	ResultSet rs=null;

	//判断取出本单位的所有用户是否为空的表记
	int intUserListNullFlg=1;
	//取出本单位所有用户
	String UserList=GetUserList((int);sysUserID);;
	//如果这个用户不属于任何部门,UserList则可能为空,SQL语句可能出错,因此设为0
	if(UserList.equals(""););{
		//该用户的本单位所有用户为空则标记为1
		intUserListNullFlg=0;
		UserList="0";
	}

	Connection con = ComFunc.getConnection();;

	TodayWeekFlg=request.getParameter("TodayWeekFlg");;
	if(TodayWeekFlg==null);{
		TodayWeekFlg="";
	}
	LaiFaFlg=request.getParameter("LaiFaFlg");;
	if(LaiFaFlg==null);{
		LaiFaFlg="";
	}
	
	//当前页号
	sPage=request.getParameter("Page");;
	if(sPage==null);{
		sPage="1";
	}
	try{
		val= new Integer(sPage);;
		nPage=val.intValue();;
	}
	catch(NumberFormatException	nef);{
	}
	nStart=(nPage-1);*nCount;
	nEnd=(nPage);*nCount;
	
	//取出日期
	java.util.Date Date	= new java.util.Date();;
	SimpleDateFormat smpDateFormat = new SimpleDateFormat("yyyy-MM-dd");;
	strToday =	smpDateFormat.format(Date);;

////该用户的本单位所有用户为空则不必到数据库中检索,不为空则到数据库中检索
if(intUserListNullFlg==1);{
//发文查阅
//***********计算总页数的SQL***S***
	strSqlCount="";
	strSqlCount=strSqlCount + " SELECT	count(*); ";
	strSqlCount=strSqlCount + " FROM WF_DOC_GW A,WF_WORK_GW_FAWEN B	";

	///计算出从来文日期到当前日期之间的节假日天数的视图 D///S///
	strSqlCount=strSqlCount + " ,(select sum(decode(WorkDay(N.dateAdjusted);, 0, -1, 1);); as iDays, ";
	strSqlCount=strSqlCount + "			M.NDOCID AS NDOCID ";
	strSqlCount=strSqlCount + "	  from WF_DOC_GW M,tbCalendar N ";
	strSqlCount=strSqlCount + "   where TO_CHAR(M.DCREATEDATE,'YYYY-MM-DD'); <= TO_CHAR(N.dateAdjusted,'YYYY-MM-DD'); ";
	strSqlCount=strSqlCount + "   and TO_CHAR(N.dateAdjusted,'YYYY-MM-DD'); <= '" +strToday+"' ";
	strSqlCount=strSqlCount + "	GROUP BY M.NDOCID); D ";
	///计算出从来文日期到当前日期之间的节假日天数的视图 D///E///

	//在发文表中存在的文件
	strSqlCount=strSqlCount + " WHERE A.NDOCID=B.NDOCID ";
	//已经不在流程中的文件
	strSqlCount=strSqlCount + " AND A.NSTATE=0  AND A.NTYPEID<>8";
	//仅列出本单位所有用户的文件
	strSqlCount=strSqlCount + " AND A.NDOCID IN (SELECT NDOCID FROM WF_MSG WHERE NUSERID IN ("+UserList+"); AND CACTION<> 'TEYUE'); ";
	//一周内的判定///S///
	//从来文日期到当前日期的工作日天数-节假日天数<=5天(为一周内);
	strSqlCount=strSqlCount + " AND 6-(WorkDays(A.DCREATEDATE,TO_DATE('"+strToday+"','YYYY-MM-DD'););-NVL(D.iDays,0););>0 ";
	//从来文日期<=当前日期
	strSqlCount=strSqlCount + " AND TO_CHAR(A.DCREATEDATE, 'YYYY-MM-DD'); <=	TO_CHAR(TO_DATE('"+strToday+"','YYYY-MM-DD');,'YYYY-MM-DD'); ";
	//主文档表与做成的节假日视图D的关联
	strSqlCount=strSqlCount + " AND A.NDOCID=D.NDOCID(+); ";
	//一周内的判定///E///
//***********计算总页数的SQL***E***

//***********计算总页数---S---
	pstmt =	con.prepareStatement(strSqlCount);;
	rs=pstmt.executeQuery();;
	if(rs.next(););{
		nPages=rs.getInt(1);;
		if(nPages%nCount==0);{
			nPages=nPages/nCount;
		} else {
			nPages=nPages/nCount+1;
		}
	}
	nStart=(nPage-1);*nCount;
	nEnd=(nPage);*nCount;
	rs.close();;
	pstmt.close();;
	rs=null;
	pstmt=null;
//***********计算总页数---E---

//***********取列出文件的SQL***S***
	strSql="";
	strSql=strSql + " SELECT A.NDOCID,A.NCURRENTWORKSORTID,A.NCURRENTRSID,A.CTITLE,A.NTYPEID ";
	strSql=strSql + " FROM WF_DOC_GW A,WF_WORK_GW_FAWEN B	";
	///计算出从来文日期到当前日期之间的节假日天数的视图 D///S///
	strSql=strSql + " ,(select sum(decode(WorkDay(N.dateAdjusted);, 0, -1, 1);); as iDays, ";
	strSql=strSql + "			M.NDOCID AS NDOCID ";
	strSql=strSql + "	from WF_DOC_GW M,tbCalendar N ";
	strSql=strSql + "   where TO_CHAR(M.DCREATEDATE,'YYYY-MM-DD'); <= TO_CHAR(N.dateAdjusted,'YYYY-MM-DD'); ";
	strSql=strSql + "   and TO_CHAR(N.dateAdjusted,'YYYY-MM-DD'); <= '" +strToday+"' ";
	strSql=strSql + "	GROUP BY M.NDOCID); D ";
	///计算出从来文日期到当前日期之间的节假日天数的视图 D///E///
	//在发文表中存在的文件
	strSql=strSql + " WHERE A.NDOCID=B.NDOCID ";
	//已经不在流程中的文件
	strSql=strSql + " AND A.NSTATE=0  AND A.NTYPEID<>8";
	//仅列出本单位所有用户的文件
	strSql=strSql + " AND A.NDOCID IN (SELECT NDOCID FROM WF_MSG WHERE NUSERID IN ("+UserList+"); AND CACTION<> 'TEYUE'); ";

	//一周内的判定///S///
	//从来文日期到当前日期的工作日天数-节假日天数<=5天(为一周内);
	strSql=strSql + " AND 6-(WorkDays(A.DCREATEDATE,TO_DATE('"+strToday+"','YYYY-MM-DD'););-NVL(D.iDays,0););>0 ";
	//从来文日期<=当前日期
	strSql=strSql + " AND TO_CHAR(A.DCREATEDATE, 'YYYY-MM-DD'); <=	TO_CHAR(TO_DATE('"+strToday+"','YYYY-MM-DD');,'YYYY-MM-DD'); ";
	//主文档表与做成的节假日视图D的关联
	strSql=strSql + " AND A.NDOCID=D.NDOCID(+); ";
	//一周内的判定///E///
	//文档列表排序设定
	strSql=strSql + " ORDER	BY A.NDOCID DESC";
//***********取列出文件的SQL***E***

//合成取当前页列出的15条文件的SQL---S---
	strDqYeWjSql="";
	strDqYeWjSql=strDqYeWjSql + "SELECT * FROM (" + strSql +"); WHERE ROWNUM<" + (nEnd+1);;
	strDqYeWjSql=strDqYeWjSql + " minus (SELECT * FROM (" + strSql +"); WHERE ROWNUM<" + (nStart+1); + ");";
//合成取当前页列出的15条文件的SQL---E---
}
	strShow="一周发文";
 %>

这样的注释,我称之为“除臭剂”式的注释。在超烂的代码里加上超多的注释,以掩盖代码的腐臭。

还有这样的文档,几百页甚至上千页的word文档,充斥着工工整整的表格,内容千篇一律而又空无一物。当我想通过阅读文档,来帮助我维护程序时,却发现自己陷入了“文档泥潭”。

也许有人会说,为了维护而写文档,这样的要求当然好,但是我无法预见今后的修改呀。天知道别人会怎么改我的程序。

我的考虑是这样的,开发的水平的高低,或者说设计水平的高低,就在于能够在大多程度上准确的预见今后可能的更改。然后在设计的时候,就将这些可能的更改纳入考虑的范畴,在注释的时候,就将这些可能的更加纳入说明的范畴。如果你考虑得过多,那当然就是浪费,考虑得过少,那当然也是不足。经验的积累,也就是体现在这里。

这个话题,在讨论怎样才是好的设计的时候,我还会再进一步分析的。
0 请登录后投票
   发表时间:2004-04-26  
庄表伟 写道
前面的这段话,其实隐含了一个判断,“文档是为了维护服务的”。但是很多人将这个判断,和另一句话混淆了:“文档是为了理解程序服务的”。但是,这两句话是有区别的。我认为,“文档不应该是为了让你理解程序而写的。”


强烈同意这一句:“文档是为了维护服务的”。

可以说代码注释也是一门学问,什么时候应该注释?什么时候不应该注释?注释应该达到什么样的目标?注释应该表达什么样的内容?是让你的代码替你说话还是让注释替你说话?

我所接触的程序员当中很多人(先殴打我自己,呵呵)往往过于强调注释的形式而忽略了注释根本的意义所在,甚至有时是为了注释而注释。其实往往一个好的变量名或者一个好的函数名就能更加清楚的表达你的意思。

过多的,或者烂的注释也是程序的bad smell。

对于设计文档等等,我认为应该尽量简洁,更专注于高程的概念和设计原理。太过于细节的东西应该让源代码来体现。过于细节的文档和代码的同步本身就是一个巨大的工作量。
0 请登录后投票
   发表时间:2004-04-28  
注释与文档这部分,写得不好,决定重写。

三、注释与文档

首先讨论一个计算工作量的数学模型。

1.没有注释的情况下:
第一次开发工作量=代码行数×单位代码工作量
维护工作量=理解代码工作量+决定方案工作量+修改代码行数×单位代码工作量

2、有注释的情况下:
第一次开发工作量=代码行数×单位代码工作量+代码行数×注释率×单位注释工作量
维护工作量=基于注释理解代码工作量+决定方案工作量+修改代码行数×单位代码工作量+修改代码行数×注释率×单位注释工作量

增加注释之后,增机了注释的工作量,这些增加需要通过节约理解代码的工作量赚回来。

3、理解代码的工作量讨论
在没有注释的情况下:
理解代码工作量=代码总量×理解代码的效率
在有注释的情况下:
理解代码工作量=注释总量×理解注释的效率
又因为
注释总量=代码总量×注释率

因此,要想通过注释减少维护工作量,就要切实降低注释率,并且提高注释的效率。

4、这些数据的含义

注释率:软件的注释行数/软件的程序行数

有人像这样写代码
//给i赋初始值
int i=10;
//开始循环
for(int j=0;j<i;j++);{
//输出j的值
System.out.println(j);;
}//循环结束


这样的一比一注释,注释率就是1。一般来说,注释率大于1的程序,肯定是垃圾。因为写这个程序的人,根本不知道该注释哪些地方。

但是,并不是说注释率越低越好,因为注释少了之后,可能会影响理解注释的效率。

面对一次维护需求时:
理解注释的效率:需要阅读的注释行数/注释总数

如果一个维护者,在阅读了所有的注释之后,还没有理解程序,更不要说找到解决方案,那么他就必须去阅读代码。理解代码的工作量,就是没有注释时的工作量再加上被垃圾注释浪费掉的工作量。

这时的理解注释的效率,就是大于1的。写出这样的注释的人,不配做程序员。

切实提高理解注释的效率,是提高维护效率的根本。可以有以下手段:
1、结构化文档
2、HTML格式的合理的超链接
3、关键字索引(方便对于注释全文检索)
4、逻辑清晰的概要介绍

上面的这些公式都有一条:决定方案工作量。
这个工作量其实是可以合并的。以往的理解是,必须理解了现有的程序,才能决定维护的方案。但是我们可以假设,如果通过查询注释和文档,就能找到如何维护的答案,那么这样的维护就是最轻松的。

我们不假设所有的维护都能通过查询注释和文档,直接得到答案,但是如果有一个预先的合理的考虑,而写下了系统维护指南这样的文档,那么这样的文档,将是最有价值的。

另外一个手段,就是在每次维护之后,写下一个维护记录,大致格式是:
我的维护需求,我查询了哪些注释和文档,最后我在哪里找到了答案

这样的记录,将会为后来者提供极大的方便。

下面说一点题外话,因为在软件工程中,还存在这样的文档:
《功能需求描述》《概要设计》《详细设计》《.......》
我不理解为什么需要这样的文档,也不理解谁会来阅读这样的文档。为了模仿建筑工程,软件工程已经大大的扭曲了自己的本性。

注释与文档的本质,是为了便于软件的开发和维护,而不是在一道一道的工序之间作为“交接班”的说明。

如果一份文档,无法回答这两个问题,那么这份文档就是浪费了:

谁愿意写这样的文档?
谁愿意读这样的文档?
0 请登录后投票
   发表时间:2004-05-01  
软件开发当中的计量经济学的模型是在一项非常大的空白.我相信,那位开路先锋能够在这个方面上自成一派,成为大家,对计算机界的贡献绝对不输给Von Neumann或者是Turing.


我曾经想过在Javadoc里面,类似todo的tag太少了.
如果developer能够把写程序当成是写bbs那该多好,随时随地来个表情,当然这是理想状态,实际上有巨大的压力.

有些的assumption,有些的weak point,有些的future revision point, pending Point的方向,甚至心情的tag,这些都是在冰冷的代码里面的非常有用的信息.

我觉的代码的注释是先一定要能体现算法的思想,然后才是细节的问题.

注释应该能把developer当时的思路记录下来,更细节的东西,应该去看代码,如果看代码看不懂,那就是说明,
要么
  1. dev的思路没到
   2. 备份的人员的技术素质不够,对使用的技术不熟,需要培训或者招聘.

还有,最好的详细文档确实是代码(这里指的是把原来一部分的word文牍的内容变成了代码的一部分)

但是无论如何,设计思想和设计的决策一定要逼着开发的下来,否则,再详细的代码都没有用的.
0 请登录后投票
   发表时间:2004-12-30  
同意效率的观点,进一步细化来看
可以发现存在着矛盾,而这正是各种方法论试图要解决的

笼统的考察四个变量
    近期投入,近期产出,远期投入,远期产出

    我们所能够控制的只有“近期投入”这个变量
策略一:主要考虑近期产出。敏捷方法?
策略二:主要考虑远期产出。充分设计?
策略三:主要考虑远期投入的削减。良好设计、文档和注释、完美代码?

注意以上三行最后的问号,是否存在一种方法论可以同时兼顾这三种策略呢?

最后,还有一个根本问题,什么是近期,什么是远期?
0 请登录后投票
   发表时间:2004-12-30  
clamp 写道
同意效率的观点,进一步细化来看
可以发现存在着矛盾,而这正是各种方法论试图要解决的

笼统的考察四个变量
    近期投入,近期产出,远期投入,远期产出

    我们所能够控制的只有“近期投入”这个变量
策略一:主要考虑近期产出。敏捷方法?
策略二:主要考虑远期产出。充分设计?
策略三:主要考虑远期投入的削减。良好设计、文档和注释、完美代码?

注意以上三行最后的问号,是否存在一种方法论可以同时兼顾这三种策略呢?

最后,还有一个根本问题,什么是近期,什么是远期?


你好,你的问题提得非常深入。

但是,这其实不是一个方法论的问题,而是一个经验的问题。

也就是说,站在当前这个位置,我们能够看多远?这个不是由方法论决定的,而是由设计决策人员的经验决定的。

做一个决策,这个决策将会如何影响今后的总投入和总产出。这个判断是否准确,是由人的水平决定的。

一个好的方法论,能够做到的,就是充分考虑到其中人员的经验的影响因素,而不是假设所有的人,都可以借助自己的方法论而获得成功。
0 请登录后投票
   发表时间:2004-12-30  
庄表伟 写道
也就是说,站在当前这个位置,我们能够看多远?这个不是由方法论决定的,而是由设计决策人员的经验决定的。
做一个决策,这个决策将会如何影响今后的总投入和总产出。这个判断是否准确,是由人的水平决定的。


有点空泛了。
举个例子
现在手头有若干需求,根据经验知道它“可能”不完整、“可能”有变化
那么现在有两条路:
1、继续完善需求,尽可能控制其变化。    重型?
2、在现有需求的基础上开始下一步工作。  轻型?
方法论告诉我们在什么情况下应该走第一条路,在什么情况下应该走第二条路。

庄表伟 写道
一个好的方法论,能够做到的,就是充分考虑到其中人员的经验的影响因素,而不是假设所有的人,都可以借助自己的方法论而获得成功。


同意后一半,什么都能做的方法论没有意义。
不同意前一半,经验这个影响因素太虚无缥缈了,很难评估。
我认为一个好的方法论需要明确指明自己适用于什么样的前提条件。包括哪些要素。人是其中最难评估的,因此一个对人员要求过高的方法论不是一个好的方法论。
0 请登录后投票
   发表时间:2004-12-30  
clamp 写道
同意效率的观点,进一步细化来看
可以发现存在着矛盾,而这正是各种方法论试图要解决的

笼统的考察四个变量
    近期投入,近期产出,远期投入,远期产出

    我们所能够控制的只有“近期投入”这个变量
策略一:主要考虑近期产出。敏捷方法?
策略二:主要考虑远期产出。充分设计?
策略三:主要考虑远期投入的削减。良好设计、文档和注释、完美代码?

注意以上三行最后的问号,是否存在一种方法论可以同时兼顾这三种策略呢?

最后,还有一个根本问题,什么是近期,什么是远期?


从经济学的角度来讲,近期和远期的转化是通过利率来表示的,也就是把资源放到其它地方的机会成本,对软件工程呢?有没有办法来衡量近期和远期的的价值?如果有合适的度量,加上不确定性的分析,可能可以做为一个度量的原型。
0 请登录后投票
   发表时间:2004-12-30  
因为注释分散在各个jsp, java代码中,有时的确需要一份《概要设计》向开发人员,维护人员传递思路。当然,前提是这份文档必须是为目的,而不是为形式存在的。
0 请登录后投票
   发表时间:2004-12-30  
to:clamp

施主着相了。

所谓方法论,是一组指导原则,如果一组指导原则,不明白自己的“边界”,自认为万能,那么这样的方法论,就有银弹的危险。

清醒的方法论,也就是知道自己“被不同的人使用,会有极为悬殊的效果”的方法论。这样的方法论,并不能让老板放心——老板总是希望自己的企业能够通过“XX认证”,所有的事情都由数据图表来表示,然后公司就成了成熟的软件企业——哪有这么容易的事情。

真正的方法论,一定会告诉老板:
你要重视人!
你要重视经验!
你要信任高手!

注意,叫人迷信高手的,也不是方法论,那是“迷信”。

to:jiwenke
这个思路不错的,但是要有计量经济学的功力才行。
0 请登录后投票
论坛首页 综合技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics