论软件工程

论软件工程

昨天一位同学问我对软件工程的体会?当时愣住了一下,不知道该怎么回答,感觉话到嘴边却不知道怎么表达,因为这个话题比较沉重,不知道该怎么用简短的语言来进行描述,要说可能一天都说不完,可能是自己的表达能力有限,语文没学好(高考语文不到80分),当时只是给出了:“这叫我怎么回答”这样的答案。随后在我的脑海里一直回忆自己开发经历,想从中找到一些信息。第一次听到软件工程是大三的时候有一门课程叫做软件工程,我依稀记得里面几个关键词语:髙聚类.低耦合、可扩展性、可行性、可维护性,同时还包括一些软件开发过程。下面将结合这几点以及我的经历来谈谈我眼中的软件工程,同时也对自己进行一次总结。

髙聚类.低耦合

这个词是自从我上过软件工程的课之后一直没忘记过,并且一直影响这我着设计开发过程。什么叫髙聚类?一句话就可以概要:物以类聚,人以群分。什么意思?就是将相同属性的事物抽离出来,从而简化对这一类事物的控制,以方便管理。那什么叫低耦合?上面说了将相同属性的事物抽离出来以达到分组,那么低耦合就是降低各个分组之间的关系,这样可以达到结构清晰,管理灵活。上面都是解释字面上的意思,完全没有扯到一点关于软件方面的东西,那么这两个词放在软件设计里面是怎么体现的呢?

这个可以分为宏观和微观,宏观上面,一句话:模块化,层次化。模块化和层次化是髙聚类和低耦合在软件设计宏观上面的体现,具体的又可分为纵向业务划分,和横向架构划分(这也称为二维架构,之前还听过蔡学镛老师说的四维架构这个就更加的抽象了)。将相同业务抽离到一个模块里面,将整个业务线切分成多个层次,上层只能依赖下层,下层不能依赖上层的图结构。从而使得整个软件在结构上层次分明,业务线清晰。这里举个例子:比如我现在要做一个电商网站,首先要做的第一件事情不是探讨技术上面是否有难点,而是探讨整个网站业务方向,从而确定整个业务模型,对里面进行建模,分析里面包含的哪些领域模型。先不谈复杂的电商,就说一个初期的C2C网站,首先它的业务方向是一个个人买家交易平台,一个用户即可以扮演买家也可以扮演卖家,确定这个之后,我们再对里面进行建模,从而可以进行模块划分。上面的网站可以简单划分为商品模块,订单模块,用户模块等等,那么这个模块就是在软件架构里面的髙聚类,那么低耦合怎么体现呢?模块划分之后需要确定模块在整个系统里面的定位,从而可以确定它们之间的依赖关系,以达到确定它们所处的层级。下面通过一个图来描述上面三个模块的层次关系:

用户模块依赖订单和商品模块,订单模块依赖商品模块,那么可以确定的是商品模块是在整个系统正处于底层模块,而用户属于上层模块,那么订单模块不能反向去依赖用户模块,商品模块也不能反向依赖订单和用户模块,这就是在架构设计上的分层,这样做的好处就是系统的结构清晰,而且对功能的分层降级可以简化业务的复杂度,并且可以达到对局部的优化不会对其他模块的影响,只要当前对外暴露的接口定义不变。那么这样分层之后,各个模块之间通过什么来关联呢?通过底层模块向上次模块之间暴露它内部的接口,比如商品暴露查询商品的接口,那么用户就可以查询用户所上架的商品,订单包含哪些商品,以及订单暴露它的接口给外部,那么用户就可以查询某个用户所拥有的订单。至于模块之间通过什么方式通信?如果是项目前期这些模块都放在一个工程里面,这个就可以直接引用各个模块的接口实现,如果项目发展到最后,需要将各个模块独立出来,单独部署,那么就需要一个远程通信的方案,这样面有很多成熟的方案,比如比较通用的Webservice,性能比较高的RPC(这一块有很多工具,比如最近听到的跨语言的ICE(Internet communications engine),比如阿里系的dubbo和HSF,facebook的thrift),这就是软件工程里面的低耦合

