架构漫谈系列(3)其他原则

在最开始,先重复一下第一篇的内容,这个系列是写我们如何来组织代码,如何提高可扩展性和维护性的,并不涉及到网络拓补结构或各类中间件的使用。

首先,提一提面向对象设计的五大原则:SOLID。

SOLID原则

SOLID都是些什么呢?

  • SRP, Single responsibility principle,单一职责。一个类只能有一个职责,如果这个类需要被修改,那只能是某一个需求更改导致的(仅此一个,没有更多的)。例如,book类里面有一个print的函数,当我们修改book类的书名时,我们需要改book类,当我们把book的打印从打印到A4改成打印成6寸时,也需要修改此类,这就违背了SRP原则。
  • OCP, Open/closed principle,开闭原则,Open for extension, but closed for modification
  • LSP, Liskov substitution principle,父类能够被子类无忧的替代,不必担心产生副作用。
  • ISP, Interface segregation principle,如果一个接口能够被拆分成多个接口,那就不该用这个通用的接口来呈现。
  • DIP, Dependency Inversion principle,依赖于抽象,而不依赖与具体的实现。

今天先从SRP和DI开始。

单一职责

它的定义上面已经讲过了。
这一条是用来帮助我们创建更为松耦合和模块化的程序,因为每种不同的行为我们都封装到一个新的类或对象里面去了。未来要增加新的行为或特性,新增的内容也会增加新的对象里面,而不是把这些行为加到原来的对象上去。
这样做的好处是更安全,更不容易出错,因为以前的类可能是含有多种多样的依赖关系的,但新增加的类却没有那些依赖在里面。所以我们可以放心的修改系统行为,不必担心修改带来的副作用。

在分层结构中,这个思想也有体现。我们的UI/Presentation层专管UI的显示,logic层专攻业务逻辑,数据访问层只做数据访问相关内容。其实,都是同样的道理。

如果SRP思想贯穿了你的整个程序,你的逻辑层里的某一个服务是不是就成了微服务,一个微服务只有一个单一的职责,你要给你的程序增加功能,那再加一个微服务即可(切忌在已有的微服务上添加)

依赖反转(Dependency Inversion)

应用程序内部的依赖不应该依赖于具体的实现,应该依赖于抽象。
也是五大原则之一。

很多应用的依赖是在编译期就确定了依赖的顺序,就如同模块A调用了模块B的一个函数,刚好这个模块B内的函数又调用了模块C的某个函数,于是,A就依赖B,B依赖于C。

应用了DIP之后,就像这个样子:

可以看到,ClassA不再依赖于Class B,转而依赖InterfaceB,ClassB也不再依赖于Class C,转变成Interface B依赖于Interface C。

ClassA现在依赖的是B的抽象(即Interface B),这使得A在运行时依然可以调用到B的函数,运行时应有的行为还能得以保留。不过B依赖已经变化了,它现在依赖于Interface B了(反转依赖),现在的好处在于,不但B的功能可用,未来你想变成B1,B2,B3,这些都不必修改Class A了(如果你现在还不明白如何去实现,参考Ioc Container的实现)。

依赖翻转是构建松耦合应用的关键所在,既然具体的实现都依赖于更高层的抽象了,那么程序就应该更容易被测试、维护和模块化。其实依赖注入(Dependency injection)也是准照这个原则来扩展实现的,所以掌握这个方法也是十分的重要。

以下原则跟SOLID无关,但我认为也是比较重要的。

显式依赖

如果函数或类必须依赖其他类或对象的某些状态才能正常工作,那应该显式的声明其依赖的对象或类。
这有点拗口。

实际上类的构造函数提供了一个约定:我要构造这个类A的对象,需要提供1、2、3、4个参数,如果没有这几个参数,我的类就可能工作不正常。
ClassA的依赖关系很明确,就是要1、2、3、4个参数,这是合理的。

假设这样一种情况,ClassA除了上述依赖以外,后来增加了一个新的依赖,在增加这个依赖的时候,我没有将它显示的写在构造函数里面,因为这个依赖项是个全局对象。

