面向对象设计5大基本原则及Java举例说明

下面我将通过学习面向对象设计5大基本原则的思考,逐步讲解每一中设计原则。

单一职责原则

概念

单一职责原则(Single Responsibility Principle,SRP) 单一职责原则是指一个类只负责一项职责,只有一个引起它变化的原因。如果一个类承担的职责过多,就会导致耦合性和复杂性提高,降低其可读性和可维护性。


怎么理解单一职责中的一项职责,一项职责是指一个功能还是一类功能

在单一职责原则中的“一项职责”,通常指的是“有且仅有一个原因造成类的变更”。在具体实现中,可能有很多个任务和功能集成在某一个类中,但是只要这些任务和功能变化的原因不同,就需要将其拆分成不同的类去处理。

例如,一个登录页面的功能,需要包括输入用户名和密码、验证身份、应对登录失败等多个任务。如果这些任务都固执地集成在同一个类中,即使其中一个功能有所变化,就会涉及到整个登录页面的修改和测试,不利于软件的维护和可扩展性。

所以,一项职责不是指类只有一种功能,而是指类的职责尽可能细化,关注点越单一越好。如果一个类的职责过于复杂,往往会导致代码难以维护,扩展时风险也会增大,因此拆分出单一职责的类更易于复用和维护。


java举例说明单一职责原则

在Java编程中,如果一个类具有多个职责,就会使代码变得复杂、不够灵活,并且难以进行单元测试和扩展。因此,单一职责原则是Java开发中非常重要的一个原则。

举个例子,假如我们要开发一个员工信息表的程序,并且需要实现员工信息的录入、查询、统计等功能。这里,就可以将这些功能分别定义在三个不同的类中:
员工信息录入类:只负责员工信息的录入操作,不涉及查询和统计等其他操作;
员工信息查询类:只负责员工信息的查询操作,不涉及录入和统计等其他操作;
员工信息统计类:只负责员工信息的统计操作,不涉及录入和查询等其他操作。
通过这样的拆分,每个类都只负责单一的职责,遵循单一职责原则,代码更清晰易懂,也减少了类之间的耦合。如果后续需求有变化,只需要对指定的职责进行修改,不会对其他的职责造成一定的影响

具体代码实现如下:
public class Employee {
    private int id;
    private String name;
    private String department;
    
    public void setId(int id) {
        this.id = id;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public void setDepartment(String department) {
        this.department = department;
    }
    
    public void save() {
        // 这里实现员工信息的保存操作
    }
}

这个示例代码中,Employee类只负责员工信息的录入功能,其中setId()、setName()和setDepartment()方法用来设置员工ID、姓名和部门,save()方法用来保存员工信息。通过这样的拆分,每个类只负责单一的职责,实现了单一职责原则。


单一职责只对应java实体类吗

不仅仅是Java实体类,单一职责原则可以适用于所有面向对象编程语言和程序设计领域。
无论是Java实体类、控制类、工具类等等,只要它们承担的职责单一、功能清晰,就可以遵循单一职责原则。在面向对象编程中,对一个对象进行分解,将其拆分为多个单一职责的类,有助于降低代码的复杂性,提高代码的可维护性、可扩展性和可重用性,从而更好地满足软件设计的要求。
此外,单一职责原则也可以应用于面向接口编程、模块化开发、函数式编程等方面,都有类似的设计规范和理念。例如,在Android开发中,我们可以将不同的业务逻辑拆分成多个Activity、Fragment、Presenter、Service等,使它们各自具有不同的职责,从而更好地体现单一职责原则。


举一个控制类的单一职责例子

当我们使用MVC(Model-View-Controller)架构来开发应用程序时,控制类(Controller)扮演了非常重要的角色,它负责管理视图和模型之间的交互,以及处理输入和输出。下面举一个控制类的单一职责例子:

假设我们正在开发一个学生成绩管理系统,其中包括以下功能:
学生管理(增删改查)
成绩管理(录入、查询、统计)
我们可以将控制类StudentController和GradeController拆分为多个单一职责的类,如下:

public class StudentController {
    // 学生查询类
    StudentQuery query;
    // 学生添加类
    StudentAdd add;

