一文读懂面向对象基本原则

1. 概述

设计模式与语言无关,用来解决一些反复出现的问题的解决方案。

2. 基本原则

遵循面向对象设计的基本原则,有几个目标是我们想要达到的:

  • 可维护性。
  • 可扩展性。
  • 可复用性。

可维护性好的代码,就是需求改动对现有的代码改动小、副作用低而且可控,而不是牵一发而动全身。

可扩展性好的代码,新增需求无需修改旧代码,新增代码即可,最理想的是 热插拔 ,即插即用。

可复用性好的代码,在相同或类似的业务场景下,复用程度分成几种类型:

  • 直接复用。代码可以直接复用,直接省去重复造轮子的时间。
  • 复用需要微调。对代码进行少量的修改或者能够新增扩展就可以复用。这也是大部分复用可以达到的程度。
  • 复用需要大改。因为代码各个对象之间复杂的依赖和关联,业务不够清晰,设计不够干净,为了复用需要进行大量改动,剪不断理还乱,重构的代码比重写的代价高。

产品需要通过迭代来进行完善的,而且需求也在不断地变化。如果维护性、扩展性、复用性降低,我们的迭代速度会越来越慢。一些细小的的改动而产生一系列 bug,一个简单的需求需要大量的重写。开发周期变长,在工期内能做的需求变少,需要解决的 bug 变多,严重的话会引起产品在市场上失去竞争力。

所以,一定要注意代码的设计,以面向对象基本原则为参考标准。前人栽树,后人乘凉。

面向对象一共有六大基本原则,SOLID + 迪米特法则。

这些只是提供了一些编程理论,具体的使用还要根据业务情况来,去达到一个或者多个原则。即使是设计模式,也可能遵循某些原则,而背离某些原则。这个就和计算机的时空矛盾类似,有时候需要时间换空间,有时候需要空间换时间,怎么用还要放到具体的业务场景进行分析。

2.1. 单一职责原则

Single Responsibility Principle,SRP

定义:一个类只负责一个功能领域中的相应职责

当一个类的职责膨胀,这会产生两个问题:

  • 职责复用困难。其他地方也需要复用这个职责,因为没有从该类解耦出去,导致其他地方重写或者直接引用该类。
  • 职责依赖复杂。过多的职责在一个类中进行业务操作,形成复杂的网状依赖,一个职责发生变化,其他职责也会受影响。

这里举一个例子,假设我们有一个类,拥有了数据访问、业务逻辑、UI 展示等。然后各部分相互耦合相互依赖。迭代过一阵子,代码会迅速膨胀,而且其他场景如果要进行功能复用的话会很难受。

所以这里可以做拆分,拆成几个类,各司其职。

单一职责

该原则的效果就是不同职责间的解耦,相同职责的内聚。类的复杂度降低,逻辑也变得简单,更好理解和维护。

也要控制好粒度。实际应用中,也不要把粒度做得非常小,这会导致类的数量膨胀,过犹不及。

2.2. 开闭原则

Open Closed Principle, OCP

定义:软件实体应对扩展开放,而对修改关闭

这里的软件实体指:

  • 按一定逻辑规则划分的模块。
  • 抽象和类。
  • 方法。

这个意味着,新需求过来后,我们尽量不修改旧代码中的方法和类,而是创建新方法、新类。

该原则的一种思路,用抽象类构建整体框架,用实现类扩展细节。

比如这样:

开闭原则

改原则的实现对设计会有比较高的要求。

抽象层要考虑全面,足够稳定,然后需求来了后,我们只需要去修改或者扩展子类实现,用极小的改动完成需求。

如果抽象层考虑不全面,会导致频繁修改抽象层,导致其他实现类也跟着调整,是不符合开闭原则的。

2.3. 里氏替换原则

Liskov Substitution Principle,LSP

定义:所有引用基类对象的地方能够透明地使用其子类的对象

如果我们定义了一个基类,如果把它的引用换成了子类的对象,不会对程序造成破坏。

