关于java:优雅代码的秘密都藏在这6个设计原则中

6次阅读

共计 6778 个字符,预计需要花费 17 分钟才能阅读完成。

优雅的代码,犹如亭亭玉立的美女,让人赏心悦目。而蹩脚的代码,却犹如屎山,让人避而远之。

如何写出优雅的代码呢?那就要了解并相熟利用这 6 个设计准则啦:开闭准则、繁多职责准则、接口隔离准则、迪米特法令、里氏替换准则、依赖倒置准则。本文呢,将通过代码 demo,让大家轻松了解这 6 个代码设计准则,加油~

1. 开闭准则

开闭准则,即 对扩大凋谢,对批改敞开

对于扩大和批改,咱们怎么去了解它呢?扩大凋谢 示意,将来业务需要是变动万千,代码应该放弃灵便的应变能力 批改敞开 示意不容许在原来类批改,放弃稳定性

因为日常需要是一直迭代更新的,所以咱们常常须要在原来的代码中批改。如果代码设计得不好,扩展性不强,每次需要迭代,都要在原来代码中批改,很可能会引入 bug。因而,咱们的代码应该遵循开闭准则,也就是 对扩大凋谢,对批改敞开

为了不便大家了解开闭准则,咱们来看个例子:假如有这样的业务场景,大数据系统把文件推送过去,依据不同类型采取不同的解析形式。少数的小伙伴就会写出以下的代码:

