软件设计概述

from:http://blog.csdn.net/guoxiaoqian8028/article/details/24204273

概述

软件设计是把需求转化为软件系统的最重要的环节,系统设计的优劣在根本上决定了软件系统的质量。

在此,主要阐述软件系统设计的5个核心内容:体系结构设计、用户界面设计、数据库设计、模块设计、数据结构和算法设计。旨在帮助开发人员搞清楚“设计什么”以及“如何设计”。

一般把设计过程划分为两个阶段:概要设计阶段和详细设计阶段,如下所示:

  • 概要设计阶段的重点是体系结构设计。
  • 详细设计阶段的重点是用户界面设计、数据库设计、模块设计、数据结构与算法设计等。

可根据项目的情况进行文档裁减和过程合并,如项目开发过程只有一个设计阶段和设计文档。

体系结构

体系结构如同人的骨架。如果某个家伙的骨架是猴子,那么无论怎样喂养和美容,这家伙始终都是猴子,不会成为人。

由此可见,体系结构乃是系统设计的重中之重。

目前业界比较流行的软件结构模式有C/S(客户/服务器)、B/S(BROWSE/SERVER)、层次结构(上下级层次结构、顺序相邻的层次结构、含中间件的层次结构)

体系结构设计原则

 合适性

即体系结构是否适合于软件的“功能性需求”和“非功能性需求”。高水平的设计师高就高在“设计出恰好满足客户需求的软件,并且使开发方和客户方获取最大的利益,而不是不惜代价设计出最先进的软件。

 结构稳定性

详细设计阶段的工作如用户界面设计、数据库设计、模块设计、数据结构与算法设计等等,都是在体系结构确定之后开展的,而编程和测试则是更后面的工作,因此体系结构应在一定的时间内保持稳定。

软件开发最怕的就是需求变化,但“需求会发生变化”是个无法逃避的现实。人们希望在需求发生变化时,最好只对软件做些皮皮毛毛的修改,可千万别改动软件的体系结构。如果当需求发生变化时,程序员不得不去修改软件的体系结构,那么这个软件的系统设计是失败的。

高水平的设计师应当能够分析需求文档,判断出哪些需求是稳定不变的,哪些需求是可能变动的。于是根据那些稳定不变的需求设计体系结构,而根据那些可变的需求设计软件的“可扩展性”。

 可扩展性

可扩展性是指软件扩展新功能的容易程度。可扩展性越好,表示软件适应“变化”的能力越强。

可扩展性越来越重要,这是由现代软件的商业模式决定的:

  • 社会的商业越发达,需求变化就越快。需求变化必将导致修改(或者扩展)软件的功能,现代软件的规模和复杂性要比十年前的大得多(对比一下操作系统的变化就明白了),如果软件的可扩展性比较差的话,那么修改(或者扩展)功能的代价会很高。
  • 现代软件产品通常采用“增量开发模式”,开发商不断地推出软件产品的新版本,从而不断地获取增值利润。如果软件的可扩展性比较差的话,每次开发新版本的代价就会很高。虽然开发商抓住了商机,但却由于设计水平差而导致没有赚取多少利润,真是要活活气死。

 可复用性

由经验可知,通常在一个新系统中,大部分的内容是成熟的,只有小部分内容是创新的。一般地可以相信成熟的东西总是比较可靠的(即具有高质量),而大量成熟的工作可以通过复用来快速实现(即具有高生产率)。

可复用性是设计出来的,而不是偶然碰到的。要使体系结构具有良好的可复用性,设计师应当分析应用域的共性问题,然后设计出一种通用的体系结构模式,这样的体系结构才可以被复用。

用 户 界 面 设 

为了提高用户界面的易用性和美观程度,总结了十个设计原则。用于提高易用性的界面设计原则有8个:

  • 用户界面适合于软件的功能
  • 容易理解
  • 风格一致
  • 及时反馈信息
  • 出错处理
  • 适应各种用户
  • 国际化
  • 个性化

用于提高美观程度的设计原则有:

  • 合理的布局
  • 和谐的色彩

 用户界面适合于软件的功能

用户界面的合适性是指界面与软件功能相融洽的程度。软件的功能需要通过用户界面来展现,用户界面一定要适合于软件的功能,这是最基本的要求。界面的合适性既提倡外美内秀,又强调恰如其分。

 容易理解

