VB Visual Basic 2026 中文教程
第 29 课 · 从“能运行”到“容易维护”

面向对象设计

前面的数据库项目已经可以新增、查询、筛选、修改和删除学生资料,但越来越多 SQL、验证与界面代码都集中在 Form1.vb。真正可维护的应用需要把不同职责拆开。本课将建立 Student 实体、StudentRepository 数据访问类与 StudentService 业务逻辑类,让 Form 回到它最应该做的工作:处理用户界面。

环境:Visual Studio 2026 · VB.NET · .NET 10重点:职责分离与对象协作实作:StudentOopManager2026

本课学习目标

  • 从“类与对象基础”进一步理解面向对象设计。
  • 理解单一职责(Single Responsibility)的基本思想。
  • 建立 Student 实体类。
  • 建立 StudentRepository 负责数据库操作。
  • 建立 StudentService 负责业务规则与验证。
  • 让 Form 只负责读取控件、调用服务和显示结果。
  • 理解 UI、Service、Repository 与 Database 的关系。
  • 把 SQL 从 Form1.vb 移出。
  • 减少重复代码与窗体耦合。
  • 为第 30 课的继承、接口与多态做好准备。

29.1 会写 Class 不等于会做面向对象设计

第 15 课已经学习了 Class、Property、对象与构造函数。面向对象设计进一步思考的是:哪个对象应该负责哪一件事?

所有事情都放在 Form1

验证、SQL、数据库连接、业务规则、MessageBox、DataGridView 全部混在按钮事件里。

让不同对象各负其责

Student 表示学生;Repository 管数据库;Service 管业务;Form 管界面。

29.2 什么是“职责”?

职责就是一个类“应该负责什么”。

Student
保存学生数据与与学生本身直接相关的行为。
StudentRepository
执行 INSERT、SELECT、UPDATE、DELETE。
StudentService
执行输入验证和业务规则,再调用 Repository。
Form1
读取 TextBox、响应按钮、绑定 DataGridView、显示 MessageBox。
一个类应该有一个清楚的主要理由发生变化 如果数据库表结构改变,主要修改 Repository;如果验证规则改变,主要修改 Service;如果界面布局改变,主要修改 Form。

29.3 本课的四层结构

UIForm1
Service业务逻辑
Repository数据访问
DatabaseStudentManagementDB

调用方向:

Form1
→ StudentService
→ StudentRepository
→ SQL Server LocalDB

29.4 建立 StudentOopManager2026

建立新的 Windows Forms 项目:

StudentOopManager2026

加入以下文件:

StudentOopManager2026
├─ Models
│  └─ Student.vb
├─ Data
│  ├─ DatabaseHelper.vb
│  └─ StudentRepository.vb
├─ Services
│  └─ StudentService.vb
└─ Form1.vb
项目类关系
Student
StudentID
Name
Course
Score
StudentService
ValidateStudent()
AddStudent()
UpdateStudent()
DeleteStudent()
StudentRepository
GetAll()
Insert()
Update()
Delete()

29.5 建立 Student 实体类

创建:

Models\Student.vb
Public Class Student

    Public Property StudentID As Integer
    Public Property Name As String
    Public Property Course As String
    Public Property Score As Decimal

    Public Sub New()
    End Sub

    Public Sub New(
        name As String,
        course As String,
        score As Decimal)

        Me.Name = name
        Me.Course = course
        Me.Score = score

    End Sub

    Public Sub New(
        studentId As Integer,
        name As String,
        course As String,
        score As Decimal)

        Me.StudentID = studentId
        Me.Name = name
        Me.Course = course
        Me.Score = score

    End Sub

End Class

现在程序中的“一位学生”不再是四个分散变量,而是一个 Student 对象。

29.6 为什么用 Student 对象传资料?

多个独立参数

InsertStudent(name, course, score);字段越来越多时,方法签名会越来越长。

Student 对象

Insert(student)。资料集中在一个对象中,更容易传递和扩充。

29.7 Repository 的角色

Repository 可以理解为“负责与数据库交谈的对象”。Form 不需要知道 SQL 长什么样,只要说:

repository.GetAll()
repository.Insert(student)
repository.Update(student)
repository.Delete(studentId)

29.8 StudentRepository.GetAll()

