架构师修炼 III - 掌握设计原则

关于软件的设计原则有很多,对于设计原则的掌握、理解、实践及升华是架构师的一项极为之必要的修炼。 记得在12年前第一次阅读《敏捷开发》时,五大基本设计原则就深深地植入到我的脑海中一直影响至今,我也由此获益良多。设计原则当然不止只有五种,最主要的面向对象的设计原则有以下这些:
 
  • 单一职责原则 (SRP) - 就一个类而言,应该仅有一个引起它变化的原因
  • 开-闭原则 (OCP)- 软件实体(类,模块,函数等)应该是可以扩展的,但是不可以修改
  • 里氏替换原则 (LSP)- 子类必须能够替换它们的基类型
  • 依赖倒置原则 (DIP)- 抽象不应该依赖于细节。细节应该依赖于抽象。
  • 接口隔离原则 (ISP)- 不应该强迫客户依赖于它们不用的方法。接口属于客户,不属于它所在的类层次结构。
  • 重用发布等阶原则 (REP)- 重用的粒度就是发布的粒度。
  • 共同封闭原则 (CCP)- 包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包产生影响,则将对该包中的所有类产生影响,而对于其他的包不造成影响。
  • 共同重用原则(CRP)-  一个包中所有类应该是共同重用的。如果重用了包中的一个类,那么就要重用包中的所有类。
  • 无环依赖原则(ADP)- 在包的依赖关系图中不允许存在环。
  • 稳定依赖原则 (SDP)- 朝着稳定的方向进行依赖。
  • 稳定抽象原则(SAP)- 包的抽象程度应该和其稳定程度一致。
  • 合成/聚合 复用原则(CARP)- 要尽量使用合成/聚合 ,尽量不要使用继承
  •  …..
 
当然面向对象的设计原则远远不止这些,设计原则是伴随着开发语言的发展应用和软件开发经验的累加总结得出的经验汇总,随着语言的演变、开发方法的进步还会不断地衍生和进化出更多的的设计原则。应用设计原则可以避开很多的设计中的陷阱与误区,但在应用设计原则的同时需要紧记一点:设计原则本质上是一些经验条框,是设计的导盲杖而不要让它们成为束缚设计思想的牢笼。
 
每个架构师在经历长期的实践后也会慢慢建立属于自己的设计原则。多年来我也总结出了一些设计原则,并将上面这些这种可用于代码设计的原则归纳为:“代码设计原则”,另外一些应用于意识与设计过程中的原则称为“意识-行为原则”。以下我将会分别讲述我对这些设计原则的理解与运用的经验。
 

意识 - 行为原则

 
  意识决定行为,很多的设计失误并不单纯源自于对设计原则的把握不足,而更多可能源自于架构师在意识指导上的错误, 所以在开始设计之前应该先建立正确的思想与意识引导。以下的这些意识-行为原则是我从很多次的跌倒中总结出的一些心得,将期作为原则是为了时刻引导自己不会在类似问题中犯错。

坚持创新原则

  首先谈谈模板式设计,我相信模板对于每一位开发人员和设计人员来说都是非常好的东西,因为它可以“快速”构建出“成熟”的代码、结构或UI。“拿来主义”在业界盛极不衰,对于架构师而言模板也有这种功效,在设计的过程中我们会经常遇到很多必须而不重要的“鸡肋”模块,没有它们系统会变得不完整,而它们的存在并不能为系统增加任何的“特色功能”,如:用户管理、角色管理或系统设置等。常见做法是,直接采用第三方模块或是从已有的其它项目中复用类似的模块,你是这样的吗 ?至少我是经常这样做的,因为我们的中国式项目通常是“验收驱动”,能通过验收、成熟可用就好。如果整个项目都只是由各类模板化的模块所构成,那么这个项目其实不需要架构师,因为不存在任何设计,所有的工作只是一种“融合”(Fusion)。可能这样说会有很多人会吐槽说这是一种“资源整合”能力,从“赶项目”的角度来说这无可口非,但从技术含量与本质上说确实不存在任何设计成分,这类拼装性或是“复制”性的项目只需要项目经理配备几个高级程序员就能完成了。