    // 单一职责1:管理学生查询功能
    public void queryStudents() {
        query.queryStudents();
    }

    // 单一职责2:管理学生添加功能
    public void addStudent(String name, int age, String gender) {
        add.addStudent(name, age, gender);
    }
}

public class GradeController {
    // 成绩录入类
    GradeUpdate update;
    // 成绩查询类
    GradeQuery query;
    // 成绩统计类
    GradeStatistics statistics;

    // 单一职责1:管理成绩录入功能
    public void updateGrade(int studentId, String subject, int mark) {
        update.updateGrade(studentId, subject, mark);
    }

    // 单一职责2:管理成绩查询功能
    public void queryGrade(int studentId) {
        query.queryGradeByStudentId(studentId);
    }

    // 单一职责3:管理成绩统计功能
    public void calculateGrade() {
        statistics.calculateGrade();
    }
}

在以上示例代码中,StudentController类负责管理学生的查询和添加功能,而GradeController类负责管理成绩的录入、查询和统计功能。每个类只负责单一的职责,遵循了单一职责原则,代码更清晰易懂,也减少了类之间的耦合。如果后续需求有变化,只需要对指定的职责进行修改,不会对其他的职责造成一定的影响。


单一职责只体现在类当中吗

单一职责原则一般是用来指导如何设计类的,但是它同样适用于其他方面的设计,包括模块、函数以及软件系统等。

在模块化开发中,我们可以将模块拆分为更小的单一职责模块,每个模块只关注单独的任务或领域,以便于模块化的管理、开发和维护。在函数式编程中,函数也应该具有单一职责原则,即每个函数只负责完成单一的任务,避免一个函数完成多个任务,从而保证函数的清晰性和可复用性。在软件系统设计中,也可以将一个系统拆分为更小的单一职责组件,每个组件只负责逻辑的一部分,方便系统的拓展和维护。

因此,单一职责原则的应用范围不限于类的设计,而是贯穿整个软件开发过程,是一种良好的软件设计原则和最佳实践。



开放封闭原则(Open-Closed Principle,OCP)

概念

开放封闭原则是指一个软件实体(如类、模块、函数等)应该对扩展开放,对修改关闭。也就是说,对于已经实现的模块,应尽量避免对其进行修改,而是通过扩展的方式来实现它的修改。


怎么理解开放封闭原则中的开放封闭

开放封闭原则是面向对象设计中的一个基本原则,其中的“开放(Open)”和“封闭(Closed)”分别指的是软件系统的可扩展性和可维护性。

开放的含义是指软件系统应该对扩展开放,新的需求和功能应该通过添加新的代码模块、类、接口等来实现,同时不会影响原有代码模块的工作。也就是说,软件设计应该尽量做到代码的可拓展性,以适应不断变化的需求。

封闭的含义是指软件系统应该对修改关闭,即一旦个别模块的需求发生变化,就不应该直接修改这些模块的代码,而是通过更改周围的代码和结构,间接地实现对修改的支持,保障软件设计的稳定性、健壮性和可维护性。也就是说,软件设计应该尽量做到代码的稳定性和健壮性,以避免修改过多地影响原有结构。

在软件开发过程中,开放封闭原则是非常重要的一个思想,它能够提高代码的可维护性和可扩展性,保证代码质量、可重用性和性能等方面的稳定性与可靠性。在不断变化的需求环境下,也能够帮助软件开发团队快速响应客户的需求,提高工作效率,实现业务目标。


在java中,一般采用哪些方式实现开放原则

在Java中,一般采用以下方式来实现开放原则:

接口(interface):使用接口来定义实现规范,使得实现类具有更高的扩展性和灵活性;当需要增加新的功能时,只需实现一个新的接口,而不用修改原有的代码。

抽象类(abstract class):使用抽象类来定义基类,子类继承基类,并覆盖或者增加相关的属性和行为。当需要修改程序的部分功能时,可以通过新增、删除、替换子类来实现需求的变化,而不需要修改基类的代码。

泛型(generics):使用泛型可以使代码更加通用和灵活,可以适应不同的数据类型和对象类型。泛型可以在编译期间进行类型检查,提高程序的健壮性和安全性。

注解(annotation):使用注解可以为程序添加一些额外的元数据信息,同时可以动态地增加、删除或替换实例对象。注解可以在程序运行期间动态绑定和解绑。

总之,Java的灵活性和可扩展性是通过多种方式实现的,这些方式都是为了让程序更加具有可重用性,能够适应不断变化的需求。在实际开发中,我们可以根据具体的业务场景和需求,选择最为合适的方式来设计和实现程序。

以下通过具体的例子实现

接口(interface)

接口可以定义方法的签名,并且不需要提供方法的实现。这样,实现类可以根据接口的规范来实现自己的方法。

public interface Animal {
    public void move();
}

public class Dog implements Animal {
    public void move() {
        System.out.println("Dog moves by running.");
    }
}

public class Fish implements Animal {
    public void move() {
        System.out.println("Fish moves by swimming.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal fish = new Fish();
        dog.move();
        fish.move();
    }
}

在上面的例子中,Animal接口规定了move()方法,Dog和Fish实现了Animal接口,并实现了move()方法。当需要增加新的动物时,只需实现Animal接口并实现自己的move()方法即可。

抽象类(abstract class)
抽象类可以定义一些抽象的方法,来规范子类的实现。具体的实现由子类来实现。举个例子:
public abstract class Shape {
    public abstract double getArea();
    public abstract double getPerimeter();
}

public class Rectangle extends Shape {
    private double width, height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double getArea() {
        return width * height;
    }

