2024年6月26日 星期三

18.VB.NET 筆記 基礎篇 - Module

VB.NET Module 筆記(基礎篇)

VB.NET Module 筆記(基礎篇)

Module 是 VB.NET 中用來集中放置共用成員的結構。它不需要先建立物件,就能直接呼叫其中的函式、程序、常數或屬性,因此常用來放置工具方法、固定規則、格式化函式與擴充方法。

Module 的重點不是把程式碼集中丟到同一個地方,而是放置「不依附某個物件狀態」的共用邏輯。若功能需要保存每個物件自己的資料,或需要建立多個不同實例,就應優先考慮使用 Class

先理解 Module 在做什麼

Module 放的是共用功能,不是物件狀態

Module 內的成員具有共享性質。呼叫時不需要 New,通常直接寫 ModuleName.FunctionName()。因此,它很適合放沒有個別生命週期的功能。

  • 共用工具:文字整理、格式化、代碼產生。
  • 固定規則:費用試算、檢查規則、顯示轉換。
  • 共用常數:系統名稱、固定費率、固定上限。
  • 擴充方法:讓既有型別多出更順手的呼叫方式。

Module 適合的判斷方式:若一段功能不需要保存自己的獨立資料,也不需要建立多份實例,就可以考慮放進 Module。若每筆資料都有自己的狀態,例如一張會員卡、一筆訂單、一個計時器,就比較適合使用 Class。

基本語法

VB.NET
Public Module 模組名稱
    Public Const 固定值名稱 As String = "固定內容"

    Public Function 共用函式名稱() As String
        Return "共用結果"
    End Function
End Module

Module 成員與存取範圍

修飾詞 可存取範圍 常見用途
Public 專案其他地方可存取。 共用函式、共用常數。
Private 只在同一個 Module 內使用。 輔助方法、內部整理邏輯。
Friend 同一組件內可存取。 同專案內部工具,不公開到外部組件。

Module 不是萬用收納盒

若把所有函式、全域變數與畫面流程都放進同一個 Module,程式短期看似方便,長期會變得難以追蹤。Module 應保持明確主題,例如格式化、驗證、費用規則或擴充方法。

共用函式:不需要建立物件即可呼叫

場景一:產生候位號碼文字

候位號碼格式化不需要保存個別物件狀態,只要輸入代碼與號碼,就能得到固定格式結果。這類單純轉換很適合放在 Module。

需要的主控項
  • TextBoxPrefix:輸入區域代碼。
  • TextBoxNumber:輸入號碼。
  • ButtonBuildQueue:產生候位號碼。
  • LabelQueue:顯示結果。
範例程式碼
VB.NET / Windows Forms
Public Module QueueNumberText
    Public Function BuildQueueCode(ByVal prefix As String, ByVal number As Integer) As String
        Dim safePrefix As String = NormalizePrefix(prefix)
        Return safePrefix & "-" & number.ToString("000")
    End Function

    Private Function NormalizePrefix(ByVal prefix As String) As String
        If String.IsNullOrWhiteSpace(prefix) Then
            Return "Q"
        End If

        Return prefix.Trim().ToUpper()
    End Function
End Module

Public Class Form1
    Private Sub ButtonBuildQueue_Click(sender As Object, e As EventArgs) Handles ButtonBuildQueue.Click
        Dim queueNo As Integer

        If Not Integer.TryParse(TextBoxNumber.Text.Trim(), queueNo) OrElse queueNo < 0 Then
            LabelQueue.Text = "請輸入正確號碼"
            Return
        End If

        LabelQueue.Text = "候位號碼:" & QueueNumberText.BuildQueueCode(TextBoxPrefix.Text, queueNo)
    End Sub
End Class
畫面輸出結果(TextBoxPrefix = r,TextBoxNumber = 12)
候位號碼:R-012
邏輯解析
  • BuildQueueCode 是公開函式,表單可以直接呼叫。
  • NormalizePrefix 是 Private,只作為模組內部輔助方法。
  • 候位號碼格式化不需要建立物件,因此適合放在 Module。

常數與固定規則

場景二:寄物櫃費用試算

固定費率與計算規則若會被多個表單使用,可以集中放在 Module。這樣規則修改時,不必到各個按鈕事件中逐一尋找。

需要的主控項
  • TextBoxHours:輸入使用小時數。
  • ButtonCalculateFee:計算寄物費。
  • LabelFee:顯示結果。
範例程式碼
VB.NET / Windows Forms
Public Module LockerFeeRules
    Public Const FirstHourFee As Decimal = 20D
    Public Const ExtraHourFee As Decimal = 10D

    Public Function CalculateFee(ByVal hours As Integer) As Decimal
        If hours <= 0 Then
            Return 0D
        End If

        If hours = 1 Then
            Return FirstHourFee
        End If

        Return FirstHourFee + (hours - 1) * ExtraHourFee
    End Function