一般来说这样的代码运行起来也没有什么问题,但是在实际上,它在逻辑上已经引发了一个问题:你的这个类的依赖关系内外不一致。试想如果在另外一个场景里,这个全局的对象失效了或不存在,那么你的这个类就不能用了。
你的类已经不能用了,但是你们团队里面的其他成员可能并不知道,他们仍然按照之前的模式继续使用你的类,于是这就引起了潜在的错误。

如果你的类能够显式声明它的所有依赖,你的类才能够更友好,代码本身也能更清楚的表达自身的含义。你的小组其他成员相信,只要完全的传递了你要的参数,你的代码就一定会按既定的逻辑运行下去。

避免重复(Don't repeat yourself)

重复代码是后期bug修复的大敌。

我们要尽量避免编程时使用复制和粘贴的功能,如果同样的逻辑在代码里的多个位置出现,每次我们维护的时候就不得不同时维护多处。当功能转交给其他人时,其他人也会厌烦多处查找这些问题;其他人将这类代码转交给你时,你也会头疼不已。

忽略持久化(Persistence Ignorance)

这个概念PI,有些地方也称作持久性无知,是指你需要存储某个对象,但是这些代码不应该受到持久化方式更改的的影响。
在.Net里,有一种类叫POCOs(Plain Old CLR Objects),在Java里,这种叫POJOs(Plain Old Java Objects),这种类是不需要从基类或接口来继承的。
我们需要保证这类对象不要插手如何保存或如何获取(持久化和获取数据回来)。

做到这一点,我们的业务逻辑能更纯粹,而存储对象时,也能灵活地按需调整(比如说用Redis或Azure或Sql Server等等),无论存储的策略如何调整,我们的业务逻辑都是稳定的。

注意,如果你的代码有以下几类情况,那这些代码可能就是违反PI规则的:

  • 必须继承自某个基类或必须实现某个接口
  • 类的内部含有保存他们自己的代码
  • 必须要写构造函数
  • 属性上要求加Virtual
  • 有自定义的存储相关的Attribute

如果类里面有上面的逻辑,暗示着这些类的持久化跟持久化的策略产生关联性,可以理解为她们是耦合的,将来如果要更换新的持久化方式,也许就比较困难了。

有界上下文(Bounded Contexts)

有界上下文是DDD中的一个核心模式,这种模式是帮助我们剥离开复杂的应用程序,将他们隔离开,形成不同的上下文边界。不同的模块有着不同的上下文,且能独立调用,而各自的模块可以有自己的持久化的,互不干扰。

在大型的应用程序中,不同的人对不同的的东西可能取相同的名字,这跟我们程序的类一样,为何我们要在外面放一个namespace在外面,其实也是形成一个边界。程序内部也是如此。

例如,售楼部内的员工把商品房认为是产品(Product);但是在工程部,可能他们把刷灰和修理管道的服务叫做产品,为了消除这些歧义,可以定义一个边界,分离开两种情况,以免在系统内产生混淆。

结语

这是这个系列的最后内容,实际上每一个小点都能展开成一大章节来说,后续有时间可以进行延伸的讨论。而因为这篇的内容除了SOLID外,跨度都还比较大,以至于我都不好写标题,暂以《其他原则》为题好了。

面向对象编程其实挺难的,我们很多的人都没有真正的理解面向对象,以至于所谓的面向对象,就是我们用上了class关键字而已。如果有时间,可以专门看看设计模式的书,相信还是会有很多的收获的。

这些内容可能过于偏于理论,但是有了这些理论的指导,我们的开发的日子才会更容易一些。但话又说回来,如果你要做的项目是个一杆子买卖,根本不会持续维护,那你还是尽量的使用反模式吧,又快又省心不烧脑。可是如果你的项目是长度维护的项目,必要的考量还是需要的,否则,你就陷入了泥潭,总是发梦期望公司将原来的推翻重写,一次偿还所有的技术债了。

这种人呢,我们把他叫做『老赖』,以前的技术债还不清了……那不还了,申请『破产』,我们重来吧。

小春微信
本文地址:http://www.cnblogs.com/asis/p/architecture-others.html
https://1few.com/architecture-others/

原文地址:https://www.cnblogs.com/asis/p/architecture-others.html