    public double getPerimeter() {
        return 2 * (width + height);
    }
}

public class Circle extends Shape {
    private double radius;
    private final double PI = 3.14;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double getArea() {
        return PI * radius * radius;
    }

    public double getPerimeter() {
        return 2 * PI * radius;
    }
}

public class Main {
    public static void main(String[] args) {
        Shape rectangle = new Rectangle(3, 4);
        Shape circle = new Circle(2);
        System.out.println("Area of rectangle is: " + rectangle.getArea());
        System.out.println("Perimeter of rectangle is: " + rectangle.getPerimeter());
        System.out.println("Area of circle is: " + circle.getArea());
        System.out.println("Perimeter of circle is: " + circle.getPerimeter());
    }
}

在上面的例子中,Shape是一个抽象类,它规定了getArea()和getPerimeter()两个抽象方法。Rectangle和Circle继承了Shape,并且实现了自己的getArea()和getPerimeter()方法

泛型(generics)
泛型可以是代码更加通用和灵活,可以适应不同的数据类型和对象类型。举个例子:
public class Data<T> {
    private T data;

    public Data(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

public class Main {
    public static void main(String[] args) {
        Data<String> data1 = new Data<>("Hello World!");
        Data<Integer> data2 = new Data<>(123);
        System.out.println("Data1: " + data1.getData());
        System.out.println("Data2: " + data2.getData());
    }
}

在上面的例子中,Data是一个泛型类,它的类型可以是任意的类型。在Main方法中,我们可以创建不同类型的Data对象,例如data1是一个Data对象,data2是一个Data对象。

注解(annotation)

注解可以为程序添加一些额外的元数据信息,同时可以动态地增加、删除或替换实例对象。举个例子:
public class MyClass {
    @Deprecated
    public void oldMethod() {
        System.out.println("oldMethod");
    }

    public void newMethod() {
        System.out.println("newMethod");
    }
}

public class Main {
    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        // 调用被标记为@Deprecated的方法
        myClass.oldMethod();
        // 调用新的方法
        myClass.newMethod();
    }
}

在上面的例子中,MyClass类中的oldMethod()方法被标记为@Deprecated,表示这个方法已经不再推荐使用。当我们调用这个方法时,编译器会提示警告信息。新的方法newMethod()没有被标记为@Deprecated,可以直接调用。


在java中,一般采用哪些方式实现封闭原则

final关键字:final关键字可以定义不可修改的常量或者不可继承的类,从而保证程序的稳定性。

private、protected、public关键字:合理使用访问修饰符可以将类中的属性和方法进行封装,从而保证数据和方法的安全性和封闭性。

设计模式:采用设计模式能够将程序结构进行分离和封装,从而保护程序的核心和重要部分。

静态内部类:静态内部类的方法和属性只能在外部类中进行访问,这种方式可以避免外部类直接访问静态内部类中的方法和属性。

总之,封闭原则是指软件系统应该对修改关闭。在Java中,采用这些方式能够保证程序的封闭性和稳定性,防止代码的修改对程序的稳定性造成不利影响。


开放封闭原则在java开源框架当中的应用举例

Spring框架:Spring框架是Java开源框架中最著名的一个,其中就广泛应用了开放封闭原则。Spring提供了很多可插拔的扩展点,例如AOP、事务管理、事件监听等,这些扩展点能够让开发者在不改变原有代码的情况下进行系统的改进和升级。

Mybatis框架:Mybatis是Java中一款开源的ORM框架,旨在封装JDBC的复杂性,从而让开发者能够更加专注业务逻辑的开发。Mybatis通过XML配置文件和Java代码相结合的方式来实现数据库的操作,这种方式能够很好地遵循开放封闭原则,使得扩展更加容易。

Hibernate框架:Hibernate是Java中最流行的ORM框架之一,用于映射Java对象和数据库表。Hibernate采用面向对象的模型描述数据和关系,利用XML或注解来配置对象与数据库之间的映射关系,同时又提供众多拓展点,例如自定义数据类型、拓展事件等,从而实现了开放封闭原则。

Log4j框架:Log4j是Java中最著名的日志框架之一,它定义了一套可配置的日志输出规范,从而让开发者能够通过配置文件来决定日志输出的级别和方式。这种方式很好地遵循了开放封闭原则,使得系统具有更好的拓展性和可维护性。



里氏替换原则(Liskov Substituion Principle,LSP)

概念

里氏替换原则是指一个对象应该可以被它的子类所替换而不改变程序的正确性。它主要是基于继承的思想,保证子类的可替换性,减少继承引起的风险。


java实现里氏替换原则的方式有哪些

在Java中,实现里氏替换原则的方式主要有以下几种:

继承关系:在使用继承关系时,子类可以完全替代父类,并且父类的任何属性和方法都可以在子类中使用。在实现继承时,需要保证子类的行为与父类一致,即子类的方法参数和返回值类型应该和父类一致。

接口实现:使用接口的方式可以遵循里氏替换原则,接口是一种规范,它定义了一组方法的规范,实现类需要实现接口中定义的所有方法。接口实现的方式需要保证接口的方法参数及返回值类型与接口定义的一致性,从而实现了里氏替换原则。

抽象类实现:抽象类是一种特殊的类,它不能被实例化,只能通过子类实现。在使用抽象类时,需要保证子类实现了抽象类中的所有抽象方法。

总之,在Java中,实现里氏替换原则的方式主要是通过继承关系、接口实现、抽象类实现等方式来实现,通过这些方式能够保证程序的可扩展性和健壮性。在具体的实现过程中,需要遵循好里氏替换原则,以保证程序的正确性和稳定性。

下面是具体的例子:

继承关系的实现:
// 父类Shape,定义形状的面积属性
public class Shape {
    protected double area;
    public double getArea() {
        return area;
    }
}

// 子类Rectangle,继承Shape类,定义矩形的宽、长属性和计算面积方法
public class Rectangle extends Shape {
    private double width, length;
    public Rectangle(double width, double length) {
        this.width = width;
        this.length = length;
    }
    public void calculateArea() {
        this.area = width * length;
    }
}

// 子类Square,继承Shape类,定义正方形的边长属性和计算面积方法
public class Square extends Shape {
    private double side;
    public Square(double side) {
        this.side = side;
    }
    public void calculateArea() {
        this.area = side * side;
    }
}

// 测试类,创建对应的矩形和正方形对象,并调用其计算面积和获取面积的方法
public class Main {
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle(4, 5);
        rectangle.calculateArea();
        System.out.println("The area of rectangle is: " + rectangle.getArea());