提高用户界面可理解性的一些规则如下:

  • 界面中的所有元素(如菜单、工具条等)没有错误,也不会让人误解。
  • 所有的界面元素应当提供充分而必要的提示,例如当鼠标移动到工具条上的某个图标按钮时,应当在该图标旁边出现功能提示。
  • 界面结构能够清晰地反映工作流程,以便用户按部就班地操作。
  • 对于复杂的用户界面而言,最好提供界面“向导”,及时让用户知道自己在界面结构中所处的位置。例如对于基于Web的应用软件,应该在界面上显示“当前位置”,否则用户很容易在众多的页面中迷失方向。

● 风格一致

风格一致有两方面的含义:

(1) 一个软件的用户界面中,同类的界面元素应当有相同的视感和相同的操作方式。例如命令按钮是最常见的界面元素,所有命令按钮的形状、色彩以及对鼠标的响应方式都是一致的。

(2) 同一类型软件的用户界面应当有一定程度的相似性。例如Microsoft公司的Office家族里有Word、Excel、PowerPoint、Outlook等软件,这些软件提供的“复制、剪切、粘贴”功能的操作方式都是相同的。

 及时反馈信息

用户进行某项操作后,如果过了一会儿(几秒钟)用户界面一点反应都没有,这将使用户感到迷茫和不安,因为他不知道是自己操作错了还是软件的原因导致死机了。所以及时反馈信息很重要,至少要让用户心里有数,知道该任务处理得怎么样了,有什么样的结果。

例如下载一个文件,界面上应当显示“百分比”或相关数字来表示下载的进度,否则人们不知道要等待多少时间。如果某些事务处理不能提供进度等数据,那么至少要给出提示信息如“正在处理,请等待…”,最好是提供合适的动画,让用户明白软件正在干活、没有死机。

 出错处理

在设计用户界面时必须考虑出错处理,目的是让用户不必为避免犯错误而提心吊胆、小心翼翼地操作。常见的错误处理方式有:

  • 提供对输入数据进行校验的功能。当用户输入错误的数据时,及时提醒用户改正数据。
  • 对于在某些情况下不应该使用的菜单项和命令按钮,将其“失效”(屏蔽)可以有效防止该项功能被错误地使用。例如:对于某些管理软件,不同的用户有不同的操作权限。如果低权限的用户登录到系统,那些只有高级权限用户才能使用的功能应当被屏蔽(如变成“灰色”不可操作)。
  • 提供Undo功能,用以撤销不期望的操作。
  • 执行破坏性的操作之前,应当获得用户的确认。例如用户删除一个文件时,应当弹出对话框:“真的要删除该文件吗”,当用户确认后才真正删除文件。

 合理的布局

首先,界面的布局应当符合逻辑,最好能够与工作流程吻合。界面设计人员只有仔细地分析软件的需求,才能提取对界面布局有价值的信息。

其次,界面的布局应当整洁(整齐清爽)。界面元素应当在水平或者垂直方向对齐,行、列的间距保持一致。窗体的尺寸要合适,各种控件不能过分拥挤也不能过分宽松。要善于利用窗体和控件的空白,以及分割用的线条。

 和谐的色彩

用户界面是否美观,主要取决于该界面的布局和色彩搭配。实现“合理的布局”相对比较容易一些,设计和谐的色彩太困难了,因为色彩的组合千变万化,并且人们对颜色的喜好也极不相同。

对于广大软件开发人员而言,虽然我们没有必要让普通软件的界面漂亮到Windows XP这种程度,但是掌握一些界面色彩的设计原则无疑是非常有益的。

  • 如果不是为了显示真实感的图形和图像,那么应当限制一帧屏幕的色彩数目,因为人们在观察屏幕的时候很难同时记住多种色彩。
  • 应当根据对象的重要性来选择颜色,重要的对象应当用醒目的色彩表示。
  • 使用颜色的时候应当保持一致性,例如错误提示信息用红色表示,正常信息用绿色表示,那么切勿篡用红色和绿色。
  • 在表达信息时,不要过分依赖颜色,因为有些用户是色盲或色弱。

数据库设计

 开发与平台无关的数据库应用程序

目前国际上应用最广泛的数据库系统有Oracle、DB2、Informix、Sybase和SQL Server。

这些数据库系统之间的激烈竞争即有好处又有坏处。竞争的好处是使数据库系统不断发展和完善,并且避免价格垄断。竞争的最大坏处是逼迫数据库厂商不断开发出独特的功能以吸引更多的用户,所以各个数据库系统的独特功能无法形成统一标准,导致用户难以开发出与平台无关的数据库应用程序,因为用户很难抵御数据库系统独特功能的诱惑。

