MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java类的设计与重构技巧

2021-10-026.8k 阅读

类的设计基础

在Java中,类是面向对象编程的核心构建块。一个设计良好的类应该具有清晰的职责,这意味着每个类应该专注于完成一项特定的任务。例如,我们有一个处理用户信息的场景,创建一个User类来封装用户相关的数据和操作就是一个合理的设计。

public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

在上述代码中,User类负责表示用户的基本信息,它有两个私有字段nameage,通过构造函数进行初始化,并提供了获取这些信息的公共方法。这种设计使得User类职责明确,只关注用户信息的管理。

单一职责原则(SRP)

单一职责原则指出,一个类应该只有一个引起它变化的原因。如果一个类承担了过多的职责,那么当其中一个职责发生变化时,可能会影响到其他职责的正常运行。例如,假设我们在User类中加入文件存储相关的方法,如下:

public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void saveToFile(String filePath) {
        // 实现将用户信息保存到文件的逻辑
    }
}

此时User类不仅负责用户信息的管理,还承担了文件存储的职责。如果文件存储的逻辑发生变化,比如文件格式改变,就可能会影响到User类原本的用户信息管理功能。更好的做法是将文件存储功能提取到一个独立的类中,例如UserFileStorage类:

public class UserFileStorage {
    public void saveUserToFile(User user, String filePath) {
        // 实现将用户信息保存到文件的逻辑
    }
}

这样,User类专注于用户信息的管理,UserFileStorage类专注于文件存储,符合单一职责原则,降低了类之间的耦合度。

类的属性和方法设计

  1. 属性的可见性:类的属性通常应该设置为私有(private),通过公共的访问器(getter)和修改器(setter)方法来访问和修改属性。这有助于封装数据,保护数据的完整性。例如,在User类中,nameage属性是私有的,通过getName()getAge()方法来获取,这样外部代码无法直接修改属性的值,保证了数据的安全性。

  2. 方法的设计:方法应该具有明确的功能和单一的目的。方法的命名应该清晰,能够准确描述其功能。例如,calculateTotalPrice()方法就很明确地表示该方法用于计算总价。方法的参数列表也应该简洁明了,避免传递过多的参数。如果确实需要传递多个参数,可以考虑将相关参数封装成一个对象。

类的继承与多态设计

继承是Java中实现代码复用和层次结构的重要机制。一个类可以继承另一个类,从而获得父类的属性和方法。例如,我们有一个Animal类,然后创建Dog类继承自Animal类:

public class Animal {
    protected String name;

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

    public void eat() {
        System.out.println(name + " is eating.");
    }
}

public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    public void bark() {
        System.out.println(name + " is barking.");
    }
}

在上述代码中,Dog类继承了Animal类,获得了name属性和eat()方法。同时,Dog类还可以定义自己特有的方法bark()

里氏替换原则(LSP)

里氏替换原则指出,所有引用基类(父类)的地方必须能透明地使用其子类的对象。这意味着子类对象应该可以完全替代父类对象,而不会影响程序的正确性。例如,我们有一个方法接受Animal类型的参数:

public class Zoo {
    public void feedAnimal(Animal animal) {
        animal.eat();
    }
}

那么,我们可以传递Dog类的对象给feedAnimal()方法,因为DogAnimal的子类,符合里氏替换原则:

public class Main {
    public static void main(String[] args) {
        Zoo zoo = new Zoo();
        Dog dog = new Dog("Buddy");
        zoo.feedAnimal(dog);
    }
}

多态的实现

多态是指同一个方法调用在不同的对象上可以产生不同的行为。在Java中,多态主要通过方法重写和接口实现来实现。以刚才的AnimalDog类为例,Dog类可以重写Animal类的eat()方法:

public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    public void eat() {
        System.out.println(name + " is eating dog food.");
    }

    public void bark() {
        System.out.println(name + " is barking.");
    }
}

现在,当我们调用feedAnimal()方法并传递Dog对象时,实际执行的是Dog类重写后的eat()方法,这就是多态的体现。

接口与抽象类设计

接口和抽象类是Java中用于定义规范和抽象行为的重要工具。

抽象类

抽象类是一种不能被实例化的类,它通常包含一些抽象方法,这些方法只有声明而没有实现。抽象类的主要目的是为子类提供一个通用的框架。例如,我们定义一个抽象的Shape类:

public abstract class Shape {
    protected String color;

    public Shape(String color) {
        this.color = color;
    }

    public abstract double calculateArea();
}

Shape类是抽象的,因为它不能被直接实例化。它定义了一个抽象方法calculateArea(),具体的形状类(如CircleRectangle)需要继承Shape类并实现这个方法:

public class Circle extends Shape {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

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

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

    @Override
    public double calculateArea() {
        return width * height;
    }
}