我曾在“表达思维与驾驭方法论”一文中提到与销售的沟通方法,其中就有一条:“至少说出系统的三个特色”,这个表述对销售具有市场意义以外 , 其实对于架构师是起到一个重要的提醒作用同时也是在建立一种设计原则:

  架构设计中模板的拼装是不可避免的,重要的是必须加入属于你的特色设计

很难有人记得住整个软件的设计师,而却很容易记住某项极具特色功能的设计者。“特色” 是架构师在软件中所留下的一种重要的印记,也是在团队中配备架构师的意义所在。设计出完全可被模板化重用的设计是一功力,而当中小型企业内出现这样的设计之日就是架构师离开企业之时,或许这也是当下中国架构师之殇。保持特色保住饭碗,你懂的。

 

固守本质原则 

  唯一不变的就是变化本身 —  Jerry Marktos《人月神话》
  不变只是愿望,变化才是永恒 —  Swift
 
看到这两句经典是不是猜到我想就“变化”二字来一次老生常谈 ?其实不然,这两个字在业内估计也讨论了20多年了,也说烂了。我之所以引用这两位大师的名言只是想无时无刻提醒自己要了解身边的每一个变化,对他们的源头产生兴趣,从而深入了解。世界上不会有无缘无故的爱,也没有无缘无故的恨一切皆有根源,那是 “本质”。我们来将 “本质” 与 “变化” 这两个哲学性的问题应用到软件开发的范畴内来看一个软件产品的迭代:
  • 用户的需求在变 - 他们需要增加更多的功能,要求更高质量的用户体验。
  • 代码在变 - 不断的重构、测试,持续集成,让代码变得容读,稳定。
  • 老板的想法在变 - 因为市场需求在变,需要为软件加入更多的特色满足市场。
  • 架构在变 - 采用更新式的技术体系,获得更高效的生产力,更为稳定、安全的运行环境。
 
而唯一不变的是:软件的核心。正如:Windows 变了N个版本最后还是操作平台,Office 衍生了多代后若然在处理文档文件 。
 
  变化是表像,不稳定且可定制的;本质是核心,必须稳定,可扩展而不可修改;被固定的变化则可纳入核心。
 
  架构应从本质入手,一切复杂的事物都应可被分解为简单的原理和构成,本质之外的内容皆可变化。我们来举例说明,设计一个电子商务网站,其核心就可被分解为 “购物车” 与 “订单状态跟踪”这是不可变的除非大众的整体购物行为发生了本质上的改变,为了增加用户体验我们选用美观舒适的界面套件如BootStrap,如果进一步提升用户体验则可以采用SPA的架构让客户在Web上获得Native式的使用体验;为了让用户使用不同的支付方式,我们就需要定义支付网关接口(引入变化)支持已有的支付平台,也为将来“可能”出现的支付平台留有扩展。为了增强网站对用户的粘性,我们就需要增加社区模块,并采用云存储或是其它的BigData技术以支撑大数据量的运转;....  最后,一切的本质仍然不变,电商网站,变的是扩展性、易用性、伸缩性等等。架构师可以向其中添加的功能太多太多,但必须固守本质才能让整个产品不会成为一个由高技术打造出来的怪物,在增加新功能时参考 “代码商人”原则的指引。
 

“代码商人” 原则

永远不要投资未来,绝不设计没有回报的功能