        Square square = new Square(6);
        square.calculateArea();
        System.out.println("The area of square is: " + square.getArea());
    }
}
接口实现的方式:
// 接口Printable,定义打印的方法
interface Printable {
    void print();
}

// 实现接口的子类Document,定义文档名属性和打印方法
class Document implements Printable {
    String name;
    public Document (String name) {
        this.name = name;
    }
    public void print() {
        System.out.println("The document is printed with name: " + name);
    }
}

// 实现接口的子类Image,定义图片名属性和打印方法
class Image implements Printable {
   String name;
    public Image (String name) {
        this.name = name;
    }
    public void print() {
        System.out.println("The image is printed with name: " + name);
    }
}

// 测试类,分别创建两个实现了Printable接口的对象,并调用其打印方法
class Main {
    public static void main(String[] args) {
        Printable printable1 = new Document("document001");
        Printable printable2 = new Image("image001");
        printable1.print();
        printable2.print();
    }
}
抽象类实现的方式:
// 抽象类Animal,定义动物的移动方式
abstract class Animal {
    public abstract void move();
}

// 实现了抽象类的子类Dog,定义狗的移动方式
class Dog extends Animal {
    public void move() {
        System.out.println("Dog moves by running.");
    }
}

// 实现了抽象类的子类Fish,定义鱼的移动方式
class Fish extends Animal {
    public void move() {
        System.out.println("Fish moves by swimming.");
    }
}

// 测试类,创建了Dog和Fish两个实例,并调用其移动方法
class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal fish = new Fish();
        dog.move();
        fish.move();
    }
}