读者也许会问:“结构化查询语言(SQL)难道不是数据库系统的标准吗?”

是的,SQL是数据库系统的标准查询语言。可是数据库厂商提供了太多超出SQL标准的特色功能,使人们陷入了进退两难的境地:

  • 如果你想使程序与数据库平台无关,那么只能使用SQL,放弃各个数据库系统的独特功能。
  • 如果你超越SQL,使用了某个数据库系统的独特功能,那么这样的程序就是与平台相关的。
  • 类似问题也存在于操作系统、Web浏览器这些领域。理论上讲,只有绝对垄断才能形成绝对统一的标准,但是人们既希望打破垄断又希望有统一的标准,这种矛盾无法彻底解决,只能折衷、妥协。建议如下:
  • 如果你开发的是通用的数据库应用软件,不想让应用软件与特定的数据库系统捆绑在一起,那么你就老老实实地用SQL语言写程序。
  • 如果你开发的是行业专用的数据库应用软件,并且这个行业已经指定了数据库系统(这种局部垄断现象普遍存在),最近若干年都不会改变的话,那么你可以超越SQL使用该数据库系统的独特功能。

 数据库性能优化问题

数据库设计的主要挑战是“高速处理大容量的数据”。如何优化数据库的性能是设计人员经常面临的问题。数据库性能优化主要有两种途径:

  • 优化表结构本身。例如对第三范式的表结构进行反规范化处理,允许表中存在冗余数据,从而减少多个表链接操作,达到提高性能的目的。
  • 优化数据库的环境参数。例如提高硬件设施,调整表的空间尽量减少数据碎片等。

在表的物理设计阶段,设计人员应当按照第三范式设计表结构(即规范化处理)。这样做的好处是:表中没有冗余数据,表结构很清晰,将来修改或者扩充非常方便。但是按第三范式设计也存在一些缺点:产生了许多表,每个表有相对较少的列,并且这些列必须使用“主健/外健”关联起来,因此某个查询操作可能会产生复杂的表链接,导致性能降低。

反规范化处理是指对第三范式的表进行修改,通过合并一些表,或者在表中创建冗余的列,从而减少表链接操作代价,达到提高性能的目的。要注意的是反规范化处理存在很大的负面影响:管理冗余数据很麻烦,如果冗余数据不同步的话,那么会发生数据错误这种严重的问题。

所以,对表进行第三范式的规范化处理是第一重要的,而反规范化处理则需谨慎考虑、不宜过多使用。“规范化处理”以及“反规范化处理”不是自相矛盾之举,而是性能优化的策略。

除了优化表结构之外,优化数据库的环境参数也能够提高数据库的性能。例如给服务器配置更快的CPU,增加内存。运行数据库是非常消耗内存的,内存对数据库性能影响比较大。由于现在市场上的内存条越来越便宜,所以为服务器配置足够多的内存恐怕是成本最低、难度最低、见效最快的性能优化方法。

在安装数据库系统时,要为系统指定“块大小”(一次物理读写操作所设计的字节数)。在创建表时,也要为表指定一定的空间。如果“块大小”和“表空间”与实际的数据存储不匹配的话,那么会产生许多磁盘碎片,这将降低数据库物理操作的性能。

能否有效地优化应用软件数据库的性能,主要取决于开发者对数据库系统的熟悉程度以及开发经验。

● 数据库安全问题

提高软件系统的安全性应当从“管理”和“技术”两方面着手。这里仅考虑技术手段(因为安全管理超出了软件工程范畴),一般原则如下:

  • 用户只能用帐号登陆到应用软件,通过应用软件访问数据库,而没有其它途径可以操作数据库。
  • 对用户帐号的密码进行加密处理,确保在任何地方都不会出现密码的明文。

确定每个角色对数据库表的操作权限,如创建、检索、更新、删除等。每个角色拥有刚好能够完成任务的权限,不多也不少。在应用时再为用户分配角色,则每个用户的权限等于他所兼角色的权限之和。

模块设计

在设计好软件的体系结构后,就已经在宏观上明确了各个模块应具有什么功能,应放在体系结构的哪个位置。我们习惯地从功能上划分模块,保持“功能独立”是模块化设计的基本原则。因为,“功能独立”的模块可以降低开发、测试、维护等阶段的代价。但是“功能独立”并不意味着模块之间保持绝对的孤立。一个系统要完成某项任务,需要各个模块相互配合才能实现,此时模块之间就要进行信息交流。