不知道你是否拥有类似的经历:

  • 在与客户的交流中,你的老板和经理在不断地向客户描绘“未来”图景,而在其中包含了很多几乎是客户没有需要的特色 ?
  • 在你设计整体架构时,有一种冲动让你很想将某项由灵感触发对于系统“将来”的扩展需要很有用的功能或模块加入其中呢 ?
  • 在你的代码里面有多少个方法或类是可以被删除,但你认为他们可以用于“以后”扩展而“手下留码”的呢 ?
  • 你是否曾与经理或项目组长为了是否增加某个很有可能成为特色且被你实现出来的功能争论不休呢 ?

  衡量标准的尺子掌握在架构师手中,如果设计中出现林林总总的这些“未来功能”您会如何来对待呢 ?是直接砍掉还是将其包装成为“特色”呢 ?此时架构师不单单是需要作为一名技术人员的角度考虑这个功能是否在将来可用,而更多的是需要考虑“成本”。每个功能甚至每行代码都需要付出“人-月”成本,一旦成本失控,软件就会化身“人狼”吞掉你的项目,而最后也只能后悔没有找到“银弹”。每个“未来”功能如何不能对现有项目带来即时性的回报,必须砍掉!即使这个功能有如何的美妙、高深或是在将来具有非凡的意义,还是将它放入“研究室”成为其它项目的技术储备吧。站在商人的立场:每一分钱的成本投入,都需要有足够的利益回报

  未来永远是美好的、丰满的同时也是浮云,而现实却往往是充满骨感。在架构或代码中透支未来极少数可获得回报,因为这些“投资”都具有不可预见性只是一些尝试,在产品中除了“市场策略”需要外的这类过分投资就得有陷入“维护未来”的心理觉悟。新的功能、未来的特色更应该收集起来,作为一下版本中可选项,通过详细的市场研究再考虑加入到产品中。当然,对于大型软件企业这个原则基本上是多余的,因为很多成熟的软件企业对需求的控制极其严格与规范。但如果你所在的企业还没有这样的管理意识,或具有超脱性的设计自由,那么这条原则是非常重要的,我们是用代码换钱的人,更少的代码换更多的钱才是我们最基本的生存需要。

 

重构优先原则

在没有代码的时候就应该重构,重构是写出优雅代码的方法而不单纯是修改代码的理论。

骆驼与帐篷的故事

  在风沙弥漫的大沙漠,骆驼在四处寻找温暖的家。后来它终于找到一顶帐篷,可是,帐篷是别人的(也许你的处境跟它一样)! 

  最初,骆驼哀求说,主人,我的头都冻僵了,让我把头伸进来缓和暖和吧!主人可怜它,答应了。过了一阵子,骆驼又说,主人,我的肩膀都冻麻了,让我再进来一点吧!主人可怜它,又答应了。接着,骆驼不断的提出要求,想把整个身体都放进来。 

  主人有点犹豫,一方面,他害怕骆驼粗大的鼻孔;另一方面,外面的风沙那么大,他好像也需要这样一位伙伴,和他共同抵御风寒和危险。于是,他有些无奈地背转身去,给骆驼腾出更多的位子。等到骆驼完全精神并可以掌握帐篷的控制权的时候,它很不耐烦地说,主人,这顶帐篷是如此狭小以致连我转身都很困难,你就给我出去吧

  这是一个很有寓意故事,如果将其比喻为开发过程也很有意思。对于“发臭”甚至“腐烂”代码我们会马上说“重构”,但重构是否能解决一切问题 ?你是否试过重构失败呢 ?重构在什么情况下是不可用的呢 ?如果这些问题在你心中是没有准确答案的话, 我建议可以重新去阅读一次《代码重构》一书。我认为重构不单纯是一种开发期与代码回顾期所使用的方法,而是一种设计与编码的思想指导!在设计期就应运用重构中的原则,那是否就可以“防腐”呢 ?答案显然是确定的。重构的往往不单纯是代码,而是开发人员、设计人员的思想,不执行甚至没有代码规范、随意命名、随意复制/粘贴、随意调用这些都必须被杜绝。我并不是指在设计重构就不需要重构,只是这样做的意义可以大量减少由于发现“臭”代码而去重构的成本 。

  这也可以说是一个团队性的开发原则,在项目之始就得有统一的编码规范(直接使用官方规范),并将重构中的基本代码重构方法也纳入规范中,在开发过程中强制执行规范,对任何可能“腐化”的代码绝对的“零”容忍,痛苦只是一时,但好处却是长久的。

 