接口

接口是一种特殊的抽象类型,它只包含抽象方法和常量。接口可以被类实现,一个类可以实现多个接口。接口常用于定义一组相关的行为,而不关心具体的实现。例如,我们定义一个Drawable接口:

public interface Drawable {
    void draw();
}

CircleRectangle类可以实现Drawable接口,以表明它们具有可绘制的行为:

public class Circle extends Shape implements Drawable {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }

    @Override
    public void draw() {
        System.out.println("Drawing a circle with color " + color);
    }
}

public class Rectangle extends Shape implements Drawable {
    private double width;
    private double height;

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

    @Override
    public double calculateArea() {
        return width * height;
    }

    @Override
    public void draw() {
        System.out.println("Drawing a rectangle with color " + color);
    }
}

接口和抽象类的选择:当需要定义一些具有共同属性和行为的类的框架时,优先考虑抽象类;当需要定义一些不相关类的共同行为时,优先考虑接口。

类的重构技巧

随着项目的发展,类的设计可能需要不断改进,这就涉及到重构。重构是在不改变软件外部行为的前提下,改善其内部结构,提高代码的可读性、可维护性和可扩展性。

提取方法

如果一个方法中代码过长且完成了多个不同的功能,可以将其中部分功能提取到单独的方法中。例如,我们有一个处理订单的方法:

public class OrderProcessor {
    public void processOrder(Order order) {
        System.out.println("Processing order: " + order.getOrderId());
        // 检查库存
        boolean hasStock = checkStock(order);
        if (hasStock) {
            // 计算总价
            double totalPrice = calculateTotalPrice(order);
            // 保存订单
            saveOrder(order, totalPrice);
        } else {
            System.out.println("Out of stock for order: " + order.getOrderId());
        }
    }

    private boolean checkStock(Order order) {
        // 实现检查库存的逻辑
        return true;
    }

    private double calculateTotalPrice(Order order) {
        // 实现计算总价的逻辑
        return 100.0;
    }

    private void saveOrder(Order order, double totalPrice) {
        // 实现保存订单的逻辑
    }
}

在上述代码中,processOrder()方法原本包含了检查库存、计算总价和保存订单等多个功能,通过提取方法,使得每个功能都有独立的方法,代码结构更加清晰。

提取类

如果一个类承担了过多的职责,可以将部分职责提取到一个新的类中。例如,在一个图形绘制的项目中,我们有一个GraphicObject类,它既负责图形的绘制,又负责图形的存储:

public class GraphicObject {
    private String type;

    public GraphicObject(String type) {
        this.type = type;
    }

    public void draw() {
        System.out.println("Drawing " + type);
    }

    public void saveToFile(String filePath) {
        // 实现将图形保存到文件的逻辑
    }
}

此时,我们可以将文件存储功能提取到一个新的GraphicObjectFileStorage类中:

public class GraphicObject {
    private String type;

    public GraphicObject(String type) {
        this.type = type;
    }

    public void draw() {
        System.out.println("Drawing " + type);
    }
}

public class GraphicObjectFileStorage {
    public void saveGraphicObjectToFile(GraphicObject graphicObject, String filePath) {
        // 实现将图形保存到文件的逻辑
    }
}

这样,GraphicObject类专注于图形的绘制,GraphicObjectFileStorage类专注于文件存储,符合单一职责原则。

重构继承关系

有时候,继承关系可能过于复杂或者不合理,需要进行重构。例如,我们有一个Employee类和Manager类,Manager类继承自Employee类,但是Manager类有一些特有的属性和方法,与普通员工有很大的差异:

public class Employee {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    public double getSalary() {
        return salary;
    }
}

public class Manager extends Employee {
    private int numberOfSubordinates;

    public Manager(String name, double salary, int numberOfSubordinates) {
        super(name, salary);
        this.numberOfSubordinates = numberOfSubordinates;
    }

    public int getNumberOfSubordinates() {
        return numberOfSubordinates;
    }

    public void assignTask(Employee employee, String task) {
        // 实现分配任务的逻辑
    }
}

在这种情况下,Manager类与Employee类的继承关系可能不太合适,因为Manager类有很多独特的行为。可以考虑采用组合的方式来重构,例如:

public class Employee {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    public double getSalary() {
        return salary;
    }
}

public class Management {
    private int numberOfSubordinates;

    public Management(int numberOfSubordinates) {
        this.numberOfSubordinates = numberOfSubordinates;
    }

    public int getNumberOfSubordinates() {
        return numberOfSubordinates;
    }

    public void assignTask(Employee employee, String task) {
        // 实现分配任务的逻辑
    }
}

public class Manager {
    private Employee employee;
    private Management management;

    public Manager(String name, double salary, int numberOfSubordinates) {
        this.employee = new Employee(name, salary);
        this.management = new Management(numberOfSubordinates);
    }