创建:

Data\StudentRepository.vb
Imports Microsoft.Data.SqlClient

Public Class StudentRepository

    Public Function GetAll() As List(Of Student)

        Const sql As String =
            "SELECT StudentID, Name, Course, Score " &
            "FROM dbo.Students " &
            "ORDER BY StudentID;"

        Dim students As New List(Of Student)()

        Using connection As New SqlConnection(
            DatabaseHelper.ConnectionString)

            Using command As New SqlCommand(
                sql,
                connection)

                connection.Open()

                Using reader As SqlDataReader =
                    command.ExecuteReader()

                    While reader.Read()

                        Dim student As New Student(
                            reader.GetInt32(0),
                            reader.GetString(1),
                            reader.GetString(2),
                            reader.GetDecimal(3))

                        students.Add(student)

                    End While

                End Using

            End Using

        End Using

        Return students

    End Function

29.9 StudentRepository.Insert()

Public Function Insert(
    student As Student) As Integer

    Const sql As String =
        "INSERT INTO dbo.Students " &
        "(Name, Course, Score) " &
        "VALUES (@Name, @Course, @Score);"

    Using connection As New SqlConnection(
        DatabaseHelper.ConnectionString)

        Using command As New SqlCommand(
            sql,
            connection)

            command.Parameters.Add(
                "@Name",
                SqlDbType.NVarChar,
                100).Value =
                student.Name

            command.Parameters.Add(
                "@Course",
                SqlDbType.NVarChar,
                100).Value =
                student.Course

            Dim scoreParameter As SqlParameter =
                command.Parameters.Add(
                    "@Score",
                    SqlDbType.Decimal)

            scoreParameter.Precision = 5
            scoreParameter.Scale = 2
            scoreParameter.Value =
                student.Score

            connection.Open()

            Return command.ExecuteNonQuery()

        End Using

    End Using

End Function

29.10 StudentRepository.Update()

Public Function Update(
    student As Student) As Integer

    Const sql As String =
        "UPDATE dbo.Students " &
        "SET Name = @Name, " &
        "Course = @Course, " &
        "Score = @Score " &
        "WHERE StudentID = @StudentID;"

    Using connection As New SqlConnection(
        DatabaseHelper.ConnectionString)

        Using command As New SqlCommand(
            sql,
            connection)

            command.Parameters.Add(
                "@StudentID",
                SqlDbType.Int).Value =
                student.StudentID

            command.Parameters.Add(
                "@Name",
                SqlDbType.NVarChar,
                100).Value =
                student.Name

            command.Parameters.Add(
                "@Course",
                SqlDbType.NVarChar,
                100).Value =
                student.Course

            Dim scoreParameter As SqlParameter =
                command.Parameters.Add(
                    "@Score",
                    SqlDbType.Decimal)

            scoreParameter.Precision = 5
            scoreParameter.Scale = 2
            scoreParameter.Value =
                student.Score

            connection.Open()

            Return command.ExecuteNonQuery()

        End Using

    End Using

End Function

29.11 StudentRepository.Delete()

Public Function Delete(
    studentId As Integer) As Integer

    Const sql As String =
        "DELETE FROM dbo.Students " &
        "WHERE StudentID = @StudentID;"

    Using connection As New SqlConnection(
        DatabaseHelper.ConnectionString)

        Using command As New SqlCommand(
            sql,
            connection)

            command.Parameters.Add(
                "@StudentID",
                SqlDbType.Int).Value =
                studentId

            connection.Open()

            Return command.ExecuteNonQuery()

        End Using

    End Using

End Function

End Class

29.12 Service 的角色

Repository 负责数据库,但它不应该决定:

姓名是否为空?
成绩是否介于 0 与 100?
课程是否为空?

这些属于业务规则,可以交给 StudentService。

29.13 建立 StudentService

创建:

Services\StudentService.vb
Public Class StudentService

    Private ReadOnly repository As StudentRepository

    Public Sub New(
        repository As StudentRepository)

        Me.repository =
            repository

    End Sub

    Public Function GetStudents() As List(Of Student)

        Return repository.GetAll()

    End Function

    Public Function ValidateStudent(
        student As Student,
        ByRef errorMessage As String) As Boolean

        If String.IsNullOrWhiteSpace(
            student.Name) Then

            errorMessage =
                "请输入学生姓名。"

            Return False

        End If

        If String.IsNullOrWhiteSpace(
            student.Course) Then

            errorMessage =
                "请输入课程名称。"

            Return False

        End If

        If student.Score < 0D OrElse
           student.Score > 100D Then

            errorMessage =
                "成绩必须介于 0 与 100。"

            Return False

        End If

        errorMessage = ""
        Return True

    End Function

29.14 Service 调用 Repository

Public Function AddStudent(
    student As Student,
    ByRef errorMessage As String) As Boolean

    If Not ValidateStudent(
        student,
        errorMessage) Then

        Return False

    End If

    Return repository.Insert(
        student) = 1

End Function

Public Function UpdateStudent(
    student As Student,
    ByRef errorMessage As String) As Boolean

    If student.StudentID <= 0 Then

        errorMessage =
            "Student ID 无效。"

        Return False

    End If

    If Not ValidateStudent(
        student,
        errorMessage) Then

        Return False

    End If

    Return repository.Update(
        student) = 1

End Function

Public Function DeleteStudent(
    studentId As Integer) As Boolean

    If studentId <= 0 Then
        Return False
    End If

    Return repository.Delete(
        studentId) = 1

End Function

End Class

29.15 Form1 只持有 Service

Public Class Form1

    Private ReadOnly studentService As StudentService

    Public Sub New()

        InitializeComponent()

        Dim repository As New StudentRepository()

        studentService =
            New StudentService(
                repository)

    End Sub

现在 Form1 不再需要自己建立 SqlConnection 或写 SQL。

ButtonClick
Form1建立 Student
StudentService验证规则
Repository执行 SQL
Database保存资料

29.16 Form 从控件建立 Student

Private Function TryCreateStudent(
    ByRef student As Student) As Boolean

    Dim score As Decimal

    If Not Decimal.TryParse(
        txtScore.Text,
        score) Then

        MessageBox.Show(
            "请输入有效成绩。")

        txtScore.Focus()
        Return False

    End If

    student =
        New Student(
            txtName.Text.Trim(),
            txtCourse.Text.Trim(),
            score)

    Return True

End Function

注意:Form 只负责把界面输入转换成对象。真正的姓名空白、课程空白、成绩范围规则交给 Service。

29.17 新增按钮变得更简单

Private Sub btnAdd_Click(
    sender As Object,
    e As EventArgs) Handles btnAdd.Click

    Dim student As Student = Nothing

    If Not TryCreateStudent(
        student) Then

        Exit Sub

    End If

    Dim errorMessage As String = ""

    Try

        If studentService.AddStudent(
            student,
            errorMessage) Then

            lblStatus.Text =
                "学生新增成功。"

            ClearInputFields()
            LoadStudents()

        Else

            MessageBox.Show(
                errorMessage,
                "无法新增",
                MessageBoxButtons.OK,
                MessageBoxIcon.Warning)

        End If

    Catch ex As Exception

        MessageBox.Show(
            ex.Message,
            "错误",
            MessageBoxButtons.OK,
            MessageBoxIcon.Error)

    End Try

End Sub

29.18 用 List(Of Student) 绑定 DataGridView

Private Sub LoadStudents()

    Dim students As List(Of Student) =
        studentService.GetStudents()

    dgvStudents.DataSource =
        Nothing

    dgvStudents.DataSource =
        students

    ConfigureGrid()

    lblStatus.Text =
        $"已加载 {students.Count} 位学生。"

End Sub

这一次 Repository 返回的是:

List(Of Student)

而不是 DataTable。DataGridView 同样可以绑定对象列表。

29.19 修改按钮:传递 Student 对象

Private Sub btnUpdate_Click(
    sender As Object,
    e As EventArgs) Handles btnUpdate.Click

    Dim studentId As Integer

    If Not Integer.TryParse(
        txtStudentID.Text,
        studentId) Then

        MessageBox.Show(
            "请先选择学生。")

        Exit Sub

    End If

    Dim score As Decimal

    If Not Decimal.TryParse(
        txtScore.Text,
        score) Then

        MessageBox.Show(
            "请输入有效成绩。")

        Exit Sub

    End If

    Dim student As New Student(
        studentId,
        txtName.Text.Trim(),
        txtCourse.Text.Trim(),
        score)

    Dim errorMessage As String = ""

    If studentService.UpdateStudent(
        student,
        errorMessage) Then

        lblStatus.Text =
            "学生资料修改成功。"

        ClearInputFields()
        LoadStudents()

    Else

        MessageBox.Show(
            If(
                String.IsNullOrWhiteSpace(errorMessage),
                "找不到要修改的学生。",
                errorMessage))

    End If

