【面向对象设计原则】

134 阅读9分钟

概述

面向对象设计原则是学习设计模式的基础,每一种设计模式都符合某一种或多种面向对象原则。通过在软件开发中使用这些原则,可以提高软件的可维护性和可复用性,让我们可以设计出更加灵活也更容易扩展的软件系统,实现可维护性复用的目标。

面向对象七大原则

  1. 单一职责原则
    • 类的职责要单一,不能将太多的职责放在一个类中
    • 用于控制类的粒度大小
  2. 开闭原则
    • 软件实体对扩展是开放的,但是对修改是关闭的,即在不修改一个软件实体的基础上去扩展其功能
    • 最重要的面向对象设计原则
  3. 里氏替换原则
    • 在软件系统中,一个可以接收基类对象的地方必然可以接收一个子类对象
    • 核心是对系统进行抽象
  4. 依赖倒转原则
    • 要针对抽象层编程,而不是针对具体类编程
    • 代码要依赖于抽象的类,而不是具体的类
  5. 接口隔离原则
    • 使用多个专门的接口来取代一个统一的接口
  6. 合成复用原则
    • 在复用功能时,应该尽量多使用组合和聚合关联关系,尽量少使用甚至不使用继承关系
  7. 迪米特法则
    • 一个软甲实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三者发生间接交互

单一职责原则实例

某基于Java的C/S系统的“登录功能”通过如下登录类(Login)实现

classDiagram
    class Login
    Login : + init() void
    Login : + display() void
    Login : + validate() void
    Login : + getConnection() Connection
    Login : + findUser(String name, String pwd) boolean
    Login : + main() void

说明

  • init() 方法用于初始化初始化按钮、文本框等界面控件
  • display() 方法用于向界面容器中添加界面控件并显示窗口
  • validate() 方法用于校验登录数据
  • getConnection() 方法用于获取数据库连接对象
  • findUser() 方法用于查询用户
  • main() 方法是程序的入口

使用单一职责原则对其进行重构

拆分为如下4个类

  1. LoginForm 负责界面展示,它只包含与界面有关的方法和事件处理方法
  2. UserDAO 负责用户表的CRUD操作
  3. DBUtil 负责数据库的连接
  4. MainClass 负责启动系统

重构后类图如下

classDiagram
class MainClass{
+ main(String args[]) void
}

class LoginForm{
- dao : UserDAO
+ init() void
+ display() void
+ validate() void
}

class UserDAO{
- db : DBUtil
+ findUser(String name, String pwd) boolean
}

class DBUtil{
+ getConnection() Connection
}

MainClass ..> LoginForm
LoginForm --> UserDAO
UserDAO --> DBUtil

总结

通过单一职责原则重构后系统中类的个数增加,但是类的复用性很好。DBUtil类可供多个DAO类使用,而UserDAO类也可供多个界面类使用,一个类的修改不会对其他类产生影响,系统的可维护性也将增强

开闭原则实例

说明

某图形界面系统提供了各种不同形状的按钮,客户端代码可针对这些按钮进行编程,用户可能会改变需求,要求使用不同的按钮,原始设计方案如下:

classDiagram
direction LR
class LoginForm{
- button : CircleButton
+ display()  void
}

class CircleButton{
+ display()  void
}

LoginForm --> CircleButton
classDiagram
direction LR
class LoginForm{
- button : RectangleButton
+ display()  void
}

class RectangleButton{
+ display()  void
}

LoginForm --> RectangleButton

如果界面类 LoginForm需要将圆形按钮(CircleButton)改为矩形按钮(RectangleButton),则需要修改LoginForm的源代码,修改按钮类的类名,由于圆形按钮和矩形按钮的显示方法不相同,因此还需要修改LoginForm的display()方法

使用开闭原则重构

增加一个抽象类,只需要修改配置文件,不需要修改类的源代码,即可使用不同的按钮

classDiagram
direction LR
class LoginForm{
- button : RectangleButton
+ display()  void
}

class CircleButton{
+ display()  void
}

class RectangleButton{
+ display()  void
}

class AbstracButton{
+ display()  void
}


LoginForm --> AbstracButton

CircleButton --|> AbstracButton

RectangleButton --|> AbstracButton

里氏替换原则

注意事项

  1. 如果一个方法只存在子类中,父类中不提供相应的声明,则无法在父类对象中直接使用该方法
  2. 使用里氏替换原则时,尽量把父类设计为抽象类或接口

里氏替换原则实例

