设计原则可以指导我们写出更加通用、可扩展、可读、健壮的代码。设计原则大多都比较好理解,但是如果将这些原则结合业务逻辑灵活应用,却是一个具有考验的挑战。下面就将常见的设计原则总结如下:
基于接口而非实现编程
- 这里的接口,指的的抽象,具体到代码层面,是使用编程语言的接口和抽象类机制实现的对代码逻辑的抽象。
- 实现是不稳定的,但是我们提供的接口是稳定的。上游系统面向接口而非实现编程,使得它摆脱了不稳定的实现细节,当实现发生变化的时候,上游系统的代码基本上不需要做改动。这样可以降低耦合性,提高扩展性
- 越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,高灵活性可以让我们更好地面对未来的需求变化。好的代码设计,不仅能应对当下的需求,更可以在需求变更的时候,在不破坏原有代码设计的情况下应对。
- 而抽象就是提高代码扩展性、灵活性、维护性的最有效的手段之一。
单一职责原则
- 描述:一个类或者模块只负责一个职责或功能。
- 指导:以类为例,不要设计一个大而全的类,要设计粒度小,功能单一的类。
- 思想:功能单一的模块,可以提高内聚,减少耦合。功能复杂的模块,会让依赖和被依赖的关系变得庞杂,这种代码的耦合性非常高。
- 难点:职责过度单一,会降低内聚,所以,如何判断一个模块的功能是否足够单一?这个问题就像是要明确厨师所说的“放盐少许”究竟是多少一样。事实上,这个问题是随着项目代码的发展而不断变化的,项目初期,一个模块只负责一个很简单的功能,但随着需求的增加,越来越多的功能被放入其中,职责也就不再单一了。这时你就需要重构代码了,事实上,这也是持续重构的意义所在。
如果你依然无法判断,可以参考下面的几个判断
1.类中的代码过多,属性过多:可能你的代码已经有了太多功能。
2.类依赖和被依赖的其他类过多:耦合度高。
3.私有方法过多:是否可以将这些私有方法抽离出来,变成一个公用类,以提升代码复用。
4.类的取名困难:根本原因是你的类职责不明晰,所以你无法命名。
5.某几个方法和属性被大量使用:考虑把它们抽出来。
开闭原则
- 描述:对扩展开放,对修改关闭
- 指导:在代码中预留扩展点,当需要添加新功能的时候,更多的是新增代码(这些代码应当是高内聚,可抽象的),而不是侵入式地修改原代码。
- 思想:这个原则是代码扩展性的体现,我们需要在代码中预留扩展点,这就要求编写者有较强的扩展、封装、抽象的意识。在这里有一个判断是否开闭的技巧:扩展如果没有破坏原有代码的正常运行,没有破坏原有的单元测试,这个改动就合格了。
实际上,大部分的设计模式、多态、依赖注入等方式都是开闭原则的侧面体现。 - 难点:你并没有办法预留出所有需要的扩展点,而且过多的扩展口会影响代码的可读性。对此的建议是,不要过度设计,用持续重构应对变化。
里式替换
- 描述:子类对象可以替换父类对象出现的任何地方,并保证原有的逻辑和正确性不被破坏。
- 指导:让你的类符合一套协议。最方便的方式就是使用接口(interface)。
- 思想:里式替换的本质是制定一套抽象协议,父类和子类都遵守这个协议。这样的好处是,我们可以使用一些类似依赖注入的方式,使用同一个抽象协议(接口)完成不同的实现(调用不同类的代码)。
- 注意:有几个常见的违背里式替换原则的行为:
1.子类违背父类声明要实现的功能:例如父类的方法对输入的数据实现了升序排序,而子类却进行降序排序。
2.子类违背父类对输入、输出、异常的约定:输入输出要同意,可能出现的异常也要统一。例如父类对某个逻辑没有抛异常的操作,而子类有,这就会导致代码走另一套逻辑,这就破坏约定的逻辑。
3.子类违背父类注释中的说明:其实你修改注释就好。🤣
接口隔离原则
- 描述:客户端不应该被强迫依赖它不需要的接口。其中的客户端,可以理解为这个接口的使用者。
- 指导:如果接口的使用者只使用了接口的部分功能,那这个接口就不符合接口隔离原则。
- 思想:接口隔离和单一职责的概念有一点像,它们都强调某个抽象调用的职责应当单一。但是两者是有区别的:单一职责针对模块、类的设计,更多的是强调高内聚和弱耦合。但是接口隔离原则更侧重于接口的设计,更多强调的是隔离。
- 注意:你应该只为某一类调用提供它所需要的接口功能,这可能要求你对每一种调用设计同一个接口。这样,接口的调用者没有办法使用其他它不需要的功能,也就是将接口隔离了,例如:如果用户登录的接口被赋予了删除用户的功能,这可能会带来误删用户的风险。
依赖反转
首先我们需要了解几个概念:
- 控制反转:一般情况下,程序的执行流程都由程序员编写的代码直接控制,这时候,程序的控制权在程序员手中。但是如果你利用框架进行开发,只需要在框架预留的扩展点中编写代码即可,这时,程序的控制权就在框架手中了。
这种控制权的反转,就叫控制反转,这种设计思想常常用来指导框架的开发(框架的开发者要肩负控制程序执行流程的重任,并在合适的地方提供扩展点)。 - 依赖注入:依赖注入是说,不通过 new 在类内部创建依赖的对象,而是在外部创建依赖的对象,通过参数的形式让类使用这个对象。
这个方式有一些多态的味道,只不过我感觉依赖注入更加适合在编写框架的时候使用。依赖注入比较灵活,你可以使用接口对这种依赖进行抽象。
有了上面的概念,我们就可以说一说依赖反转原则了,它的描述是:高层模块不要依赖低层模块,两者应当通过抽象相互依赖。抽象不该依赖具体实现,但是具体实现依赖这种抽象。
其中,高层模块是指处于调用者的模块,低层模块是指处于被调用者的模块。
- 指导:这个原则主要用于框架的开发,下面是一个具体的例子:
Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。
- 理解:依赖反转的本质是要我们在框架代码和具体业务代码之间抽象出一组“功能协议”,这个协议在框架代码中是框架预留出的扩展点,在具体的业务代码中,是使用者需要根据需求实现的功能。
这种协议的实现很灵活,其中之一的手法就是使用依赖注入(在框架代码中注入具体实现)。在这过程中,你还可以使用接口,让整个扩展更加顺滑。 - 我认为这个原则最重要的概念就是模块之间的抽象协议,它的作用和里式替换中的抽象协议的作用很相似。只不过在框架设计中,程序的控制权在框架手中,所以我们要使用依赖反转原则控制低层代码的行为。
KISS原则
- 描述:保持简单
- 难点:“简单”这个词,就像是艺术,没有标准。总体来说,如果你的代码可以方便同事的理解,就能算是简单。这其中的切实可行的建议有:不要炫技写谁都看不懂的牛x代码,不要重复造轮子,不要过度优化而牺牲代码的可读性,不要过度设计。
- 总之:写人看的代码。
DRY原则
- 描述:Don’t Repeat Yourself(不要重复自己),即:不要写重复的代码。
- 理解:这实际上是对代码的可复用性提出了要求,更深一步,它要求你的代码有更好的扩展性,更低的耦合度等。
需要说明的是,有些代码的重复是为了让代码整体有更好结构,这种重复没有必要优化。而有些代码即使没有重复,但是实现的是重复的相同的功能,这时候就需要优化了。 - 复用性:
1.复用性的根本,是为了减少代码量,提升代码的可读性、可维护性等。
2.复用需要考虑粒度的问题,太细的复用粒度可能适得其反。
3.提升复用性的一些方法:减少代码耦合、单一职责原则、代码模块化、业务和非业务逻辑分离(业务代码难以复用,非业务代码比较容易)、通用代码下沉(越底层的代码越通用,越应当被设计的可复用)、面向对象的特性(封装、继承、抽象、多态)、一些设计模式。
迪米特法则
要了解迪米特法则,首先要明确“高内聚、松耦合”的概念:
- 高内聚:相近的功能放到同一个组织单位中(可能是类、模块),不相近的功能放到不同的类中。这样做的好处是,修改某一功能时,修改会很集中,代码容易维护。
- 松耦合:代码中的依赖关系应当清晰且简单,这样当你修改某段代码的时候,需要考虑的影响范围会比较小。如果你的代码高度耦合,那很可能会“牵一发而动全身”。
- 整体来说,就是功能集中,代码负责的边界清晰;不同模块之间的依赖关系明确且简单,以防止“牵一发而动全身”
迪米特法则:
- 描述:不应该有直接依赖关系的类之间不要有依赖;有依赖的类之间,只依赖必要的接口。
- 理解:这个法则实际上就是一个解耦合的原则,它要求不同模块之间的依赖尽量简洁。其实这也是一个艺术性的问题:怎样的关系算是足够简洁?怎样是过度设计?这可能要依靠工程师的经验和具体业务逻辑了。
总结
学习了面向对象和设计原则的相关知识,有一些思考想分享给大家
高内聚、松耦合
- 模块内部要功能单一,边界清晰。这样方便扩展和维护。
- 依赖关系要简洁清晰。当需求修改的时候,只需要考虑很小的一部分依赖关系。
- 高内聚和松耦合,可以减小代码的维护成本。实际上,我认为这是编写代码的最重要的一个原则。
抽象和具体实现
- 抽象是稳定且简单的,它解决了“做什么”的问题。最常见的抽象实现就是抽象类和接口。
- 具体实现是不稳定且复杂的,它需要程序员编码,以解决“怎么做”的问题。
- 编写代码时的抽象非常重要,这是提高扩展、复用、维护的基础。
我们在定义类的职责时会使用抽象类,以确定类需要完成的功能。
我们在定义类与类之间的交互关系的时候可以使用接口,以确定两个类之间要实现怎样的交互和依赖。 - 接口的稳定 和 松耦合的交互,是很多设计原则背后的指导思想。
协议
- 更进一步,你会发现抽象实际上是实现了某种协议,例如某个接口定义实际上是说:“A 类在 xxx 的情况下将 xxx 数据交付给 B 类”。
- 这种协议是灵活又严格的,灵活是说,你可以随意定义你的你想要的接口。严格是说,实现接口的具体代码必须遵从接口的输入、输出、异常处理等约束。
- 基于这种严格性,有一些设计原则被提出。
- 实现这种协议的方法不只有接口一种方法,设计框架时的依赖注入也算是一种方法。
扩展性
- 实现代码的扩展性有两个要求:对可能存在的业务需求变动敏感、在交互抽象中预留扩展点以应对变动。
- 当日,如果你没有留下扩展点也无妨,持续重构会保持你代码的整洁。
如何重构?下一篇我们将讲解与之相关的内容。
网友评论