编者按:本文节选自周爱民著《程序原本》一书中的部分章节。
任何一个所谓的“应用”,首先必然是一个或一组“程序”。这意味着它总是能用此前讨论的技术来完成“程序的功能”,例如,我们总是可以将一个现实的问题抽象为对象系统,并面向该对象系统来实现程序逻辑。这一过程在我们此前的讨论中已经一再复述。
但是一旦我们开始讨论应用本身的问题,则必然涉及它的两种内在驱动力量:
1 这是DSL(领域特定语言,Domain Specific Languages)的出处。
由于这里的“用户”是指非专业的计算机操作者,因此他们使用与维护程序的方法决定了两类与“现实的问题抽象”相距甚远的需求:
这些,就是应用开发所面临的全部问题的背景与焦点。
对于一个软件产品,我们需要将功能性需求与非功能性需求分开,但这并不是一件容易的事情。程序员总是按他们的习惯来理解眼前的事物,例如一个用户身份卡(User Id Card)首先是一个内存块,而不是一个抽象数据类型(ADT,Abstract Data Type)或真实的塑料卡片。那么基于这样的认识,什么才是对这个User_IdCard
的功能性需求,什么又是其非功能性需求呢?
问题在于:User_IdCard
具有多个层面的数据信息,并且对于不同的程序员来说,对其不同层面的理解都是正确、可实施、可计算的。缘于此,在不同层面上对其功能性需求的定义也就不同。例如对于偏向操作系统的开发人员,User_IdCard
只有一个信息是有用的,即2:
2 这里基于Delphi语法惯例,用前缀字符“T”来表示数据类型,而SizeOf()
函数用于取数据类型所需存储的大小。
SizeOf(TUser_IdCard)
这个长度值决定了程序如何分配和管理该数据的存储。因此这个开发人员会将他所理解的、以该长度值为核心的操作作为一个相对独立的部分区别出来,这些操作可能是如下一些函数或方法的运用3:
3 该例援引的是Delphi中的一些内存管理函数,在不同的语言或平台中可能存在差异。
FreeMem GetMem ReallocMem
例如,一种可能的情况是:
// 身份卡的批量分配
// - Delphi/Pascal Syntax
function BatchAllocCards(count: Integer): PCards;
begin
GetMem(Result, count * SizeOf(TUser_IdCard));
end;
这个函数与User_IdCard
这种数据是有关的,但又与在最终界面上操作该应用软件的用户(例如户籍管理员张三)毫无关系。
接下来我们还会面临对这个数据的第二类理解:如果这个数据是一系列性质的集合(例如结构体或对象),则每一个性质将对应于现实系统的数据。简单地说,例如:
User_IdCard.Age`
其性质Age
代表了现实系统中某个用户的年龄。与此相类似,User_IdCard
的每个性质都有其确定意义,并有相应的行为。这些行为与User_IdCard
有关,但仍然是与张三无关。例如:
User_IdCard.setAge() User_IdCard.getAge()
到现在为止,随着计算机应用软件开发技术的发展,我们已经将上述两类对User_IdCard
的理解放在一个语言的基础部分去实现了。大多数情况下,(高级的)计算编程语言通过抽象数据类型及其管理技术(例如对象、读写器、数据验证器4以及垃圾回收机制等)来将这些问题隔离在应用程序开发者的视野之外。
4 这里的验证器指的是类似JAVA中的通过注解来进行数据验证的技术。
忽略了类似上述的问题之后,我们才触到了应用开发的边缘。5
5 “如何屏蔽底层细节”也是应用开发技术的一部分,但对大多数应用开发语言/环境来说,这些都是内建机制。
对于“张三”这个户籍管理员来说,第一个真正有意义的功能是什么呢?答案是:查看身份档案。
应用开发必须站在用户视角来看问题。张三的日常工作之一是查看身份档案,这也就是他的功能需求。这项需求可能需要分成三个实现过程:
我们需要明确,张三所需功能其实是第三项,即显示身份档案。这可以实现为在用户操作环境中的图形化界面,或是可以进行交互操作的文字框等。而过程一,其实是一个附带的需求,因为从用户操作流程上来讲,张三可能6需要先看到一个列表,然后才能选择到某个User_IdCard
。
6 这只是“可能”,因为张三也可以有许多不同的方式来接触到这个User_IdCard
,并最终显示它(亦即是过程三)。这涉及到种种不同的人机交互技术的现实应用,但这些交互行为之任一,都并非是实现过程三所必然的选项。
过程二则是一个从计算机角度来看待问题的结果。也就是说,对于计算机的数据系统来说,“显示身份档案”是“调出身份档案”之后的一个后续行为。从计算机的角度来看问题,对于张三有意义的功能是第二项;但从张三的角度来说,他只关心第三项功能。
在实际开发中,我们讨论的将是类似如下的代码7:
7 对于现实的户籍管理系统来说,这可能算不得“好的”实现过程,因为它混杂了过程式与面向对象编程风格,并且对于多线程/多界面操作来说存在隐患。
// - Delphi/Pascal Syntax
(**
* 列举身份档案
* 功能:列举一批档案,返回档案列表
* - ListCard()可能是一个数据库或内存中的数据结构相关的函数
* - TIdCards可能是一个与界面无关的数据结构,因此在ListCard之后通常有相应的界面过程
*)
function ListCard(fromId: Integer): TIdCards;
begin
// …
end;
(**
* 调出身份档案
* 功能:使用既有的数据查询/访问接口,从基础数据层获得指定的身份档案
*)
function LoadCard(Id: Integer): TUser_IdCard;
begin
// …
end;
(**
* 显示身份档案
* 功能:将身份档案显示在输出设备上,可能是手持设备,或GUI,或控制台。返回用户行为。
* - “显示输出(Show)”是一个抽象的功能,对于特定环境下的实现并没有说明。
* - “用户行为(Action)”也是抽象定义,但在不同的交互环境中可能是相同的(或仅有实现的不同)。
*)
var
CurrentCard : TOperatingCard;
function ShowCard(Id: Integer): TUserAction;
begin
CurrentCard.User_IdCard := LoadCard(Id);
// …
TModalResult(Result) := TShowCardForm.Create(MainForm).ShowModal;
end;
在这个示例中,我们严格地将“程序员视角下的过程”与“用户操作视角下的过程”区隔开来:只有当TShowCardForm.Create()
并ShowModal()
发生时8,整个程序才是用户交互相关的。这一示例典型地将业务逻辑与用户交互逻辑分离开来,使得观察“什么是用户需求”成为显而易见的事情。
8 这里使用了Delphi的一些技巧:(1)TShowCardForm()
在关闭时是可以自动释放的;(2)窗体可以向调用过程返回一个值以表明用户所做的界面操作;(3)使用ShowModal()
方式打开的窗体能阻止该应用中其他窗体的操作,即这种情况下的用户界面是独占的。
对应于过程一和过程二所述的需求,就其功能的实现来看,其实包括了在三个层次(机器数据层、基础数据层和应用数据层)上对数据的所有操作。其中:
TIdCards
等类型的设计,以及Card作为可操作数据的内存分配与管理等,这些是与计算环境相关,针对机器数据层的设计;CurrentCard
作为单例的存在,TUserAction
以及它与TModalResult
之间的关系等,这些是应用数据层的设定——基本上来说,将应用数据层整体去掉之后,基础数据层仍然足以支撑其他的用户需求与业务。注意一些高级语言在应用开发中刻意地屏蔽了这里提出的三个数据层次中的多数细节 。例如在Java中可以直接使用对象来表达IdCard,并用内置的ObjectList来实现IdCards列表,因此开发人员并不需要接触到机器数据层和基础数据层,例如不必了解对象结构,以及它们在32位或64位机器上的不同实现。类似的,在多层设计中,也倾向用O/R Mapping技术来屏蔽后端数据库的细节,因为它们同样处于基础数据层或机器数据层。这一切的主要目标,就是让“面向应用开发的程序员”将重心放在应用数据层的设计之上,并基于那些相对确定的、稳定的,以及更远离用户的数据层次以及解决方案来完成开发过程。
然而这些对于一个具体的操作人员(例如张三)来说,没有什么意义。最后,对于功能三,如果不考虑显示的具体效果的话,它只是在应用数据层上一个“界面交互”的实作而已。但正是在这一点上,我们发现“用户需求”完整地影响了过程三的设计。
领取专属 10元无门槛券
私享最新 技术干货