Skip to content

软件工程(Java)

Java与开发基础

在学习软件工程前,我们先了解一下Java和相关开发工具的概念。

概念 版本 解释
JDK Java21 JDK即Java Development Kit,是Java语言运行的环境。
Java21是主流的LTS版本,此外25、17、8、11也很常见。
Maven Apache Maven 3.9.x Maven是Build Tool,负责构建自动化。
Maven 3.9.x系列是最主流的版本。
IDE IntelliJ IDEA 用Terminal写java效率太低,一般用IDE来开发。IDE集成了编辑器、终端、Maven控制面板、调试器等。
IDE一般保持最新版就可以,可单独配置JDK和Maven的版本。

其中JDK=解释器+编译器+标准库+调试工具。

可以这么认为:

  • JDK = JRE + 开发工具(如javac编译器、调试器、文档工具等)
  • JRE = JVM + 运行库,如果不需要开发,只需要运行java程序,安装JRE就够了
  • JVM: 最核心的Java虚拟机,负责读取编译好的字节码.class文件并运行

Code Skeleton

Code Skeleton(核心代码骨架)是一个程序的基础结构,一般包括:

  • Package declarations,包声明,定义代码放在哪个文件夹下
  • Classes & Interfaces,类和接口定义,定义了有哪些对象
  • Empty Methods,空的方法,方法名、参数、返回值都写好了,但大括号内是空的
  • Comments/TODOs

package一般用像是网址一样的方式来组织,实质上是定义了代码在磁盘上的物理存放路径。比如 edu.tufts.project1java.graph,其实质对应的路径是 .../src/main/java/edu/tufts/project1java/graph/,这种写法叫做反向域名命名法。之所以这么写,是因为全球可能会有无数个程序员开发叫做graph.java的文件,所以这样写是为了方便区分,这也是软件工程课程中主要的内容:了解最佳实践。

例如下列代码块:

import java.util.*; // Makes List available

public interface Graph {
    boolean addNode(String n);
    boolean addEdge(String n1, String n2);
    boolean hasNode(String n);
    boolean hasEdge(String n1, String n2);
    boolean removeNode(String n);
    boolean removeEdge(String n1, String n2);
    List<String> nodes();
    List<String> succ(String n);
    List<String> pred(String n);
    Graph union(Graph g);
    Graph subGraph(Set<String> nodes);
    boolean connected(String n1, String n2);
}

上述骨架基本勾勒出了Graph程序有哪些功能。

为了实现这些功能,我们需要去构建具体的程序模块,比如Edge.java, ListGraph.java等。在具体的.java文件中,我们把class写好。这种将“规范”(接口,Skeleton, Interfaces)与“实现”(具体代码, Implementation)分离的思想,在软件工程中被称为 面向接口编程

Testing

一般用JUnit测试。JUnit是Java中最流行的单元测试框架(Unit Testing Framework)

Configuration

上面所有的相关内容都要在Configuration(配置文件)中配置好。一般在pom.xml中完成项目级配置,在Run/Debug Configurations中配置好java语言、断言、测试类等。

POM(Project Object Model,项目对象模型),是Maven的管理体系中的项目配置文件,每个项目都必须在根目录下拥有一个pom.xml文件,否则Maven无法识别并管理该项目。

pom.xml会管理以下内容:

  • dependencies,依赖,即项目运行所需的全部外部代码库
  • plugins, 插件,规定生产流程
  • coordinates,定义项目身份,规定了项目的唯一标识(GroupId, ArtifactId)以及版本号(如1.0-SNAPSHOT)等。

在没有Maven和pom.xml的年代,程序员需要手动下载各种 .jar 库文件并手动配置复杂的路径。因此这个配置文件是非常重要的,只要配置好,Maven就会自动通过互联网下载所需的依赖包。

Framework

最常用的Java Framework是Spring Framework。Spring Boot是基于Spring Framework的增强型快速开发框架(包括自动配置、启动器等)。简单来说,Spring Boot可以把后端开发中麻烦的东西都自动做好,从而让程序员无需关注配Web服务器、写XML/配置文件、处理依赖版本冲突、组织项目结构等。

Spring Boot非常适合:

  • Web接口(REST API)
  • 后端服务
  • 微服务

TDD