上面说的是软件设计宏观上的髙聚类.低耦合,那么细化到具体代码实现上面来说,这个就是在实现某个逻辑的时候需要带着这个思维去看待你的代码,这就是微观的髙聚类.低耦合。在开发过程中我们一直提倡提高代码的重用性,这就是髙聚类,同时也要保持代码的结构清晰,这就是低耦合。我在写代码过程中有一个洁癖,就是如果写一段业务逻辑,类似的逻辑出现了3次以上,那么我就和得了强迫症一样,把这一块抽离出来,通过对外暴露变量,以达到具体业务的需求。在Java里面提供接口和抽象的功能,而合适使用抽象也是一种髙聚类的体现,我们一般把一些公用的,模板的业务逻辑放在抽象类里面,这样就避免了其子类的复杂度,子类只需要关注它自己需要体现的业务,而一些通用的抽象父类提供了实现,这是一种在代码的重用度方面的髙聚类。在具体细节中其他地方也体现了髙聚类特性,比如我们总喜欢把对某一类操作放在一个类里面或者一个包里面,比如对文件的操作,我们可以放在一个FileUtil,这样做是为了方便管理整个项目对文件的操作,而不是东一个,西一个,如果某一天业务要求我们对操作文件的时候加上某些东西,这个时候如果所有的操作都放在FileUtil里面,面对这样的变更,我们是如此的得心应手,否则那你去一个一个的文件里面找对文件的操作吧!这也是一种高聚类的体现。那么低耦合怎么体现的呢?比如我们写代码过程中经常采取MVC模型,将数据展示层,业务层,以及数据存储层分离开来,这就是一种低耦合的体现,各个层次分工明确,比如我优化数据存储层,那么上层的业务层基本无感知,这种现象就表明你的耦合度很低,而不是一切东西都杂糅在一起。

可扩展性

如果软件开发设计一直遵循上面高聚类.低耦合,那么一个软件的可扩展性理论上是没有什么问题的。这里的可扩展性是只在软件生命周期里面能够适应和满足业务的发展需求,在原有的架构设计上将新的业务可以实现平滑的嵌入,或者可以实现插件式将一个业务直接嵌入当前架构里面,如果一个系统真正实现了插件化,那么它的扩展性应该是相当好的。还是拿上面C2C的电商网站来举例,之前只是满足一个简单的C2C的交易平台,可能随着业务的发展,为了能够吸引更多的客户,需要推出抢购业务。如何更好的将这个抢购业务嵌入到之前的C2C系统里面,这就要看之前系统的架构师的设计是否合理,如果为了满足这个抢购业务,把整个项目都进行了重构,那么之间的架构是很不合理的,如果能够对之前架构进行很小的调整就能把这个抢购业务上线就说明之前的架构设计是很合理。当然这个过程除了分析抢购业务流程,也要分析当前系统的架构,通过业务的优化和架构关键因素的分析以达到最佳的实现方式。针对上面画出模块依赖关系,可以大致分为两个途径去实现这个抢购业务。

第一、如果当前网站业务量很大,如果上了抢购,能够刺激用户参与度,可能需要单独建立一个抢购模块,这样就可以当抢购很火热或者可能抢购业务因为流量太大,导致它的业务整体瘫痪了,那么普通正常的业务依然能够运行,因为你已经将整个抢购业务独立出去了,它只是会通过调用用户模块,商品模块和订单模块接口来执行相关的业务逻辑,同时也可以单独对这个模块进行优化,比如分析抢购模块是一个持续时间很短,数据比较集中,对某个数据的修改比较集中的操作,可以将数据的整体操作放在缓存(这是大部分电商对抢购业务常用的方式),那么这样就可以降低对数据库I/O的压力,并且提高抢购模块的系统吞吐量。这种实现方式可以降低系统的耦合度,也使得抢购业务以及整个系统的扩展性上面有所提高,但是对系统维护来说增加了一项工作以及对开发工作量来说有所增加了,因为一个新的模块创建毕竟也是重新开始的。

第二、如果当前网站流量还不是很大,只是想通过抢购来获取更多的用户,那么可以将抢购业务嵌入到原有的架构模块里面,而无需通过新建模块来达到目的。以我以前参与过的项目来说说这种方式的实现方案,由于我之前负责的一个电商网站流量不是很大,为了提供用户的参与度以及获取更多的用户,需要上线抢购业务,但是又不想能够快速上线,所以在商品模块对商品添加了一个抢购属性,并且设定该商品的抢购时间区间,那么这样就可以简单的将抢购业务简单的嵌入到整个系统里面。当然这个前提是你的业务量不是很大,不然上线一个抢购业务可能会将你整个系统弄得瘫痪,那个时候你再将抢购独立出来,那就是无数日夜的加班熬夜了。