评价模块设计优劣的三个特征因素:“信息隐藏”、“内聚与耦合”和“封闭——开放性”。

 信息隐藏

为了尽量避免某个模块的行为去干扰同一系统中的其它模块,在设计模块时就要注意信息隐藏。应该让模块仅仅公开必须要让外界知道的内容,而隐藏其它一切内容。

模块的信息隐藏可以通过接口设计来实现。接口是模块的外部特征,应当公开;而数据结构、算法、实现体等则是模块的内部特征,应当隐藏。一个模块仅提供有限个接口(Interface),执行模块的功能或与模块交流信息必须且只须通过调用公有接口来实现。如果模块是一个C++对象,那么该模块的公有接口就对应于对象的公有函数。如果模块是一个COM对象,那么该模块的公有接口就是COM对象的接口。一个COM对象可以有多个接口,而每个接口实质上是一些函数的集合。

 高内聚

内聚(Cohesion)是一个模块内部各成分之间相关联程度的度量。内聚程度从低到高大致划分为低端、中段和高端,如图3-15所示。模块设计者没有必要确定内聚的精确级别,重要的是尽量争取高内聚,避免低内聚。

顺序性内聚 功能性内聚

时序性内聚 过程性内聚 通讯性内聚

偶然性内聚 逻辑性内聚

低端… 中段… 高端…

各种内聚类型的含义如下:

  • 偶然性内聚。如果一个模块的各成分之间的关系彼此松散(几乎无关),称为偶然性内聚。
  • 逻辑性内聚。几个逻辑上相关的功能被放在同一模块中,则称为逻辑性内聚。例如一个模块读取各种不同类型外设的输入。
  • 时序性内聚。如果一个模块内的几个功能必须在同一时间内执行(如系统初始化),但这些功能只是因为时间因素关联在一起,则称为时间性内聚。
  • 过程性内聚。如果一个模块内部的处理成分是相关的,而且这些处理必须以特定的次序执行,则称为过程性内聚。
  • 通信内聚。如果一个模块的所有成分都操作同一数据集或生成同一数据集,则称为通信内聚。
  • 顺序内聚。如果模块内的某个成分的输出作为另一个成分的输入,则称为顺序内聚。
  • 功能内聚。模块的所有成分对于完成单一的功能都是必须的,则称为功能内聚。

 低耦合

耦合(Coupling)是模块之间依赖程度的度量。内聚和耦合是密切相关的,与其它模块存在强耦合的模块通常意味着弱内聚,而强内聚的模块通常意味着与其它模块之间存在弱耦合。

耦合的强度依赖于以下几个因素:(1)一个模块对另一个模块的函数调用数量;(2)一个模块向另一个模块传递的数据量;(3)一个模块施加到另一个模块的控制的多少;(4)模块之间接口的复杂程度。

耦合程度从低到高大致划分为低端、中段和高端,如图3-16所示。模块设计应当争取“高内聚、低耦合”,而避免“低内聚、高耦合”。

印记耦合 控制耦合

公共耦合 内容耦合

非直接耦合 数据耦合

低端… 中段… 高端…

各种耦合类型的含义如下:

  • 非直接耦合。模块之间没有直接的信息传递,称为非直接耦合。
  • 数据耦合。模块之间通过接口传递参数(数据),称为数据耦合。
  • 标记耦合。模块间通过接口传递内部数据结构的一部分(而不是简单的参数),称为印记(Stamp)耦合。此数据结构的变化将使相关的模块发生变化。
  • 控制耦合。模块传递信号(如开关值、标志量等)给另一个模块,接收信号的模块根据信号值调整动作,称为控制耦合。
  • 公共耦合。两个以上的模块共同引用一个全局数据项,称为公共耦合。
  • 内容耦合。当一个模块直接修改或操作另一个模块的数据,或者直接转入另一个模块时,就发生了内容耦合。

数据结构与算法设计

设计高效率的程序是基于良好的数据结构与算法,而不是基于编程小技巧。

一般说来,数据结构与算法就是一类数据的表示及其相关的操作(这里算法不是指数值计算的算法)。从数据表示的观点来看,存储在数组中的一个有序整数表也是一种数据结构。算法是指对数据结构施加的一些操作,例如对一个线性表进行检索、插入、删除等操作。一个算法如果能在所要求的资源限制(Resource Constraints)范围内将问题解决好,则称这个算法是有效率(Efficient)的。例如一个资源限制可能是“用于存储数据的内存有限”,或者“允许执行每个子任务所需的时间有限”。一个算法如果比其它已知算法所需要的资源都少,这个算法也被称为是有效率的。算法的代价(Cost)是指消耗的资源量。一般说来,代价是由一个关键资源例如时间或空间来评估的。

