คิดซักนิดก่อนใช้ switch
Tags: tips, coding
Published: 2022-06-21 18:27:04

switch นี่ง่ายเนอะ ใคร ๆ ก็ใช้เป็น เช่น

```kotlin
enum class CustType {
    Normal, Silver, Gold
}

fun doSomething(custType: CustType) {
    // ...
    val discountRate =
        when (custType) {
            CustType.Normal -> BigDecimal.ZERO
            CustType.Silver -> BigDecimal(5)
            CustType.Gold -> BigDecimal(10)
        }
    // เอา discount rate ไปใช้ต่อ
}
```

_(ใครไม่เขียน kotlin ให้คิดว่า when ก็เหมือน switch ที่สามารถ return ค่ากลับไปใส่ variable ได้)_

ทุกอย่างโอเค แฮปปี้ แต่ถ้าวันนึงเรามี customer type ใหม่อย่าง Platinum โผล่ขึ้นมาล่ะ ? ใครเขียนภาษาที่ compiler ดี ๆ หน่อย (เช่น kotlin) ก็ยังพอทน เพราะ compiler จะช่วยให้ compile ไม่ผ่าน เรามีหน้าที่แค่ต้องมาไล่แก้

!['when' expression must be exhaustive](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hyptf8jvdg9mnbtz6clk.png)

_รูปแสดงความดีงามของ kotlin เวลา case ใหม่งอกมาแล้วเราไม่ได้ handle (compile ไม่ผ่าน)_


## ความซวยจะบังเกิด

ถ้า compiler ที่เราใช้ มันไม่ช่วย หรือเป็นภาษาที่ไม่ถูก compile เลย กลายเป็นว่า logic เราผิดทันทีแบบไม่รู้ตัว ถ้าโชคดีมีเงื่อนไขแบบนี้ไม่กี่ที่ก็ยังพอไหว แต่ถ้าเยอะมากเป็น 10 ๆ 100 ๆ จุด ลุ้นกันหน้ามืดแน่นอน (เช่น เอา customer type ไปหาส่วนลด หาวิธีส่ง หา ฯลฯ เราต้องนั่งระลึกชาติเองว่าเราเอาไปหาอะไรไว้ตรงไหนบ้าง)

เคสแบบนี้ถึงมี automated test ก็ไม่ช่วย เพราะ test มันครอบคลุมแค่ type เก่า ถ้าจะ test ให้ครบ ก็จะกลับมาเจอปัญหาเดิมอยู่ดี คือต้องไล่ให้หมดว่าต้องใส่ test เพิ่มตรงไหนบ้าง


## แล้วทำยังไงล่ะ ?

ใช้ grep (หาข้อความทั้ง project) ก็พอได้ แต่ครบเคสมั้ย บางทีเราก็เขียนแบบ switch บางทีเราก็ if / else if ต่อ ๆ กัน บางทีเราก็ใช้ ternary operator ก็ต้องไล่ grep ให้ครบทุกแบบที่คิดว่าน่าจะมีใช้


##ทางแก้ดีสุดคือ
เลี่ยงได้ให้เลี่ยงครับ ทั้ง switch, if else ซ้อน ๆ และ ternaryฯ ซึ่งการเช็คแบบตัวอย่างที่ยกมาโชคดีเราเลี่ยงได้ แต่รายละเอียดว่าเลี่ยงยังไงอันนี้แล้วแต่ feature ภาษาที่ใช้ เช่น ใครเขียน oop อย่าง java หรือ kotlin เราจับค่าพวกนี้ยัดใส่ enum ได้

```kotlin
enum class CustType(val discountRate: BigDecimal) {
    Normal(BigDecimal.ZERO),
    Silver(BigDecimal(5)),
    Gold(BigDecimal(10));
}

fun doSomething(custType: CustType) {
    // ...
    val discountRate = custType.discountRate
    // เอา discount rate ไปใช้ต่อ
}
```

หรือถ้ามันเยอะมาก เป็นค่าที่ load มาจาก database เราก็มัดรวมกันไปเลย

```kotlin
data class CustConfig(
    val id: Long,
    val discountRate: BigDecimal,
    val vatRate: BigDecimal,
    // ...
)

fun loadConfig(custId: Long): CustConfig {
    // load from db
}

fun doSomething(custId: Long) {
    val custConfig = loadConfig(custId)
    // use discountRate and vatRate in custConfig
}
```


## จะรู้ได้ไงว่าแบบไหนเลี่ยง switch ได้

แบบง่ายสุดคือดูว่าไอ้แต่ละ case ที่เราแยก ๆ ออกมา มันทำจุดประสงค์เดียวกันรึเปล่า เช่น จากตัวอย่างที่ยก ทุก case คือหา discount เหมือนกันหมด ถ้าทำเพื่อจุดประสงค์เดียวกันแบบนี้ พอแยกแล้วกลับมารวมกันทำต่อแบบนี้ แสดงว่าน่าจะเลี่ยง switch ได้

แต่ถ้าแยก case แล้วไปคนละทาง เช่นการรับคำสั่งมาจากระบบอื่น ทำแล้วไม่กลับมารวมเส้นทางกันอีก แบบนี้น่าจะเลี่ยงไม่ได้ (ซึ่งปกติการทำแบบนี้ก็จะมีไม่กี่จุดในระบบอยู่แล้ว)

```kotlin
sealed class CartCommand {
    object ListItems : CartCommand()
    data class AddItem(val itemId: Long, val qty: Long) : CartCommand()
    data class RemoveItem(val itemId: Long) : CartCommand()
}

fun handleCartCommand(cmd: CartCommand) {
    // ...
    when (cmd) {
        CartCommand.ListItems -> {
            // ดึงข้อมูลจาก database
        }
        is CartCommand.AddItem -> {
            // เช็คว่ามีของมั้ย ของเหลือพอ add รึเปล่า
            // อัพเดทข้อมูลใน database ใส่ของและจำนวนเข้าไป
            // ถ้าของไม่พอให้ใส่เท่าที่มี
            // return ผล ว่าใส่ได้กี่ชิ้น
        }
        is CartCommand.RemoveItem -> {
            // ลบข้อมูลใน database
            // ถ้าลบของที่ไม่มีใน cart ก็ไม่ต้องทำอะไร
        }
    }
    // ...
}
```

แต่สุดท้าย.. มันก็แล้วแต่ว่าเราสามารถบิด code เรา ให้รองรับอนาคตได้มากน้อยแค่ไหน (อย่า over-engineer ก็แล้วกัน)