可行性

这个是体现上面所有工作做的是否有价值,如果你的架构很好,你的架构完全具备了髙聚类.低耦合,也具备了可扩展性,但是完全是异想天开,不具备可行性,这只能说这是一种YY的架构。我认为的可行性可以分为:在满足上面髙聚类.低耦合,可扩展性的条件下,能够让业务在这个架构上跑起来,以及整个系统是一种高效率的执行,整个系统在部署上可以轻松实现水平扩展以支撑后续的业务发展。那么将结合这三点来说说可行性。

业务跑起来

这个应该是一个系统设计的最基本的要求,因为这是设计的初衷。所有的架构设计应该是不能脱离业务去异想天开,合适围绕业务,站在业务的角度去设计当前系统,当然这里需要设计者对业务有深入的理解,而且和业务方的理解达成一致,这样设计的软件最后才能通过UAT。这就好比写作文,要紧贴标题,不然和跑火车一样,跑题跑到十万八千里,这样的作文,你把李白,杜甫哥俩搬上来也是白搭。

高效

这个也可以通过宏观和微观去看待,宏观方面各个模块之间的通信协议,比如webservice虽然可以跨语言,但是它的性能确实不敢恭维,或许RPC更高效点。同时也可以通过梳理简化业务流程提高整个系统的执行效率,这就好比寻求一条最短路径一样,减少业务的流程对提高系统执行效率比任何方法都见效,因为它优化的不是一次通信采取什么协议,某段实现采用什么算法,而是对整个业务线进行抽象。那么微观的优化就涉及到代码的具体实现了,比如一段业务逻辑的实现,避免没必要的循环,采用更高效的算法,减少一个变量的定义,减少内存的使用等等这些都是在具体细节方面的让系统更加的高效执行。

部署的水平扩展

要说这个就要说到分布式集群部署了,一谈到这个或许真的谈半天都说不完。围绕这个,在软件开发过程的每个细节都有很多内容。这里我只是简单的描述一下,一个系统要达到可集群部署的要求,一定要达到系统的无状态性,什么叫无状态?就是不能依托单个服务实体状态来支撑业务的执行,比如通过将一用户信息缓存到服务器内存,比如你将任务放在本地的内存中,这些地方都是集群部署的障碍点。

举一个简单的例子:在标准的J2EE里面或者是标准的web开发,每次用户的访问都会有Session的概念,如果你的Session是依托单个服务实体,那么该系统要进行集群部署,这个用户信息同步就是一个问题,因为你进行集群部署,用户的请求具体是落到哪个服务器上,完全是有前端的负载均衡器(nginx,apache或者F5)决定的,如果用户登录是在A服务器上执行的,你将用户登录态存储在A服务器的session里面,那么下次用户的请求可能落到B服务器上,由于你将session放在了A服务器上,而B服务器没有用户登录信息,这个时候用户可能就不能完成一次请求,这对用户来说不能理解,因为他请求的就是你们系统,他并不知道你具体是把他的请求落在你们哪个服务器上。针对这个问题,业界有很多解决办法,有依托容器的session同步,有通过第三方缓存服务实现session共享或者CAS的单点登录方案。个人比较偏向通过第三方缓存服务实现session共享方案,因为通过容器来实现session同步,那么就对系统跨平台增加了一种限制条件,只能在具体的容器下面才能运行。这里就简单描述一下session共享怎么来做到的,session共享是将用户的登录信息不存储在具体的服务器上面,而是存储在所有服务器都能访问的第三方缓存里面(redis或者memcache,当然数据库也可以只是性能稍微不能接受),这样依赖所有的服务器均从第三方缓存里面获取用户的登录信息,这样用户的登录请求不管是落到哪台服务器上,所有的服务器都能感知到,这样不管你后续的请求落在哪台服务器上,服务器都能获取你的登录态。用户登录态是系统集群部署首先需要解决的问题。

