七大设计原则

121 阅读7分钟

开闭原则

定义

开闭原则(Open-Closed Principle OCP),指一个软件实体如类、模块和函数应该对外扩展开放,对修改关闭

强调用抽象构建看框架,用实现扩展细节,可以提高软件系统的可复用性及可维护性。

例子

课程生态有Java,Python,Web课程。现在要给Java课程做活动,给予8折的优惠处理。此时如果修改Java课程的价格,则会存在一定的风险,可能影响其他地方的调用结果。那么如果可以在不修改原有代码的的前提下,实现价格优惠呢?

public interface Course {
   Integer getId();
   String getName();
   Double getPrice();
}
public class JavaCourse implements Course{
   private Integer id;
   private String name;
   private Double price;
​
   public JavaCourse(Integer id, String name, Double price) {
       this.id = id;
       this.name = name;
       this.price = price;
  }
​
   @Override
   public Integer getId() {
       return this.id;
  }
​
   @Override
   public String getName() {
       return this.name;
  }
​
   @Override
   public Double getPrice() {
       return this.price;
  }
}

由于直接修改类的价格方法,会影响调用。那么可以 写一个 处理优惠逻辑的类,通过这类来专门处理优惠活动。

public class JavaDiscountCourse extends JavaCourse{
   public JavaDiscountCourse(Integer id, String name, Double price) {
       super(id, name, price);
  }
​
   public Double getOriginPrice(){
       return super.getPrice() * 0.8;
  }
}

依赖倒置原则

定义

依赖倒置原则(Dependence Inversion Principle,DIP),指设计代码结构时,高层模块不应该依赖底层模块,二者应该依赖起抽象。

通过依赖倒置可以降低类与类之间的耦合度,提高系统的稳定性,提高代码的可读性和可维护性,并降低修改程序带来的风险。

例子

存在一类学生热爱学习,目前正在学习Java课程和Python课程。当这类学生还想学习Web课程时,则需要进行业务员扩展。代码便要从底层到高层(调用层)一次修改代码。这样则是不稳定的,容易伴随意想不到的风险。

public class Student {
   public void studyJavaCourse(){
       System.out.println("study Java...");
  }
   
   public void studyPythonCourse(){
       System.out.println("study Python...");
  }
   
}

此时,可以通过高度依赖抽象进行修改。可以创建课程接口,然后实现各个课程的实现类,再对学生类进行改造。

public interface Course {
   void study();
}
​
public class JavaCourse implements Course{
   @Override
   public void study() {
       System.out.println("study Java...");
  }
}
​
public class PythonCourse implements Course{
   @Override
   public void study() {
       System.out.println("study Python...");
  }
}
public class Student {
   public void study(Course course){
       course.study();
  }
}

此时,不管学生类学习什么课程,都只需要新建一个类,便可以知道该学生正在学习什么。这种方式叫依赖注入。还有构造器注入方式和Setter注入方式。

构造器注入方式

public class Student {
   private Course course;
​
   public Student(Course course){
       this.course = course;
  }
   public void study(){
       course.study();
  }
}

根据构造器注入方式,调用时,每次都要创建实例。而当学生类是单例时,则只能选择Setter注入方式。

public class Student {
   private Course course;
​
   public void setCourse(Course course){
       this.course = course;
  }
   
   public void study(){
       course.study();
  }
}
​

以抽象为基准 比 以细节为基准 搭建起来的架构要稳定得多,因此在拿到需求时,要面向接口编程,按照先顶层再谢姐的顺序设计代码结构。


单一职责原则

定义

单一职责原则(Simple Responsibilty Principle SRP),指一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。

当一个Class负责两个职责,如若有一个职责发生需求变更,修改了逻辑代码,则可能会导致另一个职责的功能发生故障。

根据单一职责,需要分别用两个Class来实现来给那个职责,进行解藕。后期需求变更时互不影响。

一个Class、Interface、Method只负责一项职责

例子

课程含有直播课和录播课,其中直播课不可以快进和快退,录播课可以任意地观看。假如选择需要对课程进行加密,而直播课和录播课的加密逻辑时不同的,此时必须要修改代码,势必会互相影响,带来不可控的风险。

public class Course {
    public void study(String courseName){
        if ("Live".equals(courseName)){
            System.out.println("不能快退");
        }else{
            System.out.println("可以任意播放");
        }
    } 
}

此时,需要对职责进行分离解藕,分别创建两给类LiveCourse和ReplayCourse。

public class LiveCourse {
    public void study(String courseName){
        System.out.println(courseName + "不能快进");
    }
}

