首先谈谈模板式设计,我相信模板对于每一位开发人员和设计人员来说都是非常好的东西,因为它可以“快速”构建出“成熟”的代码、结构或UI。“拿来主义”在业界盛极不衰,对于架构师而言模板也有这种功效,在设计的过程中我们会经常遇到很多必须而不重要的“鸡肋”模块,没有它们系统会变得不完整,而它们的存在并不能为系统增加任何的“特色功能”,如:用户管理、角色管理或系统设置等。常见做法是,直接采用第三方模块或是从已有的其它项目中复用类似的模块,你是这样的吗 ?至少我是经常这样做的,因为我们的中国式项目通常是“验收驱动”,能通过验收、成熟可用就好。如果整个项目都只是由各类模板化的模块所构成,那么这个项目其实不需要架构师,因为不存在任何设计,所有的工作只是一种“融合”(Fusion)。可能这样说会有很多人会吐槽说这是一种“资源整合”能力,从“赶项目”的角度来说这无可口非,但从技术含量与本质上说确实不存在任何设计成分,这类拼装性或是“复制”性的项目只需要项目经理配备几个高级程序员就能完成了。
我曾在“表达思维与驾驭方法论”一文中提到与销售的沟通方法,其中就有一条:“至少说出系统的三个特色”,这个表述对销售具有市场意义以外 , 其实对于架构师是起到一个重要的提醒作用同时也是在建立一种设计原则:
架构设计中模板的拼装是不可避免的,重要的是必须加入属于你的特色设计
很难有人记得住整个软件的设计师,而却很容易记住某项极具特色功能的设计者。“特色” 是架构师在软件中所留下的一种重要的印记,也是在团队中配备架构师的意义所在。设计出完全可被模板化重用的设计是一功力,而当中小型企业内出现这样的设计之日就是架构师离开企业之时,或许这也是当下中国架构师之殇。保持特色保住饭碗,你懂的。
唯一不变的就是变化本身 — Jerry Marktos《人月神话》
不变只是愿望,变化才是永恒 — Swift
变化是表像,不稳定且可定制的;本质是核心,必须稳定,可扩展而不可修改;被固定的变化则可纳入核心。
永远不要投资未来,绝不设计没有回报的功能
不知道你是否拥有类似的经历:
衡量标准的尺子掌握在架构师手中,如果设计中出现林林总总的这些“未来功能”您会如何来对待呢 ?是直接砍掉还是将其包装成为“特色”呢 ?此时架构师不单单是需要作为一名技术人员的角度考虑这个功能是否在将来可用,而更多的是需要考虑“成本”。每个功能甚至每行代码都需要付出“人-月”成本,一旦成本失控,软件就会化身“人狼”吞掉你的项目,而最后也只能后悔没有找到“银弹”。每个“未来”功能如何不能对现有项目带来即时性的回报,必须砍掉!即使这个功能有如何的美妙、高深或是在将来具有非凡的意义,还是将它放入“研究室”成为其它项目的技术储备吧。站在商人的立场:每一分钱的成本投入,都需要有足够的利益回报。
未来永远是美好的、丰满的同时也是浮云,而现实却往往是充满骨感。在架构或代码中透支未来极少数可获得回报,因为这些“投资”都具有不可预见性只是一些尝试,在产品中除了“市场策略”需要外的这类过分投资就得有陷入“维护未来”的心理觉悟。新的功能、未来的特色更应该收集起来,作为一下版本中可选项,通过详细的市场研究再考虑加入到产品中。当然,对于大型软件企业这个原则基本上是多余的,因为很多成熟的软件企业对需求的控制极其严格与规范。但如果你所在的企业还没有这样的管理意识,或具有超脱性的设计自由,那么这条原则是非常重要的,我们是用代码换钱的人,更少的代码换更多的钱才是我们最基本的生存需要。
在没有代码的时候就应该重构,重构是写出优雅代码的方法而不单纯是修改代码的理论。
骆驼与帐篷的故事
在风沙弥漫的大沙漠,骆驼在四处寻找温暖的家。后来它终于找到一顶帐篷,可是,帐篷是别人的(也许你的处境跟它一样)!
最初,骆驼哀求说,主人,我的头都冻僵了,让我把头伸进来缓和暖和吧!主人可怜它,答应了。过了一阵子,骆驼又说,主人,我的肩膀都冻麻了,让我再进来一点吧!主人可怜它,又答应了。接着,骆驼不断的提出要求,想把整个身体都放进来。
主人有点犹豫,一方面,他害怕骆驼粗大的鼻孔;另一方面,外面的风沙那么大,他好像也需要这样一位伙伴,和他共同抵御风寒和危险。于是,他有些无奈地背转身去,给骆驼腾出更多的位子。等到骆驼完全精神并可以掌握帐篷的控制权的时候,它很不耐烦地说,主人,这顶帐篷是如此狭小以致连我转身都很困难,你就给我出去吧
这是一个很有寓意故事,如果将其比喻为开发过程也很有意思。对于“发臭”甚至“腐烂”代码我们会马上说“重构”,但重构是否能解决一切问题 ?你是否试过重构失败呢 ?重构在什么情况下是不可用的呢 ?如果这些问题在你心中是没有准确答案的话, 我建议可以重新去阅读一次《代码重构》一书。我认为重构不单纯是一种开发期与代码回顾期所使用的方法,而是一种设计与编码的思想指导!在设计期就应运用重构中的原则,那是否就可以“防腐”呢 ?答案显然是确定的。重构的往往不单纯是代码,而是开发人员、设计人员的思想,不执行甚至没有代码规范、随意命名、随意复制/粘贴、随意调用这些都必须被杜绝。我并不是指在设计重构就不需要重构,只是这样做的意义可以大量减少由于发现“臭”代码而去重构的成本 。
这也可以说是一个团队性的开发原则,在项目之始就得有统一的编码规范(直接使用官方规范),并将重构中的基本代码重构方法也纳入规范中,在开发过程中强制执行规范,对任何可能“腐化”的代码绝对的“零”容忍,痛苦只是一时,但好处却是长久的。
开放封闭原则又称 开-闭原则 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#的方法参数的种类。
在C#中我们则需要从“注入”这方面来思考和充分发挥语言自身的特性,以达到简化代码,增强易读性的效果。 这里谈的“注入”主要指两个方面,一 是 “代码注入”,二是 “类型注入”。
“代码注入”就是向方法传入“代理”类就是在方法内部开辟出某一“可扩展”的部分以执行未知、可变的功能 ,那么我们就可以对相对“封闭”的方法增强其“开放”性。
通过泛型方法的使用,我们可以在对类型“开放”的情况下对类型的通用操作相对地“封闭”起来,这样可以在很大程度上利用泛型复合取代类继承,降低类的多态耦合度。
凡是基类适用的地方,子类一定适用
要依赖抽象,不要依赖具体。
使用多个专门的接口比适用单一的接口要好
要尽量使用合成/聚合 ,尽量不要使用继承
只有当以下Coad条件全部被满足时,才应当使用继承关系:
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- Items{get;set;} public Computer(ComputerFactory factory) { this.Items.Add(factory.Keyboard()); this.Items.Add(factory.Mouse()); this.Items.Add(factory.Monitor()); } }
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 ListGetOrder() { using (var ctx=new DbContext( ) { //… } } }
当这个new 在代码各处出现时就坏菜了!构造引用的耦合度每调用一次就增加一分,当遍布整个访问层甚至系统时 DBContext就是一个不可变更的超巨型耦合肿瘤!要解决这个问题可以采用单件模式自构造或是用IoC、DI将构造移到一个集中的地方,防止构造耦合散播。