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

Java设计原则与最佳实践

2022-08-246.1k 阅读

单一职责原则(Single Responsibility Principle, SRP)

单一职责原则是指一个类应该只有一个引起它变化的原因,也就是说一个类应该只负责一项职责。如果一个类承担了过多的职责,那么当其中一个职责发生变化时,可能会影响到其他职责的正常运行。

违背SRP的示例

假设我们有一个 UserService 类,它既负责用户信息的存储,又负责用户登录功能:

public class UserService {
    public void saveUser(User user) {
        // 保存用户信息到数据库
        System.out.println("Saving user to database: " + user);
    }

    public boolean login(String username, String password) {
        // 验证用户名和密码
        if ("admin".equals(username) && "password".equals(password)) {
            return true;
        }
        return false;
    }
}

在这个例子中,UserService 类承担了两个职责:用户信息存储和用户登录验证。如果数据库存储方式发生变化,saveUser 方法需要修改,这可能会意外影响到 login 方法。

遵循SRP的重构

我们可以将这两个职责分离到不同的类中:

public class UserStorageService {
    public void saveUser(User user) {
        // 保存用户信息到数据库
        System.out.println("Saving user to database: " + user);
    }
}

public class UserAuthenticationService {
    public boolean login(String username, String password) {
        // 验证用户名和密码
        if ("admin".equals(username) && "password".equals(password)) {
            return true;
        }
        return false;
    }
}

这样,当数据库存储方式发生变化时,只会影响 UserStorageService 类,而 UserAuthenticationService 类不受影响。

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

开闭原则是指软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着当需求发生变化时,我们应该通过扩展现有代码,而不是修改现有代码来满足新需求。

违背OCP的示例

假设我们有一个 Shape 类和一个计算形状面积的函数:

class Shape {
    int type;
    double radius;
    double length;
    double width;

    public Shape(int type, double radius, double length, double width) {
        this.type = type;
        this.radius = radius;
        this.length = length;
        this.width = width;
    }
}

public class AreaCalculator {
    public double calculateArea(Shape shape) {
        if (shape.type == 1) {
            // 圆形
            return Math.PI * shape.radius * shape.radius;
        } else if (shape.type == 2) {
            // 矩形
            return shape.length * shape.width;
        }
        return 0;
    }
}

如果我们要添加一个新的形状(比如三角形),就需要修改 AreaCalculator 类的 calculateArea 方法。

遵循OCP的重构

我们可以使用接口和多态来实现开闭原则:

interface Shape {
    double calculateArea();
}

class Circle implements Shape {
    double radius;

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

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

class Rectangle implements Shape {
    double length;
    double width;

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

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

public class AreaCalculator {
    public double calculateArea(Shape shape) {
        return shape.calculateArea();
    }
}

现在,如果要添加一个新的形状,只需要创建一个新的类实现 Shape 接口,而不需要修改 AreaCalculator 类。

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

里氏替换原则是指所有引用基类(父类)的地方必须能透明地使用其子类的对象。这意味着子类对象必须能够替换掉它们的父类对象,而程序的行为不会发生改变。

违背LSP的示例

假设有一个 Rectangle 类和一个 Square 类,Square 类继承自 Rectangle

class Rectangle {
    protected double width;
    protected double height;

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

    public void setWidth(double width) {
        this.width = width;
    }

    public void setHeight(double height) {
        this.height = height;
    }

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

class Square extends Rectangle {
    public Square(double side) {
        super(side, side);
    }