在软件工程中,测试驱动开发(TDD - Test-Driven Development)是一种需求导向的开发方式。换句话说,在动手写功能之前,你必须先搞清楚这个功能到底应该怎么表现。此外,TDD还可以构建安全网,让你在没写一个新的功能后就能测试,来回测程序是否被破坏。同时,先写测试也能够集思广益,让我们思考一个程序的Edge Cases有哪些。

经典思想

模块化设计

在软件工程领域的经典文献《On the Criteria To Be Used in Decomposing Systems into Modules》(关于系统模块分解准则的探讨,1972年)中,D.L. Parnas主要探讨了如何将一个系统划分为不同的模块。文章指出,虽然模块化的益处广为人知,但人们往往缺乏如何划分模块的具体准则。坐着对比了两种完全不同的模块化方法:

  1. 传统方法:基于处理步骤(流水线)进行分解
  2. 非传统方法:基于信息隐藏准则进行分解

文章中提出的信息隐藏(Information Hiding)的概念,提出模块不应仅仅被看作是子程序,而应被视为一种“责任分配”(Responsibility Assignment)。每个模块应该封装一个特定的、容易发生变化的设计决策(如数据结构的设计、硬件接口的具体实现等),并对外隐藏这些细节 。作者批评了当时流行的“按流程步骤划分模块”的传统做法,认为这种方式会导致模块间高度耦合,一旦某个数据结构发生变化,所有相关模块都需要修改 。

模块化最重要的目标包括:

  1. 灵活性,便于修改
  2. 可理解性,便于学习
  3. 减少团队间的依赖来缩短总开发时间(管理需求)

四种设计模式

在软件工程课程的国际象棋项目中,提到了软件开发中的几种非常重要的技巧,他们是解决常见设计问题的通用模板。

1. 单例模式 (Singleton Pattern)

核心思想: 确保一个类在整个程序运行期间 只有一个实例 ,并提供一个全局访问点。

  • 项目中怎么用: Board 类被设计为 Singleton。因为在一局象棋比赛中,棋盘只能有一个。
  • 为什么要这么做: 避免程序意外创建出两个棋盘(比如一个棋盘在走棋,另一个在显示),导致数据不同步。
  • 代码套路:
    1. 私有化构造函数 private Board() {}(防止外部 new)。
    2. 内部持有一个静态实例 private static Board instance;
    3. 提供静态方法 public static Board theBoard() 来获取这个唯一的实例。

2. 工厂模式 (Factory Pattern)

核心思想: 定义一个创建对象的接口,但让子类决定实例化哪一个类。它把“对象的使用”和“对象的创建”分离开来。

  • 项目中怎么用: 当你从文件读到 br(黑车)时,你不直接 new Rook(Color.BLACK),而是通过 Piece.createPiece("br")。它会去一个“注册表”里找对应的 RookFactory 来帮你创建。
  • 为什么要这么做: 解耦 。如果以后你想增加一种新棋子(比如“超级兵”),你只需要写一个新类并注册它的工厂,而不需要修改处理逻辑的主循环(避免了写一堆 if-elseswitch)。

3. 观察者模式 (Observer Pattern)

核心思想: 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会得到通知并自动更新。

  • 项目中怎么用: Board 是“被观察者”(Subject),BoardListener 是“观察者”(Observer)。
  • 为什么要这么做: 棋盘只管走棋(逻辑),而不需要管走完棋后是要“在屏幕打印一行字”还是“播放一段音效”。你可以挂载多个监听器,一个负责记谱,一个负责UI刷新,它们之间互不干扰。
  • 关键点: Board 里要有一个 List<BoardListener>,每次 movePiece 成功后,遍历这个列表调用 onMove 方法。

4. 内部迭代器模式 (Internal Iterator Pattern)

核心思想: 由容器(棋盘)自己控制遍历过程,客户端只需要定义“对每个元素做什么”。

  • 项目中怎么用: Board 提供一个 iterate(BoardInternalIterator it) 方法。你不需要自己写两个嵌套循环去跑 8x8 的格子,而是把一个“动作”(visit 方法)传给棋盘,棋盘自己内部跑完 64 个格子。
  • 为什么要这么做: 它隐藏了棋盘内部存储结构的细节。无论棋盘是用二维数组存的,还是用一维数组存的,外部使用者都不需要关心,只需要关注逻辑。