这个要求了子类在实现的时候,尽量不去覆盖或者修改父类的实现。

如果修改了父类的实现,而且业务逻辑完全不一样,会导致在面向父类编程的地方,该子类就无法使用了,甚至有可能在不知情的情况下使用导致程序的异常。

子类通过继承的方式去扩展父类,尽量新增方法。

如果需要修改父类实现的:

  • 也要保持整体的业务逻辑不发生大的变化。来替换父类替换子类程序正常。这个需要从业务去分析。
  • 在重写的方法体里面,要调用父类的实现。

里氏替换

2.4. 接口隔离原则

Interface Segregation Principle,ISP

定义:使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。

如果让一个接口承担不同职责的方法,数量多了之后,接口会很难维护。因为我们实现了某接口,就要实现它的所有方法,但如果这个接口大杂烩而我们只需要部分方法,却要去实现大量空方法,还要去理解它,这个封装就不达标了。合格的封装,应该让使用者只看到和实现需要的方法

所有我们需要对接口进行拆分。拆分的方式,可以对接口中的方法进行归类,找到哪些方法是一起使用的,让接口中的方法数尽量少。

比如下面的例子。像左边的情况,一个接口内堆积了大量的方法,但是 A 只需要三个,导致一些 A 不需要的方法也暴露出来了。B 也是一样。可以拆分成右边的情况。实际情况会更复杂

接口隔离

接口拆分粒度也不能太小,粒度太小接口类多起来也不好维护

如果接口内方法大量堆积,臃肿,而且实现方每次只使用部分方法,可以考虑拆分一下了。

2.5. 依赖倒置原则

Dependence Inversion Principle,DIP

定义:抽象不应该依赖于细节,细节应该依赖于抽象

Java 中,抽象就是接口或者抽象类,细节就是实现类。

原因是实现细节变化很多,如果让抽象依赖于实现细节,会导致某些细节的变化引起抽象类的变化,进而又引起该抽象类的实现类的变化,代码维护困难,改动量大容易犯错。

比如,我们需要一个列表,直接使用 ArrayList,后来需求发现变动,发现这个 ArrayList 被多线程访问了,想用 CopyOnWriteArrayList 替换,但基本上所有的参数声明、变量声明都用了 ArrayList,整个改动很大。

但如果我们一开始就是基于 ArrayList 的抽象 List 编程,只需要调整一下,选择 CopyOnWriteArrayList 实现就好了

依赖倒置

要实现该原则,可以遵循一些规则:

  • 类尽量有抽象类或者接口
  • 变量声明的类型尽量是接口或者抽象类

面向接口编程,在开发可以很方便地更换实现。

依赖倒置原则虽好,但是使用起来还是要根据具体的业务场景,如果强行去创建各种抽象类和接口,过度使用,会造成很多类的浪费。而且依赖于抽象也要看场景,有时候就必须依赖于细节,比如访问者模式的使用。

2.6. 迪米特法则

一个软件实体应当尽可能少地与其他实体发生相互作用。

让类与类之间的关联和耦合,这样一个模块发生修改时,尽量地少影响其他模块。

法则定义包括:不要和陌生人说话,只与直接朋友通信。

一个对象的朋友有:

  • 当前对象本身
  • 以参数形式传入到当前对象方法中对象
  • 当前对象的成员对象
  • 如果当前对象的成员是一个集合,集合中的元素也都是朋友
  • 当前对象所创建的对象

每一个类都应当降低成员变量和成员函数的访问权限。

而一个对象对其他对象的访问,可以使用第三者来代理或者转发。

中介者模式

也可以采用抽象的方式,去依赖其他对象的抽象而不是具体实现。比如 MVP 的设计模式,各层之间采用接口的形式进行通信,而具体的实现不可见。这样子后面改实现或者替换实现,各层次不会有大的干扰。

发布了61 篇原创文章 · 获赞 43 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/firefile/article/details/90345091