class Animal {
    public void makeSound() {
        System.out.println("动物发出声音");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("汪汪汪");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("喵喵喵");
    }
}

class Test {
    public static void main(String[] args) {
        Animal animal1 = new Dog();
        Animal animal2 = new Cat();
        animal1.makeSound();
        animal2.makeSound();
    }
}

在这个例子中,DogCat类都是Animal类的子类。在main方法中,我们可以将DogCat的对象赋值给Animal类型的变量,这体现了里氏替换原则,即子类可以替换父类出现在任何父类能够出现的地方,并且不会产生错误的行为。当调用makeSound方法时,会根据实际的对象类型执行相应子类的方法。

依赖倒转原则

以一个电商系统为例:

  1. 假设没有遵循依赖倒转原则:

    • 业务逻辑层的订单处理类直接依赖于具体的数据库实现类,如MySQLOrderDao。如果以后需要更换数据库,比如从 MySQL 切换到 Oracle,那么订单处理类就需要进行大量的修改,因为它与具体的数据库实现紧密耦合。
  2. 遵循依赖倒转原则:

    • 定义一个抽象的数据访问接口OrderDao,其中包含了订单数据的增删改查等方法。
    • MySQLOrderDaoOracleOrderDao等具体的数据库实现类都实现这个接口。
    • 订单处理类只依赖于OrderDao接口,而不关心具体的数据库实现。这样,无论以后更换哪种数据库,只需要提供新的数据库实现类,而订单处理类无需修改。

接口隔离原则

假设我们正在开发一个图形绘制系统,有不同类型的图形需要绘制,如圆形(Circle)、矩形(Rectangle)和三角形(Triangle)。

  1. 不遵循接口隔离原则的情况:
   interface Shape {
       void drawCircle();
       void drawRectangle();
       void drawTriangle();
   }

   class Circle implements Shape {
       @Override
       public void drawCircle() {
           // 绘制圆形的代码
       }

       @Override
       public void drawRectangle() {
           // 什么也不做,因为圆形不需要绘制矩形的方法
       }

       @Override
       public void drawTriangle() {
           // 什么也不做,因为圆形不需要绘制三角形的方法
       }
   }

   class Rectangle implements Shape {
       @Override
       public void drawCircle() {
           // 什么也不做,因为矩形不需要绘制圆形的方法
       }

       @Override
       public void drawRectangle() {
           // 绘制矩形的代码
       }

       @Override
       public void drawTriangle() {
           // 什么也不做,因为矩形不需要绘制三角形的方法
       }
   }

   class Triangle implements Shape {
       @Override
       public void drawCircle() {
           // 什么也不做,因为三角形不需要绘制圆形的方法
       }

       @Override
       public void drawRectangle() {
           // 什么也不做,因为三角形不需要绘制矩形的方法
       }

       @Override
       public void drawTriangle() {
           // 绘制三角形的代码
       }
   }
  1. 遵循接口隔离原则的情况:
   interface CircleShape {
       void drawCircle();
   }

   interface RectangleShape {
       void drawRectangle();
   }

   interface TriangleShape {
       void drawTriangle();
   }

   class Circle implements CircleShape {
       @Override
       public void drawCircle() {
           // 绘制圆形的代码
       }
   }

   class Rectangle implements RectangleShape {
       @Override
       public void drawRectangle() {
           // 绘制矩形的代码
       }
   }

   class Triangle implements TriangleShape {
       @Override
       public void drawTriangle() {
           // 绘制三角形的代码
       }
   }

合成复用原则

假设我们正在开发一个图形绘制系统,有不同类型的图形需要绘制,并且我们希望能够对图形进行移动和缩放操作。

  1. 不遵循合成复用原则,过度使用继承的情况:
   class Shape {
       public void draw() {
           // 绘制图形的通用方法
       }
   }

   class Circle extends Shape {
       @Override
       public void draw() {
           // 绘制圆形的具体方法
       }
   }

   class Rectangle extends Shape {
       @Override
       public void draw() {
           // 绘制矩形的具体方法
       }
   }

   class MoveableShape extends Shape {
       public void move() {
           // 移动图形的方法
       }
   }

   class ScalableShape extends Shape {
       public void scale() {
           // 缩放图形的方法
       }
   }

