开闭原则

定义: 一个软件实体如类、模块和函数应该对扩展开放,对修改关闭

用抽象构建框架,用实现扩展细节

优点:提高软件系统的可复用性及可维护性

案例

原程序为

  • ICourse.java

    public interface ICourse {
        Integer getId();
        Double getPrice();
    }
    
  • JavaCourse.java

    java课程implements 总接口

    public class JavaCourse implements ICourse {
        private Integer id;
        private Double price;
    
        public JavaCourse(Integer id, Double price) {
            this.id = id;
            this.price = price;
        }
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public Double getPrice() {
            return price;
        }
    
        public void setPrice(Double price) {
            this.price = price;
        }
    }
    

接下来我想实现一个打折功能,可以直接新建一个打折课程类继承JavaCourse

public class JavaDiscountCourse extends JavaCourse {
    public JavaDiscountCourse(Integer id, Double price) {
        super(id, price);
    }

    public Double getPrice() {
        return super.getPrice();
    }

    public Double getDiscountPrice() {
        return super.getPrice() * 0.8;
    }
}

这样做,没有更改原代码,只是在其基础上添加了其他功能,提高了软件系统的可复用性及可维护性

依赖倒置原则

定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象

抽象不应该依赖细节;细节应该依赖抽象

针对接口编程,不要针对实现编程

优点:可以减少类间的耦合性、提高系统稳定性,提高代码可读性 和可维护性,可降低修改程序所造成的风险

一、面向实现编程(不提倡)

耦合度过高,增添新功能不方便

  • Person.java

    public class Person {
        public void studyJavaCourse() {
            new JavaCourse().studyCourse();
        }
    
        public void studyPythonCourse() {
            new PythonCourse().studyCourse();
        }
    }
    

二、改进方法一

使JavaCourse、PythonCourse等implements自统一的接口ICourse,并实现studyCourse()方法

public class Person {
    public void studyCourse(ICourse course) {
        course.studyCourse();
    }
}
  • Main.java

    Person person = new Person();
    person.studyCourse(new JavaCourse());
    person.studyCourse(new PythonCourse());
    

三、改进方法二

在构造方法中指定ICourse类型

  • Person.java

    public class Person {
        private ICourse course;
    
        public Person(ICourse course){
            this.course=course;
        }
    
        public void studyCourse() {
            course.studyCourse();
        }
    }
    
  • Main.java

    Person person = new Person(new JavaCourse());
    person.studyCourse();
    

但是这种方法一个person只能指定学习一门课程,因此可以增加一个设置课程set方法

四、改进方法三

  • Person.java

    public class Person {
        private ICourse course;
    
        public void setCourse(ICourse course) {
            this.course = course;
        }
    
        public void studyCourse() {
            course.studyCourse();
        }
    }
    
  • Main.java

    Person person = new Person();
    person.setCourse(new JavaCourse());
    person.studyCourse();
    
    person.setCourse(new PythonCourse());
    person.studyCourse();
    

单一职责原则

定义:不要存在多于一个导致类变更的原因

一个类/接口/方法只负责一项职责

优点: 降低类的复杂度、提高类的可读性, 提高系统的可维护性、降低变更引起的风险

样例一:分解类

  • 原代码

    • Bird

      public class Bird {
          public void mainMoveMode(String birdName){
              if("鸵鸟".equals(birdName)){
                  System.out.println(birdName+"用脚走");
              }else{
                  System.out.println(birdName+"用翅膀飞");
              }
          }
      }
      

      方法中既有走的,也有飞的,可以将其分解为两个类

  • 修改后

    • WalkBird

      public class WalkBird {
          public void mainMoveMode(String birdName) {
              System.out.println(birdName + "用脚走");
          }
      }
      
    • FlyBird

      public class FlyBird {
          public void mainMoveMode(String birdName) {
              System.out.println(birdName + "用翅膀飞");
          }
      }
      

样例二:分解接口

  • 原代码

    前两个方法和后两个方法类型功能不一样,需要分解

    public interface ICourse {
        String getCourseName(); // 获取课程名称
    
        byte[] getCourseVideo(); // 获取课程视频
    
        void studyCourse(); //学习课程
    
        void refundCourse(); //退还课程
    }
    
  • 更改后

    • ICourseContent

      public interface ICourseContent {
          String getCourseName();
      
          byte[] getCourseVideo();
      }
      
    • ICourseManager

      public interface ICourseManager {
          void studyCourse();
      
          void refundCourse();
      }
      
    • CourseImpl

      实现类组合两个接口

      public class CourseImpl implements ICourseManager, ICourseContent {
          @Override
          public void studyCourse() {
          }
      
          @Override
          public void refundCourse() {
          }
      
          @Override
          public String getCourseName() {
              return null;
          }
      
          @Override
          public byte[] getCourseVideo() {
              return new byte[0];
          }
      }
      