public class ReplayCourse {
    public void study(String courseName){
        System.out.println(courseName + "可以任意播放");
    }
}

当业务继续发展时,要对课程做权限。没有付费的学院可以获得课程的基本信息,已经付费的学员可以获取视频流,即学习权限。“那么对于控制课程层面,至少有两个职责。我们可以把展示职责和管理职责分离开,都实现同一个抽象依赖。设计一个顶层接口,创建Course接口

public interface Course {
    //获取课程的基本信息
    String getCourse();

    //获取视频流
    byte[] studyCourse();
    
    //退款
    void refundCourse();
}

可以再将这个接口拆分成两个接口

public interface CourseInfo {
    String getCourseName();
    
    byte[] getCourseVideo();
}

public interface CourseManager {
    void studyCourse();
    void refundCourse();
}

接口隔离原则

定义

接口隔离原则(Interface Segregation Principle,ISP)指用多个专门的接口,而不是用单一的总接口,并且客户端不应该依赖它不需要的接口。

这个原则需要注意几点:

1.一个类对另一个类的依赖应该建立在最小接口上

2.建立单一接口,不要建立庞大臃肿的接口

3.尽量细化接口,接口中的方法尽量少

例子

一个动物接口,存在吃、飞、游等行为

public interface Animal {
    void eat();
    void fly();
    void swim();
}
public class Bird implements Animal{
    @Override
    public void eat() {}

    @Override
    public void fly() {}

    @Override
    public void swim() {}
}

public class Dog implements Animal{
    @Override
    public void eat() {}

    @Override
    public void fly() {}

    @Override
    public void swim() {}
}

此时,Bird的swim()无法实现,而Dog的fly()也无法实现。故采取接口隔离原则,来设计不同的接口诠释不同动物的行为。

public interface AnimalEat {
    void eat();
}

public interface AnimalFly {
    void fly();
}

public interface AnimalSwim {
    void swim();
}

此时Dog和Brild的类便更加完善

public class Dog implements AnimalSwim,AnimalEat{
    @Override
    public void eat() {}
    
    @Override
    public void swim() {}
}

public class Bird implements AnimalFly,AnimalEat{
    @Override
    public void eat() {}

    @Override
    public void fly() {}
}

迪米特法则

定义

迪米特法则(Law of Demeter LoD),又叫最少知道原则,指一个对象应该对其他对象保持最少的了解,尽量降低类之间的耦合。

强调只和朋友交流(成员变量、方法的输入,输出参数),不和陌生人(方法体内部)说话。

例子

设计一个权限系统,TeamLeader需要查看目前发布到线上的课程数量。此时TeamLeader要让Employee去统计。

Course.class

public class Course {
}

Emploee.class

public class Employee {
    public void checkNumberOfCourses(List<Course> coureseList){
        System.out.println("目前已发布的课程数量为"+coureseList.size());
    }
}

TeamLeader.class

public class TeamLeader {
    public void commandCheckNumber(Employee employee){
        List<Course> courseList = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            courseList.add(new Course());
        }
        employee.checkNumberOfCourses(courseList);
    }
}

此时Employee统计需要引用Course对象,TeamLeader和Course并不是朋友。

改造Employee

public class Employee {
    public void checkNumberOfCourses(){
        List<Course> courseList = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            courseList.add(new Course());
        }
        System.out.println("目前已发布的课程数量为"+courseList.size());
    }
}
public class TeamLeader {
   public void commandCheckNumber(Employee employee){
       employee.checkNumberOfCourses();
  }
}

里氏替换原则

定义

里氏替换原则(Liskov Substitution Principle LSP)指如果对每一个类型为T1的对象O1,都有T2的对象O2,使得T1定义的所有程序P在所有的对象O1都替换成O2时,程序P的行为没有发生变化,那么T2是类型T1的字类型。

一个软件实体如果适用于一个父类,则一定适用于子类。“所有引用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变。也可以理解为,子类可以扩展父类的功能,但不能改变父类原有的功能。

1.子类可以实现父类的抽象方法,但不能覆盖非抽象方法

2.子类可以增加自己特有的方法

3.当子类的方法重载父类的方法时,方法对的前置条件(即方法的输入参数)要比父类的方法更宽松。

4.当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类的方法更严格或相等。

优点:

约束继承泛滥,是开闭原则的一种体现
加强程序的健壮性,同时变更可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险。

合成复用原则

定义

合成复用(Composite/Aggregate Reuse Principle,CARP)指尽量使用对象组合(has-a)或者对象聚合(contains-a)的方式实现代码复用,而不是用继承关系达成代码复用的目的。