在面向对象的软件系统设计中,可维护性,可复用性是衡量一个软件系统是否健壮的重要指标。而可维护性和可复用性是可以遵从基本的准则来得以实现。而这些准则就是面向对象设计的基本原则。面向对象的设计原则一共有7种,这些原则都是以前的软件工程师在实践中总结的基本原则,经得起项目的考验,时间的检验,这些原则也是设计模式的基础。每种原则都蕴含面向对象的思想,从而从不同的纬度去提高软件结构的设计水平。
1:单一指责原则(SRP:Simple Responsibility Principle)
一个类只负责一个功能领域的相应职责。
在使用该原则来进行软件设计的时候一定要注意,不要让类的承担过多的职能,承担的职能越多,功能模块间的耦合性越强,维护成本也就越高,而且这个类被复用的可能性就越小。我们要将不同类型的职责拆分开来,分别封装到不同的类中,来实现职责的单一化。
2:开闭原则(OCP:Open-Close Principle)
软件实体对扩展开放,对修改关闭。软件实体应当在不修改源代码的情况下对功能进行扩展。
在软件开发中,我们不得不面临一个问题,那就是软件的需求会随着时间的推移而不断的变化,这时候一个可扩展性强,灵活度高的软件系统就显的十分必要了。要满足这样要求的系统,必须要具备一些基本特定。如果一个软件系统符合开闭原则,那么这个系统就可以很好的满足扩展性,灵活性的需求。
而在实现开闭原则的过程中。抽象化的设计是重中之重,抽象化才是开闭原则的关键,在Java中我们可以利用接口或抽象类来构建一个抽象层,而将具体的实现行为移至实现层完成。当需要修改系统的行为时,我们不需要修改抽象层,只需要新增新的具体类来实现新的业务逻辑就可以了。这样我们就可以在不修改源代码的基础上实现新的功能,满足开闭原则的要求。
3:里氏代换原则(LSP:Liskov Substitution Principle)
所有引用基类对象的地方能够透明地使用其子类的对象。
如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。
换言之,一个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出基类对象和子类对象的区别。
比如,假设有两个类,一个是Base类,另一个是Child类,并且Child类是Base的子类。那么一个方法如果可以接受一个基类对象b的话:method1(Base b)那么它必然可以接受一个子类的对象method1(Child c).
注意事项
- 为了保证软件系统的扩展性,子类要重写父类中所有的方法。若在子类中存在父类中没有的特有方法,那么通过父类则无法调用该方法。
- 在运用里氏代换原则时候,父类尽量设计成接口或者抽象类,子类继承父类或者父接口,实现父类中声明的方法,在运行时,子类实例替代父类实例。这样就可以很方便的扩展系统的功能,
- Java语言中,在编译阶段,Java编译器会检查一个程序是否符合里氏代换原则,这是一个与实现无关的、纯语法意义上的检查,但Java编译器的检查是有局限的。
4:依赖倒转原则(DIP:Dependence Inversion Principle)
抽象不应该依赖于细节,细节应该依赖于抽象。换句话说,要针对接口编程,而不是针对实现编程。
根据依赖倒置原则,我们应当尽量使用抽象类或接口来定义成员变量,参数类型,返回值类型,不要使用具体的实现类来做这些事情,
为了保证依赖倒置原则的良好应用,具体类应该只实现抽象层的方法,不要给出多余的方法,抽象类无法调用具体类中特有的方法。
在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入(DependencyInjection, DI)的方式注入到其他对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。
5:接口隔离原则(ISP:Interface Segregation Principle)
使用多个专门的接口,而不使用单一的总接口。客户端不应该依赖那些不需要的接口。每一种接口应该承担相对独立的功能,不该干的不要干,自己该干的就要干好。
其中的接口有两层含义
- 某个类型中所有的方法,是逻辑上的抽象。
当把“接口”理解成一个类型所提供的所有方法特征的集合的时候,这就是一种逻辑上的概念,接口的划分将直接带来类型的划分。可以把接口理解成角色,一个接口只能代表一个角色,每个角色都有它特定的一个接口,此时,这个原则可以叫做“角色隔离原则”。 - 具体指代某个编程编程语言,“接口”的定义。
如果把“接口”理解成狭义的特定语言的接口,那么ISP表达的意思是指接口仅仅提供客户端需要的行为,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。在面向对象编程语言中,实现一个接口就需要实现该接口中定义的所有方法,因此大的总接口使用起来不一定很方便,为了使接口的职责单一,需要将大接口中的方法根据其职责不同分别放在不同的小接口中,以确保每个接口使用起来都较为方便,并都承担某一单一角色。接口应该尽量细化,同时接口中的方法应该尽量少,每个接口中只包含一个客户端(如子模块或业务逻辑类)所需的方法即可,这种机制也称为“定制服务”,即为不同的客户端提供宽窄不同的接口。
在使用接口隔离原则时,我们需要注意控制接口的粒度,接口不能太小,如果太小会导致系统中接口泛滥,不利于维护;接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。一般而言,接口中仅包含为某一类用户定制的方法即可,不应该强迫客户依赖于那些它们不用的方法。
6:合成复用原则(CRP:Composite Reuse Principle)
尽量使用对象组合,而不是继承来达到复用的目的。
一个对象通过关联关系来引入其他已有的对象,通过委派调用已有对象中的方法,来实现复用功能的目的。
通常来说,如果两个类之间是“Has-A”的关系应使用组合或聚合,如果是“Is-A”关系可使用继承。"Is-A"是严格的分类学意义上的定义,意思是一个类是另一个类的"一种";而"Has-A"则不同,它表示某一个角色具有某一项责任。
7:迪米特法则(LoD:Law of Demeter)
一个软件实体应当尽可能少地与其他实体发生相互作用。
迪米特法则要求,当系统中的一个模块发生变更时,应该尽可能少的影响其他模块,那样系统的扩展会比较容易。迪米特原则可以降低系统的耦合程度,使类与类之间保持一种松耦合的关系。
迪米特原则要求我们“不要跟陌生人交谈,只跟熟人打交道”,以下条件就是我们需要打交道的熟人。
- 当前对象this。
- 对象中方法的形式参数。
- 对象中映入的成员对象。
- 如果成员对象是集合,那么集合中的元素也是我们的熟人。
- 当前对象中创建的其他对象。
迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
在将迪米特法则运用到系统设计中时,要注意下面的几点:在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;在类的设计上,只要有可能,一个类型应当设计成不变类;在对其他类的引用上,一个对象对其他对象的引用应当降到最低。