接口隔离原则

定义: 用多个专门的接口,而不使用单一的总接口, 客户端不应该依赖它不需要的接口

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

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

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

  • 注意适度原则,一定要适度
  • 优点 : 符合我们常说的高内聚低耦合的设计思想 ,从而使得类具有很好的可读性、可扩展性和可维护性。

原样例: 一个animal的类

缺点很明显,实现类不管需不需要这三种方法,都需要实现,内聚度比较低

public interface IAnimalAction {
    void eat();
    void fly();
    void swim();
}

将该接口拆分为三个接口

public interface IEatAnimalAction {
    void eat();
}

-----------------------------------
    
public interface IFlyAnimalAction {
    void fly();
}

--------------------------------
    
public interface ISwimAnimalAction {
    void swim();
}
  • Dog实现类

    public class Dog implements ISwimAnimalAction,IEatAnimalAction {
        @Override
        public void eat() {
            System.out.println("eat");
        }
    
        @Override
        public void swim() {
            System.out.println("swim");
        }
    }
    

迪米特原则

定义:一个对象应该对其他对象保持最少的了解。又叫最少知道原则

尽量降低类与类之间的耦合

优点:降低类之间的耦合

  • 强调只和朋友交流,不和陌生人说话
  • 朋友: 出现在成员变量、方法的输入、输出参数中的类称为成员朋友类, 而出现在方法体内部的类不属于朋友类。

原样例

  • Boss让员工去查课程数,但是boss的方法却涉及到了course

    public class Boss {
        public void commandCheckNumber(TeamLeader teamLeader) {
            List<Course> courses = new ArrayList<>();
            teamLeader.checkNumberOfCourses(courses);
        }
    }
    
  • Boss表示不想看到课程,一切都让苦逼的员工去做,于是修改为

    • Boss

      public class Boss {
          public void commandCheckNumber(TeamLeader teamLeader) {
              teamLeader.checkNumberOfCourses();
          }
      }
      
    • TeamLeader

      public class TeamLeader {
          public void checkNumberOfCourses(){
              List<Course> courseList = new ArrayList<Course>();
              for(int i = 0 ;i < 20;i++){
                  courseList.add(new Course());
              }
              System.out.println("在线课程的数量是:"+courseList.size());
          }
      }
      
  • Main

    public static void main(String[] args) {
        Boss boss = new Boss();
        TeamLeader teamLeader = new TeamLeader();
        boss.commandCheckNumber(teamLeader);
    }
    

里氏替换原则

所有引用基类(父类)的地方必须能透明地使用其子类的对象。
通俗的说,子类可以扩展父类功能,但不能改变父类原有功能。

核心思想是继承。 通过继承,引用基类的地方就可以使用其子类的对象了。例如:

Parent parent = new Child();

重点来了,那么如何透明地使用呢?

我们来思考个问题,子类可以改变父类的原有功能吗?

public class Parent {
    public int add(int a, int b){
        return a+b;
    }
}

public class Child extends Parent{
    @Override
    public int add(int a, int b) {
        return a-b;
    }
}

这样好不好?

肯定是不好的,本来是加法却修改成了减法,这显然是不符合认知的。

它违背了里氏替换原则,子类改变了父类原有功能后,当我们在引用父类的地方使用其子类的时候,没办法透明使用add方法了。

父类中凡是已经实现好的方法,实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些规范,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。

所以,透明使用的关键就是,子类不能改变父类原有功能

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

2、子类中可以增加自己特有的方法。

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

注意,是子类重载父类,而不是子类重写父类。

4、当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

合成/复用原则(组合/复用原则)

合成/聚合复用原则是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的

简述为:要尽量使用合成/聚合,尽量不要使用继承

合成和聚合的区别: 依赖和关联

合成(Composition)和聚合(Aggregation)都是关联(Association)的特殊种类。用C语言来讲,合成是值的聚合(Aggregation by Value),聚合是则是引用的聚合(Aggregation by Reference)

(1)聚合用来表示“拥有”关系或者整体与部分的关系。代表部分的对象有可能会被多个代表整体的对象所共享,而且不一定会随着某个代表整体的对象被销毁或破坏而被销毁或破坏,部分的生命周期可以超越整体。例如,班级和学生,当班级删除后,学生还能存在,学生可以被培训机构引用。