if(type=="A"){// 依照 A 格局解析}else if(type=="B"){// 按 B 格局解析}else{// 依照默认格局解析}

这段代码有什么问题呢?

  • 如果分支变多,这里的代码就会变得臃肿,难以保护,可读性低。
  • 如果你须要接入一种新的解析类型,那只能在原有代码上批改。

显然,减少、删除某个逻辑 ,都须要批改到原来类的代码,这就违反了 开闭准则 了。为了解决这个问题,咱们能够应用 策略模式 去优化它。

你能够先申明一个文件解析的接口,如下:

public interface IFileStrategy {
    
    // 属于哪种文件解析类型,A 或者 B
    FileTypeResolveEnum gainFileType();
    
    // 封装的专用算法(具体的解析办法)void resolve(Object param);
}

而后实现不同策略的解析文件,如类型 A 解析:

@Component
public class AFileResolve implements IFileStrategy {
    
    @Override
    public FileTypeResolveEnum gainFileType() {return FileTypeResolveEnum.File_A_RESOLVE;}

    @Override
    public void resolve(Object objectparam) {logger.info("A 类型解析文件,参数:{}",objectparam);
      // A 类型解析具体逻辑
    }
}

如果将来需要变更的话,比方减少、删除某个逻辑,不会再批改到原来的类啦,只须要批改对应的文件解析类型的类即可。

2. 繁多职责准则

繁多职责准则:一个类或者一个接口,最好只负责一项职责。比方一个类 C 违反了繁多准则,它负责两个职责 P1P2。当职责 P1 须要批改时,就会改变到类 C,这就可能导致本来失常的P2 也受影响。

如何更好了解呢?比方你实现一个图书管理系统,一个类既有图书的增删改查,又有读者的增删改查,你就能够认为这个类违反了 繁多准则。因为这个类波及了不同的性能职责点,你能够把这个拆分。

以上图书管理系统这个例子,违反繁多准则,按业务拆分。这比拟好了解,然而有时候,一个类并不是那么好辨别。这时候大家能够看这个规范,来判断性能职责是否繁多:

  • 类中的公有办法过多
  • 你很难给类起一个适合的名字
  • 类中的代码行数、函数或者属性过多
  • 类中大量的办法都是集中操作类中的某几个属性
  • 类依赖的其余类过多,或者依赖类的其余类过多

比方,你写了一个办法,这个办法包含了 日期解决 借还书 的业务操作,你就能够把日期解决抽到公有办法。再而后,如果你发现,很多公有办法,都是相似的日期解决,你就能够把这个日期解决办法抽成一个工具类。

日常开发中,繁多准则的思维都有体现的。比方微服务拆分。

3. 接口隔离准则

接口隔离准则:接口的调用者或者使用者,不应该强制依赖它不须要的接口。它要求建设繁多的接口,不要建设宏大臃肿的接口,尽量细化接口,接口中的办法尽量少,让接口中只蕴含客户(调用者)感兴趣的办法。即一个类对另一个类的依赖应该建设在最小的接口上。

比方类 A 通过接口 I 依赖类 B,类C 通过接口 I 依赖类 D,如果接口I 对于类 A 和类 B 来说,都不是最小接口,则类 B 和类 D 必须去实现他们不须要的办法。如下图:

这个图表白的意思是:类 A 依赖接口 I 中的 method1method2,类 B 是对类 A 依赖的实现。类 C 依赖接口I 中的method1method3,类 D 是对类 C 依赖的实现。对于实现类 B 和 D,它们都存在用不到的办法,然而因为实现了接口 I,所以必须要实现这些用不到的办法。

能够看下以下代码:

public interface I {void method1();

    void method2();

    void method3();}

@Service
public class A {@Resource(name="B")
    private I i;

    public void depend1() {i.method1();
    }

    public void depend2(){i.method2();
    }

}

@Service("B")
public class B implements I {

    @Override
    public void method1() {System.out.println("类 B 实现接口 I 的办法 1");
    }

    @Override
    public void method2() {System.out.println("类 B 实现接口 I 的办法 2");
    }

    // 没用到这个办法,然而也要默认实现,因为 I 有这个接口办法
    @Override
    public void method3() {}
}

@Service
public class C {@Resource(name="D")
    private I i;

    public void depend1(I i){i.method1();
    }

    public void depend3(I i){i.method3();
    }

}

@Service("D")
public class D implements I {

    @Override
    public void method1() {System.out.println("类 D 实现接口 I 的办法 1");
    }

    // 没用到这个办法,然而也要默认实现,因为 I 有这个接口办法
    @Override
    public void method2() {}

    @Override
    public void method3() {System.out.println("类 D 实现接口 I 的办法 3");
    }
}

大家能够发现,如果接口过于臃肿,只有接口中呈现的办法,不论对依赖于它的类有没有用到,实现类都必须去实现这些办法。实现类 B 没用到 method3,它也要有个默认实现。实现类D 没用到method2,它也要有个默认实现。

显然,这不是一个好的设计,违反了接口隔离准则。咱们能够对接口 I 进行拆分。拆分后的设计如图 2 所示:

接口是不是分得越细越好呢?并不是。日常开发中,采纳接口隔离准则对接口进行束缚时,要留神以下几点:

  • 接口尽量小,然而要有限度。对接口进行细化能够进步程序设计灵活性是不挣的事实,然而如果过小,则会造成接口数量过多,使设计复杂化。所以肯定要适度。
  • 为依赖接口的类定制服务,只裸露给调用的类它须要的办法,它不须要的办法则暗藏起来。只有专一地为一个模块提供定制服务,能力建设最小的依赖关系。
  • 进步内聚,缩小对外交互。使接口用起码的办法去实现最多的事件。使用接口隔离准则,肯定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些工夫去思考和策划,能力精确地实际这一准则。

4. 迪米特法令

定义:又叫起码晓得准则。一个类对于其余类晓得的越少越好,就是说一个对象该当对其余对象有尽可能少的理解, 只和敌人谈心,不和陌生人谈话。它的核心思想就是,尽量升高类与类之间的耦合,尽最大能力减小代码批改带来的对原有的零碎的影响

比方一个生存例子:你对你的对象必定理解的很多,然而如果你对他人的对象也理解很多,你的对象要是晓得,那就要出小事了。

咱们来看下一个违反迪米特法令的例子,业务场景是这样的:一个学校,要求打印出所有师生的 ID。

// 学生  
class Student{  
    private String id;  
    public void setId(String id){this.id = id;}  
    public String getId(){return id;}  
}  

// 老师  
class Teacher{  
    private String id;  
    public void setId(String id){this.id = id;}  
    public String getId(){return id;}  
}  

// 管理者(班长)public class Monitor {
 
    // 所有学生
    public List<Student> getAllStudent(){List<Student> list = new ArrayList<Student>();
        for(int i=0; i<100; i++){Student student = new Student();
            // 为每个学生调配个 ID
            student.setId("学生 Id:"+i);
            list.add(student);
        }
        return list;
    }

}

// 校长
public class Principal {

    // 所有老师
    public List<Teacher> getAllTeacher(){List<Teacher> list = new ArrayList<Teacher>();
        for(int i=0; i<20; i++){Teacher emp = new Teacher();
            // 为全校老师按程序调配一个 ID
            emp.setId("老师编号"+i);
            list.add(emp);
        }
        return list;
    }

    // 所有师生
    public void printAllTeacherAndStudent(ClassMonitor classMonitor) {List<Student> list1 = classMonitor.getAllStudent();
        for (Student s : list1) {System.out.println(s.getId());
        }

        List<Teacher> list2 = this.getAllTeacher();
        for (Teacher teacher : list2) {System.out.println(teacher.getId());
        }
    }
}

