[SDD] SDD 實作 #4 - 移植到 Kotlin

本文探討使用 SDD (規格驅動開發) 概念在其它環境上的心得。

自從我在上一篇系列文中實做了產生類別的 SDD 練習之後, 我嘗試著將這個概念移植到我自己的 Android 專案中, 結果也大獲成功。

其實原理也是非常簡單的, 因為 Kotlin 與 C# 是極為相似的語言, 所以如果在 C# 行得通, 在 Kotlin 裡也應該行得通。

不過有個地方不太一樣。在 Kotlin 中並沒有 IEquatable 這樣的介面, 也不需要。但是你必須使用的類別是所謂的 data class, 它已經自帶 IEquatable 的必須方法, 它本來就必須 override 對應的方法。此外, 在 Kotlin 中實作 Comparable 介面後,不需要自己手動實作 >、<、>=、<= 這些運算子。這是 Kotlin 的「運算子過載(Operator Overloading)」特性帶來的便利, 也就是自動轉換。一旦實作了 compareTo 方法後,Kotlin 編譯器會自動將比較運算子轉換成對 compareTo 的呼叫。

於是我就把原來的 ClassGeneration.md 改了一下。此外我又把一些稍嫌累贅的部份刪除了, 看起來比較精簡:

<!-- 程式一. ClassGeneration.md -->

# Kotlin data 類別生成需求文件

## 概述

本文件指定了在 `MyStockApp` 方案中生成新 Kotlin data class 的需求。

## 類別定義

- **命名空間:** `MyStockApp`
- **類別名稱:** `[MyComparableObject]`(或開發者指定的名稱)
- **實作介面:**
    - `Comparable<[MyComparableObject]>`

## 屬性

類別必須至少包含以下屬性:

1.  **Id**
    - **型別:** `string`
    - **文件註解:** `/** 取得或設定識別碼 */`

2.  **Name**
    - **型別:** `string`
    - **文件註解:** `/** 取得或設定名稱 */`

## 必需方法

必須實作以下方法以滿足 `Comparable` 介面以及對應的方法

### 比較邏輯建議

- **相等性比較**:建議基於 `Id` 屬性進行比較(如果 `Id` 是唯一識別符)
- **排序比較**:建議優先依據 `Name` 屬性,次要依據 `Id` 屬性進行排序

### 測試考量

生成的類別應能支援:
1. 物件相等性比較測試
2. 集合排序測試
3. `null` 參考處理測試
4. 雜湊碼一致性測試

## 使用指南

當需要生成符合此需求的類別時:
1. 確定類別名稱
2. 根據業務邏輯調整屬性的 KDoc
3. 根據實際需求調整相等性和比較邏輯
4. 確保所有必要的命名空間都已引用

在 Android Studio 裡有 Gemini 作為 AI 助手, 所以我把同樣的指令丟給它:

1. 根據 ClassGeneration.md 中述明的規格, 在本專案中建立一個 StockEntry data 類別檔案。該類別中除了規則中指定的屬性之外, 必須包括以下屬性: StockType (股票種類) (of EnumStockType), SalesType (買賣類別) (of EnumSalesType), Amount (買賣金額) (of float), SalesDate (買賣日期) (of Datetime), Cost (成本) (of float) 等等。

2. 上述的 EnumStockType (股票種類) (of Enum) 有以下幾個值: Listed=0 (上市股票), OTC=1 (上櫃股票), Innovation=2 (創新板), ETF=3, Unlisted=99 (未上市股票)。

3. 上述的 EnumStockType (買賣類別) 有兩個值: Buy=0 (買進), Sell=1 (賣出)。 

然後 Gemini 就自動產生 StockEntry.kt 檔案並且準確地放進 data 子目錄下面:

/** 程式二. StockEntry.kt */

package com.khl.mystockapp.data

import java.time.LocalDateTime

/**
 * 股票種類
 */
enum class EnumStockType(val value: Int) {
    /** 上市股票 */
    Listed(0),
    /** 上櫃股票 */
    OTC(1),
    /** 創新板 */
    Innovation(2),
    ETF(3),
    /** 未上市股票 */
    Unlisted(99)
}

/**
 * 買賣類別
 */
enum class EnumSalesType(val value: Int) {
    /** 買進 */
    Buy(0),
    /** 賣出 */
    Sell(1)
}

/**
 * 股票交易項目
 */
data class StockEntry(
    /** 取得或設定識別碼 */
    val id: String,

    /** 取得或設定名稱 */
    val name: String,

    /** 股票種類 */
    val stockType: EnumStockType,

    /** 買賣類別 */
    val salesType: EnumSalesType,

    /** 買賣金額 */
    val amount: Float,

    /** 買賣日期 */
    val salesDate: LocalDateTime,

    /** 成本 */
    val cost: Float
) : Comparable<StockEntry> {

    /**
     * 排序比較
     * 優先依據 Name 屬性,次要依據 Id 屬性進行排序
     */
    override fun compareTo(other: StockEntry): Int {
        val nameResult = this.name.compareTo(other.name)
        if (nameResult != 0) {
            return nameResult
        }
        return this.id.compareTo(other.id)
    }

    /**
     * 相等性比較
     * 基於 Id 屬性進行比較
     */
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as StockEntry

        return id == other.id
    }

    /**
     * 雜湊碼
     * 基於 Id 屬性
     */
    override fun hashCode(): Int {
        return id.hashCode()
    }
}

不過話說回來, Gemini 幫我做出來的 compareTo 和 equals 方法的比較邏輯並不是完全一致, 這是有點奇怪的地方。像這樣的小細節, 仍然需要由開發者自己去發現和注意。如果你已經有自己偏好的比較邏輯, 而且希望未來它一次就把事情做好, 那麼可以考慮再去修改上述 .md 文件以符合你的需求。不過由於我在這裡只是為了做簡單的示範, 就不把我自己在專案裡的特殊需求貼上來了。


Dev 2Share @ 點部落