毋庸置疑,人们编写程序是为了解决问题。只有通过预先分析问题来确定必须达到的性能目标,才有希望挑选出正确的数据结构。有相当多的程序员忽视了这一分析过程,而直接选用某一个他们习惯使用的,但是与问题不相称的数据结构,结果设计出一个低效率的程序。如果使用简单的设计就能够达到性能目标时,选用复杂的数据结构也是没有道理的。

人们对常用的数据结构与算法的研究已经相当透彻,可以归纳出一些设计原则:

1) 一种数据结构与算法都有其时间、空间的开销和收益。当面临一个新的设计问题时,设计者要彻底地掌握怎样权衡时空开销和算法有效性的方法。这就需要懂得算法分析的原理,而且还需要了解所使用的物理介质的特性(例如,数据存储在磁盘上与存储在内存中,就有不同的考虑)。

2) 开销和收益有关的是时间——空间的权衡。通常可以用更大的时间开销来换取空间的收益,反之亦然。时间——空间的权衡普遍地存在于软件开发的各个阶段中。

3) 设计人员应该充分地了解一些常用的数据结构与算法,避免不必要的重复设计工作。

4) 数据结构与算法为应用服务。我们必须先了解应用的需求,再寻找或设计与实际应用相匹配的数据结构。

数据结构与算法设计的一般流程如下:

(1)数据结构与算法有全局和局部之分,当然先设计全局的,后设计局部的(通常在模块设计时进行)。

(2)根据问题的特征,先查找已经存在的数据结构与算法,挑选最合适的(并不一定是最先进的)。如果不存在现成的,那么自己设计。

(3)设计并且编写代码之后,要进行测试。如果不满足性能要求,那么要进一步优化数据结构和算法。

1  系统设计的关键

软件设计活动的关键又是什么呢?还是让我们回到现实世界去寻找答案吧!

在远古时期,人类只能通过徒步从一个地方到达另一个地方。后来发现马可以被驯服,通过马车能更快地从一处到达另一处。再后来,人类逐步发明了自行车、汽车和飞机,且每一次发明都使得交通效率得以大幅提高。在这里,马车、自行车、汽车和飞机都共同地为了解决交通效率问题。很显然,马车、自行车、汽车和飞机都是不同的概念,人类通过抽象发明这些概念,并通过相互学习的方式使得大家掌握其含义。如果将焦点放在交通工具上,我们会发现交通效率越高则其概念越抽象,复杂度也越高。或者说,越是抽象的概念,其所隐藏的复杂度就越高。

从这个关于交通的例子中我们不难发现,现实世界中我们是通过转移复杂度的方式解决复杂问题的,且每一次不同的转移都伴随着抽象概念的建立,转移的复杂度越多所建立的概念就越抽象。由于复杂度被转移了后,使得一小部分人专注于被转移的复杂度(比如制造飞机),而其他绝大部分人在享用复杂度被转移所带来好处的同时不用关心被转移走的复杂度(乘飞机的人不用关心飞机的驾驶与制造)。同样的事情也发生在软件行业!不同的是软件行业“制造”的不是汽车和飞机,而是编程语言、数据库、平台和框架等。

不难发现,软件设计的关键活动应是抽象,通过抽象建立新的概念并将部分复杂度转移到概念背后。尽管不断的抽象增加了新概念的复杂度,但也简化了所需解决问题的复杂度。显然,好的设计应是其简化的复杂度要大于因为抽象所创造的复杂度。

2  系统设计的目标

系统设计的目标是在保证实现用户功能和必要的性能的前提下,保证系统的质量特性达到规定的要求。

系统的功能、性能和质量特性必须通过设计来体现,通过设计方案的选择来证明系统能够满足用户在系统功能、性能和质量特性方面的用户需求。

3  系统设计的依据

系统设计以需求规格说明书为输入,以产生满足功能需求和非功能需求的设计方案为输出。

首先,待开发的软件系统必须满足功能需求。确切地讲,首先必须满足用户功能需求,即软件系统可以帮助用户(包括系统所属用户和直接使用系统的用户)解决用户希望解决的问题。

其次,待开发的软件系统必须满足非功能需求。满足非功能需求意味着软件系统能够像用户所希望的那样解决问题,并且维护方便。这里,有三个方面的问题:

1、对约束的满足。软件系统能够在用户给定的环境下有效运行。约束是多方面的,包括硬件环境、软件环境、社会环境(包括国家法律法规)、企业管理环境。当企业信息化进行到一定阶段时,约束对软件系统开发构成了不可逾越的障碍。

2、软件的可用性。软件的可用性除了软件系统满足约束外,还有软件使用的方便性、可靠性、安全性等。--运行期质量

3、软件的可维护性。软件的可维护性不仅仅是软件开发组织降低软件维护成本的要求,实际上也是用户的要求。当用户需求发生变化后,用户希望能够很快地修改软件系统而不影响对业务的处理。

开发架构设计关注软件开发环境中软件模块的实际组织方式,即软件系统由那些程序包组成,以及它们之间的关系。程序包不仅包括要编写的源程序,而且包括直接使用的第三方SDK和现存的框架、类库,支持系统运行的操作系统或中间件。

开发架构设计的重点考虑开发期质量,软件模块的组织有利于可扩展性、可重用性、可移植性、易理解性、易测试性等,设计中的关键技术主要体现软件开发期质量有关的设计,如设计模式的运用,框架的选择等。

开发架构设计的主要工作是:

确定要开发的程序包或直接利用的程序包(如函数库)之间的依赖关系。

采用的技术,包之间采用什么技术实现连接,以保证必要的开发期质量,如有依赖关系的包之间的松耦合。

确定采用的框架。如果实现连接的技术有对应的框架,采用框架是值得推荐的选择。

分层模型开发架构设计的主要工具。三层架构模型把程序代码分成负责与用户交互的表现层(用户界面层)代码、负责业务处理的业务层代码、负责数据服务的数据层代码。表现层可以按用例分成若干个包,业务层可以可以按照业务逻辑独立性分成若干个包,数据层可以由若干实体对象组成。其中,业务层由领域模型细化得到。由于领域模型相对稳定,所以业务层也相对稳定。表现层受表现设备和表现方式影响,数据层受数据库系统影响,因此这两层必须满足可扩展性、可修改性等要求。

分层架构模式为“把变化点封装起来”提供了手段。分层架构的最大优点是将整体问题局部化,把可能的变化封装到不同层中。最终将系统规划为单向依赖的分层体系,利于修改、扩展、替换。将代码分为层的好处是:层形成了开发小组的自然边界—分层的开发人员需要的技巧是不同的。表现层的开发小组要深入了解用户界面工具包的使用;数据层开发小组许傲熟悉相关的数据库、数据持久化工具或文件系统。

采用分层和分区的描述方式能够反映直接使用的程序包或框架的用法,使开发架构更清晰


附录美文:http://www.csdn.net/article/2015-08-18/2825486

为了给企业提供稳定可靠且优质的服务,作为一名软件架构师,在应用的架构设计上也是费尽心思,本文作者来自“风语者客服+”的CTO黄耀华,他从自己多年的实践出发,总结了软件架构设计的一些经验,分享给大家。

“风语者客服+”是针对中小型企业推出的客服SaaS,节约了企业自建客服系统所需的巨大成本。为了给企业提供稳定可靠且优质的服务,我们在整体架构上费尽心思。虽然不尽完美,希望借此抛砖引玉,互相切磋。

前言

”Look deep into nature, and then you will understand everything better.“ -- Albert Einstein

我国传统文化上,要做成一件事,讲究三个方面:明道,优术,取势。在软件架构设计方面而言,也是类似的道理:遵循自然规律以明确大的方向,使用优秀的实操战术,再根据实际情况落地。

这是个快餐年代,几乎所有人都只做一件事 -“取势”。 几乎没有多少人会去理解一个Servlet的工作原理,去理解一次HTTP请求的完整流程,因为有超多框架帮你屏蔽了这里的细节。询问一个人会什么技术,回答也往往是我会Hibernate、Spring、Ibatis、会PullToRefresh组件、会使用SDWebimage。不过这些框架(Framework)其实并不是软件架构。软件架构是一所有生命力的房子,而这些框架只是大一点的板砖。

因为笔者水平有限,这里只提一些普遍准则,也就是”正确的废话“,以飨视听。不会深入到实操战术上,比如怎么用Spring实施MVC架构,怎么使用Maven管理依赖,Redis的常用操作,怎么搭建一个负载均衡的集群,如何使用阿里巴巴的Dubbo框架进行服务化等等。如果大家有兴趣,可以自行搜索,有很多优秀的文章可供参考。