代码设计原则

 

开放-封闭原则 

开放封闭原则又称 开-闭原则 Open-Closed Principle (OCP) 

软件实体(如类,模块,函数等)应该是可以扩展的,但是不可以修改。

  OCP是一个极为之出名的设计原则,简单的一句话就概括了可时该“开放”可时该“封闭”。这句话看起来很简单,一看似乎也会觉得自己领悟了什么,仔细咀嚼却觉得内中深意无限,到底应怎样理解这句话且将其应用于设计中呢 ? 我参考了不少国内的资料对此原则的总结,感觉就是雾里看花,没有办法找到最为贴切的解释。

我想分几个方面来诠释这个原则:

从类设计的角度

  在类设计的应用中开-闭原则是一种对类的“多态”控制原则。开闭原则在基类或超类的设计中由为重要, 可以简单地理为对 成员对象的作用域 和 可“重载”成员 的控制指引原则。按 “里氏替换原则” 基类成员通常对于子类都应该可见,也就是说基类成员的作用域的最小作用范围应该是 protect , 如果出现大量的 private 成员时就应该考虑将private 成员们分离成其它的类,因为些成员都不适用于其子代而违反了“替换原则”,而更适用“合成/聚合原则“。

  在运用 virtual 关键字时需甚重考虑,除了针对某些特殊的设计模式如 ”装饰“模式需要大量 virtual 的支持以外,在没有必要的情况下尽量避免。定义可重写的成员为子类预留了”改变行为“的余地,但同时也是为子类违反”替换原则“埋下了地雷。当子类中出现大量重写成员的时候就得考虑该子类是否还应该继承于此类族,因为子类在大量地违反”替换原则“时就意味着它满足了被分离出类族的条件。同理,在C#内一但需要在子类内部实现基类接口时也需要作出同样的考虑。

 

 注:里氏替换原则是开-闭原则的一种重要补充,在类设计中一般是同时使用。

 

从模块设计的角度

  模块设计的“开-闭原则”是侧重于对接口的控制。而这个在整个架构中也尤为重要,因为模块间的“开-闭”是直接影响系统级的耦合度。模块间的开闭需要“衡量成本”,并不是将所有的细节都开放使用模块具有极强的可扩展性就会有很高的重用度。首先要看了解几点:

开放性与维护成本成正比关系

  接口的开放必须带有使用说明,这会增加团队开放的沟通成本同时一但接口发生改变将可能带来额外的“说明性重构”成本。在某些情况下我们很容易被“高扩展性”所引诱将很多“可能”被复用的功能通过扩展接口暴露出来。当这种高扩展性的诱惑主导了设计师的思维,随着模块的增多项目的变大、慢慢地设计师就会进入自己所创建的“注释恶梦”中。

 

开放性与耦合度成正比关系

  模块的开放性接口是具有耦合传导效应的,控制模块间的耦合度就能在很大程度上控制了系统的耦合度。模块间的依赖性越小,耦合度越低才更易于变化尽量将耦合度集中在某一两个模块中(如:Facade 模式),而不是分散在各模块间。耦合度高的模块自然而然地成为“核心”模块,而其实的“外部”模块则需要保持自身的封闭性,这样的设计就很多容易适对未知的变化。

 

由这两个正比关系结合对实现成本的控制上我们做出两个最为简单可行的推论:

推论1:“正常情况下请保持封闭,没有必要的情况下绝不开放”。

推论2:“集中开放性,让模块间保持陌生”

 

开-闭原则从理论上来谈会有很多内容,但实现起来却很简单, 就以C#为例控制模块开放性的最简单办法就是控制作用域:internal , public。

 

3.从函数/方法设计的角度

我为认为OCP用到极至的情况就是应用于方法级,众所周知:参数越少的方法越好用。开-闭原则可以简单地理解为参数的多寡与返会值的控制

 