GoF设计模式

  • 适配器模式 (Adapter Pattern)
    • 作用 :作为两个不兼容接口之间的桥梁。
    • 代码示例 :将数据提供方输出的 XML 格式,通过 Adapter 转换为分析库所需的 JSON 格式。
  • 代理模式 (Proxy Pattern)
    • 作用 :为另一个对象提供代理或占位符,以控制对该对象的访问(通常用于处理耗时操作、安全保护或添加额外行为)。
    • 代码示例 :支付系统。信用卡支付作为“代理”,在调用实际的“现金支付”处理逻辑前,先进行用户身份验证 (IAM) 和余额/欺诈检查。

OOP

Encapsulation

  • 核心概念 :通过访问修饰符(private, protected, public, 默认包可见)限制对对象内部组件的访问,隐藏内部状态和实现细节。
  • 实现方式 :将字段设为 private,并通过公有的 Getter (Accessor) 和 Setter (Mutator) 方法来访问和修改(例如:BankAccount 类中的余额管理)。
  • 优点 :模块化、降低复杂性、提高代码可重用性、保护内部状态不被随意篡改。
  • 缺点
  • 代码变得冗长(Boilerplate code)。
  • 可能带来微小的性能开销(例如在巨大循环中调用方法,尽管 JIT 编译器可能会进行内联优化)。
  • 可能导致设计过于复杂(例如过度嵌套抽象层)。
  • 增加单元测试的难度(隐藏了内部状态,需要额外测试私有的辅助方法)。

Inheritence

  • 核心概念 :子类(Subclass)继承父类(Superclass)的属性和方法。Java 仅支持 单继承 ,通过接口来避免C++中的“菱形继承问题”(Diamond Problem)。
  • 优点
  • 代码重用与可扩展性 :子类可以直接使用父类的通用功能(如使用 super 调用父类构造函数),并添加独特功能。
  • 多态性 (Polymorphism) :允许以父类的形式引用子类对象,在运行时动态决定调用哪个具体实现(例如:TeacherAdmin 都重写了 StaffMemberwork() 方法)。
  • 抽象 :隐藏复杂实现,只暴露必要接口(强制子类实现抽象方法)。
  • 缺点
  • 紧耦合 (Tight Coupling) :父类的修改会引起所有子类的连锁反应。
  • 复杂性增加 :继承层次过深(超过3层)会导致代码极难理解和维护。
  • 方法重写风险 :如果不小心处理(例如忘记调用 super),可能会导致意外行为。
  • 资源消耗 :增加了内存消耗和动态方法解析的性能开销。
  • 替代方案 :推荐使用 组合 (Composition) ,通过将其他类的实例作为成员变量注入到当前类中(常用于 Spring 框架中的依赖注入)。

开发实例

Graph项目

本部分以一个具体的项目案例来说明如何使用java进行软件开发。

环境搭建与项目初始化

首先配置java开发环境,具体包括:

  • JDK,这里采用JDK21
  • Maven
  • IDE,这里采用IntelliJ IDEA

这里要注意,在现代 Java 开发中,仅仅有 .java 文件是不够的。你需要一个符合 Maven 标准 的项目环境来管理依赖、编译代码和运行测试。比如说,合作方(比如本项目)提供了一些零散的.java文件,仅仅有这些.java文件是不够的,我们还需要Spring Initializr来生成一个带有pom.xml的标准Maven项目。

操作步骤如下:

  • 访问 Spring Initializr
  • 配置参数 : 选择 Maven、Java 21,设置 Group 为edu.tufts,Artifact 为project1java。
  • 下载并保存 : 点击GENERATE,下载 zip 包并解压到你的工作目录。

1769471806668

下载后我们将得到一个叫做project1java的文件夹,然后我们将合作方的skeleton导入到 src -> main -> java -> edu -> tufts -> project1java中。

到这一步,环境配置和skeleton衔接等工作就完成了。正确的目录如下:

1769472234908

搭建好环境、代码骨架后,就可以进入代码实现阶段了。在开始前,建议检查一下项目设置:

  1. 按下快捷键 Ctrl + Alt + Shift + S(Mac 是 Command + ;)打开 Project Structure 窗口。
  2. 点击左侧的 Project 选项卡。
  3. 确保 SDK 栏位显示的是 21
  4. 确保 Language level 栏位也选的是 21
  5. 点击 OK

SDK和语言版本确认后,我们还要确认Maven状态。如果IntelliJ右侧没有Maven图标,可能是还没有识别出这是一个Maven项目。此时我们可以手动“点醒”它:

  1. 在左侧项目树中找到文件 pom.xml
  2. pom.xml 上点击 鼠标右键
  3. 在弹出的菜单底部,找到并点击 "Add as Maven Project"
  4. 完成后,界面右侧通常就会出现Maven 选项卡了。