End Module

Public Class Form1
    Private Sub ButtonCalculateFee_Click(sender As Object, e As EventArgs) Handles ButtonCalculateFee.Click
        Dim hours As Integer

        If Not Integer.TryParse(TextBoxHours.Text.Trim(), hours) Then
            LabelFee.Text = "請輸入使用小時數"
            Return
        End If

        Dim fee As Decimal = LockerFeeRules.CalculateFee(hours)
        LabelFee.Text = "寄物費用:" & fee.ToString("N0") & " 元"
    End Sub
End Class
畫面輸出結果(TextBoxHours = 4)
寄物費用:50 元
邏輯解析
  • FirstHourFeeExtraHourFee 是固定規則,適合宣告成常數。
  • CalculateFee 不需要保存使用者狀態,只依照傳入小時數計算。
  • 多個表單若都要計算寄物費,可共用同一個 Module。

Public 函式與 Private 輔助方法

場景三:活動名牌顯示文字

Module 可以公開主要函式,同時把細節整理成 Private 輔助方法。外部只需要知道要呼叫哪個公開函式,不需要知道內部如何清理姓名或轉換組別代碼。

需要的主控項
  • TextBoxGuestName:輸入姓名。
  • TextBoxGroupCode:輸入組別代碼。
  • ButtonBuildBadge:建立名牌文字。
  • LabelBadge:顯示結果。
範例程式碼
VB.NET / Windows Forms
Public Module BadgeTextRules
    Public Const EventTitle As String = "城市小講堂"

    Public Function BuildBadgeText(ByVal guestName As String, ByVal groupCode As String) As String
        Dim safeName As String = CleanGuestName(guestName)
        Dim safeGroup As String = NormalizeGroupCode(groupCode)

        Return EventTitle & "|" & safeName & "|" & safeGroup
    End Function

    Private Function CleanGuestName(ByVal guestName As String) As String
        If String.IsNullOrWhiteSpace(guestName) Then
            Return "未填姓名"
        End If

        Return guestName.Trim()
    End Function

    Friend Function NormalizeGroupCode(ByVal groupCode As String) As String
        If String.IsNullOrWhiteSpace(groupCode) Then
            Return "GENERAL"
        End If

        Return groupCode.Trim().ToUpper()
    End Function
End Module

Public Class Form1
    Private Sub ButtonBuildBadge_Click(sender As Object, e As EventArgs) Handles ButtonBuildBadge.Click
        LabelBadge.Text = BadgeTextRules.BuildBadgeText(TextBoxGuestName.Text, TextBoxGroupCode.Text)
    End Sub
End Class
畫面輸出結果(TextBoxGuestName = 王先生,TextBoxGroupCode = a2)
城市小講堂|王先生|A2
邏輯解析
  • BuildBadgeText 是外部主要呼叫入口。
  • CleanGuestName 是 Private,只在 Module 內部使用。
  • NormalizeGroupCode 是 Friend,表示同一專案組件內可使用。
  • 把內部細節藏起來,可讓表單端只專注於輸入與顯示。

驗證工具 Module

場景四:取件代碼格式檢查

驗證規則如果不依附特定物件,可以放在 Module 中集中管理。這個範例檢查取件代碼是否為 P 開頭,後面接 6 個數字。

需要的主控項
  • TextBoxPickupCode:輸入取件代碼。
  • ButtonCheckCode:檢查代碼。
  • LabelCodeResult:顯示檢查結果。
範例程式碼
VB.NET / Windows Forms
Public Module PickupCodeRules
    Public Function IsValidPickupCode(ByVal pickupCode As String) As Boolean
        If String.IsNullOrWhiteSpace(pickupCode) Then
            Return False
        End If

        Dim cleanCode As String = pickupCode.Trim().ToUpper()

        If cleanCode.Length <> 7 Then
            Return False
        End If

        If Not cleanCode.StartsWith("P") Then
            Return False
        End If

        Dim numberPart As String = cleanCode.Substring(1)

        For Each ch As Char In numberPart
            If Not Char.IsDigit(ch) Then
                Return False
            End If
        Next

        Return True
    End Function
End Module

Public Class Form1
    Private Sub ButtonCheckCode_Click(sender As Object, e As EventArgs) Handles ButtonCheckCode.Click
        If PickupCodeRules.IsValidPickupCode(TextBoxPickupCode.Text) Then
            LabelCodeResult.Text = "取件代碼格式正確"
        Else
            LabelCodeResult.Text = "取件代碼格式錯誤"
        End If
    End Sub