在此我更想谈谈“开-闭原则”在C#中的应用。首先在方法设计上,C# 给了设计人员与开发人员一个极大的空间,到了4.5我们甚至可以使用async 方法来简单控异步方法,那么先来总结一下C#的方法参数的种类。

  • 固定参数:public void methodName(string a, out b, ref c);
  • 动态参数:public void methodName(string a, string b=“defautlString”) 
  • 可变参数:public void methodName(params string[] a);
  • 表达式参数(方法注入):public void methodName(Func<string> func, Action act);
  • 泛型参数:public void methodName<T>( T a) where a : class;

在C#中我们则需要从“注入”这方面来思考和充分发挥语言自身的特性,以达到简化代码,增强易读性的效果。 这里谈的“注入”主要指两个方面,一 是 “代码注入”,二是 “类型注入”。

 

“代码注入”就是向方法传入“代理”类就是在方法内部开辟出某一“可扩展”的部分以执行未知、可变的功能 ,那么我们就可以对相对“封闭”的方法增强其“开放”性。

通过泛型方法的使用,我们可以在对类型“开放”的情况下对类型的通用操作相对地“封闭”起来,这样可以在很大程度上利用泛型复合取代类继承,降低类的多态耦合度。

 

里氏替换原则(LSP) 

  凡是基类适用的地方,子类一定适用
 
