我发现,软件开发中有一个尴尬的地方,负责开发软件的人并不是使用软件的人,更不是每天都依赖它开展工作的人。在开发管理后台系统时,尤为如此,除了开发技术人员自己使用的工具后台外。这一点客观现象导致了我们作为软件开发工程师很难深入切身体会到真实用户最迫切的诉求。
一个友好的管理后台、系统或平台,不是在于它的界面有多炫酷,或者背后使用的技术有多前沿,而是在于它能与人们日常的工作吻合起来,共同解决问题,成为人们得力的助手。所以,毕业后第一年我就体会到,软件应该是用于提高效率,减轻负担的,而不是额外增加人们的工作压力。以我妻子在工作中使用的采购平台为例,她经常会担心辛辛苦苦花费近一小时填写的资料到最后提交时系统出现异常而导致前功尽废,还别说原来只消10分钟就能完成的事情,现在“升级”到采购平台后需要好几倍的时间。
可以说,我们绝大部分程序员的职业生涯都是从开发管理后台系统开始的,或者极大部分程序员曾经参与过管理后台系统的开发和维护工作。我想,这其中的原因是因为我们大众的意识里觉得开发管理后台系统是一件很简单的事情,它的用户很少,对性能要求也不高。
但,管理后台系统真的很简单吗?不管是否真的很简单,我们都应秉持这样的思想:软件应该是用于提高效率,减轻负担的,而不是额外增加人们的工作压力。
1 管理后台的分类
根据以往的工作经验,我把管理后台分为以下四类,分别是:
-
- 内部使用的运营后台
-
- 技术人员专用的工具
-
- 供应商合作平台
-
- 开放管理平台
内部使用的运营后台,以及技术人员专用的工具都是公司内部使用的,而供应商合作平台与开放管理平台则是提供给公司外部人员使用为主。
内部使用的运营后台,面向的目标人群有运营、客服、商务、产品、财务、行政等。常见的例子有论坛后台、CMS内容管理后台、客服系统、OA系统、ERP企业级管理系统、财务系统等。这类管理后台系统,在背后和公司内部人员一起,通常支撑起公司主营业务,有举足轻重的作用。其中,又可以细分成两个系列,一个系列是不同行业的特定管理后台系统,一如游戏行业的管理后台、旅游行业的景点门票管理后台、电商行业的进销存管理系统,物流仓储管理系统等,不一而足;另一个系列是通用的管理后台系统,有财务系统、OA系统等。第二系列的系统,为了减少公司的成本,会采用外包或者直接采购的方式。而第一系列的系统,因为涉及公司的核心业务,则需要我们自主开发,持续维护,也是我们需要重点关注的对象。
技术人员专用的工具,则可以说相对简单,甚至界面可以是简陋的。因为它的使用对象则是我们技术人员自已,或者是运维人员、DBA同事,又或者是测试部门。这类系统主要是为了方便我们技术人员进行对线上的管理、操作、维护、故障排查和线上变更等。例如日志系统、变更系统、大数据平台。
供应商合作平台,与前面两类管理后台系统不同,它是提供给与公司有合作关系,或者由于战略需要,为第三方供应商或者合作方而开发和提供的后台。通常是以企业对企业的方式进行双边合作。例如电商行业中的供应商录入系统。
最后,再来看下开放管理平台。这一类管理平台的外部受众则更广。与供应商合作平台的目标人群不同,这类开放管理平台的注册用户是开放式,而供应商合作平台的用户是内部受限式的。即通常你不能去注册供应商合作平台,而是需要分配和开通接入权限,在这之前需要签定合同。但开放管理平台则完全不一样,可以说,这是一个多边平台,它努力吸引生产者和商家进驻,为供应商提供有利的平台资源,进而吸引更多的消费者进入此平台,促进核心交互。明显的一个例子是微信管理平台。任何一个主体,不管是企业还是个人,都能在开放平台上注册,认证通过后即可使用。
简单了解管理后台的分类,认清不同管理后台系统的使用场景以及目标使用人群,对于日后的开发、维护和升级有重要的指导意义。
2 全栈开发的基本素质
在开发管理后台系统时,尤其需要全栈开发的技能。因为管理后台系统的界面、样式和交互都是轻量级的,项目的规模和复杂度较为一般,出于人力成本的考虑,通常情况下,样式、布局、交互、数据库设计、后端开发等,都需要一名或数名后端软件开发工程师来完成。除此之外,掌握全栈开发的基本素质也是我们每一名后端开发人员迈向更为广阔的职业生涯的基本功。
这一节,我将重点分享,从后端开发的角度所观察、学习和理解的全栈开发基本素质,其中重点围绕Javascript开发这一部分。
2.1 活用Javascript
Javascript是另一门伟大的语言,它能做的事情,完全超乎了我们的想象。
我们将从后端的视角来学习Javascript的使用,包括懒人的做法、初级Javascript使用、ES6高级特性、JQeury和Vue的使用。我们不会探讨Javascript在不同浏览器下的兼容性问题,也不会深入研究Javascript这一完整的体系,而是着重关心如何活用Javascript完成管理后台系统的开发任务,增强交互,同时提升我们全栈开发的技能。
懒人做法
首先,要介绍的是懒人做法。懒人做法,即通过纯PHP的操作和开发,就能在背后完成对HTML、CSS,包括Javascript等代码的自动渲染和生成。其好处不言而喻,大大降低了后端开发人员的学习成本,并在一般性的场景中极大提升了项目的开发效率。
最经典和最强大的是Yii开源框架中的视图操作,回顾前面实现的迷你招聘网站,配合Yii框架开发管理后台系统,那么在开发后台功能界面,在渲染招聘岗位列表时,可以用纯PHP代码就能完成全部的操作。
首先,准备好数据集,并使用CArrayDataProvider准备数据供给器,使用非常简单。
$jobs = array( array( 'id' => 21, 'job_name' => 'PHP软件开发工程师', 'job_snapshot' => '6k-8k / 经验1年以下 / 大专 / 全职', 'score' => 8.8, ), // 省略其他三组类似的数据…… array( 'id' => 25, 'job_name' => '高级PHP开发工程师', 'job_snapshot' => '14k-20k / 广州 / 经验1-3年 / 本科 / 全职', 'score' => 9.3, ), ); $dataProvider = new CArrayDataProvider($jobs, array( 'pagination'=>array( 'pageSize'=> 3, ),));
然后是视图的操作和渲染,就更为简单了。只需要使用Yii默认内置的视图组件,就能完成一系列的前端操作。
<?php $this->widget('zii.widgets.grid.CGridView', array( 'id' => 'jobs-grid', 'dataProvider' => $dataProvider, 'columns' => array( 'id', array('header' => '职位', 'name' => 'job_name'), array('header' => '职位描述', 'name' => 'job_snapshot'), array('header' => '评分', 'name' => 'score'), ), )); ?>
最后,页面展示的效果类似如下,一张完整的报表就诞生了!
图1 Yii的CGridView表格
在这张表格,有表头,表数据,还具备了常见的分页和翻页功能。如果关联的是数据库的表模型,还能实现字段排序、过滤、更新等高级操作。
在PHP代码的背后,它生成了CSS、Javascript和HTML等相关的代码片段。这是一段非常奇妙的旅程,通过PHP代码再生成其他代码,然后其他代码构建了可展示的静态页面,结合动态的实时数据,就完成了管理后台系统的开发!
CSS样式部分,Yii引入了外部的样式,例如下面的gridview/styles.css图表样式和pager.css分页样式。
<link rel="stylesheet" type="text/css" href="/assets/2093bf7c/gridview/styles.css" /> <link rel="stylesheet" type="text/css" href="/assets/6dd336c/pager.css" />
HTML页面标签部分,则生成了Table标签,以及翻页相关的代码片段。
<div id="jobs-grid" class="grid-view"> <div class="summary">第 1-3 条, 共 5 条.</div> <table class="items"> <thead> <tr> <th id="jobs-grid_c0">id</th><th id="jobs-grid_c1">职位</th><th id="jobs-grid_c2">职位描述</th><th id="jobs-grid_c3">评分</th></tr> </thead> <tbody> <tr class="odd"> <td>21</td><td>PHP软件开发工程师</td><td>6k-8k / 经验1年以下 / 大专 / 全职</td><td>8.8</td></tr> …… </tbody> </table> <div class="pager">翻页: <ul id="yw0" class="yiiPager"><li class="first hidden"><a href="/index.php?r=site/test"><< 首页</a></li> <li class="previous hidden"><a href="/index.php?r=site/test">< 前页</a></li> …… </div>
而最后是Javascript部分,有:
<script type="text/javascript"> /*<![CDATA[*/ jQuery(function($) { jQuery('#jobs-grid').yiiGridView({'ajaxUpdate':['jobs-grid'],'ajaxVar':'ajax','pagerClass':'pager','loadingClass':'grid-view-loading','filterClass':'filters','tableClass':'items','selectableRows':1,'enableHistory':false,'updateSelector':'{page}, {sort}','filterSelector':'{filter}','pageVar':'page'}); …… }); /*]]>*/ </script>
至此,Yii框架中的视图使用及其效果体验,以及懒人做法的一般形式,我们已经有了完整的认识。
显然,这一做法是有好处的,但也有其缺点,毕竟不是放之四海而皆准的“银弹”。它的不足在于,一方面,技术层面它蒙蔽了后端开发工程师的双眼,某种程度上减少了接触前端开发的机会;另一方面,假使希望能进行定制化的开发,灵活增强界面交互功能,则很难自然与现有的架构组件融合。
这时,可以考虑从Javascript的初级使用入手。
Javascript的初级使用
Javascript可以做的事情非常多,它能修改页面DOM元素,可以侦听输入框的更新,能启动定时器,发起AJAX异步请求……
这些常用的操作,既可以使用基本的、原始的Javascript代码来完成,也可以使用耳闻能详的JQuery类库来完成。
例如,对于一些页面的初始化操作,可以这样编写:
<script type="text/javascript"> // TODO console.log("在这里完成页面初始化操作……");
也可以使用JQuery的实现方式,如下:
$(document).ready(function(){ // TODO console.log("在这里完成页面初始化操作……"); }); </script>
此部分已经有大量的资料、文档和教程,这里不再赘述。
探索ES6和Vue框架
由阮一峰编写的《ECMAScript 6 入门》,是我学习ES6的启蒙书籍,这本书充分整理并介绍了ECMAScript 6的高级特性,为我日后使用诸如Vue等新型框架提供了扎实的基础。可谓是,万变不离其中。
在《ECMAScript 6 入门》一书中所介绍的每个特性、语法糖或专题,都让我受益匪浅,给了我很多启发、灵感和收获,建议大家有空也可以阅读一下。值得称赞的是,这本书还是一本开源的 JavaScript 语言教程,可以免费阅读。
学习不同的编程语言,学习不同的编程范式,我们将习得不同思维模式下的编程技艺。例如《ECMAScript 6 入门》中介绍的对象的解构赋值、模板字符串、for…of循环、Module的语法等,其中又数上下文作用域、匿名和回调函数更加吸引人。
在熟悉ES6的语法后,再去使用一些新型的Javascript框架就非常顺畅了。下面我们来看一个简单的例子,在管理后台中经常需要开发的模块——网站访问简单统计。最终使用Vue框架实现的运行效果截图如下。
图2 Vue的简单示例
实现过程也很简单,首先,准备好HTML标签和界面布局。在页面上,我们准备了webVisitStat这一个id,方便后续Vue的开发。此外,还准备了三个模板变量,分别是:日访问次数day_visit_times、月访问次数month_visit_times和总访问次数all_visit_times。与传统的网站开发不同,这是一种前后端分离的开发方式,而不是通过后端PHP代码来完成界面的渲染。
<!-- 网站访问简单统计 (START) --> <div class="row" id="webVisitStat" > <div class="col-xs-12 col-lg-12"> <h3>网站访问简单统计</h3> <hr /> <div class="row" style="text-align: center;"> <div class="col-xs-6 col-sm-3 placeholder"> <img class="img-circle" src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" alt="Generic placeholder image" width="140" height="140"> <h2>{{ day_visit_times }}</h2> <p>日访问次数</p> </div> <div class="col-xs-6 col-sm-3 placeholder"> <img class="img-circle" src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" alt="Generic placeholder image" width="140" height="140"> <h2>{{ month_visit_times }}</h2> <p>月访问次数</p> </div> <div class="col-xs-6 col-sm-3 placeholder"> <img class="img-circle" src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" alt="Generic placeholder image" width="140" height="140"> <h2>{{ all_visit_times }}</h2> <p>总访问次数</p> </div> </div> </div> </div> <!-- 网站访问简单统计 (END) -->
紧接下来,是Vue框架下的开发过程。实现思路是在进行初始化的操作时,通过AJAX异步接口获取相应的统计数据。内部实现可以分为三部分,一部分是与模板变量对应的属性数据,一部分是初始化init操作,最后一部分是公共的更新操作子函数。
以下是大体的代码轮廓,它绑定了webVisitStat这一id,在data内定义并初始化三个页面变量,在methods内实现了初始化操作,并提取了update子函数,最后当一切就绪后调用init方法。
var webVisitStat = new Vue({ el: '#webVisitStat', data: { day_visit_times: 0, month_visit_times: 0, all_visit_times: 0 }, methods: { init: function() { let _self = this // 日访问次数 this.update('day', 'demo_day_visit_times', 1, function (afterValue) { _self.day_visit_times = afterValue }) // 月访问次数 this.update('month', 'demo_month_visit_times', 1, function (afterValue) { _self.month_visit_times = afterValue }) // 总访问次数 this.update('forever', 'demo_all_visit_times', 1, function (afterValue) { _self.all_visit_times = afterValue }) }, update: function(type, name, value, afterCallback) { …… }, } }) webVisitStat.init()
在上面简短的代码中,使用了部分ES6新特性,例如let关键字,并且省略了结束符分号,更为简洁。在update函数内,则是具体业务的逻辑处理,并通过afterCallback回调函数更新对应的统计数据。
Vue有一个很棒的设计,就是数据的双向绑定,你既可以通过修改变量来刷新页面数据,也可以通过修改页面的数据来影响变量的值。例如,对于刚实现的统计页面,我们可以打开浏览器F12的调试模式,在Chrome浏览器下,依次获取和修改页面的三个变量。
> webVisitStat.day_visit_times 7 > webVisitStat.day_visit_times = 2018 2018 > webVisitStat.month_visit_times 97 > webVisitStat.month_visit_times = 9527 9527 > webVisitStat.all_visit_times 189256 > webVisitStat.all_visit_times = 888888 888888
在执行Javascript脚本的过程中,可以看到页面的数据也在实时、无缝刷新。最后的效果截图如下:
图3 数据绑定背后的实时刷新
Vue.js还有很多引人入胜的特性,可以在日后的开发过程中慢慢体验。一般来说,开发管理后台系统界面功能的顺序是:
-
- 第1步、创建HTML静态页面,并模拟实现静态数据
-
- 第2步、引入Javacript相关类库,诸如Vue.js,增强页面交互
-
- 第3步、在PHP后端实现动态数据的处理获取和逻辑处理
除了Vue.js外,开源社区还有很多优秀的开源框架,都值得我们去了解、去使用、去领悟。
2.2 Javascript职能链模式
思考越多,收获越多。
行文到此,简单分享了Javascript在管理后台系统的应用,顺此延伸的分享的是利用职能链模式对Javascript的代码进行小步重构的实际项目经验。
先前,我在进行H5页面开发时,曾经有一个业务场景,根据不同的逻辑会在三种弹窗中至多显示一种弹窗。为了以示区分,假设这三种弹窗,分别是:文字弹窗、图片弹窗、视频弹窗。并且,假设优先级是:文字弹窗 > 图片弹窗 > 视频弹窗(产品同学为了照顾用户可贵的流量,先用低流量的文字,到最后高流量的视频)。
经抽离关键代码后,一开始的代码实现大概如下:
var h5 = { init: function() { // 弹出优先级 : 文字 - 图片 - 视频 h5.popupText(); }, /* * 文字弹窗 */ popupText: function() { api.getText().done(function(re) { if (re.code == 1) { // 显示文字弹窗 ... } }).always(function(re = {}) { if (re.code != 1) { h5.popupImg(); // 继续图片弹窗 } }); }, /* * 图片弹窗 */ popupImg: function() { api.getImg().done(function(re) { if (re.code == 1) { // 显示图片弹窗 ... } }).always(function(re = {}) { if (re.code != 1) { h5.popupVideo(); // 继续视频弹窗 } }); }, /* * 视频弹窗 */ popupVideo: function() { // 显示视频弹窗 ... } }
上面的代码虽然有点长,但还是不难看出其中的业务逻辑的。这里的问题是,重要的业务规则得不到很好的体现,并且复杂的业务场景实现没有使用通用的、既有的模式得到恰当的解决。
这里打算使用职能链模式进行重构优化。举一个员工的问题为例子,假设有一名基层员工遇到问题,他解决不了,然后去找他的老大;如果他老大也解决不了的话,就会继续再往上一级找老大的老大,依次类推,直到问题被解决。这就是职能链模式。
经过一番改造后,使用职能链模式重构后的代码,主要如下:
var h5 = { init: function() { // 弹窗职能链调用 var chain = new MiniChain(); chain.next(h5.popupText).next(h5.popupImg).next(h5.popupVideo).go(); }, /* * 文字弹窗 */ popupText: function() { var isShow = false; api.getText().done(function(re) { if (re.code == 1) { // 显示文字弹窗 ... isShow = true; } else { // 其他场景处理 ... } return re; }); return isShow; }, /* * 图片弹窗 */ popupImg: function() { var isShow = false; api.getImg().done(function(re) { if (re. switch == 1) { isShow = true; // 图片弹窗显示 } }); return isShow; }, /* * 视频弹窗 */ popupVideo: function() { // 显示视频弹窗 ... } }
主要的改动,首先,是职能链的按优先级的配置和调度,极大简化了入口的调用代码,并且在最高层次表达了业务规则。
// 弹窗职能链调用 var chain = new MiniChain(); chain.next(h5.popupText).next(h5.popupImg).next(h5.popupVideo).go();
其次,是各职能链(在这里是各弹窗功能)的具体实现,主要添加了处理标识返回,但内部实现已经更专注于自身功能的实现,而非外部的判断和处理。
这样重构后,明显我们看到核心关键的业务得到了很好的突显,并且把业务规则通过恰当的模式组合最终恰如其分地表达了出来。下面再细说下重构后的好处。
好处1:关注点分离,调度与实现分离
调度的策略,应该和具体的弹窗功能实现分开。这是两个不同的关注点,如何调度,是高层的概念;而具体的实现则是技术细节的范畴,不应把这两者混淆。一旦混淆了,就会容易产生代码异味。
重构前,调度的入口是由文字弹窗的调用而开始的。这样很容易会给人一种误导,或者是隐藏了重要的信息。因为只有深入到细节,才知道当没有文字弹窗时才会继续尝试弹图片弹窗。更让人“惊讶”的是,文字弹窗没显示时会再尝试视频弹窗。。。 这里没有很好地在高层表达重要的信息,而且也没有很好的抽象高层的业务概念。这个入口,就如同一个黑黑的洞穴,只有你拿着火把,一步步深入其中,走到底,才知道还有没有路可走。
而重构后,我们把调度分离了出来,并用贴切的职能链模式表现了出来。高层的业务概念不仅得到了体现,而各自的弹窗功能实现也变得了更内聚(因为不需要再关注要不要再走下一步),这也是符合我们常常说的“高内聚、低耦合”。
好处2:对业务友好、对开发友好、对测试友好
首先,对业务友好。
回顾前面弹窗优先级的配置代码:
chain.next(h5.popupText).next(h5.popupImg).next(h5.popupVideo).go();
可以看到,对于弹窗功能的优先级顺序编排非常简单明了,因此当产品需要增加弹窗、删除弹窗、调整顺序时都非常简、快速,快速即友好,因为能快速满足产品同学的需求,快速交付有价值的功能。
其次,对开发友好。
除了快速外,它也是容易维护,即维护成本低。
因为重构后,把原来的三层嵌套调用简化了只有一层嵌套,我把这种调整称之为嵌套结构扁平化。扁平化的各个弹窗功能模块,可以独立重用、互不影响,通用职能链组合复用起来,而不是像之前那样“暗地里”耦合在一起。维护成本变低的原因还有技术层面的,因为通过配置的弹窗优先级,我们不仅可以看到当前已有多少弹窗以便评估性能方面对用户体验的影响,还可以方便判断是否存在死循环、消除复杂的耦合关系。在这里,又让我再一次想起了那段话:
“设计软件有两种方法:一种是简单到明显没有缺陷,另一种复杂到缺陷不那么明显。”
举个假设的需求以对比重构前后的维护成本。
假设,产品希望原来把弹窗优先级,从原来的:文字弹窗 -> 图片弹窗 -> 视频弹窗。调整成(全部逆转):视频弹窗 -> 图片弹窗 -> 文字弹窗 。
试想一下,如果是原来的方式,需要开发的时间是多少?1分钟、5分钟、10分钟、半小时?其中还需要进行开发自测。这里可能维护时间因人而异,但我觉得,负责维护的开发工程师至少也要想一下。
但如果是重构后,我觉得,即便是刚毕业的实习生,也能够在短短的10秒钟内完成开发(并且可以不用自测!)。因为需要修改的代码就是这么简单:
chain.next(h5.popupVideo).next(h5.popupText).next(h5.popupImg).go();
在这里,对于开发工程师,维护成本低即友好。
最后,对测试友好。
很多因素,虽然微弱,虽然看似无关联,实际上相互联系、相互作用的。从代码上的修改,反映到对产品需求的响应、再到维护成本的影响,最后也自然会影响到测试。重构后的代码,质量上得到了提升,也就得到了保障,对于测试人员,不必要担心修改后有问题而花过多时间去回归功能或者改出问题后进行故障的登记和跟进。另外,对比原来的测试路径,可以有 22
2=8 种组合场景;而扁平化嵌套结构后,测试只需要测试 2+2+2 = 6种独立的场景即可,可以少测3种场景。因为原来排列组合的方式,现在只需要单独测试每个模块即可。
即,重构前需要测试 22
2=8 种组合场景:
图4 重构前待测试的组合场景
重构后,只需要测试 2+2+2 = 6种独立的场景:
图5 重构后待测试的独立场景
在这里,对于测试工程师,减轻测试工作量即友好。
职能链的深层考虑: 明确成功后终止,抑或明确失败后继续?
在实现职能链模式时,需要考虑的一个小细节就是:到底是明确成功后终止,抑或明确失败后继续?
这里涉及到何时停止的裁定,所以在Javascript实现时是需要仔细考虑的。一开始是返回false才会继续的,但由于对于弹窗的业务功能实现时,开发工程师有可能返回undefine、或者null、或者0等其他类似false的场景,但我们使用了全等判断,故而也就会对开发工程师严格要求,否则就会容易产生bug。为了继续体现对开发者的友好性(帮助开发工程师减少出错的概率),我决定最后调整为宽松的约定,即你明确告诉我成功了才会停止调度,否则当作失败继续调度下一个处理器。
小游戏:谁是内奸
假设有这么一个游戏:找出警队里的内奸。某警队里有3个中队,分别有3人、4人、5人,其中有一个是内奸,需要FBI联邦总局命令你找出内奸。请设计合适的算法,找出内奸。
这里的问题,如果用图论中的算法结构,我们可以得到这样的树模型(假设内奸位置已按算法分配好):
图6 谁是内奸小游戏模型
利用上面刚学到的职能链,我发现可以这样来处理。在QUnit单元测试内,我们进行了小游戏的模拟运行,并且在最后进行了断言和验证。
// 小游戏:谁是内奸 QUnit.test("little game: WHO IS SPY", function (assert) { var i_am_police = function () { console.log("I am police."); return false; } var i_am_spy = function () { console.log("I am spy!"); return true; } var team_1 = new MiniChain(); team_1.next(i_am_police).next(i_am_police).next(i_am_police); var team_2 = new MiniChain(); team_2.next(i_am_police).next(i_am_police).next(i_am_spy).next(i_am_police); // 内奸在这! var team_3 = new MiniChain(); team_3.next(i_am_police).next(i_am_police).next(i_am_police).next(i_am_police).next(i_am_police); var chain = new MiniChain(); chain.next(team_1.go).next(team_2.go).next(team_3.go); var rs = chain.go(); assert.deepEqual(rs, true); // 找到内奸 });
运行的效果如下(期望效果):
LOG: 'I am police.' LOG: 'I am police.' LOG: 'I am police.' LOG: 'I am police.' LOG: 'I am police.' LOG: 'I am spy!'
很遗憾的是,目前上面的代码运行还是有问题的。看来暂时还不宜找出内奸。 这里明显虽然不一定要用职能链模式,而职能链模式也不是构建树最好的解决方案,但通过这种的分解后,我们可以得到的是可重用、可复用的独立模块,进而组合产生出你所想要的功能。再强调一次:复用而非耦合。
不适应性场景: 适合于操作类功能,不适宜返回结果
任何负责任的技术或者思想,都应该告诉你其不适应性。同样,就这里的JS职能链,也有其不适应性。它不适合用于返回结果的功能,因为我们约定了返回true作为成功授理的标识,否则会再进行下一步。
当然,也可以根据场景需要,改造成带返回结果的职能链。但我觉得,模式也应该是专注的,即职能链本质就适合于操作类不带结果返回的实现。
最后,模式不要滥用,仅当需要时才用。实现细节不要过于生搬硬套。重要是思想的体现。
3 配置式编程
对于管理后台系统,可以说很多功能界面都是类似的,只是存在个别微妙的区别。
以我曾经负责开发的游戏管理后台中的统计页面为例,总结下来,它所需要实现的功能流程无非是:
-
- 第一步,根据时间范围和指定的类似,从远程接口拉取统计数据
-
- 第二步,将拉取的文本数据进行分割,并转换为在线的数据表格
-
- 第三步,生成相关的可视化柱状形图形
-
- 第四步,提供Excel数据导出的功能
最初的时候,每次需要增加一个类似这样的功能界面,都需要重复开发一次,大概需要2人天左右。后来,我把这块做成了可配置化,只需要几分钟,简单配置一下就能上线一个新的统计功能。高效,并且富有创造性。
下面,我们通过其他的例子,进一步探讨配置式编程。
3.1 简单,多样,统一
在业余时间,我曾经一度钟情于摄影,从中了解到摄影从入门到精通分为三个阶段,依次就是:简单、多样、统一。在做了这么多项目开发后,我发现,软件开发也同样经历着以上三个阶段。开发人员像摄影师一样,通过不断的实践,从中摸索,略有所悟。
简单
记得刚上大学的时候,教我们编程的那位老先生就说:编程,越简单越好。
诚然,开发领域也一直信奉着KISS原则,把软件开发得简单明了以致看不到的缺陷(而不是把项目搞得复杂以致找不到明显的缺陷)。
然而理想总是很丰满,现实总是很露骨。基本上所有的项目都是要求拥有各种各样的功能、多个模块、N个子系统。虽然秉承着“高内聚、低耦合”的思想,还是逃避不了复杂多变的命运,更别说客户频频变更的需求。于是也就有了以下所说的多样。
多样
当一个项目拥有一定人用户时,或者成为产品时,其功能多样性导致系统的复杂性,总让开发人员很纠结。特别当没有统一的策略或者解决方案时,更会让项目陷入混乱,甚至让项目胎死腹中。
比如有这样的一个项目:一个需要同步本地和服务器邮件联系人的模块,当联系人发生改变时,需要同步更新本地和服务器相应联系人的最后更新时间,因为在本地维护了一份所有联系人最后更新时间的列表,用于对比与服务器的时间,如果一致则不用更新。那么,何时调用更新最后修改时间操作呢?
目前假设在以下几个时机需要更新:
-
- 客户修改本地联系人时
-
- 从服务器更新联系人到本地时
-
- 同步本地联系人到服务器时(此情况发生在客户有多个客户端时,类似SVN)
如果让以上时机(可能还会有更时机),再由开发人员手动调用的话,问题就来了。因为开发人员往往不会意识到需要更新本地最后修改时间而忘了调用。特别是当有多个开发人员分别负责以上各个模块时!当发现因为没有更新最后修改时间而出问题追究到开发人员时,他或许很无辜并惊讶问道:“什么?!需要更新最后修改时间吗?什么时候的事……”
这时,就需要一种统一的手段进行控制,也就是以下将要重点讨论的:统一。
统一
不得不说,软件开发行业虽然发展才几十年,但已经有了很多前人宝贵丰富的系统体系理论,并蕴含着各种哲理。
现在,很多框架,系统或者网站都采用了统一的处理方案,如统一的异常处理机制、统一的日志纪录方式、统一的接入方式。正是这样高度统一的思想,使得系统集成更加方便。像Eclipse就提供了一致的插件接入,从而吸引了众多的开发人员;像Yii开发框架使用了一致的日记纪录方式,使得开发人员很好查看错误信息;像新浪的开放平台也一样。
可以看到,统一的思想已经普遍并成熟,更重要的是得到了很好的应用。
3.2 优先考虑配置编程,而不实现编程
前方已有章节对配置编程有所说明,这里再通过其他例子再次分享,以加深读者对这一思想的理解。
继续以方才提及的项目为例,在此项目中,有一模块是需要对本地和服务器间的邮件联系人进行同步。而同步策略有以下4种:
-
- 冲突时提示
-
- 双向同步
-
- 单向同步,Outlook到ExtMail
-
- 单向同步,ExtMail到Outlook
利用策略模式,设计的类图如下:
图7 策略模式下的设计
尽管这样的设计在整体上效果很好,但当到了各个策略具体实现时,问题就来了。因为源自于以下的项目需求:
图8 四种项目需求
如果让各个策略类各自完成相应的同步操作话,会导致每个策略类都会很复杂,业务层面不易统一维护。而通过分析可知,同步最终的操作,也就是实现可分别以下5种情况:
-
- 不需要进行任何处理
-
- 同步本地到服务器
-
- 更新到本地
-
- 从本地删除
-
- 从服务器删除
试想一下,如果没有一种统一的处理方式,各个策略类各自需要处理以上各种情况会是怎样的复杂?虽然有些策略不需要考虑其中某些操作情况。但是,如果具体的策略类不需要考虑实现,而是考虑配置编程,开发难度会不会大大降低?
关键的转变就在这里。配置?对,就是配置!
我们可以就以上5种情况分别作相应的标记,如下所示:
protected bool isLocalNeedUpdate; protected bool isServerNeedUpdate; protected bool isLocalNeedDelete; protected bool isServerNeedDelete;
那么再结合模板方法,在父类的策略类中加下以下的模板方法函数:
class ExtSyncPersonalContactStrategy { protected bool isLocalNeedUpdate; protected bool isServerNeedUpdate; protected bool isLocalNeedDelete; protected bool isServerNeedDelete; /// <summary> /// 模板方法 /// </summary> /// <param name="strvCardName">等待同步的联系人</param> virtual public void DownloadContactFormServerByNames(List<string> strvCardName) { foreach (string strName in strvCardName) { isLocalNeedUpdate = isServerNeedUpdate = false; isLocalNeedDelete = isServerNeedDelete = false; //分情况处理 if (IsLocalExits(strName) && IsServerExits(strName)) { //1. 本地与服务器都存在 SyncContactBetweenLoaclAndServer(strName); } else if (IsLocalExits(strName) && ! IsServerExits(strName)) { //2. 本地存在,但服务器没有 SyncContactIfLocalExist(strName); } else { //3. 本地没有,但服务器存在 SyncContactIfServerExist(strName); } if(isLocalNeedUpdate) { //更新到本地操作 } if(isServerNeedUpdate) { //同步到服务器操作 } if(isLocalNeedDelete) { //从本地删除操作 } if(isServerNeedDelete) { //从服务器删除操作 } } } virtual protected void SyncContactBetweenLoaclAndServer(string strName) { } virtual protected void SyncContactIfLocalExist(string strName) { } virtual protected void SyncContactIfServerExist(string strName) { } }
以上代码是简化版,以突出需要分享的思想:优先考虑配置编程,而不实现编程。
其中,bool IsLocalExits(string strName) 与 bool IsServerExits(string strName)分别用来判断是否存在本地或者存在服务器。而三个虚函数则是分别针对三种不同情况的具体不同处理,主要是修改相关配置。
经过这样统一后,开发人员就可以根据不同的具体策略类选择重写感兴趣的虚函数,从而修改不同的配置,即可实现同步。而无须再编写各种实现操作代码。
虽然这是C#的代码,开发的项目与管理后台没有直接的关系,但是配置式编程这一思想是相通的。
4 本章小结
管理后台作为数据最初录入的发源地,同时作为上游系统,一定要严格控制录入数据的质量。
如果一份糟糕的数据,从管理后台录入而未被发现,就会通过计划任务同步到下游系统。接着,中间层接口系统就会读取到一份有问题的数据,交由前台系统。前台系统如果也没有严格控制输出数据的格式和标准,就极有可能导致前端在渲染页面时产生解析问题或者引发线上故障,甚至成为外界攻击的漏洞入口。
作为小结,在开发管理后台系统时,应怀揣着一颗帮助他人,而不是增加别人工作压力的心。除了要掌握全栈开发的基本素质,还应能够活用Javascript增强交互,提高用户的体验。
通过配置式编程,将能大大减少重复开发和维护的时间与成本,使得开发管理后台系统是一件充满创造性而非苦不堪言的工作。