这块代码。问题出在类 Principal 中,依据迪米特法令,只能与间接的敌人产生通信,而 Student 类并不是 Principal 类的间接敌人(以局部变量呈现的耦合不属于间接敌人 ),从逻辑上讲校长Principal 只与管理者 Monitor 耦合就行了,能够让 Principal 继承类 Monitor,重写一个printMember 的办法。优化后的代码如下:

public class Monitor {public List<Student> getAllStudent(){List<Student> list = new ArrayList<Student>();
        for(int i=0; i<100; i++){Student student = new Student();
            // 为每个学生调配个 ID
            student.setId("学生 Id:"+i);
            list.add(student);
        }
        return list;
    }

    public void printMember() {List<Student> list = this.getAllStudent();
        for (Student student : list) {System.out.println(student.getId());
        }
    }
}

public class Principal extends Monitor {public List<Teacher> getAllTeacher(){List<Teacher> list = new ArrayList<Teacher>();
        for(int i=0; i<30; i++){Teacher emp = new Teacher();
            // 为全校老师按程序调配一个 ID
            emp.setId("老师编号"+i);
            list.add(emp);
        }
        return list;
    }

    public void printMember() {super.printMember();

        for (Teacher teacher : this.getAllTeacher()) {System.out.println(teacher.getId());
        }
    }
}

5. 里氏替换准则

里氏替换准则:

如果对每一个类型为 S 的对象 o1,都有类型为T 的对象 o2,使得以T 定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 S 是类型 T 的子类型。

一句话来形容就是:只有有父类呈现的中央,都能够用子类来代替,而且不会呈现任何谬误和异样。 更艰深点讲,就是子类能够扩大父类的性能,然而不能扭转父类原有的性能。

其实,对里氏替换准则的定义能够总结如下:

  • 子类能够实现父类的形象办法,但不能笼罩父类的非形象办法
  • 子类中能够减少本人特有的办法
  • 当子类的办法重载父类的办法时,办法的前置条件(即办法的输出参数)要比父类的办法更宽松
  • 当子类的办法实现父类的办法时(重写 / 重载或实现形象办法),办法的后置条件(即办法的的输入 / 返回值)要比父类的办法更严格或相等

咱们来看个例子:


public class Cache {public void set(String key, String value) {}}

public class RedisCache extends Cache {public void set(String key, String value) {}}

这里例子是没有违反里氏替换准则的,任何父类、父接口呈现的中央子类都能够呈现。如果给 RedisCache 加上参数校验,如下:

public class Cache {public void set(String key, String value) {}}

public class RedisCache extends Cache {public void set(String key, String value) {if (key == null || key.length() < 10 || key.length() > 100) {System.out.println("key 的长度不符合要求");
            throw new IllegalArgumentException();}
    }
}

这就违反了里氏替换准则了,因为子类办法减少了加了参数校验,抛出了异样,尽管子类依然能够来替换父类。

6. 依赖倒置准则

依赖倒置准则定义:

高层模块不应该依赖低层模块,两者都应该依赖其形象;形象不应该依赖细节,细节应该依赖形象。它的核心思想是:要面向接口编程,而不要面向实现编程

依赖倒置准则能够升高类间的耦合性、进步零碎的稳定性、缩小并行开发引起的危险、进步代码的可读性和可维护性。要满足依赖倒置准则,咱们须要在我的项目中满足这个规定:

  • 每个类尽量提供接口或抽象类,或者两者都具备。
  • 变量的申明类型尽量是接口或者是抽象类。
  • 任何类都不应该从具体类派生。
  • 应用继承时尽量遵循里氏替换准则。

咱们来看一段 违反依赖倒置准则 的代码,业务需要是:顾客从淘宝购物。代码如下:

class Customer{public void shopping(TaoBaoShop shop){
        // 购物
        System.out.println(shop.buy());
    }
}

以上代码是存在问题的,如果将来产品变更需要,改为顾客从京东上购物,就须要把代码批改为:

class Customer{public void shopping(JingDongShop shop){
        // 购物
        System.out.println(shop.buy());
    }
}

如果产品又变更为从天猫购物呢?那有得批改代码了,显然这违反了 开闭准则 。顾客类设计时,同具体的购物平台类绑定了,这违反了 依赖倒置 准则。能够设计一个 shop 接口,不同购物平台(如淘宝、京东)实现于这个接口,即批改顾客类面向 该接口编程,就能够解决这个问题了。代码如下:

class Customer{public void shopping(Shop shop){
        // 购物
        System.out.println(shop.buy());
    }
}

interface Shop{String buy();
}

Class TaoBaoShop implements Shop{public String buy(){return "从淘宝购物";}
}

Class JingDongShop implements Shop{public String buy(){return "从京东购物";}
}
正文完
 0