MST

星途 面试题库

面试题:Java企业级应用中分层架构的反模式及深度重构

在一个Java企业级Web应用中,采用了经典的三层架构(表现层、业务逻辑层、数据访问层)。随着业务的发展,出现了表现层与业务逻辑层耦合严重,业务逻辑层对数据访问层的调用混乱,导致代码维护困难,扩展性差等问题。请全面分析该场景下可能存在的各种反模式,并给出一套完整且详细的重构技巧与方案,包括涉及到的设计模式、架构原则以及如何在不影响现有业务的前提下逐步实施重构。
25.0万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

可能存在的反模式分析

1. 紧密耦合(Tight Coupling)

  • 表现层与业务逻辑层:表现层可能直接依赖业务逻辑层的具体实现类,而不是通过接口。这使得业务逻辑层的任何修改,比如更换实现算法,都可能影响到表现层。例如,在JSP页面中直接调用业务逻辑层的某个具体服务类的方法,而非通过抽象接口。
  • 业务逻辑层与数据访问层:业务逻辑层可能直接调用数据访问层具体实现类的方法,没有进行合理的抽象。例如,业务逻辑层直接调用MySQL数据访问层的特定SQL查询方法,当数据库从MySQL切换到Oracle时,业务逻辑层代码需要大量修改。

2. 不当的依赖倒置(Inverted Dependency)

  • 业务逻辑层本应依赖抽象的数据访问层接口,但实际却依赖具体的数据访问层实现类,违背了依赖倒置原则。这导致数据访问层的变化直接影响业务逻辑层,降低了系统的可维护性和扩展性。

3. 上帝对象(God Object)

  • 在业务逻辑层,可能存在一个或几个类承担了过多的职责,负责处理各种不同的业务逻辑和对数据访问层的调用。这些类就像“上帝”一样,知道和处理所有事情,使得代码难以理解和维护。

4. 重复代码(Duplicated Code)

  • 在表现层和业务逻辑层之间,可能存在一些重复的业务逻辑代码。例如,在多个JSP页面或者Servlet中,对用户输入数据的验证逻辑重复出现,这不仅增加了代码量,也使得修改一处逻辑时需要同时修改多处,容易出错。

重构技巧与方案

设计模式应用

1. 依赖注入(Dependency Injection,DI)

  • 在表现层与业务逻辑层之间:表现层通过接口依赖业务逻辑层的服务。可以使用Spring框架的依赖注入功能,在配置文件或者使用注解的方式,将业务逻辑层的具体实现类注入到表现层组件中。例如,在Spring的配置文件中定义:
<bean id="userService" class="com.example.service.impl.UserServiceImpl"/>
<bean id="userController" class="com.example.controller.UserController">
    <property name="userService" ref="userService"/>
</bean>
  • 在业务逻辑层与数据访问层之间:业务逻辑层通过接口依赖数据访问层的操作。同样利用Spring的依赖注入,将数据访问层的具体实现类注入到业务逻辑层。例如:
<bean id="userDao" class="com.example.dao.impl.UserDaoImpl"/>
<bean id="userService" class="com.example.service.impl.UserServiceImpl">
    <property name="userDao" ref="userDao"/>
</bean>

2. 策略模式(Strategy Pattern)

  • 对于业务逻辑层中不同的业务处理逻辑,可以使用策略模式。将不同的业务逻辑封装成不同的策略类,业务逻辑层通过一个上下文类来选择具体的策略。例如,在订单处理业务中,不同类型的订单(普通订单、促销订单等)有不同的处理逻辑,可将这些逻辑分别封装成策略类,订单处理服务类根据订单类型选择相应的策略。

架构原则遵循

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

  • 在业务逻辑层:将业务逻辑分解成多个小的类,每个类只负责一项明确的业务功能。例如,将用户注册、用户登录、用户信息修改等业务逻辑分别封装到不同的类中,避免一个类承担过多职责。
  • 在数据访问层:每个数据访问类只负责一种数据实体的操作。如UserDao类只负责用户相关的数据操作,ProductDao类只负责产品相关的数据操作。

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

  • 确保高层次的模块(表现层和业务逻辑层)不依赖低层次的模块(数据访问层)的具体实现,而是依赖抽象。即表现层依赖业务逻辑层的接口,业务逻辑层依赖数据访问层的接口。

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

  • 在业务逻辑层和数据访问层之间,定义细粒度的接口。避免业务逻辑层依赖数据访问层中不必要的方法。例如,数据访问层可以针对不同的操作(增、删、改、查)分别定义接口,业务逻辑层根据实际需求依赖相应的接口。

重构实施步骤

1. 接口定义

  • 数据访问层:为数据访问操作定义接口,如UserDao接口定义用户数据的增删改查方法。
public interface UserDao {
    User findById(int id);
    void save(User user);
    void update(User user);
    void delete(int id);
}
  • 业务逻辑层:为业务逻辑服务定义接口,如UserService接口定义用户相关的业务操作。
public interface UserService {
    User getUserById(int id);
    void registerUser(User user);
    void updateUser(User user);
    void deleteUser(int id);
}

2. 依赖关系调整

  • 表现层:修改表现层代码,使其依赖业务逻辑层接口。例如,在Servlet中通过依赖注入获取业务逻辑层服务。
public class UserServlet extends HttpServlet {
    private UserService userService;

    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    //...
}
  • 业务逻辑层:修改业务逻辑层代码,使其依赖数据访问层接口。例如,在UserServiceImpl中通过依赖注入获取数据访问层对象。
public class UserServiceImpl implements UserService {
    private UserDao userDao;

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    //...
}

3. 重构业务逻辑

  • 按照单一职责原则,拆分业务逻辑层中过于庞大的类。例如,将原来一个包含多种用户业务逻辑的类拆分成UserRegistrationServiceUserLoginService等类。

4. 逐步替换与测试

  • 在不影响现有业务的前提下,逐步将原有的紧密耦合代码替换为重构后的代码。每替换一部分,进行充分的单元测试和集成测试。可以先从一些非核心业务功能开始重构,逐渐过渡到核心业务。
  • 例如,先重构用户注册功能的代码,测试通过后再重构用户登录功能的代码,以此类推,确保整个系统在重构过程中能够稳定运行。