    public double getSalary() {
        return employee.getSalary();
    }

    public int getNumberOfSubordinates() {
        return management.getNumberOfSubordinates();
    }

    public void assignTask(Employee employee, String task) {
        management.assignTask(employee, task);
    }
}

通过这种重构,Manager类通过组合EmployeeManagement对象来实现其功能,使得代码结构更加清晰和灵活。

使用设计模式进行重构

设计模式是在软件开发过程中反复出现的问题的通用解决方案。在重构过程中,合理应用设计模式可以显著改善类的设计。

  1. 策略模式:策略模式定义了一系列算法,将每个算法封装起来,并使它们可以相互替换。例如,我们有一个支付系统,支持多种支付方式,如信用卡支付、支付宝支付等。可以使用策略模式来重构:
public interface PaymentStrategy {
    void pay(double amount);
}

public class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;
    private String expirationDate;
    private String cvv;

    public CreditCardPayment(String cardNumber, String expirationDate, String cvv) {
        this.cardNumber = cardNumber;
        this.expirationDate = expirationDate;
        this.cvv = cvv;
    }

    @Override
    public void pay(double amount) {
        System.out.println("Paying " + amount + " with credit card " + cardNumber);
    }
}

public class AlipayPayment implements PaymentStrategy {
    private String account;

    public AlipayPayment(String account) {
        this.account = account;
    }

    @Override
    public void pay(double amount) {
        System.out.println("Paying " + amount + " with Alipay account " + account);
    }
}

public class ShoppingCart {
    private PaymentStrategy paymentStrategy;

    public ShoppingCart(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public void checkout(double amount) {
        paymentStrategy.pay(amount);
    }
}

在上述代码中,PaymentStrategy接口定义了支付的策略,CreditCardPaymentAlipayPayment类实现了具体的支付策略。ShoppingCart类通过组合PaymentStrategy对象来实现不同的支付方式,这样在需要添加新的支付方式时,只需要实现PaymentStrategy接口即可,不需要修改ShoppingCart类的核心代码。

  1. 观察者模式:观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象状态发生变化时,会通知所有观察者对象,使它们能够自动更新。例如,在一个新闻发布系统中,有新闻发布者和多个订阅者:
import java.util.ArrayList;
import java.util.List;

public interface Observer {
    void update(String news);
}

public interface Subject {
    void attach(Observer observer);
    void detach(Observer observer);
    void notifyObservers(String news);
}

public class NewsPublisher implements Subject {
    private List<Observer> observers = new ArrayList<>();

    @Override
    public void attach(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers(String news) {
        for (Observer observer : observers) {
            observer.update(news);
        }
    }
}

public class NewsSubscriber implements Observer {
    private String name;

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

    @Override
    public void update(String news) {
        System.out.println(name + " received news: " + news);
    }
}

在上述代码中,NewsPublisher是主题对象,NewsSubscriber是观察者对象。NewsPublisher可以添加和移除NewsSubscriber,并在有新新闻时通知所有的订阅者。通过这种方式,实现了发布者和订阅者之间的松耦合,便于系统的扩展和维护。

类的设计与重构中的常见问题及解决

  1. 过度设计:有些开发者在设计类时,可能会过早地考虑过多的扩展性和灵活性,导致类的设计过于复杂,增加了不必要的开发和维护成本。解决方法是遵循“简单设计原则”,在满足当前需求的前提下,尽量保持类的简单性。随着需求的变化,再逐步进行优化和扩展。

  2. 忽视可测试性:如果类的设计使得其难以进行单元测试,那么在开发过程中就很难保证代码的质量。例如,类中包含大量的静态方法和全局变量,或者依赖关系不明确。解决方法是采用依赖注入等技术,将类的依赖关系明确化,使得可以通过替换依赖对象来进行单元测试。同时,避免过多使用静态方法和全局变量,尽量将逻辑封装在实例方法中。

  3. 缺乏文档化:没有良好的文档,其他开发者很难理解类的功能、使用方法以及设计意图。在类的设计和重构过程中,应该及时添加注释和文档,包括类的功能描述、方法的参数和返回值说明、使用示例等。可以使用JavaDoc等工具来生成规范的文档。

总结类的设计与重构实践要点

在Java类的设计与重构过程中,要始终牢记面向对象的基本原则,如单一职责原则、里氏替换原则等。合理运用继承、多态、接口和抽象类等特性,构建清晰、灵活且可维护的类结构。在重构时,要采用合适的技巧,如提取方法、提取类等,逐步改善代码的质量。同时,注意避免常见问题,确保类的设计符合项目的需求和长远发展。通过不断地实践和总结,能够提升自己在类设计与重构方面的能力,开发出高质量的Java软件。