关于架构和编码的思考

  1. 控制反转
  2. 思考一下,组件间调用的本质
    1. 接口类映射 和 反射 的选择
  3. 依赖倒置
    1. 采用接口的成本
    2. 包装一下
  4. 单一职责和关注点分离原则
    1. 例子:Record和Log写在一起?
  5. 策略控制
  6. 创建模式的思考
  7. 契约式设计
    1. 调用方和被调用方相互负责的看法

长期更新,记录我对设计架构的理解

控制反转

A,B,C三个组件,如果可以互相调用,则ABC互相依赖。在大型项目中,这种会造成组件间耦合紧密的问题。

思考一下,组件间调用的本质

  1. 有组件的实例
  2. 接口

拥有组件的实例,我们直接拥有另一个组件,这显然是依赖的。

如何在尽量减少依赖的情况下解决这些问题呢?

  1. 可以通过反射的方式, 需要建立 {组件}<->{字符串} 的映射表,把关系转移到了配置文件中,这种方式存在安全性问题,因为没有编译器的强类型检查。
  2. 建立映射关系。我们可以 建立Class和某种物件的映射, 因为有protocol的存在,接口的问题被解决了。于是建立Class和Protocol的映射,可以获得实例的同时也知晓接口。这就构成了一个组件调用的基础条件。并且我们需要一个manager替我们管理映射。

在2中,依赖关系变成了如下
{组件}->{manager}->{interface}

组件的耦合转移到了组件和interface的耦合。这在工程维护中存在一个问题,我们需要维护一个庞大的protocol列表。不过相比于组件间相互依赖,这种做法好了很多。

接口类映射 和 反射 的选择

我个人愿意维护protocols,而不是维护组件字符串映射表。 从逻辑上说,无法实现双方相互隐藏。这无非是 “关系” 的体现方式不同。我选择程序内的映射关系,有编译器检查。

依赖倒置

传统的过程性系统的设计方法倾向于使高层次的模块依赖于低层次的模块,抽象层次依赖于具体层次。
而依赖倒置则将{高层次模块,低层次模块}->{抽象接口},这样无论上层还是下层的整个模块出现了替换,只要接口不变化,就不需要修改另一层次的模块。

这里值得注意的是,低层次模块依赖了高层次模块中定义的接口。当我们把低层次模块拆分后,将其用于别的项目时,会出现问题。对此我的想法是:

  1. 如果高层次模块需要和底层次模块完全隔离,方便的替换整个低层次模块的实现,那么使用依赖倒置。
  2. 如果没有1中的需求,那么还是直接依赖低层次模块,我认为低层次模块本身的接口暴露合理,内部实现的改动也不会对高层次模块有什么影响。

采用接口的成本

  1. 接口定义,接口维护,遵循类维护
  2. 如果暴露了一些定义的数据结构、类,则这些也需要抽象。会造成很多重复的代码。
    我觉得还是尽量只在必要情况下使用接口,

包装一下

我们使用高层模块时,其实就是构造一个符合接口的东西而已。
底层模块A去遵循高层模块的协议,无疑让底层模块无法复用。这时候我们可以再新建一个类C,让C去遵循协议,C通过A来构建自己。

单一职责和关注点分离原则

关注点分离要求每一个功能对应一个单独的任务,每一个功能都要在一个独立的模块中实现。每个模块都有自己的职责,而不会关注其他模块的职责。如果一个类包含多个职责,要求改其中的一个职责,则可能会影响该类里其他职责的实现。
单一职责原则要求每个类只包含一个职责,所有方法都应该为了实现该职责。要修改一个类的职责,只涉及该类。

例子:Record和Log写在一起?

场景:Record模块,存储了Crash的信息。我们需要生成某种格式的日志。Record把生成日志的代码通过分类实现。
这里通过category的实现,方便,不会引入单独的类。把格式化生成log的职责放在了record模块里。但是Log是个改动频繁的功能,兼具有相当多的扩展性(多种格式..)。仅是log当做Record的一个功能,职责和灵活性都说不过去。Record应仅关注信息的记录。把Log当做另一个同等级模块来看待。

做法1:把Log和Record拆分为同等级的模块,Log模块依赖于Record,生成相应的日志。
这里纠正一个错误,有同学说,既然存在依赖,那就用分类好了不需要拆分。模块拆分和依赖无关,关注的是职责分离和粒度控制,是通过现在情况和未来发展做出的判断。

从框架开发的角度来看,Record和Log都是独立的模块,不能互相依赖。Log模块需要元数据,我们可以定义Info协议,Log依赖Info协议。当需要生成log时,我们构造一个遵循Info协议的类RecordInfo,给Log使用即可。多了一层中间层,但是把 Record和Log隔离出来,两者都可以复用。 维护成本由 Log和Record 的使用者承担。这是做的很干净的写法,适用于需要复用的情况,如果仅仅把Log用在固定的场景,那么直接依赖元数据类也是可以的。

策略控制

现在对于 某个功能 有一个实现A, 此时,在A的实现的基础上出现了另一种分支,达到B的效果。

在A的代码中,修改逻辑是常用的做法,借此实现AB功能。
但这样破坏了原有的功能,使得逻辑日渐复杂。我认为相对合理的做法是,将AB的共同逻辑抽取,差异逻辑分开。如果实现代码很多,可以拆分成两个策略类。关于func还是strategy class可以根据代码量和复杂程度来权衡。

软件开发中是无法避免依赖的,我们可以把相关的逻辑放在类中,也可以放在函数中。我认为这本质上是一样的。粒度是开发过程中需要考虑的问题。合理的设计出最适用用与当下的框架,不过度设计。要知道我们无法从根本上改变软件逐渐增大的复杂度,我们能做的是改变自己的编码方式,将逻辑写的合理清晰就好了。

创建模式的思考

  1. 工厂模式 生产一类物品
  2. 抽象工厂模式 生产多类物品
  3. 生成器模式 构造复杂的产品

工厂模式将实现代码与暴露的接口隔离。抽象工厂模式在工厂模式的基础上将多种工厂的能力统一暴露。生成器模式关注于复杂对象的步骤生成。

创建模式都将实现与产品隔离。生成器模式相较于抽象工厂模式,在对象的生成中更加细化,将生成的对象各个步骤隔离,各个步骤可以组合。而抽象工厂模式适用于简单的对象生成。

我认为,本质上,创建者模式,通过依赖注入,以统一的接口分发不同的实现。可以根据实际情况,根据复杂度选择合适的做法。

契约式设计

DbC的核心思想是对软件系统中的元素之间相互合作以及“责任”与“义务”的比喻。这种比喻从商业活动中“客户”与“供应商”达成“契约”而得来。例如:

同样的,如果在面向对象程序设计中一个类的函数提供了某种功能,那么它要:

调用方和被调用方相互负责的看法

在写code中经常会遇到 不知道一些guard 条件到底写在哪好。
写在调用方,逻辑清晰一些。写在被调用方,可以减少代码冗余。

我个人觉得这样的写法比较合适:

  1. 调用者体内会被改变的状态,在调用者做校验。
  2. 被调用者处理上下文的一些信息。
  3. 当被调用者的guard逻辑重复多次,考虑将判断逻辑抽象成函数。
script>