架构设计的一些思考

从宏观上看,计算机系统整体上都是一种分层的架构设计,从网络的五层协议到分布式系统;从操作系统到应用软件。架构设计要解决的核心问题就是如何分层,层与层之间以及同层之前如何去交互的问题。在我们着手去做分层架构和通信设计之前,我们可能还需要一些设计理论和原则的支撑,历史一次次证明,如果只是全凭实践经验,我们是无法成功设计出一款好的产品的。(比如飞机的发明,飞行器古代就已经出现,但是现代意义上的飞机直到空气动力学理论出现后才由莱特兄弟设计制造出来,这样的例子在科技史上还有很多)这里我总结了三个部分:模块设计原则,设计模式的应用和重构原则。

模块设计原则

模块设计原则就是做架构的理论基础,只有熟练掌握以下这些原则,你在着手软件架构时才能得心应手,信手拈来,设计出更合理的架构,就好比有了九阳神功的张无忌再去修炼乾坤大挪移。

单一职能原则SRP

单一职能原则强调一个模块或者一个类只应该有一种行为,也只能对一种行为负责,比如负责UI展示的类,就不要在内部去处理业务逻辑,负责业务逻辑的类,则内部必要去修改UI布局等。这个原则在MVVM的模式中被广泛采用。

开闭原则OCP

开闭原则指类或者模块应该易于扩展,而难以修改。要遵守该原则,我们在设计模块时就要尽量设计“害羞”的代码,隐藏自己的大部分行为,只将必要的接口暴露给外部,并且尽量只暴露只读的接口,不要暴露能够修改内部属性的接口,这是难以修改部分,那易于扩展部分,我们应该预留适当的钩子接口,该钩子可以扩展类的行为,将该行为的实现交于外部去处理。

里斯替换原则LSP

里斯替换也是描述的接口扩展问题,但是和开闭描述的场景又不太一样,里斯替换更像工厂,一个接口的具体行为不依赖该类,而依赖其扩展类,该接口的行为可以很容易被其扩展类替换。

接口隔离原则ISP

接口隔离很重要,但是经常被忽视,只有正在懂的人才会在模块封装的过程中采用这个原则,大部分人则无视该原则的好处,只是为了代码写起来更方便。我们是封装系统库和三方库时,往往会使用该原则去对系统方法做一层隔离,但是大部分也仅仅只是做到了这一层,选择忽略更上层的情况。其实对更上层的业务实体来说,每一个实体都应该再加一层的隔离,这样做的利大于弊,模块的职能划分的越清晰,维护和扩展的成本就会越低。

依赖反转原则DIP

该原则主要解决模块间比较常见的相互依赖的问题,相互依赖的坏处这里不再多描述,依赖反转将双向或者多向的依赖关系梳理为单向的依赖关系,依赖反转的工具常常使用抽象接口去实现。

组件聚合原则

组件聚合原则主要介绍在做组件封装时类的归属问题,哪些类应该放入一个组件,哪些又不应该放入一个组件内,该原则主要包括以下三部分:

  • 复用/发布等同原则

    该原则指组件中的类和模块应该是可以共同发布,也能被其他组件共同复用的,即该组件中的类应该具有紧密的关系和共同的主题,而不是毫不相干的内容。

  • 共同闭包原则

    一个组件中的各类应该是会因为同一个行为而被一起修改的,如果有一些独善其身的类,那说明该类可能并不适合该组件,应该将其剥离该组件。

  • 共同复用原则

    一个组件中的类应该是可以被外部共同复用的,而不应该存在只需要复用一部分的情况,一个组件应该是不可再拆分的。

组件解耦原则

该原则主要介绍组件内部或者组件之间的关系。

  • 无依赖环原则,这个上面已经介绍过(DIP).
  • 稳定依赖原则:

    这里先介绍什么是模块的稳定性,一个模块被其他模块依赖称为入口依赖,该模块依赖其他模块称为出口依赖,入口依赖越多,该模块就越稳定。

    组件之间的依赖关系应该是从不稳定指向稳定方向的。这里已一个分层组件举例的话,越上层的组件,应该是越不稳定,因为它依赖了太多其他组件,越底层的组件,应该越稳定,因为它几乎只被其他组件依赖,分层组件的依赖关系应该是自顶向下的单向依赖。

  • 稳定抽象原则:

    抽象这个概念应该都知道,这里已接口举例,一个接口就是一个抽象方法。稳定抽象原则是指一个组件的稳定性应该和它的抽象性保持一致。

    这里又要解释一下抽象性的概念,组件中的抽象类和抽象方法 / 组件中的实现类和实现方法 = 组件的抽象性。其实很好理解,抽象类和抽象方法越多,表示该组件越抽象。

    既然稳定性和抽象性要保持一致,还是按上面的分层组件举例,就可以解释为:越是上层的组件应该越具体,越是底层的组件应该越抽象。