End Class
畫面輸出結果(TextBoxPickupCode = P482913)
取件代碼格式正確
邏輯解析
  • IsValidPickupCode 只依照傳入文字回傳 True 或 False。
  • 驗證規則集中在 Module 中,其他表單也能重複使用。
  • 若驗證需要查資料庫或依賴不同環境設定,就不一定適合直接放在簡單 Module。

擴充方法必須放在 Module

場景五:日期顯示擴充方法

VB.NET 的擴充方法需要宣告在 Module 中,並搭配 <Extension()>。這個範例替 Date 增加一個顯示取件日期的格式方法。

需要的主控項
  • DateTimePickerPickup:選擇取件日期。
  • ButtonShowDate:顯示取件日期。
  • LabelDate:顯示結果。
範例程式碼
VB.NET / Windows Forms
Imports System.Runtime.CompilerServices

Public Module DateDisplayExtensions
    <Extension()>
    Public Function ToPickupLabel(ByVal value As Date) As String
        Return value.ToString("MM/dd") & "(" & GetWeekText(value.DayOfWeek) & ")"
    End Function

    Private Function GetWeekText(ByVal day As DayOfWeek) As String
        Select Case day
            Case DayOfWeek.Monday
                Return "一"
            Case DayOfWeek.Tuesday
                Return "二"
            Case DayOfWeek.Wednesday
                Return "三"
            Case DayOfWeek.Thursday
                Return "四"
            Case DayOfWeek.Friday
                Return "五"
            Case DayOfWeek.Saturday
                Return "六"
            Case Else
                Return "日"
        End Select
    End Function
End Module

Public Class Form1
    Private Sub ButtonShowDate_Click(sender As Object, e As EventArgs) Handles ButtonShowDate.Click
        Dim pickupDate As Date = DateTimePickerPickup.Value.Date
        LabelDate.Text = "取件日期:" & pickupDate.ToPickupLabel()
    End Sub
End Class
畫面輸出結果(取件日期 = 2025-04-29)
取件日期:04/29(二)
邏輯解析
  • 擴充方法需要 Imports System.Runtime.CompilerServices
  • <Extension()>Date 可以使用 ToPickupLabel() 這種呼叫方式。
  • 擴充方法本身放在 Module 中,但使用時看起來像原本型別的方法。

Module 與 Class 的選擇

需求 適合 Module 適合 Class
是否需要 New 不需要建立物件。 需要建立多個實例。
是否保存狀態 不保存個別狀態,只做共用處理。 每個物件有自己的資料。
是否需要繼承 不適合繼承設計。 可搭配繼承、多型與介面。
常見用途 格式化、驗證、固定規則、擴充方法。 會員、訂單、設備、工作項目、計時器。

場景六:計時器應使用 Class 而不是 Module

若需求是同時管理多個計時器,每個計時器都需要保存自己的開始時間,就不適合使用 Module。這種情境應使用 Class 建立多個獨立物件。

範例程式碼
VB.NET
Public Class SimpleTimer
    Public ReadOnly Property TimerName As String
    Public ReadOnly Property StartedAt As DateTime

    Public Sub New(ByVal timerName As String)
        Me.TimerName = timerName
        Me.StartedAt = DateTime.Now
    End Sub

    Public Function GetElapsedSeconds() As Integer
        Return CInt((DateTime.Now - StartedAt).TotalSeconds)
    End Function
End Class
邏輯解析
  • 每個 SimpleTimer 都有自己的 StartedAt
  • 若用 Module 保存開始時間,只能共用同一份狀態,無法自然表示多個計時器。
  • 需要多個獨立實例時,Class 比 Module 更合適。

實務判斷與常見誤區

常見問題整理

  • 把 Module 當全域變數倉庫:共用變數越多,越難追蹤資料在哪裡被改掉。
  • 所有工具都塞同一個 Module:應依主題拆分,例如文字、費用、驗證、日期顯示。
  • 把有狀態的資料放進 Module:若資料應該屬於某個實例,應改用 Class。
  • 忽略命名:HelperUtility 太籠統,建議使用能看出責任的名稱。
  • 過度依賴共享成員:會降低測試與替換彈性。

命名建議:Module 名稱最好直接說明用途,例如 PickupCodeRulesLockerFeeRulesDateDisplayExtensions。名稱越明確,越不容易變成雜物箱。

重點整理

  1. Module 可集中放置不需要建立物件的共用成員。
  2. Module 內的成員具有共享性質,通常可直接用模組名稱呼叫。
  3. 格式化、驗證、固定規則與擴充方法很適合放在 Module。
  4. Public 成員可給外部使用,Private 成員只在 Module 內部使用。
  5. Friend 適合同一組件內共用,但不公開到外部組件。
  6. 擴充方法需要放在 Module 中,並使用 <Extension()>
  7. Module 不適合保存大量可變共享狀態。
  8. 需要建立多個獨立物件、保存實例資料或設計繼承時,應使用 Class