上面从三点描述了软件的可行性,但是一个系统,可以说一个大型系统可行性远远不知从这三点来分析和优化。针对不同地方有不同的优化手段:

  1. 针对提高用户访问速度,可以采用CDN和反向代理,将我们的数据放在离用户更近的地方
  2. 针对页面渲染速度,可以通过对JS,CSS以及HTML页面压缩或者提高页面加载的并发数来进行优化
  3. 针对提高系统的吞吐量,可以在部分环节采取异步的MQ方式进行业务的执行
  4. 针对降低数据库的压力,可以通过两级缓存(本地缓存和分布式缓存)的方式来优化整个数据的查询
  5. 针对提高系统的处理能力可以进行并发处理,从而提高系统的执行效率
  6. 针对提高每台服务器的使用率,可以使用负载均衡,使得服务器资源利用充分
  7. 针对提高系统的容灾能力,系统需要一定的冗余,一套系统需要具备一个后备环境,这里要说的东西也很多,举个简单的场景,当切到后备环境,应该先进行缓存预热,否则可能切到新的环境,由于缓存没有数据,导致缓存的命中率很低,导致巨大压力全部落到了数据库,会导致整个数据库瞬间爆棚。
  8. 针对提高数据库的性能,需要进行数据库的分布数部署,一主一从或者一主多从甚至多主多从进行数据库端的优化

上面聊了关于系统可行性方面的讨论,上面只是将我知道的方案进行了一次总结,但不一定全部都实践过。要知道能够将上面都实践过的只要大型公司才具备这样的机会(比如像BAT这样的公司才会有很多这样的机会,但不是所有人都能参与进去),所以只是有感而发。

可维护性

上面探讨的大部分是软件架构和开发设计过程的问题,而一个系统的发展不能是一个人或者一个团队一直去开发和维护,那么要使得一个系统能够在不同人的手里运作的更好,这需要从系统初期就规定开发规范,开发流程以及开发文档的整理和搜集。使得整个软件在后续的交接过程中能够平缓的过渡,而不需要过多的“进入状态”的过渡期。一个系统的可维护性是小到一个普通开发,再到开发主管,大到首席架构需要在工作过程中去关注的事情,只有一直关注这件事情,整个系统的质量才能经得住时间的考验。这就是我理解的可维护性

软件开发流程

其实这里最能体现软件工程的思想,上面大部分都是软件设计架构方面。一个软件开发过程一般遵循一下几个流程:

  1. 产品确定业务需求产出PRD文件
  2. 产品,开发,测试以及交互进行需求评审
  3. 通过之后那么开发,测试以及交互同时进行产出设计文档,测试用例以及UX
  4. 接下来是对上面三个进行评审设计评审,用例评审,UX交互评审
  5. 进行完之后那么接下来就是开发阶段
  6. 开发完毕体测,进入系统冒烟测试
  7. 系统功能测试
  8. 系统回归测试
  9. 系统UAT测试
  10. 系统部署上线

有些公司在第10步分为两部分,分为预发布和线上,这是为了提高系统上线的整体质量,预发布环境的条件是数据以及环境和线上一样,只是系统是用最新分支的代码运行,部署预发布之后将会进行预发布验证,如果验证不通过则打回给开发修复,再次进入第7

有些人认为软件工程其实就是这些流程化的东西,如果这么认为那理解就有点太过于局限于工程这个概念了。其实软件工程的目的就是使得软件开发的过程以及系统后续迭代开发能够像当前硬件发展一样,通过提供统一的接口,只需要将自己喜欢的配置组合在一起就能得到满足自己需求的硬件设备。那么要使得软件开发成为这样的模式,必然会涉及到开发每个细节,以及开发的整体过程当中,不仅仅是流程化的东西,而是系统的架构模式,代码的实现风格,以及系统上线部署维护方面,所以软件工程是一个比较大的话题,可能随着工作经验的累计有不同的理解,或者不同的岗位有不同的理解。可能今天我理解的软件工程是这样的,过了一到两年,可能会认为我现在的认识是错误的。我现在对软件工程的理解是开发过程所设计到的一切事物都是软件工程的一部分,开发过程中一切产出都是软件工程的目的。

最后我举个例子,也是前几天以为网友向我咨询过的问题,他现在有一个系统需要改造,他系统主要做的事情就是接受第三方通知,然后再将结果通知给另一个第三方系统。下面划出简单的一个系统拓扑图:

整个通信过程是第三方系统A将处理结果告知转发系统,然后转发系统将结果告知第三方系统B当作任务放入本地一个队列中,然后有一个线程专门处理这个队列的任务将结果告知第三方系统B。这种架构存在什么缺陷呢?我下面列举一下:

  1. 存在单点过障,如果流量增加单台转发系统可能根本处理不过来整个业务
  2. 存在集群部署的障碍点,转发系统是将任务放在本地的内存里面,如果集群部署分担压力,那么在本机的任务其他服务器根本无法感知导致无法分担压力
  3. 没有很好的模块划分,将接受系统A的和转发系统B的代码全部杂糅在一起,导致后面对系统进行扩展很难
  4. 存在数据丢失场景,如果系统上线新版本,服务重新部署,那么缓存在内存里面的任务如果挤压很多一时处理不完,那么重启必然导致内存里面的任务数据丢失,导致不能再通知第三方系统B

针对上面几个问题我提出了下面的解决方案,先看看改进后的拓扑图:

将原来系统拆分成两个模块,一个是接受处理结果模块通知处理结果模块,然后这两个模块之间通过异步的MQ来进行通信,现在的执行流程是:第三方系统A将处理结果发送给转发系统的前置Nginx,Nginx将请求通过负载均衡到后端具体服务器上,某台服务器即受到处理请求之后,将结果受理并执行相关业务,然后将通知任务放入MQ的消息队列中,这个时候可以立马响应结果给第三方系统A,这个时候第三方系统A通知处理结果就处理完毕,最后再就是通知处理结果模块由于监听了MQ的消息队列,那么如果有任务消息过来,将会接收到,此时将结果发送给第三方系统B。这个时候接受处理结构模块的吞吐量增加了,处理能力也增加了,因为将任务不再是缓存在本地,而是放入MQ中,那么它是无状态的,可以随意水平扩展,从而可以应对高并发的业务处理。而通知处理结果模块由于是监听MQ中的消息队列,业务压力上来并不会给它带来压力,它依然按照它的节奏在处理任务,如果想让处理速度快一点,可以在集群中多部署几台机器来解决,这样使得整个系统的水平扩展方面得到了很大的提高,也使得整体系统的处理效率以及系统结构得到一定的优化。

可能有人会说,我这样把一个简单的系统拆分成这么多模块,把整个项目弄得复杂了,需要维护和管理这么多模块,同时还加了一个MQ的维护。当然这么拆分的前提条件是你的业务量上来了,或者为以后做准备,如果你现在的系统一天才处理不到一万个请求,我也建议没必要这么搞。但是如果作为一个可以持续发展的项目,前期很好个规划可以很好的避免后面的大规模重构。上面这种方案对于实时性要求很高的系统也不是很适合,因为MQ是异步的,所以存在一定的延迟,所以对于允许有一定延迟的业务可以采用MQ来提高整条业务的响应速度,以达到业务处理的吞吐量,当然这里所指的延迟并不是延迟几个小时,这种延迟一般不会超过一分钟。

在我们现实生活中这这里模式也很多,比如我们刷银行卡,然后我们手机立马会收到银行一个短信通知我们有一笔消费,一般从你刷卡到你收到短信不会过太久就会收到短信,这个过程总就涉及到异步处理。它这里的流程一般是这样的:一般你刷卡处理的后台系统是银联(当今中国线下清结算老大,线上你们都应该知道),银联通知对应银行对某张卡进行扣款(里面具体的操作我也不太清楚,一般做法是银联通知对应银行将钱转到银联的一个账号上,然后商户和银联进行结算),银行将扣款结果通知给银联,这个时候银行也会将一条短信存储在一个类似消息中心中,并有专门的系统处理这些消息去通知用户,银联在返回给POS机,最后POS机出票,这个时候手机也会收到一条银行短信。在这个过程中银行生成短信并不是立马去发送,而是放入消息中心(这个类似于一个MQ),让其他系统异步去做,为什么要这么做呢?因为发送短信并不是业务主线内容,如果扣款和发送短信放在一起同步去做,那么会导致整个业务响应很慢,也可能由于短信发送失败导致整个业务失败,因为发送短信谁能保证百分之百的成功,而且谁能保证百分之百的快速响应?所以将这种附加在主业务分支上的业务通过异步执行来提高整条业务的成功率和处理效率,这也是企业级系统开发常用的手段。

以上是我所认识的软件工程,也不知道最后表达了什么思想,只是总结了一下到如今我对软件开发的一个认识。以上也是我主观的认识,并不具备参考意义,还望不要误导大家。

猜你喜欢

转载自blog.csdn.net/JDream314/article/details/44877473