软件复杂性简洁之道 - 设计原则篇

 服务一类     |      2019-03-12 10:09

  软件之所以这么有魔力这么繁荣,在于软件的灵活性,也正因为软件的灵活性导致了软件○▲-•■□的复杂性。绳子灵活而方便,它能360度无死角花样系东西,但有时为了解开它,相信也没少让你烦心过。

  从我们学会什么是int什么是long时,老师就教会我们什么时候该用int什么时候适合用long。当我们知道什么是面向对象时,我们就一并了解到了抽象、封装、继承和多态。似乎我们在学习如何编写软件的同时,我们就一直伴随着如何设◁☆●•○△计软件。

  那为什么我们要设计软件?软件设计的最大目标,就是降低复杂性。那什么又叫复杂性?

  面向过程就可以实现所有的软件需求,但我们依然又衍生出了面向对象。软件设计囊括很多,命名、函数、规范、建模、设计模式、设计原则等等,这里我们重点聊一聊设计原则在降低复杂性上是如何表现的。

  斯坦福的Ousterhout教授指出,复杂性的来源主要有两个:代码的含义模糊和互相依赖。

  复杂性不仅在于它本身复杂,并且还会▪•★递增。如果做错了一个决定,导致后面的代码都基于前面的错误实现,只会越来越复杂。

  单一职责原则基于康威定律推论,核心思想是每个软件模块有且只有一个被更改的理由。职责过多,引起它变化的原因就◆●△▼●越多,将导致责任依赖增加耦合性

  单一职责让我们在应对变化时更从容,将复杂进行拆解,修改一个功能时,也可以显著降低对其他功能的影响。

  但“单一”其实也有着各种各样的争议,何谓“单一”?Privacy.clear叫单一吗,它专门负责隐私数据▲=○▼的去除,但或许隐私数据的去除又分很多种:个人信息、公司信息、行为信息。那我们再分为Privacy.Personal.clear?如同抽象一样,极度抽象=没有抽象。“单一”也要按我们所面临的需求与场景来看,软件工程没有银弹。

  对扩展开放,对修改关闭。如果有新的需求和变化可以对现有代码进行扩展,以适应新的情况.而不是对原有代码进行修改。

  记得老师给我们上的第一堂课就这么讲过,“软件工程的世界里,不存在没有bug的程序。修复旧bug的同时,就有引入新b◇=△▲ug的可能”。“为什么这个会有问题,我这几天都没怎么动它”,我们在日常生活中就有这样的概念,没碰的东西,出问题的概率应该会很小。开闭原则也是如此。

  在软件的生命周期内,因为变化,升级和维护等原因需要对软件原有代码进行修改,可能会给旧代码引入错误,也有可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。解决方案就是:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现。

  装饰者模式,策略模式等都很好的遵循了开闭原则。开闭原则的核心是通过合理的抽象,利用面向对象的多态特性,来完成对扩展开放,对修改关闭。而何时进•□▼◁▼行抽象、何时不需要抽象、如何抽象也是一个见仁见智的过程,不存在绝对的方法论。

  所有引用基类的地方必须能透明地使用其子类的对象。由Barbara Liskov在1987年在一次会议上名为“数据的抽象与层次”的演说中首先提出。

  有了抽象就一定对应着具象,抽象与具象则是继承与派生的关系。我们知道继承有很多好处,它帮我们提高了代码的复用性,但继承也是侵入的,继承意味着必须拥有父类的所有属性和方法,降低了代码的灵活性,有时会形成对子类的一种约束。不合理的继承与派生关系,不仅没有解决复杂性,反而可能会无端增加新的复杂性。

  很明显,这里birdLetGo的代码违反了开闭原则,代码中会充斥着各种instanceof。开闭原则的核心是基于多态、基于抽象,而里氏替换原则保障了这一点“所有引用基类的地方必须能透明地使用其子类的对象”。如果我们不能基于多态来构建我们的架构,那我们将没有架构,而父类此时如果不能安全放心的替换起子类,那我们的多态也将毫无意义。LSP是使OCP成为可能的主要原则之一,正是因为子类的可替换性,才使得父类模块无须修改的情况就得以扩展。

  说完复杂性治理,我们再谈谈复杂性隔离。既然我们都将复杂性治理了,为什么还存在隔离一说?用户在使用微信时,觉得很轻松,但微信的工程师们的工作可不轻松且很复杂。这就是复杂性隔离的一种。

  这段代码出自eger。很明显,这些代码是复杂的,不结合上下文, 不弄懂这些数字的含义,很难了解它的作用于价值。但我们在使用Integer的时候,似乎没有印象我们会用到这里的代码。这,就是复杂性隔离。

  译:如果能把复杂性隔离在一个模块,不与其他模块互动,就达到了消除复杂性的目的。

  模块分成接口和实现。接口要简单,实现可以复杂。好的class应该是小接口,大功能,糟糕的class是大接口,小功能。好的设计是,大量的功能隐藏在简单接口之下,对用户不可见,用户感觉不到这是一个复杂的class。最好的例子就是Unix的文件读写接口,只暴露了5个方法,就囊括了所有的读写行为。

  高层模块不应该依赖低层模块,二者都应该依赖其抽◆◁•象;抽象不应该依赖细节;细节应该依赖抽象。

  依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。

  依赖倒置告诉了我们模块与模块间正确的依赖方向,实现类是复杂的,对外而言我们不需要知道这些复杂性,如同上面unix-IO的例子。我们将复杂性隔离在实现类,通过抽象,建立新的依赖关系。由于抽象将高层和细节彼此隔离,所以代码也更容易维护。

  即使开放接口也不能随意开放,接口隔离原则认为“多个特定客户端接口要好于一个宽泛用途的接口”。不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。

  根据接口隔离原则拆分接口时,必须首先满足单一职责原则。ISP最大的好▪…□▷▷•处是可以将外部依赖减到最少,你只需要依赖你需要的东西,这样可以降低模块之间的耦合,达到复杂性治理的效果。

  上述所有的设计原则,都离不开抽象,而代码抽象也有三个原则(PS.突然想起这句话“如何得出方法论,也是有一套方法论的”)。不抽象、盲目抽象与随意抽象,都可能会让我们在解决旧的复杂度上,引入新的复杂度,甚至更★◇▽▼•甚。

  自 Andrew Hunt 《The Pragmatic Programmer》,特指在程序设计以及计算中避免重复代码,因为这样会降低灵活性、简洁性,并且可能导致代码之间的矛盾。

  系统的每一个功能都应该有唯一的实现。也就是说,如果多次遇到同样的问题,就应该抽象出一个共同的解决方法,不要重复开发同样的功能。

  YAGNI原则指的是你自以为有用的功能,实际上都是用不到的。因此,除了最核心的功能,其他功能一概不要部署,这样可以大大加快开发。

  但是,这里出现了△▪▲□△一个问题。仔细推敲的话,你会发现DRY原则和YAGNI原则并非完全兼容。前者追求抽象化,要求找到通用的解决方法;后者追求快和省,意味着不要把精力放在抽象化上面,因为很可能你不会需要它。所以,就有了第三个原◇•■★▼则。

  第一次用到某个功能时,你写一个特定的解决方法;第二次又用到的时候,你拷贝上一次的代码;第三次出现的时候,你才着手抽象化,写出通用的解决方法。三次原则是DRY原则和YAGNI原则的折衷,是代码冗余和开发成本的平衡点,值得我们在抽象化时遵循。

  可以看到,哪怕是这些大家,在面对抽象问题时,也没有一个非黑即白的答案。抽象★-●=▽•●◆•▽非常简单,因为哪怕是没学过软件工程的人同样会抽象。但抽▼▼▽●▽●象又很难,优秀的抽象模型与抽象经验,是很难从书本上快速获取的,不仅需要你有优秀的软件工程的知识,还需要你又贴近业务的理解与经验,甚至在某种程度上,属于哲学的一种。

  除了上述所说大名鼎鼎的SOLID原则、抽象原则等,还有很多优秀的设计原则。了解它们能帮助我们更有效率的编写代码,帮助我们成为一名更优秀的程序员。

  (Keep It Simple and Stupid ),有趣的是它并不是由软件工程师开创的,KISS原则最早由卡普兰在著名的平衡计分卡理论中提出。把事情变复杂很简单,把事情变简单很复杂。好的目标不是越复杂▼▲越好,反而是越简洁越好。这就是KISS原则。

  最容易的莫过于忙忙碌碌,最困难的莫过于卓有成效。化繁为简,善于将复杂的问题简明化、简单化,是防止忙乱、事半功倍的法宝。不能将问题简单化,迷惑于纷繁复杂的现象,甚至让复杂的问题更加复杂,只能陷在问题的泥潭里走不出来,结果工作忙乱被动,办事效率低下。只有善于把复杂的事物简明化,办事才能又快又好。

  Principle of least astonishment,最小惊讶原则通常是在用户界面方面引用,但同样适用于编写的代码。代码应该尽可能减少让读者惊喜。也就是说,你编•●写的代码只需按照项目的要求来编写。其他华丽的功能就不必了,以免弄巧成拙。

  Write Code for the Maintainer,代码维护原则阐述,一个优秀的代码,应当使本人或是他人在将来都能够对它继续编写或维护。代码维护时,或许本人会比较容易,但对他人却比较麻烦。因此你写的代码要尽可能保证他人能够容易维护。

  写下一行只要1分钟,但未来会被一代★▽•☆■▲…◇代工程师读很多次、改很多次。代码的可读性与可维护性,是我心目中好代码的第一标准。

  Avoid Premature Optimization,避免过早优化可能是很多程序员(优秀)的通病,如果有这样的需求,我的设计也可以怎样支撑,如果发生错误,我的代码有怎样的容错性等等。很明显只有优秀的程序员才会有这样的前瞻性与对代码的追求。但优化是永无止境的,我们的解决方案也得是要在解决问题的基础上产生。突然想起一句笑话“有的困难,解决困难,没有困难,创造困难也要解决困难”

  设计原则有很多,这里并没有一一罗列。其实细心的小伙伴可能发现,有些设计原则之间相互甚至是互斥的,其实世界不也一直就是这样的吗,兔子不吃窝边草,同样也有近水楼★△◁◁▽▼台先得月。世界本就是两面,软件设计的最大目标,就是降低复杂性,万物不为我所有,但万物皆为我用。追求好代码,追求优雅,追求卓越,追求简单。

乐彩论坛17500