1. Silver Bullet
No Silver Bullet: Essence and Accidents of Software Engineering —— 无银弹理论,出自于美国1999年图领奖得主、世界软件工程的先驱Frederick P. *s。在论文中,Prof. *将软件工程中的银弹定义为一项能够将软件开发工作的轻松程度调高一个数量级的技术或工具。同时,他也将软件开发过程中的困难划分为两大部分,分别是Essence Difficulties & Accidents Difficulties。根据Prof. *对其的定义,Essence Difficulties是软件开发内在的本质问题,而Accidents Difficulties是软件开发过程中附加产生的问题,并非内在。本质问题往往是抽象概念上对软件的设计和构建,从抽象问题发展出具体概念上的解决方案如:数据集设计、了解数据间的关联、算法设计等;而附加问题则是如何用编程语言来表现这些抽象的实体。
根据Prof. *在文中的论述,附加性分体随着软件工程中的工具或技术的改善会逐渐得到改善,而真正难以解决的是本质性问题。本质性问题的思考在人脑内进行,缺乏有效的辅助工具,它的主要困难分为复杂性、隐匿性、配合性、易变性四项。本质问题和附加问题的对比关系正如下述从文中摘录的隐喻:
“I believe the hard part of building software to be the specification, design, and testing of this conceptual construct, not the labor of representing it and testing the fidelity of the representation. We still make syntax errors, to be sure; but they arefuzz compared with the conceptual errors in most systems.”
过去一些年中发展起来的高级语言、分时技术、集成开发环境等从一定程度上解决了附加性问题;一些有希望成为银弹的技术也浮现出来,如文中列举的Ada和其它高级语言的进展、面向对象的思想、人工智能、专家系统、自动化程序设计、图形化程序设计、软件验证、环境与工具、工作站。然而上述的任何技术都无法达到银色子弹的要求,Prof. *在文中断言未来的十年内(始于1986年)将不会出现“银色子弹”。
然而这篇文章受到了许多争议,如Brad J. Cox曾发表过文章称There Is a Silver Bullet,提出了可以采用软件重用(reusable)、可替换组件的方式来解决概念性的本质问题,而事实证明它确实可以成为一颗银弹:摩托罗拉公司曾在编译器项目中通过85%的重用实现了10:1的生产率改进(参考Jacobson I., et al, Software Reuse: Architecture, Process and Organization for Business Success);另一个例子:
“通用汽车公司(GM)的一个早期应用——用于汽车修理维护部件的库存计划和维护管理系统。旧的系统由265,000行PL/1代码组成,花了12.5人年,运行时需要13.6M内存,替代系统用Smalltalk 80开发,总共用了不到1人年的时间,系统只有22,000行代码,内存仅需1.1M。两个系统的性能大致相同,但二者的总体生产率之比是14:1!”
(摘自《面向对象方法原理与实践》(原书Object-Oriented Methods: Principles & Practice, 2001年Pearson第3版)机械工业出版社 2003年3月第1版 P45)
从上述两个例子来看,Prof. *在某种程度上对银弹的定义和断论太过偏激。一味地强调银弹可能会忽视其它并不可能成为银弹但同样重要的技术或方法。虽然作为一个刚刚接触软件工程的学生,但经历过软件工程的团队开发后,我意识到一个开发流程中可能存在许多瑕疵缺陷,多数不是致命的错误,但这些错误的累积绝对有可能让软件的开发效率降低一个数量级。通过系统地分析解决这些问题同样可以让软件开发效率得到飞跃的提升。因此,抱有寻找银弹的热情固然可敬,但与此同时绝不应该忽视了那些次于银弹的技术和方法。
2. Waterfall Model
根据Dr. Winston W. Royce在Managing the Development of Large Software System一
文中提出的瀑布模型完整图示如下:
瀑布模型将软件的开发周期划分为系统需求、软件需求、初步软件设计、分析、软件设计、代码实现、测试、运行等几个部分。按照瀑布模型,软件开发的每一轮迭代都应该完整地经历过每一个开发阶段,并且用系统的方式对其作出评析,有完整的规划、分析、设计、测试过程。在评估过程中,如果某个阶段存在问题,那么可能会回溯到前几个环节重新执行。因为其清晰明了的流程结构以及能够有效确保软件开发的质量,许多软件开发已经采用此套标准。
在我们的软工课程中,开发流程和此套模式类似:在第一周的开始我们针对Lets App的系统、用户需求进行了分析,考虑了不同的用户群体在共性和个性上的不同需求,通过调查问卷等形式对真正需求量大的需求进行了保留;在第一周接下来的时间内,先由PM对软件框架和模块进行了大致的设计,并结合实际需求分析,再根据组员的不同分工,让组员根据自己的职责再次分析模块设计,并提出合理的改进方式或设计细节实现方式;接下来的两周时间内我们分工进行了代码的实现;在这几周内,开发过程中组员对自己负责的各部分功能进行了模块测试,在最后一周,将各组员完成的模块组合拼装并进行完整App的各部分功能测试;然而在最后的运行过程中,我们针对App的功能提出了一个致命的设计缺陷,即Alpha阶段尚缺少用户间的交流功能,我们仅实现了用户实体和活动实体间的联系,用户与用户是通过活动间接关联,无法直接交流,因此阻碍了活动的进行;发现了缺陷后我们只能又返回最初的需求分析,再针对用户交流这个模块重新执行了一遍类似瀑布模型的流程。
通过这次软工Alpha阶段类似瀑布模型的开发,其中最大的一个收获即这套模式的应用能够让开发过程有条不紊,每个环节都有相应的分析总结,再重大的错误也会在一次又一次的在重复执行瀑布模型过程中降低到最少。
瀑布模型虽然适用于我们的软工开发,却依然存在一些缺陷,而并不适合于一些需求和环境较为灵活的开发项目。瀑布模型间虽然可以灵活地向上切换阶段,但向下过程必须严格按照瀑布流程,并且每个阶段除了开发外也要执行相应的分析,会产生大量文档增加工作量;同时,瀑布模型内部的多次迭代和其线性的开发过程导致用户必须在所有内部迭代完成,达到最后一个阶段后才能看到最终成品,这对于用户需求灵活、变化大的项目来说并不适合。
3. Big Ball of Mud
从互联网查阅了一些关于大泥球的定义,发现大多数中文理解只从字面意思强调了原
作者Brian Foote and Joseph Yoder在Big Ball of Mud一章中的描述。其中大多数对“大泥球”的定义为“杂乱无章、错综复杂、邋遢不堪、随意拼贴的大堆代码”,对于产生大泥球的原因,主要划分为如下几个部分:
1) 一次性代码
2) 碎片式增长
3) 为了让软件不出问题
4) Copy/paste导致问题转移(有问题的代码被复制到很多地方,不断蔓延)
而通过阅读原文,我发现这样对大泥球的定义只局限在“大泥球”这一比喻的“大”和“泥泞拖沓”这两个物理特性上,却忽视了泥球制造过程的粗糙和手法的原始。在原文中,作者用Shanty Town作为软工中大泥球的类比:Shanty Town中的房屋往往极为粗糙,制作方法也十分简陋。Shanty Town中单个的房屋建造工作都由住户本人独自完成,制作过程缺乏手工技术,并且十分累人。这样的房屋虽然不用过多的技术,只需付出足够的廉价劳动即可,但它只是临时的权宜之计(ad hoc tech for convenience),并无后期检查,潜在的问题往往十分严重,这就使得它难以在实用性(对代码而言,它的实用性对应于基础需求的实现,泛型能力,可拓展性等)或者审美价值(根据作者描述,个人推测可以对应于软件工程中代码的风格、简洁性)等方面得到提高。
下面摘取Big Ball of Mud一章的原文进行阐述:
“All too many of our software systems are, architecturally, little more than shantytowns. Investment in tools and infrastructure is too often inadequate. Tools are usually primitive, and infrastructure such as libraries and frameworks, is undercapitalized. Individual portions of the system grow unchecked, and the lack of infrastructure and architecture allows problems in one part of the system to erode and pollute adjacent portions.”
根据上述加红字体,可知作者就Shaty Town和软工中大泥球的原始性、基础性、粗糙性等方面进行了类比,这方面两者存在共性。而划线部分则对应了上述他人观点中总结的,大泥球容易导致问题转移的潜在危害。由于大泥球构造过程中不会进行分析检查,因此即使工程中止存在这样一部分的大泥球,也可能会侵蚀到整个系统,将问题扩散到各个模块。
下面这段话,作者同样提出了大泥球模式存在的问题:
“A BIG BALL OF MUD usually represents a triumph of utility over aesthetics, because workmanship is sacrificed for functionality. Structure and durability can be sacrificed as well, because an incomprehensible program defies attempts at maintenance.”
大泥球模式中诸如,粘贴复制代码,一次性代码的行为,都是只考虑眼前情况,当前
的功用需求,却牺牲了任何能够增加程序持续性,维护其更稳定的技术。
通过分析我们Alpha阶段的代码,确实发现了一个十分严重的“大泥球”。该代码是实现本地截取图片和上传的功能,下面给出代码(代码较长,只为展示其“大”的特性,可以折叠略过):
ImageButton imgbtn = (ImageButton) this.findViewById(R.id.initiate_event_image);
imgbtn.setOnClickListener(new Button.OnClickListener() {
public void onClick(View v) { // 显示SelectPicPopupWindow selWindow = new SelectPicPopupWindow(InitiateEventActivity.this, new OnClickListener() {
public void onClick(View v) { selWindow.dismiss(); int id = v.getId();
if (id == R.id.btn_take_photo) {
SimpleDateFormat timeStampFormat = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss");
String filename = "Lets_Head_" + timeStampFormat.format(new Date());
ContentValues values = new ContentValues();
values.put(Media.TITLE, filename);
Intent intent1 = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
photoUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
intent1.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(intent1, 1);
} else if (id == R.id.btn_pick_photo) {
Intent intent2 = new Intent();
intent2.setType("image/*"); intent2.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(intent2, 2);
} else {
Log.i("bmob", "undefined button");
Toast.makeText(InitiateEventActivity.this, "未定义的按钮!", Toast.LENGTH_SHORT).show();
}
}
}); selWindow.showAtLocation(InitiateEventActivity.this.findViewById(R.id.initiate_event_detail),
Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 0); }
}); @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case 1:
if (resultCode == RESULT_OK) {
Uri uri = null;
if (data != null && data.getData() != null)
uri = data.getData(); else if (photoUri != null)
uri = photoUri;
else {
Toast.makeText(this, "OnActivityResult出错,case1", Toast.LENGTH_SHORT);
}
cropPhoto(uri); }
break; case 2:
if (data != null) {
Uri uri = data.getData();
cropPhoto(uri); Log.e("uri", uri.toString());
} else {
Toast.makeText(this, "OnActivityResult出错,case2", Toast.LENGTH_SHORT).show();
}
break; case 3:
if (data != null) {
Bundle extras = data.getExtras();
Bitmap head = extras.getParcelable("data");
if (head != null) {
setPicToView(head);
ImageView imageView = (ImageView) findViewById(R.id.initiate_event_image);
imageView.setImageBitmap(head);
}
}
break; case 4:
Bundle bundle = data.getExtras();
if (bundle != null) {
TextView baseAddressTv = (TextView) this.findViewById(R.id.baseAddress);
TextView detailAddressTv = (TextView) this.findViewById(R.id.detailAddress);
baseAddressTv.setText(bundle.getString("baseAddress"));
detailAddressTv.setText(bundle.getString("detailAddress"));
}
default:
break;
}
super.onActivityResult(requestCode, resultCode, data);
} public void cropPhoto(Uri uri) {
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 200);
intent.putExtra("outputY", 200);
intent.putExtra("return-data", true);
startActivityForResult(intent, 3);
} private void setPicToView(Bitmap mBitmap) {
String sdStatus = Environment.getExternalStorageState();
if (!sdStatus.equals(Environment.MEDIA_MOUNTED)) {
return;
}
FileOutputStream b = null;
String path = "/sdcard/Lets/myHead/";
File file = new File(path);
if (!file.exists())
file.mkdirs(); User curUser = BmobUser.getCurrentUser(this, User.class);
fileName = path + curUser.getUsername() + "_head.jpg"; try {
b = new FileOutputStream(fileName);
mBitmap.compress(Bitmap.CompressFormat.JPEG, Toast.LENGTH_SHORT, b); } catch (FileNotFoundException e) {
e.printStackTrace();
Log.i("bmob", e.getMessage());
} finally {
try {
b.flush();
b.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在测试后期,我们发现这段代码存在一些问题,上传的图片老是出现质量下降,失真严重,布满色块等情况。然而在写代码的过程中,为了贪图方便,这段代码被直接复制粘贴到了各个需要使用的部分,这使得各部分代码不仅冗余、难以理解,而且同样代码的错误被带到了程序的各个部分。
另一个发现的泥球存在于我们的搜索界面(因代码过长在此不展示),由于应用了一个开源项目的界面框架,然而这个框架并不能完全适用于我们动态更新UI的需求,因此开发过程中,贪图方便的做法即不去完全理解代码的逻辑结构来修改定制,而是直接观察需要的参数,想尽各种办法将需要动态更新的数据传入框架底层的页面绘制部分。然而修改完成后发现,这个通过各种拙劣技术拼接起来的框架虽然能够实现既定需求,却带来了其它潜在问题:传入的参数因各种修改导致最后界面切换行为的崩溃。
经过这次阅读和分析,也提醒我在接下来的开发过程中要避免再次采用这样的权宜之
计造出粗糙的代码。并不要求代码处处精雕细琢,但至少避免大泥球模式下primary&unchecked的特点。
4. Cathedral and the Bazaar(CatB)
根据Wikipedia中对The Cathedral and the Bazaar一书的介绍,可以对大教堂和集市
作出如下定义:
大教堂:每一个发布版本的软件代码都是可以获取到的,但开发过程中的代码只在一部分开发者*享。
集市:代码开发的过程在互联网上进行公开和共享,任何版本的代码都可以获取到。
在文中,作者Raymond的主要论点是,一份代码的开发过程,如果采用集市模式经过足够多双眼的审查,那么所有的bug都不会深藏于工程中。越多的源代码能够开放给公众进行测试、审查理解、实验,那么bug被发现的速度也就更快;相反,如果使用大教堂模式则需要花费更多的时间来寻找bug。
然而也有人对集市模式的开发过程持有异议,根据A Generation Lost in the Bazaar一文,作者强调了当前的集市模式已经给软件开发带来了许多不必要的麻烦,诸如:臃肿的代码,繁琐的过程,质量低下的文件等等。
最初的Unix系统正是秉承着大教堂模式,因设计简约、功能实用、执行优雅而著称于世,然而作者提出当前的集市模式却在这个教堂上增添了一堆脓包似的权益代码。此处不禁联想到上述对大泥球的定义,由此可见集市模式的一个弊端即会存在大量“大泥球”。
集市模式虽然能够增加代码的透明性,让漏洞尽可能减少,但与此同时,我们无法指望所有集市中的买家(程序员)都拥有极高的素质,或对他所使用的工程了如指掌,并且这个集市中又缺少足够权威的明文规定,统一规范(如果有的话也就不是集市了吧)。因此一个买家获取到他所需的文件后,也许根本无法区分什么是工具本体,什么是测试用具,谨慎起见就只能一把抓地应用到所需的地方。就这样,冗余的代码一层套一层像滚雪球一样让整个市场不堪重负。
5. Agile Method
根据阅读材料和原文,可以得到Agile Method的主要内容有如下:
核心理念:适应和以人为本。
Agile方法的四个价值
(1) 较之于过程和工具,更注重人及其相互作用的价值。
(2) 较之于无所不及的各类文档,更注重可运行的软件的价值。
(3) 较之于合同谈判,更注重与客户合作的价值。
(4) 较之于按计划行事,更注重响应需求变化的价值。
Agile方法的指导原则:
(1) 在快速不断地交付用户可运行软件的过程中,将使用户满意放在第一位。
(2) 以积极的态度对待需求的变化(不管该变化出现在开发早期还是后期)。Agile过程紧密围绕变化展开并利用变化来实现客户的竞争优势。
(3) 以几周到几个月为周期,尽快、不断地交付可运行的软件供用户使用。
(4) 在项目过程中,业务人员和开发人员最好能一起工作。
(5) 以积极向上的员工为中心建立项目组,给予他们所需的环境和支持,对他们的工作予以充分的信任。
(6) 在项目组中,最有用、最有效的信息沟通手段是面对面的交谈。
(7) 项目进度度量的首要依据是可运行的软件。
(8) Agile过程高度重视可持续开发。项目发起者、开发者和用户应能始终保持步调一致。
(9) 应时刻关注技术上的精益求精和设计的合理,这样能提高软件的快速应变力。
(10) 简单化(尽可能减少不必要工作的艺术)是基本原则。
(11) 最好的框架结构、需求和设计产生于自组织的项目组。
(12) 项目组要定期对其运作方面进行反思,提出改进意见,并相应进行细调。
此外,Agile方法实施中一般采用面向对象技术(接口定义良好的其它开发技术也可),另外还强调在开发中要有足够的工具(如配置管理工具、建模工具等)支持。
上述标红的两项是我们的团队在开发过程中应用或意识到的部分。
(6)在平时的开发过程中,我尽量协同安排好大家的时间,统一在一个时段集体进行编程。这样不仅能够相互监督提高效率,也能增加面对面交流的机会,促进不同模块开发过程中的交流。尤其是遇到一些前端和后台的耦合问题,两部分开发人员面对面的交流往往能让问题尽快解决。另外,遇到代码重用的情况,写代码的一方也能清晰直接地为调用方提供思路,也能从一定程度上避免“大泥球”的情况。
(10)简单化这一原则在我们的开发过程中并非一开始就执行。由于我个人对细节较为关注,因此在开发前期就会针对一个模块力图将其不断优化做到最好,然而这样的问题就是花费过多时间在一个小问题上(比如主页面的动态效果),时间的投入和产出值的比例无法满足前期的开发需求。且另一个问题是,随着软件进一步的开发,先前的优化可能已经不适用,此时又要再进行修改(开发到后期时发现前期的动效显得十分鸡肋,不符合整体的风格等),这样不仅浪费了前期的时间,也增加了后期修改的负担。因此在敏捷开发中,一个重要概念即以简单为主,不害怕应对变化添加功能。若整个过程都十分简单,那么应对起变化也不会显得那么复杂,并且能够减少冗余的工作量。
6. 软件工程方法论
软件工程的出现一直饱受争议,似乎在我们日常的编程中并没有太多的条条框框限制
着我们,然而一旦跳出日常这一范围,往各大公司望去,任何公司都有自己的一套软件工程方法论(如微软的MSF)。
由于各种计算机系统在数量、复杂程度、应用种类等方面的增长对计算机软件提出了极高的需求,一个组织想要维持这样一个庞大的体系仅仅靠经验主义现有的方法是不足够的,此时软件工程的方法论起到了至关重要的作用。
正如MSF中提到的团队模型和过程模型(摘自邹老师博客):
一套完整的方法论体系为软件工程中的成员划分好其各自的职责,同时规定要每个开发流程,以及需要遵从的原则,无疑会让整个开发流程更清晰明确。
然而我们又不得不承认,一个团队的效率有大部分因素取决于成员的水平。虽然如此,我们也不能完全否认方法论的导向作用。也许我们确实不必严苛地遵守方法论中的每一个细枝末节,但我们需要将方法论的本质,其关键因素应用到实际开发中(如敏捷开发中追求简单的原则)。一个再牛的团队,如果没有步调一致的导向,那也难以保证其效率。而且,软件工程方法论中包含多种不同的模式,团队可以根据当前的用户需求和市场情况挑选不同的框架应用于开发(比如希望稳扎稳打,提高产出水平的可以采用瀑布模式;希望灵活应对需求变化的可以采用敏捷模式)。没有这一套流程的引导,难以想象一个团队该如何纯凭借经验达到既定的需求。