本课学习目标
- 从“类与对象基础”进一步理解面向对象设计。
- 理解单一职责(Single Responsibility)的基本思想。
- 建立 Student 实体类。
- 建立 StudentRepository 负责数据库操作。
- 建立 StudentService 负责业务规则与验证。
- 让 Form 只负责读取控件、调用服务和显示结果。
- 理解 UI、Service、Repository 与 Database 的关系。
- 把 SQL 从 Form1.vb 移出。
- 减少重复代码与窗体耦合。
- 为第 30 课的继承、接口与多态做好准备。
29.1 会写 Class 不等于会做面向对象设计
第 15 课已经学习了 Class、Property、对象与构造函数。面向对象设计进一步思考的是:哪个对象应该负责哪一件事?
验证、SQL、数据库连接、业务规则、MessageBox、DataGridView 全部混在按钮事件里。
Student 表示学生;Repository 管数据库;Service 管业务;Form 管界面。
29.2 什么是“职责”?
职责就是一个类“应该负责什么”。
29.3 本课的四层结构
调用方向:
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
Name
Course
Score
AddStudent()
UpdateStudent()
DeleteStudent()
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);字段越来越多时,方法签名会越来越长。
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。
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 初学者常见错误
Repository 里弹 MessageBox
Repository 应负责数据访问,不应该知道 UI 怎样提示用户。
StudentService 直接读取 TextBox
Service 应接收 Student 或普通参数,而不是依赖 Form 控件。
Student 类里面写 SqlConnection
实体类负责表示学生,不负责数据库连接。
为了分层复制更多代码
每一层应该有明确职责,而不是把同一逻辑复制到多个类。
29.25 小练习
- 为 Student 加入 GetGrade() 方法,根据 Score 返回 A–F。
- 在 StudentService 增加 GetHighAchievers(minScore)。
- 在 StudentRepository 增加 GetById(studentId)。
- 建立 Course 类,包含 CourseID 与 CourseName。
- 建立 CourseRepository 的基本结构,但暂时不需要完整 CRUD。
- 检查 Form1,找出还有哪些业务规则可以移到 Service。
- 画出 Form1 → StudentService → StudentRepository → Database 的调用图。
本课复习
- 为什么“会写 Class”还不等于“会做面向对象设计”?
- Student 类的主要职责是什么?
- StudentRepository 应该负责什么?
- StudentService 应该负责什么?
- Form1 应该保留哪些职责?
- 为什么 Repository 不应该弹出 MessageBox?
- 为什么 Service 不应该直接读取 TextBox?
- 使用 Student 对象代替多个参数有什么好处?
- 为什么 DataGridView 可以绑定 List(Of Student)?
- 接口会怎样进一步改善这种设计?