    @Override
    public void setWidth(double width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(double height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}

现在有一个使用 Rectangle 的函数:

public class RectangleProcessor {
    public static void processRectangle(Rectangle rectangle) {
        rectangle.setWidth(5);
        rectangle.setHeight(4);
        double area = rectangle.getArea();
        System.out.println("Rectangle area: " + area);
    }
}

如果我们将一个 Square 对象传递给 processRectangle 方法,由于 Square 类对 setWidthsetHeight 方法的重写,结果会不符合预期,这就违背了里氏替换原则。

遵循LSP的重构

一种解决方法是不使用继承关系,而是使用组合:

class Square {
    private double side;

    public Square(double side) {
        this.side = side;
    }

    public double getArea() {
        return side * side;
    }
}

class Rectangle {
    protected double width;
    protected double height;

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

    public void setWidth(double width) {
        this.width = width;
    }

    public void setHeight(double height) {
        this.height = height;
    }

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

public class ShapeProcessor {
    public static void processRectangle(Rectangle rectangle) {
        rectangle.setWidth(5);
        rectangle.setHeight(4);
        double area = rectangle.getArea();
        System.out.println("Rectangle area: " + area);
    }

    public static void processSquare(Square square) {
        double area = square.getArea();
        System.out.println("Square area: " + area);
    }
}

这样,SquareRectangle 各自独立,符合里氏替换原则。

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

接口隔离原则是指客户端不应该依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。

违背ISP的示例

假设我们有一个 Animal 接口,包含 flyrun 方法:

interface Animal {
    void fly();
    void run();
}

class Bird implements Animal {
    @Override
    public void fly() {
        System.out.println("Bird is flying");
    }

    @Override
    public void run() {
        System.out.println("Bird is running");
    }
}

class Dog implements Animal {
    @Override
    public void fly() {
        // 狗不会飞,这里实现不合理
        System.out.println("Dog can't fly");
    }

    @Override
    public void run() {
        System.out.println("Dog is running");
    }
}

Dog 类被迫实现了它不需要的 fly 方法,这违背了接口隔离原则。

遵循ISP的重构

我们可以将 Animal 接口拆分成多个接口:

interface Flyable {
    void fly();
}

interface Runnable {
    void run();
}

class Bird implements Flyable, Runnable {
    @Override
    public void fly() {
        System.out.println("Bird is flying");
    }

    @Override
    public void run() {
        System.out.println("Bird is running");
    }
}

class Dog implements Runnable {
    @Override
    public void run() {
        System.out.println("Dog is running");
    }
}

这样,Dog 类只需要实现 Runnable 接口,符合接口隔离原则。

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

依赖倒置原则是指高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。

违背DIP的示例

假设有一个 EmailSender 类和一个 UserService 类,UserService 类依赖 EmailSender 类:

class EmailSender {
    public void sendEmail(String to, String content) {
        System.out.println("Sending email to " + to + " with content: " + content);
    }
}

class UserService {
    private EmailSender emailSender;

    public UserService() {
        this.emailSender = new EmailSender();
    }

    public void registerUser(String username, String email) {
        // 注册用户逻辑
        System.out.println("User " + username + " registered");
        emailSender.sendEmail(email, "Welcome to our service!");
    }
}

在这个例子中,UserService 类依赖具体的 EmailSender 类,如果我们想要更换邮件发送方式(比如使用短信发送),就需要修改 UserService 类。

遵循DIP的重构

我们可以通过接口来实现依赖倒置:

interface MessageSender {
    void sendMessage(String to, String content);
}

class EmailSender implements MessageSender {
    @Override
    public void sendMessage(String to, String content) {
        System.out.println("Sending email to " + to + " with content: " + content);
    }
}

class SmsSender implements MessageSender {
    @Override
    public void sendMessage(String to, String content) {
        System.out.println("Sending SMS to " + to + " with content: " + content);
    }
}

class UserService {
    private MessageSender messageSender;

    public UserService(MessageSender messageSender) {
        this.messageSender = messageSender;
    }

    public void registerUser(String username, String contact) {
        // 注册用户逻辑
        System.out.println("User " + username + " registered");
        messageSender.sendMessage(contact, "Welcome to our service!");
    }
}

现在,UserService 类依赖 MessageSender 接口,而不是具体的实现类。我们可以轻松地更换消息发送方式,只需要传入不同的实现类即可。

最佳实践 - 设计模式的应用

设计模式是在软件开发过程中反复出现的问题的通用解决方案。遵循上述设计原则,许多设计模式应运而生,下面介绍几种常见设计模式及其在Java中的应用。

单例模式

单例模式确保一个类只有一个实例,并提供一个全局访问点。

饿汉式单例

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

饿汉式单例在类加载时就创建实例,优点是实现简单,缺点是可能在不需要实例时就创建了。

懒汉式单例(线程不安全)

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

懒汉式单例在第一次调用 getInstance 方法时创建实例,但这种实现线程不安全。

懒汉式单例(线程安全)

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

通过 synchronized 关键字保证线程安全,但性能较低。

双重检查锁单例

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

双重检查锁单例既保证了线程安全,又提高了性能。volatile 关键字确保 instance 在多线程环境下的可见性。

工厂模式

工厂模式提供了一种创建对象的方式,将对象的创建和使用分离。

简单工厂模式

interface Shape {
    void draw();
}

class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}

class ShapeFactory {
    public Shape createShape(String shapeType) {
        if ("circle".equals(shapeType)) {
            return new Circle();
        } else if ("rectangle".equals(shapeType)) {
            return new Rectangle();
        }
        return null;
    }
}

使用简单工厂模式:

public class Main {
    public static void main(String[] args) {
        ShapeFactory factory = new ShapeFactory();
        Shape circle = factory.createShape("circle");
        circle.draw();
    }
}

简单工厂模式将对象的创建逻辑封装在工厂类中,使用方只需要关心如何获取对象,而不关心对象的创建过程。

工厂方法模式

工厂方法模式是在简单工厂模式的基础上,将工厂类的创建方法抽象成抽象方法,由具体的工厂子类实现。

interface Shape {
    void draw();
}

class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}

abstract class ShapeFactory {
    abstract Shape createShape();
}

class CircleFactory extends ShapeFactory {
    @Override
    Shape createShape() {
        return new Circle();
    }
}

class RectangleFactory extends ShapeFactory {
    @Override
    Shape createShape() {
        return new Rectangle();
    }
}

使用工厂方法模式:

public class Main {
    public static void main(String[] args) {
        ShapeFactory circleFactory = new CircleFactory();
        Shape circle = circleFactory.createShape();
        circle.draw();

        ShapeFactory rectangleFactory = new RectangleFactory();
        Shape rectangle = rectangleFactory.createShape();
        rectangle.draw();
    }
}

工厂方法模式使得创建对象的逻辑更加灵活,当需要添加新的对象类型时,只需要创建新的工厂子类,而不需要修改现有工厂类的代码。

抽象工厂模式

抽象工厂模式提供了一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

interface Shape {
    void draw();
}

interface Color {
    void fill();
}

class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}

class Red implements Color {
    @Override
    public void fill() {
        System.out.println("Filling with red");
    }
}

class Blue implements Color {
    @Override
    public void fill() {
        System.out.println("Filling with blue");
    }
}

abstract class AbstractFactory {
    abstract Shape createShape();
    abstract Color createColor();
}

class ShapeColorFactory extends AbstractFactory {
    @Override
    Shape createShape() {
        return new Circle();
    }