class Student {
}

class Classes{
         privateStudent student;
         publicClasses(Student student){
                   this.student=student;
        }
}

**(2)合成用来表示一种强得多的“拥有”关系。**在一个合成关系里,部分和整体的生命周期是一样的。一个合成的新对象完全拥有对其组成部分的支配权,包括它们的创建和湮灭等。使用程序语言的术语来说,合成而成的新对象对组成部分的内存分配、内存释放有绝对的责任。

一个合成关系中的成分对象是不能与另一个合成关系共享的。一个成分对象在同一个时间内只能属于一个合成关系。如果一个合成关系湮灭了,那么所有的成分对象要么自己湮灭所有的成分对象(这种情况较为普遍)要么就得将这一责任交给别人(较为罕见)。

例如,一个人由头、四肢和各种器官组成,人与这些具有相同的生命周期,人死了,这些器官也就挂了。房子和房间的关系,当房子没了,房间也不可能独立存在。

class Room{
         public Room createRoom(){
                   System.out.println(“创建房间”);
                   returnnew Room();
          }
 }

class House{
         private Room room;

         public House(){
               room=new Room();
          }

         public void createHouse(){
                room.createRoom();
         }
  }

(3)依赖和关联

依赖(Dependency)

依赖是类与类之间的连接,表示一个类依赖于另外一个类的定义。依赖关系仅仅描述了类与类之间的一种使用与被使用的关系,在Java中体现为局部变量、方法的参数或者是对静态方法的调用

static class Boat{ 
       public static void row(){ 
           System.out.println("开动"); 
       } 
} 

class Person{ 
        public void crossRiver(Boatboat){ 
            boat.row(); 
        } 
         
        public void fishing(){ 
            Boat boat =new Boat() ; 
            boat.row(); 

        }

       public void patrol(){ 
            Boat.row() ; 
       } 
} 

关联(Association)

关联是类与类之间的连结。关联关系使一个类知道另外一个类的属性和方法。关联可以是双向的,也可以是单向的。体现在Java中,关联关系是通过成员变量来实现的

class Computer{ 
    public void develop(){ 
       System.out.println("Develop "); 
    } 
} 

class Person{ 
       private Computer computer ; 
         
       public Person(Computer computer){ 
           this.computer = computer ; 
       } 
         
       public void work(){ 
           computer.develop() ; 
           System.out.println("work"); 
       } 
 } 

为什么使用合成/聚合复用,而不使用继承复用?

在面向对象的设计里,有两种基本的方法可以在不同的环境中复用已有的设计和实现,即通过合成/聚合复用和通过继承复用。两者的特点和区别,优点和缺点如下。

1、合成/聚合复用

由于合成或聚合可以将已有对象纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能。这样做的好处有

(1) 新对象存取成分对象的唯一方法是通过成分对象的接口。

(2) 这种复用是黑箱复用,因为成分对象的内部细节是新对象看不见的。

(3) 这种复用支持包装。

(4) 这种复用所需的依赖较少。

(5) 每一个新的类可以将焦点集中到一个任务上。

(6) 这种复用可以再运行时间内动态进行,新对象可以动态地引用与成分对象类型相同的对象。

一般而言,如果一个角色得到了更多的责任,那么可以使用合成/聚合关系将新的责任委派到合适的对象。当然,这种复用也有缺点。最主要的缺点就是通过这种复用建造的系统会有较多的对象需要管理。

2、继承复用

继承复用通过扩展一个已有对象的实现来得到新的功能,基类明显的捕获共同的属性和方法,而子类通过增加新的属性和方法来扩展超类的实现。继承是类型的复用。

继承复用的优点。

(1) 新的实现较为容易,因为超类的大部分功能可以通过继承关系自动进入子类。

(2) 修改或扩展继承而来的实现较为容易。

继承复用的缺点。

(1) 继承复用破坏包装,因为继承将超类的实现细节暴露给了子类。因为超类的内部细节常常对子类是透明的,因此这种复用是透明的复用,又叫“白箱”复用。

(2) 如果超类的实现改变了,那么子类的实现也不得不发生改变。因此,当一个基类发生了改变时,这种改变会传导到一级又一级的子类,使得设计师不得不相应的改变这些子类,以适应超类的变化。

(3) 从超类继承而来的实现是静态的,不可能在运行时间内发生变化,因此没有足够的灵活性。

由于继承复用有以上的缺点,所有尽量使用合成/聚合而不是继承来达到对实现的复用,是非常重要的设计原则。


hhhhh