接口隔离原则(Interface Segregation Principle,ISP)

概念

接口隔离原则是指多个专门的接口比使用单一的总接口更好。它提倡类间的依赖关系应该建立在最小的接口上,尽量避免出现“胖接口”。


怎么理解接口隔离原则

接口隔离原则(Interface Segregation Principle,ISP)是指“使用多个专门的接口,而不是使用单一的总接口(即尽量将接口的功能拆分为较小的原子接口,避免出现‘胖接口’),客户端不应该依赖它不需要的接口”。开发人员应该只关注于需要的接口方法,而无需理解不需要的方法。这个原则要求类和接口要拥有最少非必须的耦合,需要将一个庞大的接口拆分为多个更细粒度的接口,实现接口的精细化管理,尽量减少接口的治理和修改成本。

简单来说,接口隔离原则是指不应该依赖不需要的接口,尽量将复杂的接口进行分割,以达到高内聚低耦合的效果,这样可以降低客户端(实现类)的依赖关系,减少因为修改接口而造成的代码变更,提高代码的灵活性和可维护性。

总之,接口隔离原则要求我们在系统设计和开发阶段尽量避免设计出庞大臃肿的接口,而是将其拆分为多个专门的接口,这些接口所代表的粒度更细,更加灵活,能够更好地适应系统变化和升级。


java举例说明接口隔离原则

假设现在我们有一个农场模拟系统,其中有多个动物,动物分为两类,一类是能够在陆地上行走的,称之为陆生动物;另一类是能够在水中游动的,称之为水生动物。

根据接口隔离原则,我们可以将接口进行拆分,分别抽象出陆生动物的行走和水生动物的游泳的两个接口:

interface Walkable {
    void walk();
}

interface Swimmable {
    void swim();
}

然后,我们创建陆生动物和水生动物的实现类:

class Dog implements Walkable {
    @Override
    public void walk() {
        System.out.println("Dog walking...");
    }
}

class Cat implements Walkable {
    @Override
    public void walk() {
        System.out.println("Cat walking...");
    }
}

class Fish implements Swimmable {
    @Override
    public void swim() {
        System.out.println("Fish swimming...");
    }
}

class Dolphin implements Swimmable {
    @Override
    public void swim() {
        System.out.println("Dolphin swimming...");
    }
}

这里,我们只需要将需要的方法定义在对应的接口中,从而实现了接口的精细化设计,避免了所有的动物都要实现行走和游泳的问题。这样,我们可以通过这些接口来实现灵活的组合,比如我们可以将水生动物和陆生动物组合在一起:

class Aquarium {
    private Swimmable[] swimmables;
    public Aquarium(Swimmable... swimmables) {
        this.swimmables = swimmables;
    }
    public void showSwimming() {
        for (Swimmable swimmable : swimmables) {
            swimmable.swim();
        }
    }
}