    @Override
    Color createColor() {
        return new Red();
    }
}

使用抽象工厂模式:

public class Main {
    public static void main(String[] args) {
        AbstractFactory factory = new ShapeColorFactory();
        Shape shape = factory.createShape();
        Color color = factory.createColor();

        shape.draw();
        color.fill();
    }
}

抽象工厂模式适用于创建对象的家族,当需要创建一组相关的对象时,使用抽象工厂模式可以保证对象之间的一致性。

最佳实践 - 代码结构与分层

在Java项目中,合理的代码结构和分层有助于提高代码的可维护性和可扩展性。

分层架构

常见的分层架构包括表现层(Presentation Layer)、业务逻辑层(Business Logic Layer)、数据访问层(Data Access Layer)。

表现层

表现层负责与用户进行交互,接收用户请求并返回响应。在Java Web应用中,通常使用Servlet、JSP或者一些Web框架(如Spring MVC、Struts)来实现表现层。

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/user")
public class UserServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 获取用户请求参数
        String username = request.getParameter("username");
        // 调用业务逻辑层
        UserService userService = new UserService();
        User user = userService.findUserByUsername(username);
        // 返回响应
        response.getWriter().println("User found: " + user);
    }
}

业务逻辑层

业务逻辑层处理应用的核心业务逻辑。它接收表现层的请求,调用数据访问层获取数据,进行业务处理,并将结果返回给表现层。

public class UserService {
    private UserDao userDao;