End Sub

29.20 删除按钮

Private Sub btnDelete_Click(
    sender As Object,
    e As EventArgs) Handles btnDelete.Click

    Dim studentId As Integer

    If Not Integer.TryParse(
        txtStudentID.Text,
        studentId) Then

        MessageBox.Show(
            "请先选择学生。")

        Exit Sub

    End If

    Dim result As DialogResult =
        MessageBox.Show(
            $"确定删除 Student ID {studentId}?",
            "确认删除",
            MessageBoxButtons.YesNo,
            MessageBoxIcon.Warning)

    If result <> DialogResult.Yes Then
        Exit Sub
    End If

    If studentService.DeleteStudent(
        studentId) Then

        lblStatus.Text =
            "学生资料删除成功。"

        ClearInputFields()
        LoadStudents()

    Else

        MessageBox.Show(
            "找不到要删除的学生。")

    End If

End Sub

29.21 这样设计有什么好处?

可读性

Form 更短

按钮事件不用再塞入大量 SQL。

可维护性

修改集中

数据库逻辑主要修改 Repository。

可测试性

业务逻辑独立

Service 不必直接依赖 TextBox 控件。

可扩展性

容易加入功能

未来可增加 CourseRepository、ReportService。

对象化

资料有明确模型

Student 不再只是零散变量。

低耦合

职责分开

UI 不需要知道每一条 SQL。

29.22 小项目也需要这么多类吗?

一个只有几十行代码的练习,不一定需要完整分层。但当项目开始出现:

  • 多个窗体
  • 多个数据库表
  • 重复 CRUD
  • 业务验证
  • 搜索、报表、API

把职责拆开会越来越有价值。

目标不是“类越多越专业” 好设计的重点是职责清楚,而不是为了面向对象而创造没有用途的类。

29.23 下一课为什么要学习接口?

目前 StudentService 直接依赖:

StudentRepository

下一课会进一步抽象成:

IStudentRepository

然后 StudentRepository 实现这个接口。这样 Service 只依赖“Repository 应该提供什么能力”,而不是依赖某个具体实现。这会带我们进入:

继承
接口
多态

29.24 初学者常见错误

错误 1

Repository 里弹 MessageBox

Repository 应负责数据访问,不应该知道 UI 怎样提示用户。

错误 2

StudentService 直接读取 TextBox

Service 应接收 Student 或普通参数,而不是依赖 Form 控件。

错误 3

Student 类里面写 SqlConnection

实体类负责表示学生,不负责数据库连接。

错误 4

为了分层复制更多代码

每一层应该有明确职责,而不是把同一逻辑复制到多个类。

29.25 小练习

  1. 为 Student 加入 GetGrade() 方法,根据 Score 返回 A–F。
  2. 在 StudentService 增加 GetHighAchievers(minScore)
  3. 在 StudentRepository 增加 GetById(studentId)
  4. 建立 Course 类,包含 CourseID 与 CourseName。
  5. 建立 CourseRepository 的基本结构,但暂时不需要完整 CRUD。
  6. 检查 Form1,找出还有哪些业务规则可以移到 Service。
  7. 画出 Form1 → StudentService → StudentRepository → Database 的调用图。

本课复习

  1. 为什么“会写 Class”还不等于“会做面向对象设计”?
  2. Student 类的主要职责是什么?
  3. StudentRepository 应该负责什么?
  4. StudentService 应该负责什么?
  5. Form1 应该保留哪些职责?
  6. 为什么 Repository 不应该弹出 MessageBox?
  7. 为什么 Service 不应该直接读取 TextBox?
  8. 使用 Student 对象代替多个参数有什么好处?
  9. 为什么 DataGridView 可以绑定 List(Of Student)?
  10. 接口会怎样进一步改善这种设计?