不幸的“程序猿”和“程序媛”各有各的痛苦,幸福的程序员都是相似的。其实说幸福有点言过其实,下面就说说怎么让他们不那么痛苦。

一. 很好的模块化支持

“At the bottom of every person's dependency, there is always pain, Discovering the pain and healing it is an essential step in ending dependency.” --Chris Prentiss

他们都在一个相对稳定的软件架构里编码,自己的代码不会依赖很多模块,不会因为自己微小的改动造成全局的失败。正如"1984"中的老大哥说的,Ignorance is strength(”对外界的“无知就是一种力量).  任何一个模块都不能有太强的存在感。

曾经在一个大型互联网公司里面,任何人只要用到一个核心模块的功能,就必须依赖一个部署在某远程服务器的库,而且还有IP限制,只能把代码部署到指定网段才能运行起来。导致基本上没法在本地进行单元测试或者简单调试。这个核心库的存在感太强,就成了开发的瓶颈,严重的降低了生产力和码农的幸福程度。

在“风语者客服+”的架构中,每个码农都可以很方便的在本地把服务启动起来,一分钟up and running,随便做一些改动就可以立竿见影的看到效果。这里要归功于几个东西:

1. Git代码管理

在团队作战中,每个程序员可以取下来完整的最新代码库,也可以在本地分支上尽情挥毫泼墨,而不担心影响别人的工作。也可以把本地修改先stash起来,review一下别人的代码,再unstash恢复回来。要想提高团队效率,代码仓库管理建议尽快迁移到Git上。

2. Maven、Gradle、Cocopods等依赖管理

Maven是一个管理依赖(Dependency)的工具,现在在Java社区应该是比较普及的,无法想象现在还有团队直接拷贝jar包来管理依赖。虽然早期没有Maven的时候,都是拷贝jar包这么过来的,碰到的问题也是显而易见的,依赖的jar包作者改了某个bug,没能及时传导到调用方。多个调用方使用不一致的jar包,导致各种奇异bug。对应的在安卓社区,使用gradle的比较多,iOS的Objective-C开发中,多采用CocoaPods。

二. 高内聚,低耦合

He should focus on his knitting, not trying to do everything. Do one thing well.-- Steve Jobs

"Do one thing well"其实不算是老乔的专利,UNIX哲学和Google哲学都提倡这一点。这句话本身不完全对,比如对于一个商人,如果只会Do one thing well,那他无法在市场中存活,但是在工程师中却是万般推崇的哲学。

我们可以期望一个人具备一百种技能,然而对一个工具只期望它把一个需求解决好解决彻底,对于实现工具的一个类,一个方法,更是如此。但是,实际经验中,我们经常看到一个5000行以上代码的类,活像一个巨人版的瑞士军刀,什么都能做,但是什么都做不好。这就是”Separation of Duty"没有做好的典范。

在风语者”客服+“对外提供的SDK和API中,我们也提倡同样的思想,力争把App使用”客服+“SDK的门槛降到最低,每个API都能自言其一,而且API直接没有时序上的依赖关系。内部各个模块的开发,也秉承同样的责任分割原则。责任分割原则的落实,没有什么好的框架或者工具来支持。只能通过老鸟经常去做Code Review,找出存在的问题,提出重构方案,并督促菜鸟改进。

个人一般采用的重构思路,仅作为参考,照搬后被老板批评乃至造成工伤概不负责:

  1. 把一个大的工具类,根据主题不同,拆分成若干个互不干扰的高内聚工具类;举个例子,一个万能的NetworkUtils可能可以拆成HttpUtils, FTPUtils,TelnetUtils等;
  2. 对于一个被频繁调用的类,仔细观察调用情况,如果有一些方法的被调用频率远远低于其他方法,那么需要考虑这个方法是不是应该放在这个类中;
  3. 存在A,B两个类之间的相互依赖,或者更多类的混乱依赖,那么就更要抽丝剥茧,通过合理安排类的功能来去除环形依赖;
  4. 尝试一句话说清楚一个类的功能,不要使用“和”,“以及”,“或者”等连接词;如果出现了这些连接词,就需要引起重视;

三. 用进化拥抱变化

“It is not the strongest or the most intelligent who will survive but those who can best manage change.” ― Leon C. Megginson