配置好Maven后我们会看到很多报错信息,所以我们首先要完善graph文件夹下每一个.java文件的package声明:package edu.tufts.project1java.graph;

实现核心逻辑

我们主要有两个任务:

  1. 完成ListGraph.java,使用HashMap和LinkedList来实现邻接表逻辑。
  2. 完成EdgeGraphAdapter.java,负责把一种图的接口(Graph)转换为另一种接口(EdgeGraph)。

然后我们要写两个测试文件:

  1. GraphTest.java
  2. EdgeGraphTest.java

这里我们注意,为什么测试文件写的是GraphTest.java而不是EdgeGraphTest.java呢?这是一个非常深入的问题,涉及到面向对象设计中的一个核心原则: 面向接口编程 (Coding to an Interface) 。在软件工程中,测试文件的命名通常反映了它所验证的 功能行为 ,而不仅仅是具体的 实现类

由于我们编写的ListGraph实际上是Graph接口的一个具体实现,所以我们要测试的应当是Graph,因为我们希望测试的是图的行为而不是邻接表的细节。

我们先打开ListGraph.java,看到如下内容:

import java.util.*;

public class ListGraph implements Graph {
    private HashMap<String, LinkedList<String>> nodes = new HashMap<>();

    public boolean addNode(String n) {
         throw new UnsupportedOperationException();
    }

    public boolean addEdge(String n1, String n2) {
         throw new UnsupportedOperationException();
    }

    public boolean hasNode(String n) {
         throw new UnsupportedOperationException();
    }

    public boolean hasEdge(String n1, String n2) {
         throw new UnsupportedOperationException();
    }

    public boolean removeNode(String n) {
         throw new UnsupportedOperationException();
    }

    public boolean removeEdge(String n1, String n2) {
         throw new UnsupportedOperationException();
    }

    public List<String> nodes() {
         throw new UnsupportedOperationException();
    }

    public List<String> succ(String n) {
         throw new UnsupportedOperationException();
    }

    public List<String> pred(String n) {
         throw new UnsupportedOperationException();
    }

    public Graph union(Graph g) {
         throw new UnsupportedOperationException();
    }

    public Graph subGraph(Set<String> nodes) {
         throw new UnsupportedOperationException();
    }

    public boolean connected(String n1, String n2) {
         throw new UnsupportedOperationException();
    }
}

我们可以看到很多 throw new UnsupportedOperationException();——这行代码在编程中被称为占位符或逻辑坑位,它的意思是“抛出一个‘不支持该操作’的异常” 。之所以写这个,是因为在项目skeleton中,虽然已经定义好了方法

实现基础功能后,我们就要为每个功能写一个test,然后进行测试。我们在test文件夹下创建对应的test文件,然后写test,比如:

    @Test
    @DisplayName("Add new node successfully")
    void testAddNodeSuccess(){
        Graph g = new ListGraph();
        boolean result = g.addNode("A");

        assertTrue(result);
        assertTrue(g.hasNode("A"));
    }

这里要注意,不同的@Test方法之间并不会互相影响,他们都在各自全新的对象实例中运行。

所有代码完成后,可以打开terminal,输入 mvn clean test;如果terminal中缺乏环境依赖,也可以用IntelliJ的Maven面板来完成:

  1. 点击 IntelliJ 窗口最右侧边缘的 Maven 标签。
  2. 依次展开:project1java -> Lifecycle
  3. 先双击 clean (清理之前的编译残留)。
  4. 再双击 test (执行单元测试)。

如果看到BUILD SUCCESS,说明你的代码编译通过,且所有的单元测试(JUnit)全部运行成功。

开发工具

减少模板代码的工具:

  • Project Lombok
    • 通过注解(如 @Data)在编译时自动生成 Getter, Setter, 构造函数, equals, hashCodetoString
    • 优缺点 :提高开发效率和代码可读性,但依赖 IDE 插件,隐藏了实现细节,且 极难进行断点调试 ,可能会破坏封装性。
  • Java Records (Java 14+ 引入):
    • 比 Lombok 更简洁的原生数据载体类,但也存在类似的调试和封装问题,适合根据具体项目需求权衡使用。

评论 #