理解软件设计的基本原则

待兔 等级 124 0 0

任何软件唯一不变的真理是变化,毕竟软件是"软"的。软件研发需要快速响应市场、需求的变化。

为了快速响应,我们可以通过增加人手来达到部分目的,但软件开发属于知识密集型工作,当人数增加到一定数量后,不仅不能够提升研发效能。反而增加管理成本,沟通成本及由于人与人沟通、理解上产生的歧义而最终造成软件实现的混乱和复杂度。

所以软件本身需要能够轻易的扩展,适应各种需求变化,即代码也要拥抱变化。

但做到这一点是非常困难的,毕竟当前软件都要复杂的领域知识,业务场景,不是几个简单的CRUD就能编写完成的应用。

降低软件成本,提高研发效能

减少软件成本最简单的理解就是投人少、产出快(老板喜欢)。我们先来看下简化了的软件成本的组成:

 软件成本 = 研发阶段人力成本 * 开发时间 + 维护阶段人力成本 * 开发时间 

一个软件的生命周期所花费的成本不仅仅是开发上线就结束了(当然开发的软件只为外包销售一次,而不使用的除外),所以我们减少成本,缩短周期不仅仅要考虑研发要快,也要考虑后续的维护(理解和变更)也要快。

如同打台球,每一杆出球都要考虑走位,使后面的击球同样简单,而最终赢得胜利。

复用

为了达到投人少,产出快的目的,复用就成了软件行业追求的目标。

通过复用而减少人力资源投入,快速发布软件。复用在一定程度上也确实取得了很多的进展和效果。

但是很遗憾复用本身也存在理解的歧义。举几个曾经工作中遇到的真实的例子:

1.曾经见到过一个表字段存储了多种概念的值,甚至是不同类型的值,如”123” 和“abc”,调用端判断数字和字符串分别执行不同逻辑。

2.一个Product对象,存储了汽车信息,和奶粉信息,调用端判断是汽车还是奶粉

3.一个微服务支持多租户,多租户的行为完全不同,根据租户的type在整个微服务中进行if else判断。

上述不同抽象层次出现的问题,我问当时的开发,给出的理由都是复用,复用字段,复用对象,复用公共微服务。

这种”复用“大大增加了系统复杂度,增加了系统的维护成本。

另外一味的追求复用会增加软件研发阶段的成本、复杂度或过度设计,而有时简洁,直接,够用就可以大大降低研发成本。

软件设计原则

既然复用不是追求的目标,那如何保证软件的可扩展,快速响应变化呢?

Code Review标准中所述的那样,

软件设计的各个方面几乎从来都不是纯粹的风格问题或个人偏好。它们是建立在基本原则基础上的,应该以这些原则为依据,而不是简单地以个人观点为依据。

同样我们只要遵循软件开发的基本原则,就能大大降低整个软件生命周期,包括研发阶段及维护阶段,需要投入的人力和时间。

所以在这里介绍一部分相关的基本原则和个人对这部分原则的理解。

DRP-不要重复你自己(Don't Repeat Yourself)

对于代码来说,重复是万恶之源。当一段代码在代码基里有多份copy时,针对于这部分代码的逻辑变更,就会修改多次,即代码坏味道中的散弹式修改。

首先工作量对应增加重复次数的倍数,但更大的问题在于遗漏了修改而造成bug。

而重复又细分为完全重复和结构性重复。完全重复不需要解释,以下两个方法即存在结构性重复:

 public List<Apple> findApple(List<Apple> appleRepo,String color) {

        List<Apple> result = new ArrayList<>();

        for(Apple apple : appleRepo){

            if(apple.getColor().equals(color)){

                result.add(apple);

            }

        }

        return result;

    }

}

public List<Apple> findApple(List<Apple> appleRepo,int weight) {

        List<Apple> result = new ArrayList<>();

        for(Apple apple : appleRepo){

            if(apple.getWeight()>=weight){

                result.add(apple);

            }

        }

        return result;

    } 

copy paste是造成重复的主要原因之一,所以一定慎用或不用copy paste。

KISS原则(Keep it simple and stupid)&& YAGNI(You Aren’t Gonna Need It)

 The KISS principle states that most systems work best if they are kept simple rather than made complex; therefore simplicity should be a key goal in design and unnecessary complexity should be avoided 

Always implement things when you actually need them, never when you just foresee that you need them.

Decide as late as possible: Delaying decisions as much as possible until they can be made based on facts and not on uncertain assumptions and predictions.


此处两个原则都是为了防止过度设计。过度设计本身比不设计还要糟糕。因为每增加一层抽象,代码复杂度就上升一个高度,后续的维护(理解,变更)都会相应复杂。这也是敏捷迭代、重构和演进式设计被人们推崇的原因之一。

最少知原则(Least Knowledge)
----------------------

只需要给定当前够用的知识即可。如果开车同时要了解发动机在多少度燃烧、车门的漆料的化学组成等等这些知识,那么汽车也就不会如此普及和提升我们的生活质量了。