里氏代换原则 (Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
 
在前文”开-闭原则“关于类设计应用部分已经基本叙述过”替换原则“的用法。 这个原则,我一直是反向理解的,这样就非常容易运用,我是这样使用的:
  • 凡是出现大量子类不适用的成员,子类就应该脱离继承关系
  • 基类中凡是出现大量虚成员,该类就失去成为基类的条件
 
 
 

依赖倒转原则(DIP) 

   要依赖抽象,不要依赖具体。
 
  DIP 就像LSP一样,原文与译文其实都非常坑爹,这里我就不直接引入原文了,因为我希望每个读这篇文章的朋友都能理解并应用这些原则而不是在玩文字游戏。DIP 用最为简单的表述就是:“面向接口编程”。子类可以引用父类方法或成员,而父类则绝对不能调用任何的子类方法或成员。一但上层类调的方法调用了子类的方法就会形成依赖环,一般上编译器会“放过”依赖环认为这不属于逻辑错误,但具有依赖环的类结构是无法序列化的(在C#中会直接抛出环状引用的异常)。
 
通俗点:“规矩是祖宗定的,子孙只能执行和完善”,用这个口决就可以完全掌握此原则 。
在过去(10年前)开发工具还比较落后,这是原则十分重要,而如今可以借助VS.net去找到出这种设计错误,也可以直接使用IoC 和 DI 就会自然而充分地尊守此原则 。
  

接口隔离原则 (ISP)  

  使用多个专门的接口比适用单一的接口要好
 
  架构师在逻辑世界就是神,设计软件的过程就是创造逻辑世界,每一个接口就是这个世界中的一种规则,类则是实现规则的做法,实例就是执行规则的人。 在实现工作中,我们会经常遇到这样的现象:一个PM可能同时在跟进好几个项目,或是一个PM要同时充当架构师、PM、程序员甚至售前的角色,这些苦B们是公司内最累的人,同时也是失败率最高的群体,为什么? 答案显而易见:人的精力是有限的,专注于某一件事才能真正有成果。同理,在逻辑世界也是一样的,当接口要承载多种的任务,被众多不同的类所调用时就会出现“接口过载”或者”接口污染“,实现这些接口的类将会产生很高的耦合度,从而代码会变得难以阅读,难以理解,也难以变化。分离接口就是隔离了客户(接口的使用者),隔离客户就自然降低耦合度。
 
  一个完美的世界就应该是专人专项,让擅长的人做其擅长的事,在现实不可能但逻辑世界却可以。那么在设计中如何来把握这种原则呢 ?很简单,当一个接口上的方法被多个类调用时就要警觉了,如果这些方法间没有依赖关系,甚至是不同类别(在做不同的事)的方法那么就得考虑使用ISP原则将接口分离成两个独立的接口,使接口的耦合度从1..n 降低至 1..1. 
 
 

合成/聚合 复用原则(CARP)

  要尽量使用合成/聚合 ,尽量不要使用继承
 
  复用原则是一个很容易被忽略而又极其重要的原则,这个原则具有非常深远的架构意义。对于小型项目(类库规模小)即使违反此原则也不会带来什么危害,但当构建大规模的类库(数百甚至数千个类)时,这个原则就可以防止出现“继承失控”、过度膨胀、无法重构等的风险,也决定了整个结构的可重用性和可维护性。在定义中它只是一句简单的话,但从“继承”、“合成”与“聚合”就引出了一系列的内容,涵盖多种设计模式和附带多个更小层级的应用原则。
 
(注:关于合成/聚合的好处请去百度吧,关于“白箱复用”与“黑箱复用”都被转烂了)
 
 首先要正确的选择合成/复用和继承,必须透彻地理解里氏替换原则和Coad法则。Coad法则由Peter Coad提出,总结了一些什么时候使用继承作为复用工具的条件。 
 
Coad法则:

只有当以下Coad条件全部被满足时,才应当使用继承关系:

  1. 子类是超类的一个特殊种类,而不是超类的一个角色。区分“Has-A”和“Is-A”。只有“Is-A”关系才符合继承关系,“Has-A”关系应当用聚合来描述。 
    • “Is-A”代表一个类是另外一个类的一种;
    • “Has-A”代表一个类是另外一个类的一个角色,而不是另外一个类的特殊种类。 
  2. 永远不会出现需要将子类换成另外一个类的子类的情况。如果不能肯定将来是否会变成另外一个子类的话,就不要使用继承。 
  3. 子类具有扩展超类的责任,而不是具有置换掉(override)或注销掉(Nullify)超类的责任。如果一个子类需要大量的置换掉超类的行为,那么这个类就不应该是这个超类的子类。 (注:在C# 中 含有 new 的方法、属性和内部实现单一基类接口就相当于Nullify)
  4. 只有在分类学角度上有意义时,才可以使用继承。不要从工具类继承。 
 
对于一个老手Coad法则只是一种总结,很容易理解与运用,但如果你是一个架构新手Coad法则就很是坑爹(我的理解力很低,当年我就被坑了很久)!所以我想另辟蹊径从其它角度来尝试解释这个原则。
 
继承控制
继承是面向对象的一种重要构型,复用原则只告诉我们“尽量不使用继承”而不是将继承魔鬼化,在很多场景下,小结构继承是非常常见与易读的。只是,我们需要了解继承的子代的增加是以整个类结构的复杂度增加n次方在递增,随着子代层级的增多“类家族”结构的变化就越来越难。其实,我们可以找一些自已手上的例子来看看,如果有3代以上继承关系的类,看看最小的子孙类与基类之间是否已经有点“面目全非”?这一点与人类的繁衍与继承是很类似的。再深入一点就是如果向最顶层的基类进行扩展,是则能完全适用“替换原则”呢 ?更改高层级结构时是否有“挥舞大刀”般的沉重感 ? 对是否有勇气对稳定的祖代类重构 ?
 
推论:“尽可能避免出现三代以外的继承关系,否则应考虑合成/聚合”
 
 
“合成”与“聚合”从字面意义上去理解是我一直以来都无法正确理解的内容。可能是我语文水平实在太低的缘故吧,对 Composite 和 Aggregation 两个单词我反而能在维基百科上找到准确的定义。
 

合成  ( Composite )   - 值聚合 (Aggregation by value)

我的通俗定义:合成的过程是在类的构造过程中(构造函数或外部的构造方法)在运行期将值或其它类实例组装到合成类内(通过变量或属性Hold住)
 
如:
public class Keyboard{}
public class Mouse{}
public class Monitor{}
 
public class Computer
{
    private Keyboard keyboard;
    private Mouse mouse;
    private Monitor monitor;
 
    public Computer() 
    {
         this.keyboard=new Keyboard();
         this.mouse=new Mouse();
         this.monitor=new Monitor();
    }
}

由这个例子可见,所谓的“值(Value)”通过构造函数合成为 “Computer”的内部成员,有如将各个功能单一的部件装配成为一个功能强大的产品。所有的依赖都被“关在”构造函数内,如果将依赖外置就可以运用工厂(Factory Pattern)和合成模式(Composite Pattern)进行演变。

public class Item{};
 
public class Keyboard:Item{}
public class Mouse:Item {}
public class Monitor:Item{}
public ComputerFactory 
{
   public Item Keyboard() { return new Keyboard(); }
   public Item Monitor() { return new Monitor(); }
   public Item Mouse() { return new Mouse(); }
}
 
public class Computer
{
    public List<Item> Items{get;set;}
    
    public Computer(ComputerFactory factory) 
    {
        this.Items.Add(factory.Keyboard());
        this.Items.Add(factory.Mouse());
        this.Items.Add(factory.Monitor()); 
    }
} 
通过简单的演变,就可以将Computer 1-3的耦合变成 1-1 的耦合,所有的依赖都集中到ComputerFactory上,只需要继承ComputerFactory创建更多的工厂传入Computer类就可以生产出各种各样的Computer实例,而无需更改Computer的任何代码,这就是所谓的“黑箱复用”。
 
思考:试试用Builder模式改写上面的例子,会有不同的效果。
 
这里只是有3个部件,但如果将部件变成30个或者更多时改变的也只是 “合成的构造者” ,应对再复杂的场景:树型合成结构也只是将构造者演变为递归式构造。由此可见到“合成”原则的运行对大量类组合的强大之处。
 
 

聚合  ( Aggregation )  - 引用聚合(Aggregation by reference)

 
聚合在面向对象的实现上是一个极为简单的代码,说白了就是:对象属性。以上面第一个范例说明 (不继承Item基类)
 
public class Computer
{
   public Mouse Mouse{ get;set; }
   public Monitor Monitor{ get; set; }
   public Keyboard Keyboard {get;set;}
}
 
public class Host
{
    public static void Main()
    {
         var computer=new Computer()
         {
              Mouse=new Mouse(),
              Monitor=new Monitor(),
              KeyBoard=new KeyBoard()
         };
    }
}

  聚合类中Hold住的是实例化类的引用,不单是值。聚合类的意义在于将引用依赖集中一处,从某意义上说这个Computer类也是一个Facade 模式。这种方式常见于大规模对象模型的入口类,如Office的 Application 对象,这样的设计可以便于开发者“寻找”类的引用。同时也可以用作 上下文的设计 如:.net中的System.Web.HttpContext。值得注意的是:聚合类是需要慎用的,对于类本身是收敛类引用耦合,同时聚合类也具有耦合传导的特性,由其是构造函数。就拿EF说事吧,我们用EF访问数据库都需要这样的代码:

public void OrderManager
{
    public List<Order> GetOrder()
    {
        using (var ctx=new DbContext( )
        {
            //
        }
    }
}

当这个new 在代码各处出现时就坏菜了!构造引用的耦合度每调用一次就增加一分,当遍布整个访问层甚至系统时 DBContext就是一个不可变更的超巨型耦合肿瘤!要解决这个问题可以采用单件模式自构造或是用IoC、DI将构造移到一个集中的地方,防止构造耦合散播。 

小结

  如果你是一位.net 体系的开发人员,只要你打开vs.net的代码检查规则你就会发现一个新的世界,一个基于原则/规范 的世界,如果你的代码能80%地通过vs.net中最高级别的代码检查准则,那么事实上你的代码已经是非常优质。内中的每一条代码检查准则都值得我们细细地去品味与学习。
posted @ 2014-07-22 13:13  Ray Liang  阅读(7686)  评论(20编辑  收藏  举报