在这个例子中,为了给图形添加移动和缩放功能,我们使用了继承。但是这样会导致一些问题,比如如果父类Shapedraw方法发生改变,所有的子类包括MoveableShapeScalableShape都可能受到影响。而且,如果我们有更多的功能需要添加,就需要不断地创建新的子类,导致类的层次结构变得复杂。

  1. 遵循合成复用原则,使用组合的情况:
   interface Drawable {
       void draw();
   }

   interface Moveable {
       void move();
   }

   interface Scalable {
       void scale();
   }

   class Circle implements Drawable {
       @Override
       public void draw() {
           // 绘制圆形的具体方法
       }
   }

   class Rectangle implements Drawable {
       @Override
       public void draw() {
           // 绘制矩形的具体方法
       }
   }

   class MoveableDecorator implements Moveable {
       private Moveable decoratedShape;

       public MoveableDecorator(Moveable decoratedShape) {
           this.decoratedShape = decoratedShape;
       }

       @Override
       public void move() {
           // 移动图形的方法
           decoratedShape.move();
       }
   }

   class ScalableDecorator implements Scalable {
       private Scalable decoratedShape;

       public ScalableDecorator(Scalable decoratedShape) {
           this.decoratedShape = decoratedShape;
       }

       @Override
       public void scale() {
           // 缩放图形的方法
           decoratedShape.scale();
       }
   }

在这个例子中,我们使用了接口来定义不同的行为,然后通过组合的方式将这些行为动态地添加到图形对象上。如果我们需要一个可移动的圆形,我们可以这样创建:

   Moveable moveableCircle = new MoveableDecorator(new Circle());
   moveableCircle.move();

如果我们需要一个可移动和可缩放的矩形,我们可以这样创建:

   Scalable scalableRectangle = new ScalableDecorator(new Rectangle());
   Moveable moveableScalableRectangle = new MoveableDecorator(scalableRectangle);
   moveableScalableRectangle.move();
   scalableRectangle.scale();

迪米特法则

只依赖直接相关的对象,不依赖间接相关的对象,通过已经依赖的对象去访问间接相关的对象

如下文中的通过student通过course去访问teacher信息

假设我们有一个学校管理系统,其中有学生(Student)、教师(Teacher)和课程(Course)三个类。

  1. 不遵循迪米特法则的情况:
   class Student {
       private String name;
       private Course[] courses;

       public Student(String name) {
           this.name = name;
       }

       public void addCourse(Course course) {
           // 添加课程的方法
           //...
       }

       public void showTeacherInfo(Teacher teacher) {
           // 直接访问教师的信息,违反迪米特法则
           System.out.println("Teacher " + teacher.getName() + " teaches " + teacher.getCoursesTaught());
       }
   }

   class Teacher {
       private String name;
       private Course[] coursesTaught;

       public Teacher(String name) {
           this.name = name;
       }

       public String getName() {
           return name;
       }

       public Course[] getCoursesTaught() {
           return coursesTaught;
       }
   }

   class Course {
       private String name;

       public Course(String name) {
           this.name = name;
       }
   }

在这个例子中,Student类直接访问了Teacher类的内部信息,违反了迪米特法则。这样会导致Student类和Teacher类之间的耦合度增加,当Teacher类的内部实现发生变化时,Student类可能也需要进行修改。

  1. 遵循迪米特法则的情况:
   class Student {
       private String name;
       private Course[] courses;

       public Student(String name) {
           this.name = name;
       }

       public void addCourse(Course course) {
           // 添加课程的方法
           //...
       }

       public void showTeacherInfo(Teacher teacher, CourseService courseService) {
           // 通过课程服务类间接获取教师信息,遵循迪米特法则
           System.out.println("Teacher " + courseService.getTeacherNameByCourse(teacher) + " teaches " + courseService.getCoursesTaughtByTeacher(teacher));
       }
   }

   class Teacher {
       private String name;
       private Course[] coursesTaught;

       public Teacher(String name) {
           this.name = name;
       }

       public String getName() {
           return name;
       }

       public Course[] getCoursesTaught() {
           return coursesTaught;
       }
   }

   class Course {
       private String name;

       public Course(String name) {
           this.name = name;
       }
   }

   class CourseService {
       public String getTeacherNameByCourse(Teacher teacher) {
           // 根据课程获取教师名称的方法
           //...
           return teacher.getName();
       }

       public Course[] getCoursesTaughtByTeacher(Teacher teacher) {
           // 获取教师所教课程的方法
           //...
           return teacher.getCoursesTaught();
       }
   }

在这个改进后的例子中,Student类不再直接访问Teacher类的内部信息,而是通过一个中间的CourseService类来获取教师的信息。这样降低了Student类和Teacher类之间的耦合度,当Teacher类的内部实现发生变化时,Student类不需要进行修改。