面对对象开发过程中,有七个基本原则(开闭原则、单一职责原则、里式替换原则、依赖倒置原则、合成聚合原则、接口隔离原则、迪米特原则)。
1、开闭原则
1)定义:软件方法应该对修改关闭,对扩展开放。
2)问题由来:随着软件的运行,因为功能的扩展、业务的调整、升级等原因,修改原有代码,可能会给旧代码引入新的问题,产生新的bug,也可能需要重构原有代码,整体功能都需要重新测试,产生额外的工作量。
3)解决方案:当软件需要调整时,尽量使用功能扩展的方式,增加新的方法来实现,避免修改原有代码。
实现方式:①抽象是关键,软件中如果没有抽象类或者接口,软件就没有了扩展点。②封装可变性,将软件中的可变因素封装到一个集成结构中,如果多个可变因素混杂,会导致软件系统复杂而混乱。
开闭原则是对象设计中的最基本的设计原则,其他的设计原则及23种设计模式都遵循开闭原则。
2、单一职责原则
1)定义:类中不要存在多余一个导致累变更的原因,一个类只做它自己做的事(高内聚)。
2)问题由来:一个类同时包含了A职责以及B职责,当A职责需要改变时,可能会引起不相关的B职责的功能,产生额外的不可预期的问题。
3)解决方案:遵循单一职责原则,设计两个类,分别实现A和B职责,使A职责与B职责分离,这样当修改其中一个职责时,不会影响到别的职责功能。
虽然单一职责原则很简单,不少人会不屑一顾,即使没有了解过设计原则,在开发中也会应用到,这是常识。但是实际开发中,即使是自身开发工程师,也有可能会违背该原则,这是为什么呢?这是因为功能的扩展,可能在最初设计的时候是符合单一职责原则,但是随着业务的发展,原有的业务需要调整,这个时候需要增加新的职责,如果遵循单一职责原则,可能改动会比较大。
比如职责P是符合单一职责原则的,这个时候要增加新的职责,遵循单一职责原则就需要将原有的类拆分成P1和P2两个类,并且调用的方法也需要修改,改动较大。如果直接在P职责的类中增加新的职责,只需要改动P职责一个地方,改动较小。但是这么做又有风险,因为我们无法预估以后会不会继续扩展出P3、P4。。。
4)单一职责原则的优点:
①可以降低累的复杂度,一个类只有一个职责,逻辑更加清晰;
②提高代码可读性,提高系统的可维护性;
③降低变更带来的风险。
3、里氏替换原则
1)定义:所有父类出现的地方都可以用子类替换,并且使用子类替换不会出错或异常。
2)问题由来:业务逻辑原本由父类F完成,功能扩展后,遵循单一职责原则,新建继承F父类的子类S来完成新功能,如果重写的方法,在引用S子类后,可能会导致原有的F父类功能异常。
3)解决方案:遵循里氏替换原则,保持原有的父类方法,除了扩展新的功能,不修改原有的父类方法。
①子类必须完全实现父类的方法。
如果子类不能完全实现父类的方法,建议断开父子关系,采用Java类之间关联关系的另外三种依赖、关联、组合去实现。
②子类可以增加自己的特有方法。
子类可以实现自己的新功能,子类可以出现在父类出现的地方,相反,父类却不可以出现在子类出现的地方。
③子类重载父类方法时,参数范围要比父类的范围更大。
④子类重写或实现父类方法时,返回值范围要比父类更小。
4、依赖倒置原则
1)定义:高层的类不应该以来低层的类,二者都依赖于抽象。抽象不依赖于细节,细节依赖于抽象。
2)问题由来:类A依赖于类B,当业务扩展时,A需要依赖于类C,这个时候直接修改A,就可能会因为新的问题,产生不必要的缺陷。
3)解决方案:采用依赖倒置原则,将B与C依赖于同样的接口,A通过接口与B和C产生关系,降低直接修改A产生的风险。
4)依赖倒置就是面向接口编程,示例:
class XiaoMing{
//在这里产生了实体类之间的依赖
public void drink(Milk milk){
milk.run();
}
}
class Milk{
public void run(){
System.out.println("drink milk......");
}
}
public class Client{
public static void main(String[] args){
XiaoMing xiaoming= new XiaoMing();
people.drink(new Milk());
}
}
当小明不想喝牛奶,想喝橙汁,除了需要增加橙汁的实现,还需要修改提供给小明喝的饮料入口,以及小明具体喝的饮料
class XiaoMing{
//在这里产生了实体类之间的依赖
public void drink(Juice juice){
juice.run();
}
}
class Milk{
public void run(){
System.out.println("drink milk......");
}
}
class Juice{
public void run(){
System.out.println("drink milk......");
}
}
public class Client{
public static void main(String[] args){
XiaoMing xiaoming= new XiaoMing();
people.drink(new Juice());
}
}
小明只是换一个饮料,却需要改动如此多的代码,风险很大,正确做法应该是给饮品增加一个IDrink接口。
interface IDrink{
public void run();
}
class XiaoMing{
//在这里产生了实体类之间的依赖
public void drink(IDrink idrink){
idrink.run();
}
}
class Milk implements IDrink {
public void run(){
System.out.println("drink milk......");
}
}
class Juice implements IDrink {
public void run(){
System.out.println("drink milk......");
}
}
public class Client{
public static void main(String[] args){
XiaoMing xiaoming= new XiaoMing();
people.drink(new Milk());
people.drink(new Juice());
}
}
这样修改,哪怕小明后期想喝水,只需要增加Water类实现IDrink接口,喝的时候直接给Water即可。
5、接口隔离原则
1)定义:一个类不应该实现它不需要的接口。一个类对另一个类的依赖建立在最小接口上。
2)问题由来:A类通过接口I引用B类,C类通过接口I引用D类,A和C之间有不同的实现接口,则B和D不是最小接口,需要实现他们不需要的接口。
3)解决方案:将接口I拆分非几个独立的接口,分别为A和C提供接口。
4)代码示例:
interface I {
public void method1();
public void method2();
public void method3();
public void method4();
public void method5();
}
class A{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method2();
}
public void depend3(I i){
i.method3();
}
}
class B implements I{
public void method1() {
System.out.println("类B实现接口I的方法1");
}
public void method2() {
System.out.println("类B实现接口I的方法2");
}
public void method3() {
System.out.println("类B实现接口I的方法3");
}
//对于类B来说,method4和method5不是必需的,但是由于接口A中有这两个方法,
//所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
public void method4() {}
public void method5() {}
}
class C{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method4();
}
public void depend3(I i){
i.method5();
}
}
class D implements I{
public void method1() {
System.out.println("类D实现接口I的方法1");
}
//对于类D来说,method2和method3不是必需的,但是由于接口A中有这两个方法,
//所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
public void method2() {}
public void method3() {}
public void method4() {
System.out.println("类D实现接口I的方法4");
}
public void method5() {
System.out.println("类D实现接口I的方法5");
}
}
public class Client{
public static void main(String[] args){
A a = new A();
a.depend1(new B());
a.depend2(new B());
a.depend3(new B());
C c = new C();
c.depend1(new D());
c.depend2(new D());
c.depend3(new D());
}
}
接口I过于臃肿,B和D都需要实现不必要的方法,过于繁琐,可以如下修改:
interface I1 {
public void method1();
}
interface I2 {
public void method2();
public void method3();
}
interface I3 {
public void method4();
public void method5();
}
class A{
public void depend1(I1 i){
i.method1();
}
public void depend2(I2 i){
i.method2();
}
public void depend3(I2 i){
i.method3();
}
}
class B implements I1, I2{
public void method1() {
System.out.println("类B实现接口I1的方法1");
}
public void method2() {
System.out.println("类B实现接口I2的方法2");
}
public void method3() {
System.out.println("类B实现接口I2的方法3");
}
}
class C{
public void depend1(I1 i){
i.method1();
}
public void depend2(I3 i){
i.method4();
}
public void depend3(I3 i){
i.method5();
}
}
class D implements I1, I3{
public void method1() {
System.out.println("类D实现接口I1的方法1");
}
public void method4() {
System.out.println("类D实现接口I3的方法4");
}
public void method5() {
System.out.println("类D实现接口I3的方法5");
}
}
5)接口隔离原则与单一职责原则的比较:
①单一职责原则主要约束的是类,其次才是接口、抽象,接口隔离原则约束的是接口;
②单一职责原则强调的是职责,由业务逻辑划分;接口隔离强调的是接口依赖间的隔离。
6、合成聚合原则
1)定义:经常又叫做合成复用原则(Composite ReusePrinciple或CRP),尽量使用对象组合,而不是继承来达到复用的目的。
2)问题由来:继承复用破坏包装,因为继承将超类的实现细节暴露给了子类;合成聚合耦合度相对较低,选择性地调用成员对象的操作;可以在运行时动态进行。
3)解决方案:组合/聚合可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
4)示例
class A {
private B b;
public A (){
b = new B();
}
}
class B {
}
迪米特原则
1)定义:一个对象应该对其他对象保持最少的了解。
2)问题由来:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
3)解决方案:尽量降低类与类之间的耦合。一个对象应该对其他对象有最少的了解,也就是说一个类要对自己需要耦合或者调用的类知道的最少。我只知道你有多少public方法可以供我调用,而其他的一切都与我无关。
4)迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,例如本例中,总公司就是通过分公司这个“中介”来与分公司的员工发生联系的。过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。