# 面向对象设计原则 # ## 概述 ## 最近在项目中发下了一些问题,随着项目需求的不断变化,项目业务的增加,经常需要去修改之前的代码。 渐渐地代码变得越来越臃肿,牵一发动全身,项目业务越复杂问题的严重性越大。 如何才能使我们的代码健壮稳定,易于维护,可扩展性好,怎么才能降低代码之间的耦合呢? 其实以前技术大师们也遇到过这种的题,他们总结了宝贵的经验就是“面向对象设计原则”。 面向对象设计原则(SOLID)是由 Robert C. Martin 在21世纪早期提出的,它指代了面向对象编程和面向对象设计的五个基本原则。 当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软件维护和扩展的系统变得更加可能。 | 首字母 | 设计原则名称 | 定义 | | :----: | :------------------------------------------------- | :----------------------------------------- | | S | 单一职责原则 (Single Responsibility Principle, SRP) | 一个类只负责一个功能领域中的相应职责 | | O | 开闭原则 (Open-Closed Principle, OCP) | 软件实体应对扩展开放,而对修改关闭 | | L | 里氏代换原则 (Liskov Substitution Principle, LSP) | 所有引用基类对象的地方能够透明地使用其子类的对象 | | I | 接口隔离原则 (Interface Segregation Principle, ISP) | 使用多个专门的接口,而不使用单一的总接口 | | D | 依赖倒置原则 (Dependence Inversion Principle, DIP) | 抽象不应该依赖于细节,细节应该依赖于抽象 | 大家一定听说过设计模式,在23种经典的设计模式背后,其实都遵循着一些基本的设计原则的。 而设计原则又由设计模式来实现,这就是二者相辅相成的关系,所以了解设计原则对于了解设计模式具有绝对的指导意义。 - 可扩展:新特性能够很容易的添加到现有系统中,不会影响原本的东西。 - 可修改:当修改某一部分的代码时,不会影响到其它不相关的部分。 - 可替代:将系统中某部分的代码用其它有相同接口的类替换时,不会影响到现有系统。 这几条可以用来检测我们的软件系统是否设计的合理,我们在进行软件设计的时候应该牢记并灵活运用设计原则,这可以让我们写出更优雅的代码。 ## 单一职责原则 (Single Responsibility Principle, SRP) ## >一个类有且仅有一个职责,只有一个引起它变化的原因。 简单来说,一个类中应该是一组相关性很高的函数、数据的封装。 一个类只做好一件事就行,不去管跟自己不相干的。 单一职责原则告诉我们:一个类不能太“累”!在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。 单一职责原则可以看做是低耦合,高内聚在面向对象原则上的引申,这个原则很容易违背,在我们的项目中经常能看到功能丰富的超级集合,整个类变得臃肿难以理解,这时候就需要我们有意识的去重构了。 ## 开闭原则 (Open-Closed Principle, OCP) ## > 一个软件实体如类,模块和函数应当对扩展开放,对修改关闭。 即软件实体应尽量在不修改原有代码的情况下进行扩展。 具体来说就是你应该通过扩展来实现变化,而不是通过修改原有的代码来实现变化,该原则是面相对象设计最基本的原则。 上面说过项目随着时间推移需求不断变化,经常修改之前的代码,很大程度就是对开闭原则理解的不够透彻。 开闭原则的关键在于抽象,我们需要抽象出那些不会变化或者基本不变的东西,这部分东西相对稳定,这也就是对修改关闭的地方(这并不意味着不可以再修改),而对于那些容易变化的部分我们也对其封装,但是这部分是可以动态修改的,这也就是对扩展开发的地方,比如设计模式中的策略模式和模板模式就是在实现这个原则。 ## 里氏代换原则 (Liskov Substitution Principle, LSP) ## > 所有引用基类的地方必须能透明地使用其子类的对象。 里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。 简单来说,所有使用基类代码的地方,如果换成子类对象的时候还能够正常运行,则满足这个原则,否则就是继承关系有问题,应该废除两者的继承关系,这个原则可以用来判断我们的对象继承关系是否合理。 ## 接口隔离原则 (Interface Segregation Principle, ISP) ## > 不能强迫用户去依赖那些他们不使用的接口。 简单来说就是客户端需要什么接口,就提供给它什么样的接口,其它多余的接口就不要提供,不要让接口变得臃肿,否则当对象一个没有使用的方法被改变了,这个对象也将会受到影响。接口的设计应该遵循最小接口原则,其实这也是高内聚的一种表现,换句话说,使用多个功能单一、高内聚的接口总比使用一个庞大的接口要好。 接口隔离原则的目的是使系统解开耦合,从而容易重构、更改和重新部署。 ## 依赖倒置原则 (Dependence Inversion Principle, DIP) ## > 高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。 其实这就是我们经常说的“面向接口编程”,这里的接口就是抽象,我们应该依赖接口,而不是依赖具体的实现来编程。 依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。 # 其他 # ## 合成复用原则 (Composite Reuse Principle, CRP) ## > 尽量使用对象组合,而不是继承来达到复用的目的。 简单来说,复用时要尽量使用组合/聚合关系(关联关系),少用继承。 在面向对象设计中,可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承,但首先应该考虑使用组合/聚合,组合/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。 ## 迪米特法则 (Law of Demeter, LoD) ## > 一个软件实体应当尽可能少地与其他实体发生相互作用。 简单来说,一个类应该对自己需要耦合或调用的类知道得最少,类的内部如何实现、如何复杂都与调用者或者依赖者没关系,调用者或者依赖者只需要知道他需要的方法即可,其他的我一概不关心。类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。 迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。 迪米特法则还有一个英文解释是:Only talk to your immedate friends,翻译过来就是:不要和“陌生人”说话、只与你的直接朋友通信。 在迪米特法则中,对于一个对象,其朋友包括以下几类: 1. 当前对象本身(this); 2. 以参数形式传入到当前对象方法中的对象; 3. 当前对象的成员对象; 4. 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友; 5. 当前对象所创建的对象。