在另一篇文章中说明了“规划项目结构”的重要性,在这篇文章中则要来谈谈如何实践。
决定结构的依据
在决定项目结构的分类方式时,不外乎是依 Feature 或是依 Layer 来设置所谓的 Package 或是 Namespace,一般都会与实体的目录名称做搭配,形成一个同步的树状结构。这二种分类的差别,其实说白了就是倒底是要依照 SA 还是 SD 的文件内容来做分类的基准。
在进行系统设计时,理所当然地会以 SA 文件为基础来开展工作,因为系统分析本来就是做为设计之前的信息分析与统整的工作。在这样的前提之下,所设计出来的 Class 就注定会带有 SA 文件分类的属性。然而在 OOP 的原则之下,单一个 Class 不太可能负担所有的工作,所以设计出一组 Class 用来实现 SA 文件所描述的功能是很常见的手法。当设计工作再演化下去,为了有系统组织设计的结果,就会在设计中导入 Design Pattern 或是 Framework,来试图形成多个 Class 的群组。这时 Class 就具备了第二种的分类属性,因为在每一组的设计中 Class 都会有特定的角色或定位来协助完成对应的工作。
在经过以上的说明后可以看到,大部份 Class 都最少有二种的分类方式。然而,项目或者是目录的结构只能以一种视点来表达,抉择就因此而产生。依 SA 文件会以功能面或是处理的数据类型为主,所以分类上就会形成类似 Customer、Product、Order 这样的结构。依 SD 文件则会是以 Class 的角色定位为主,如果 SD 文件中规定要使用 MVC 的 Design Pattern,则分类就会出现 Model、View、Controller 这样的结构。
决定结构的首要考量
至于依 Feature 或是 Layer 何者熟优熟劣,就过去的经验法则,我本身是比较倾向依 Feature 来分类。不过,在这之前其实要先考虑的是程序实体切割的问题。怎么说?在系统成长、扩张到极致时,势必得导入分散式的架构设计,也就是程序是散落在不同的运行环境之中,并且大多都以网络为交换信息的媒介。
在进入到分散式架构的设计之前没有先预留好必要的弹性,进入之后又没有足够的决心打掉重来。接着下来在设计上的调整工作,对负责的人来说将会是一个相当耗费心力的过程。
在这个过程中数据传递的问题会是最大的障壁,很多理所当然的 Class 之间交换数据之手法,在移至远端后就晋级到完全不同的次元。在不是分散式架构时,所有的 Class 共用内存,所以数据在 Class 间可以直接共享、存取。在跨设备交换时,则需要增加额外的程序来达成,不论是对数据进行包装或转换,不是单纯地把 Class 分别放置在不同的实体中就可以顺利的运作。
再来,需要进行的是:调整不适用分散式架构的设计内容。数据传递的过程变复杂了,原本的设计就有可能不敷使用,增加接脚、改变调用方式都是必经的过程。可见范围的改变也直接冲击着原有的设计思维,不像是所有的 Class 都被装在同一个容器中,在跨设备进行远端调用时不可能“看得见”远端所有的 Class,只会有被设计要用来揭露的介面,所以碰触到这些部份的设计都需要重新来过。有时候一些违反设计精神、便宜行事的做法,譬如让不相干的二个 Class 迳行互相调用,在这种环境下就会被严格地指正出来。相关的问题一般都是潜藏在设计的各个角落,等到系统运作出了问题才会发现这样的计设方式行不通。当系统出错的情况经过几次之后,对负责设计的人而言耗掉了心力不说,工作的品质也会面临严重的挑战。
既然分割这么麻烦,那就不要分,所有的 Class 都往同一个项目丢,不就什么事都没有了?以结论来说,这不是一个谨慎的架构师会采用的策略。这个方式的好处除了在更新版本时,不用考虑各端点设备版本配对的问题、直接将所有设备用相同的文件覆盖过一次之外,我想不到还有其他的优点。
首先,在开发阶段会碰到的问题是刚才提到的可见范围的议题,开发人员会因为所负责的 Class 看得见其他远端的 Class 而产生混淆,然后开始不停地质疑为什么明明就在眼前却不能直接调用。但其中的差别大概也只有负责设计的人才弄得清楚,光是解释就要花掉不少的唇舌。
再来就是部署时,不见得每一个端点的设备都有足够的硬件资源提供给程序运作之用。当所有的代码都放在一起,就会出现一个现象是不论在哪种设备上,所提供的程序文件都是一样的肥大,会出现最糟的情况是因资源不足而有运行不稳定的情况。如果是在移动平台上,就有可能会因为要下载的文件过大而使 App 的下载率降低,导因却是 App 里塞了很多用不到的代码这种低级的问题。
在设计时容易被忽略的重点
在 ISO 27001 的定义里,所谓的风险指的是威胁加上弱点的组合结果。没有威胁就算全部都是弱点也无所谓,如同把一个人放到完全没有病毐及细菌的环境中,即使免疫功能不正常也不会有致病的风险。反之,如果把一颗石头放到充满病毐及细菌的环境中,也不会有人担心石头有生病的疑虑,所以没有弱点就算有威胁也不用担心。
在网络世代中,设计系统如果不考虑安全议题,是一个不及格的设计。然而很多时候有关安全的需求并不会被载入 SA 文件内,以致安全防护的设计在有心无心之下被排除在外。而把所有的 Class 都丢在同一个项目中,这种便宜行事的做法就是一个很不安全的策略、只有不在意信息安全才会选择的方式。
延续之前的内容,当设备中所部署的程序中包含了许多不需要的部份,就会增加弱点出现的机会,即便这些程序片断在正常的情况下并不运作。而网络攻击的威胁则不可能会消失、甚至手法不断的翻新,二者结合就会大大地提高被入侵的风险值。
换个角度来说,设计与开发安全防护机制是需要成本的,在没有必要的情况下开发人员自然是多一事不如少一事,检查写得是愈少愈好。毕竟在分工程度较高的团队中,开发人员不一定会接触到部署的相关规划或执行,自然不会对资安具备敏感度。再加上 SD 的文件中没有特别指明防护要做到的层级,产出的结果一定是只有最低限度。一旦这些代码被入侵者以不正当的手法运行,就有可能形成很大的安全漏洞。所以在部署的思维上应该要做到精确的配置、只提供必要运行的部份,以期减少安全上的风险。
在安全上,被侵入是一个议题,信息的泄露则是另一个。假设在移动平台中所下载的程序中包含有 Server 端的逻辑,以目前移动平台对代码的保护等级来说,无疑是送给骇客一份大礼。在这样的情况之下,就算移动端的防护做得再好,也能够依据 App 中额外的 Server 端代码来按图索骥,找到破解的方法。
实体分割的原则
那该如何规划以进行实体分割?常见的三层式架构是一个很好的切入点,也就是把设计的内容切分为 Presentation、Business Logic、Data Access 三大区块。主要是一般的部署策略中,硬件设备的配置也是以这样的架构做为雏型。所以当数据传递的切割点以这些区块的界线做为基准时,在部署规划有异动时比较容易配合硬件架构上的需求。
在最开始就把代码以实体的方式分开,像是在 Visual Studio 里使用不同的 Project 并以 Solution 来封装,或是在 Android Studio 里在同一个 Project 中分出多个 Module。依据不同开发工具的特性,可以做到一部份的早期设计预警的效果。像是之前提到可见度的问题,在分开后多少能降低开发人员在这方面的疑惑,减少其误用的情况。同时也可以验证所设计的内容,在上线被分开部署后基本的调用过程是可以顺利地运作。如果是使用 Visual Studio 特定的版本,甚至提供了自动化检查的功能,负责设计的人员只要把相关的限制输入好,就能在编译时显示警告,防止开发人员没有按图施工,可以节省很多查验的工作。
预先做好切割还有另一项好处,可以提高 Class 的重用率、累积团队的技术资产、减少重工。因为在后续的开发工作中如果需要相同的设计,直接引用已经现有的独立单元即可。如不是,则要在过往的程序项目中巡览、在一堆 Class 中做挑选、复制的工作,而每一个新项目都要再重复一次这个循环。
当然,事情永远不可能单纯到依照原则切割后,所有的 Class 就会自动归位,接下来要烦恼的是 Class 的分配问题。譬如决定哪些是前一段提到可独立于三层结构之外的共用 Class、哪些负责显示数据、哪些用于处理商业逻辑、哪些直接存取数据。定位明确的好处理,暧昧不明、模棱两可的则是会让人烧脑。
以一个大家常用的 MVC Design Pattern 为例,View 毫无疑问地是要被放在 Presentation 层内,那 Controller 和 Model 应该要放在 Presentation 还是 Business Logic?在这里卖个关子,留给各位看倌去思考,也欢迎留言一起讨论。
分类原则的选择
做完了实体的分割,就下来就是怎么决定分类的原则。
就像一开始提到的,我倾向以依 Feature 为主,不过这样的说法并不精确。还是那句话,这个世界还没有单纯到只用一种原则就可以搞定所有的事。刚才也有提到共用的 Class 应该要被独立出来以便跨系统可以共用,既然是可以跨系统代表是系统间共同的需求,或是因应设计产生的惯例。系统间共用的需求会出现在 SA 文件中,但是因应设计产生的惯例在 SA 文件中不会有,如果要以 Feature 为主进行分类,这些 Class 被独立出来之后怎么分类?
那看起来是依 Layer 比较保险啰?不是!因为实务上依 Feature 在扩充结构、任务分配、版本控管、代码巡览上还是比较有优势。例如:在需求变更时,不同的变更项目可以被结构给分离出来,所以在发行版本时能够更精确的选择要异动的项目清单,并且把未完成的部份隔离在要发行的版本之外。
再举一个例子,想像一下,当你的系统在分散式的架构上,某个提供服务的设备超出负载,在评估后决定要进行分割程序打散到不同的硬件上以平衡负载。这时的切割方式是依照 Customer、Product、Order 比较合理?还是依照 Model、View、Controller 比较合理?
所以最后的结论是:以 Feature 为分类主干、在细部中包含依 Layer 分类的混合结构。当 Feature 的分类到了尽头,则可以用 Layer 来接力分类。或是在分类共用 Class 时,于惯用的 Library、Utility、Support 名称下,再以 Layer 做更细部的分类。