设计模式的应用

设计模式在代码中角色很奇妙,有的人可能根据自身经验采用了许多设计模式而不自知,其实设计模式本身也是从实践和经验中总结出来的一套代码设计的真理,了解并应用这些设计模式,在模块和架构设计过程中是必不可少的。

创建型

创建型设计模式这里重点介绍抽象工厂和工厂模式,工厂模式比较单一,一次只能生产一种对象,抽象工厂在工厂的基础上做了扩展,可同时生产多个对象。

考虑下面这种情况:

这个活动目前有5种类型,我们在展示UI时可以选择创建5种cell去分别适配,如果以后还有类型的扩展再新建cell,但是这种方案会造成子类爆炸,也贡献了大量的重复代码,更好的方案是新建一个类型的抽象工厂,抽象工厂提供变化的UI元素的生成接口(设计模式的核心就是对变化的概念进行抽象),具体生成逻辑放到工厂实体类进行。这样就只需要一个cell和一个抽象工厂实体即可完全展示,后续扩展也更方便。整体类结构大概长这样:

其他的创建型设计模式比如原型描述的是对象的复用(clone),单例则是对象的共享,生成器一般用在比较复杂的对象创建上,比如这个对象由很多子对象组成,如一个订单等。

结构型

结构型设计模式主要描述如何组合类和对象的行为已获得更大的结构

适配器模式主要描述如何对类和对象的行为进行扩展,方式主要是依赖和继承。iOS中还可以通过协议和消息转发扩展类。

桥接有点像抽象工厂,基类只提供行为接口,具体的行为逻辑放到实体类进行。这种设计方便了对行为进行替换和扩展。

组合是同一个类型的集合,方便统一处理一些行为,iOS中的subviews集合就是一个典型的组合模式的应用。

装饰其实也是在给类添加属性或者方法,iOS中的分类就是装饰模式的应用。

外观是将一系列相关联的方法进行封装,然后提供一个统一的入口。编译器的封装采用了外观模式,将整个编译链进行封装,隐藏内部过程,只提供一个统一的api供外部调用.

享元模式有点类似上面的原型,只是享元复用的对象可能颗粒度更细,而且只是内部数据,内部数据是不变的,外部数据是可变的。

代理模式比较好理解,类的某个行为自己不去实现,而是交给另一个代理类去实现,代理模式可以用来解耦模块间的相互依赖。

行为型

行为型设计模式具体描述算法和对象之间职能的分配方式

责任链模式描述组合对象对同一方法的调用情况,强调每个对象都有机会去处理改方法,具体实现逻辑可以参考iOS响应链传递机制中hitTest方法的处理。

命令模式将用户的行为进行封装,每一个行为都抽象为一个命令,使用命令队列进行维护,该模式主要应用于编辑软件中redo和undo操作的支持。

解释器模式对特定的语法进行解释,这些语法往往比较复杂且多变,如果直接使用比较麻烦,比如正则表达式的匹配,就需要使用专门的解释器。

迭代器模式主要来用进行集合的访问,在无需暴露集合内部具体结构的情况下。不同的遍历策略对应不同的迭代器类,即多态迭代。编程语言一般都有自己的集合迭代器。

中介者模式为各类之间的交互提供一个环境,避免各类因为相互调用而产生双向依赖。将与其他类的通信转变为和中介者的通信。

备忘录模式是一种数据持久化的应用,当我们需要保存对象信息时,通过将对象持久化本地,下次需要使用时直接从本地获取。

观察者模式提供了一种方式,保证依赖同一属性的多个类的一致性。通过注册对这个属性的观察回调,在这个属性改变时,可以很方便的通知这些依赖类。观察者模式是一对多的通信方式。iOS中的通知和KVO都属于这种模式。

状态模式用来解决对象在不同的时间节点时有不同行为的场景。我们可以将不同的时间节点抽象为不同的状态,通过对状态的观察改变对象的行为。tcp的建连和断开的过程就是应用的状态模式。

策略模式和状态模式相似,不同的是对象在各时间节点需要调用不同的算法,这里我们将变化的概念抽象为策略,每一种算法对应一种策略。

模板方法将基类的逻辑复用,而将具体的数据等属性延迟到子类实现。这样子类在继承父类的逻辑的同时还能拥有自己的行为。模板方法是基类预留的钩子,能够钩住子类的特定行为。

访问者模式提供了一种访问类簇对象的解决方案。可以将访问行为抽象为访问者,每一种具体行为对应具体的一个访问者类,该访问者为类簇中每一个类添加一个该行为的访问方法。如果以后有新的访问行为,在不修改类簇结构的情况下,即可通过扩展访问者来实现。

重构原则

类的重构

函数的重构