class Zoo {
    private Walkable[] walkables;
    public Zoo(Walkable...walkables) {
        this.walkables = walkables;
    }
    public void showWalking() {
        for(Walkable walkable : walkables) {
            walkable.walk();
        }
    }
}

这些类通过构造函数注入不同的动物实现,可以灵活组合展示不同动物的行为。这样,通过实现单一职责原则,避免了一个接口包含的方法过多的问题,实现了高内聚低耦合的效果,提高了系统的灵活性和可扩展性。

这就是通过接口隔离原则,将接口进行拆分,以细化粒度、提高聚焦度、降低修改影响的设计方法。



依赖倒置原则(Dependency Inversion Principle,DIP)

概念

依赖倒置原则是指高层模块不应该依赖低层模块,而是应该依赖于抽象接口,即“面向接口编程”而不是“面向实现编程”。同时,抽象接口不应该依赖于具体实现,而是具体实现应该依赖于抽象接口。


怎么理解依赖倒置原则

1.高层模块不应该依赖于底层模块;它们应该依赖于抽象接口。
2.抽象接口不应该依赖于具体实现;具体实现应该依赖于抽象接口。
3.调用者不应该依赖于被调用者;被调用者应该依赖于调用接口。

依赖倒置原则的重点在于“抽象接口”,该接口定义了被调用者的具体行为,而高层模块则通过调用该接口来实现对被调用者的调用,因此,当被调用者需要修改其行为时,无需修改高层模块的代码,只需修改实现类即可。这种扩展能力对系统的可维护性、可扩展性、可复用性有着很大的帮助。
总之,依赖倒置原则指导我们在编写程序时,在设计接口上要优先使用抽象接口。通过依赖于抽象,减少了实现类的耦合,提高了程序的稳定性和扩展性。这个原则的实现又依赖于大家熟知的另一项原则 — 子stitution Principle(LSP),任务下一步要实现的类实现了它的父类或接口的抽象方法后,应该可以替换掉那个父类或接口,因而不能有新的行为或者限制它们的使用方式。


java举例说明依赖倒置原则

假设现在有一个汽车工厂,其中包含多种零部件,例如轮胎、发动机、电池等,这些零部件组成了整车。其中,我们定义零部件的抽象接口为:

public interface CarPart {
    void run();
}

然后,我们通过实现该接口来实现具体的零部件:

public class Battery implements CarPart {
    @Override
    public void run() {
        System.out.println("Battery starts...");
    }
}

public class Engine implements CarPart {
    @Override
    public void run() {
        System.out.println("Engine starts...");
    }
}

public class Tire implements CarPart {
    @Override
    public void run() {
        System.out.println("Tire rolls...");
    }
}

这样,我们的汽车工厂可以通过注入零部件的实例,创建出不同种类的汽车:

public class CarFactory {
    public void createCar(CarPart... parts) {
        for (CarPart part : parts) {
            part.run();
        }
    }
}

现在,我们可以通过调用 createCar() 方法来创建不同种类的汽车,我们也不需要关心具体的零部件是如何实现的,只需要关心零部件是否实现了 CarPart 接口即可。这种依赖于抽象的方式,使我们的代码更容易扩展和维护,例如如果我们需要添加一个新的零部件,只需要实现 CarPart 接口并注入即可,不需要修改 createCar() 方法的代码,这就是依赖倒置原则的体现。

这个例子中, CarPart 接口是高层次的抽象,具体的零部件实现 Battery、Engine 和 Tire 是低层次的具体实现,CarFactory 类则依赖于 CarPart 接口。这种依赖的方式不仅让 CarPart 接口的实现可以灵活地替换和扩展,也使高层次的 CarFactory 类与底层具体实现松耦合,从而提高了程序的可扩展性和可维护性。

在实际开发中,遵循依赖倒置原则,我们通常采用依赖注入或IOC容器来实现依赖倒置(Spring)。依赖注入可以通过构造函数注入、Setter方法注入或Interface注入等不同方式实现。在Spring框架中,IOC容器就是用来实现依赖注入和依赖倒置的。

猜你喜欢

转载自blog.csdn.net/m0_37742400/article/details/130195353
今日推荐