    public UserService() {
        this.userDao = new UserDao();
    }

    public User findUserByUsername(String username) {
        return userDao.findUserByUsername(username);
    }
}

数据访问层

数据访问层负责与数据库等数据存储进行交互,执行数据的增删改查操作。

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class UserDao {
    private static final String URL = "jdbc:mysql://localhost:3306/mydb";
    private static final String USER = "root";
    private static final String PASSWORD = "password";

    public User findUserByUsername(String username) {
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
             PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users WHERE username =?")) {
            pstmt.setString(1, username);
            try (ResultSet rs = pstmt.executeQuery()) {
                if (rs.next()) {
                    String password = rs.getString("password");
                    return new User(username, password);
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }
}

通过分层架构,不同层次的代码职责清晰,当需求发生变化时,只需要在相应的层次进行修改,不会影响其他层次的代码。

最佳实践 - 异常处理

在Java中,合理的异常处理可以提高程序的健壮性。

异常类型

Java中的异常分为受检异常(Checked Exception)和非受检异常(Unchecked Exception)。受检异常在编译时必须处理,如 IOException;非受检异常在运行时抛出,如 NullPointerException

捕获异常

try {
    // 可能抛出异常的代码
    FileReader reader = new FileReader("nonexistentfile.txt");
} catch (FileNotFoundException e) {
    // 处理异常
    System.err.println("File not found: " + e.getMessage());
}

在捕获异常时,应该尽量捕获具体的异常类型,而不是捕获宽泛的 Exception 类型,这样可以更精确地处理异常。

抛出异常

public void divide(int a, int b) throws ArithmeticException {
    if (b == 0) {
        throw new ArithmeticException("Cannot divide by zero");
    }
    System.out.println(a / b);
}

当方法内部无法处理异常时,应该将异常抛出,让调用者来处理。

自定义异常

class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String message) {
        super(message);
    }
}

public class UserService {
    public User findUserById(int id) {
        // 假设这里查询数据库没有找到用户
        if (true) {
            throw new UserNotFoundException("User not found with id: " + id);
        }
        return null;
    }
}

自定义异常可以使程序的异常处理更加符合业务需求。

最佳实践 - 代码复用

代码复用是提高开发效率和代码质量的重要手段。

继承

继承允许一个类继承另一个类的属性和方法,实现代码复用。

class Animal {
    protected String name;

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

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

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

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

通过继承,Dog 类复用了 Animal 类的 name 属性和 eat 方法。

组合

组合是通过在一个类中包含另一个类的实例来实现代码复用。

class Engine {
    public void start() {
        System.out.println("Engine started");
    }
}

class Car {
    private Engine engine;

    public Car() {
        this.engine = new Engine();
    }

    public void startCar() {
        engine.start();
    }
}

Car 类中,通过组合 Engine 类的实例,复用了 Engine 类的 start 方法。

在实际开发中,应该根据具体情况选择继承或组合来实现代码复用。一般来说,组合比继承更加灵活,因为它不会破坏封装性,并且可以在运行时动态改变所包含的对象。

通过遵循这些设计原则和最佳实践,可以编写出更加健壮、可维护和可扩展的Java程序。在日常开发中,要不断实践和总结,将这些原则和实践融入到代码中。同时,随着项目规模的扩大和需求的变化,要持续优化代码结构和设计,以适应新的挑战。