很多时候违反最少知原则的原因是以防万一以后要用,就先都做了。

方法出参、入参极其臃肿,出入参存在冗长的火车残骸式调用,导致方法内部细节被强依赖,不敢轻易变更;

消息体信息过于全面,导致维护消息Producer知晓具体哪些信息是被使用了是件很困难的工作,同时造成Smart Consumer,即消费端强依赖消息体的逻辑,需要随消息体的变更而变更。

所以,够用就好,以后再说以后的(通过重构来满足后续的需求),也许就没有以后了。

COC-约定优于配置(Convention Over Configuration)
-----------------------------------------

约定好协议,大家按协议执行。优于具体执行阶段去查询相关条例。

红灯停,绿灯行,大家都遵守。如果上海黄灯停,蓝灯行;北京绿灯停,紫灯行,所有人出门都要带着各地不同的交通灯规则说明书。那大家就都不敢去外地开车了。

maven为什么能流行起来,其中主要原因就是maven约定java目录结构优于ant的自定义配置目录结构。

所以大家在增加配置项时,思考下可否通过约定而撤销该配置。

SOLID原则
-------

### SRP-单一职责(Single Responsibility Principle)

A class or entity should have one and only one reason to change.


一个类、模块、微服务应该只有一个职责,引起其变更的原因应该只有一个。

理解好单一职责,我们首先需要理解什么是职责?是不是只要一个类只有一个方法如CreateOrderAction,CheckOrderExistAction,一个模块只有一种类型,如Service类型,Dao类型,一个微服务只操作一张表,就满足单一职责了呢?

很多这样设计的作者之所以这样设计的依据就是号称满足单一职责。这是对单一职责的一种误解。如果运用到类设计上,会造成类爆炸;如果运用到微服务上,后果就是很多不具备内聚性的微服务产生,从而造成复杂度上升,维护数据一致性困难。

单一职责的关注点是高内聚,即引起变更的原因只有一个,一个订单的创建与校验一般是在一个变化维度,一个模块的Controller,Service,Repository,Domian Entity一般也会同时变更。而一个微服务也应该完成一个完整的原子性业务操作。

单一职责是**封装**的理论指导。从类的角度来看,数据与行为高内聚,即方法尽可能使用直接依赖的属性(包括属性和入参);从模块的角度来看,应用服务层、领域层、基础设施层(Repository,Component等)要高内聚在一个模块下(可以理解为同在一个java包中);从微服务的角度来看,服务应该高度自治,完成独立的业务操作。

### OCP-半开半闭(Open Closed Principle)

Software components should be open for extension, but closed for modification.


半开半闭对修改是关闭的,对扩展是开放的。所谓修改和扩展这里都是指新增特性,当原来的程序有bug时,你是无法不修改原来的代码的。

但在新增特性时,我们要设计成用新增代码来满足新功能,拒绝更改原有代码满足新功能。

想想这样做的好处吧。理论上讲只要代码没有变动,就不需要测试。所以如果你的设计满足OCP,你永远不用担心你的代码会对原系统造成什么破坏性影响。测试的也不需要大量的回归原有功能了。

### LSP-里氏替换(Liskov Substitution Principle)

Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.

`````` Functions that use pointers to base classes must be able to use objects of derived classes without knowing it.


白话解释,基类(父类、抽象类或接口)可以被子类替换而客户端无需更改,即子类与基类是"IS-A"的关系。狗是一种基类抽象,泰迪,哈士奇是满足IS-A的派生类,但是玩具狗就不是IS-A的派生,不应该使用**继承**。

同时,当做出一种抽象时,所有的实现类都可以替换基类引用而不会造成编译错误(行为是不一样的,此处只是为了验证LSP)。

LSP是**多态**的基础,而多态是面向对象的核心,通过多态我们可以灵活的扩展。

### ISP-接口隔离(Interface Segregation Principle)

Clients should not be forced to implement unnecessary methods which they will not use.


客户端不应该去实现他们不需要的接口,即同时满足最少知原则。

让你的客户使用简单,傻瓜式操作,同时你可以获得最大灵活的控制权。因为软件总是再变化,需求也总是再变化,当你需要用户依赖你很多不需要的接口,首先对方使用很不方便,偌大的接口,对方需要有使用,学习的成本。

其次,我们失去了灵活的控制权。一旦你将接口暴露出去,即使对方不需要,当你面临接口变更时,你无法确定对客户的影响,造成维护成本变高。

### DIP-依赖倒置(Dependency Inversion Principle)

Depend on abstractions, not on concretions

```

客户端依赖于抽象,实现端也依赖于抽象。通过抽象进行解耦,客户端与实现端都可以独立变化而不受影响。即所谓的面向接口编程。

总结

当然软件设计的原则与模式还有很多,这里只是介绍了几种个人认为面向对象编程比较重要的原则。由于个人能力有限,有可能存在一些错误的理解,欢迎大家留言更正。

预览图
收藏
评论区