前段时间,朋友圈疯传一篇文章 -——“架构腐化之谜”,大家都深表同感,纷纷表示对自己架构的未来的担忧。然而,说句不合时宜的话, 90%的担忧是杞人忧天,因为以现在产品更新换代的速度,90%的项目面市即意味着死亡,没等到架构腐朽,产品已经入土了。

剩下10%里面,也许有9%会一直坚持活下去,但是不会蓬勃发展,也就是说,只要保证不出现内存泄露之类的问题,代码就会一直在几台小服务器上运行下去,哪怕后面没有人维护也没关系。只有1%的产品,会日新月异的更新迭代,最终成长为巨无霸,或者巨无霸的生态下的一个环节。这个言论看似悲观,却是对现实最好的妥协。

谬用一下泰戈尔的名言:“不是槌的打击,而是水的载歌载舞,使鹅卵石臻于完美”, 不是闭门造车的架构,而是不断拥抱变化的需求,才使得架构臻于完美。

假如在早期就纠结于架构的完美性,而延迟产品的交付,是非常得不偿失的。只有生存下来,才有机会。再根据市场变化,不断优化架构,从而延长软件的生命周期。那么,假如撞大运,真的成了这1%,怎样做才能算是拥抱变化?

首先,请参考本文第一点和第二点。如果这两点基本功没有练好,那么谈架构的进化就和还没有通关十八罗汉的新手就想练成九阴真经是一个道理。

在设计之初,初步考虑系统的Scalability(可伸缩性)

下面在第四点会详细阐述。

内部的各个模块尽量做到可插拔

一方面是接口和实现的分离,可以随着需求的变化更换实现;另一方面,尽量把功能服务化,成为微服务,并且可以监控到服务的互相调用情况,当某个服务老化,可以逐步废弃或使用新的服务取代之。这一点上,阿里巴巴的Dubbo框架是一个不错的选择。

尽量采用优秀的框架,站在巨人的肩膀上

例如在Web层面,我们使用Twitter的Bootstrap前端框架来实现响应式Web编程,提高生产效率的同时减少了为解决各种设备适配问题的投入。当然,这就需要设计师配合,按照Bootstrap规范来设计页面,减少一些个性化设计。

最后,考虑系统的Resilience(弹性,也叫耐受性)

俗一点说,就是变成一只打不死的小强,代码中尽量提前预判可能遇到的各种情形。经常看到代码里面有一堆的if(){}判断语句,我就问作者,“你考虑过else{}吗?”一般回答都是,“这绝对只有if,不会有else的”,可如果真的遇到else怎么办?千年虫问题就是这么诞生的。可能很多新同学还不知道什么是千年虫问题,简单地说,就是当年的码农,为了省一点内存空间,只用了2位数来表达年份,比如int year = 98; 表达1998年。我猜码农当时的心态也是,“就我这代码,还能活到2000年,搞笑吧?”

程序员们平时可以多扩大自己的脑洞,想想有哪些else情况自己没有处理,而且可以轻易处理的。比如服务器挂了,那么App端是不是也要跟着crash,还是给出友好一点的提示,或者更友好一点,使用本地缓存。

四. 设计可扩展,但不要过度设计

it's better to have infinite scalability and not need it, than to need infinite scalability and not have it--@littleidea 网友

无限的扩展能力是一种奢望,但是起码不能让扩展能力成为0。试想一下,你辛辛苦苦为老板开发了一个网站,过了一个月,网站超负荷了,老板说,“小A啊,之前2台服务器花了我5万块,预计流量马上要翻倍了,再给你5万块,帮我扛过去啊。”结果你发现,问题不是线性增加服务器就能解决的,原来的程序没有做分层(Web,Business Logic, Data Access等),导致加服务器也只能把所有层的代码全搬到新的服务器,虽然只是Business Logic的计算有压力,却要浪费老板很多服务器。更糟糕的是,因为程序里面用到了文件系统和操作系统命令,不好做负载均衡。

这里有一些准则供参考:

  1. 代码分层是必须的,层次明朗以后,当哪个层次的负载较重,想办法对该层次进行优化或者扩容即可;
  2. 保持核心服务是无状态的,所谓无状态就是没有和请求相关的数据依赖;
  3. 尽可能的选用已被验证的广泛采用的成熟基础架构;
  4. 充分利用Zookeeper等集群管理工具,来对服务进行管理;

风语者“客服+”中,把业务相关的代码内部组装为风语者ServiceBox,使用阿里巴巴的Dubbo服务进行注册管理。当负载增加时,可以迅速在运维层面增加服务节点,以提供更高的服务能力,从而保证客户的优质体验。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章