当一辆车从苏州驶向北京时,为了缩短时间它必须不停地寻找最佳的路径。而为了寻找最佳路径,一是要知道需求,即究竟都要去哪里,这样才能设计最佳的路线;二是要不停地确认路况,哪条路通畅,哪里封路了,这样才能具体选择究竟走哪条路。
这一过程类似于软件中的设计和编码,设计和编码基本上是在大致知道了要去哪里的前提下,寻找具体路径的过程。在这一过程中,通常并没法只按单一原则行事,路况好的可能路远,路近的则可能路况不好。这意味着追求完美设计即是追求多原则下最佳平衡点的过程。
总结来看,在寻找最佳解决问题方法时,完美的设计与编码有5个关键原则需要遵守。
●正交的概念和逻辑。
●层次的最小化。
●时序清晰。
●隐藏不必要公开的信息。
●名实相符。
这5条规则与常见的面向对象设计原则不同,这起源于我们只指向一个目的:简单化。我们认为软件设计或编码的根本评价标准是在满足现有可见需求的基础上,使代码尽可能简单。而之所以选择简单性这一维度作为设计和编码的核心考量,最关键的原因是所有其他质量属性(灵活性等)都潜在地使软件有复杂化的趋势。当我们面临有矛盾的诸多选择时(如易用性和可测性),我们就必须定下君臣佐使,否则就会陷入到这段代码体现易用性,那段代码体现可测性的无秩序状态。
在此之后,我们可以从另一个侧面对完美的设计编码做点总结。
假使要求最终复杂度可以为100~120,那么完美的软件开发则要求达成以下两点:
一是设计和编码要确保总复杂度不能超过100;二是100的复杂度可以平均分摊到10个模块,那么每个模块的复杂度为10。如果10的理解难度不大,那整个程序是可以理解的,复杂度不高。但如果其中5个复杂度为15,另5个复杂度为5,那么虽然总值不变,呈现给后来程序员的复杂度却是不同。
简单来讲,达成要求以外,简单性压倒一切,接下来才是设计原则。如果遵守设计原则,代码变得更简单,那么遵守设计原则。如果相反,那么选择简单性原则。在后续的案例1中,我们会探讨如何牺牲一点原则来获取简单性。同时也需要认识到,因为需求而牺牲简单性是必然的,比如说,性能的要求可能要求把简单的代码变得复杂,这并不是例外,关键是当这么做的时候,要清楚地知道付出的代价。在案例2中,我们会探讨一个因需求而牺牲简单性的例子。
最后一个需要阐明的问题是,如果切分设计和编码,那么设计应该停在什么样的程度上?这是个经济问题,如果设计需要的投入是X,而可以获得的收益是Y,那么X<Y时设计就应该做下去,直到X差不多等于Y。直接计算X和Y有点困难,但并不是没有取巧的办法。
设计自身蕴含着一种分工方法,假设一个团队有10个人,其中1个人为架构设计师。架构设计师最终的输出必然要支持对10人的一种分工方案。
●从分工的角度看,凡是涉及两人以上的东西(接口,数据等),X<Y的可能性都极大。因为这部分工作一旦做了,并且是正确的,那节约的时间往往是要以倍计的。所以设计的终止尺度可以是每个人和别人间的关联得到澄清。
●从支撑需求的角度看,那么则要检查是否可以支撑体现软件核心价值的需求。尤其是非功能性的需求。这类需求甚至有导致既有实现推倒重来的可能性,风险太高,一定要事先考虑。比如,是否跨平台等。
●从人员状况来看,能力差的人越多,设计的程度应该越细,反之则可以使自主程度高一点。因为能力差的人自身所做的决策,错的可能性较高,即Y偏大。
面向对象设计原则和上述关键点的关系
在《敏捷软件开发中:原则,模式和实践》一书中,作者一共提及了11条面向对象的设计原则。它们分别是:
■SRP单一职责原则:就一个类而言,应该仅有一个引起它变化的原因。
■OCP开放-封闭原则:软件实体(类、模块、函数等)应该是可以扩展的,但是不可修改。(https://www.xing528.com)
■LSP Liskov替换原则:子类型必须能够替换掉它们的基类型。
■DIP依赖倒置原则:抽象不依赖于细节。细节应该依赖于抽象。
■ISP接口隔离原则:不应该强迫客户依赖于他们不用的方法,接口属于客户,不属于它所在的类层次结构。
■REP重用发布等价原则:重用的粒度就是发布的粒度。
■CCP共同封闭原则:包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包产生影响,则将对该包中的所有类产生影响,而对于其他的包不造成任何影响。
■CRP共同重用原则:一个包中的所有类应该是共同重用的。如果重用了包中的一个类,那么就要重用包中所有的类。
■ADP无环依赖原则:在包的依赖关系图中不允许存在环。
■SDP稳定依赖原则:朝着稳定的方向进行依赖。
■SAP稳定抽象原则:包的抽象程度应该和其稳定程度一致。
老实讲,这些原则并不好记。在过去很多年里,我曾经一直试图记忆它们,但总是记不全。
后来在逐步实践过程中,我个人感觉也许对这些原则可以有更精练的表示,当以降低复杂度为前提对设计和编码进行推演时,似乎找到了一个答案。
事实上,支持单一职责原则,开放-封闭原则,Liskov替换原则,接口隔离原则,依赖倒置原则,共同封闭原则,共同重用原则的要求的是同样的东西,这就是我们在这里说的“概念正交分解”、“层次适度”和“信息隐藏”。反过来讲,一旦概念是正交的,层次适度,信息隐藏适度,那基本上可以满足上述这些原则。
但反过来讲则不成立,满足了面向对象的设计原则,很多时候不能满足层次适度的要求。
而重用发布等价原则很特别,它是要求一种一般设计之外的东西,我们这里并没有覆盖。
无环依赖原则,稳定依赖原则,稳定抽象原则更类似于一种现象,但眼下收集的证据不够,无法对其原因是否就是违反了“概念正交分解”,“层次适度”和“信息隐藏”进行判断,也只能暂且搁置。
同时在谈及各种原则的时候,Robert C·Martin并没有探讨代价。这也许是因为作者认为遵守这些原则是稳赚不赔的。
这也许和对软件质量属性的认知有一定关系。在本书中,我们认为实现需求之外,唯一衡量设计编码好坏的标准是代码的简单性(复杂度最低)。而灵活性这样的质量属性,是要考虑,但优先级略低。所以我们认为原则的导入必须同时考虑投入产出,即对复杂度的影响。
面向对象原则,更多考虑的是灵活性(Robert C·Martin把僵化性排为不良设计的第一项),即靠增加复杂度来增加灵活性,而我们这里则是考虑的简单性。
这确实是两个不同的方向,增加层次,诚然可以使软件灵活,但复杂度必然增加,很多时候这很可能是在扩散需求的边界。
也许正是因为这种视角上的不同,导致了在上述面向对象设计原则中看不到与时序相关的内容。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。
