DEV Community

tenniscp25
tenniscp25

Posted on

คิดซักนิดก่อนใช้ switch

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

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 ไปใช้ต่อ
}
Enter fullscreen mode Exit fullscreen mode

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

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

'when' expression must be exhaustive

รูปแสดงความดีงามของ 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 ได้

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 ไปใช้ต่อ
}
Enter fullscreen mode Exit fullscreen mode

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

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
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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 ก็ไม่ต้องทำอะไร
        }
    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)