Basic code smell#1: Method ที่ขึ้นกับ Mutation

Chris
Taskworld Tech
Published in
2 min readAug 13, 2017

บ่อยครั้งมากๆ ที่ผมจะเจอปัญหาแบบนี้ ทั้งในโค้ดตัวเองและโค้ดของคนอื่น แล้วผมพบว่ากว่าผมจะเห็นว่ามันเป็น Code smell ก็ต้องสั่งสมประสบการณ์มาเยอะเหมือนกัน

สมมติว่าเราต้องการส่ง email ปกติเรามักจะเห็นโค้ดแบบนี้

var emailSender = new EmailSender()
emailSender.to = "ace@taskworld.com"
emailSender.body = "Hello ace"
emailSender.send()

โค้ดลักษณะนี้เห็นได้ทั่วไปมากๆ

คำถามคือแล้วถ้าเกิดว่าอยู่ดีๆ มีคนอุตริส่ง email.send() โดยที่ลืมใส่ email.to ล่ะ

ผมพบว่าปกติระบบก็จะฟ้อง Exception ว่าอย่าลืมใส่ว่าส่งอีเมล์ให้ใคร

ก็ฟังดูปกติเนอะ เราก็เคยชินกับระบบแบบนี้คลาสแบบนี้กัน ก็ยังดีกว่าอยู่ดีๆ เงียบไปเลยแล้วส่งเมล์ไม่ได้ หรือทำอะไรแปลกๆ อ่ะนะ

แต่ผมคิดว่ามันดีไม่พอ

สิ่งที่ดีกว่านั้นคือออกแบบให้โปรแกรมเมอร์ไม่สามารถเขียนโค้ดที่เกิด Runtime exception ได้ตั้งแต่แรกเลย!!!

“Making illegal states unrepresentable”

หา เราทำถึงขนาดนั้นได้ด้วยเหรอ?

ผมตอบว่าในกรณีนี้ทำได้ครับ

การเขียนโค้ดแล้วไปมี Exception ที่หลังมันให้ความรู้สึกที่แย่มากๆ ผมจะหงุดหงิดเสมอเวลาที่เขียนโค้ดที่คิดว่าเวิร์คแล้วแล้วอยู่ดีๆ ก็ไปเกิด Runtime exception ตรงที่ไม่น่าจะเกิดเพราะลืมนั่นลืมนี่

เป็นความรู้สึกที่แย่มากๆ ผมเกลียดมัน ดังนั้น ผมจะพยายามออกแบบให้คนที่ใช้โค้ดของผมไม่สามารถมีหนทางไปเจอ Runtime exception ได้ (หรือถ้าต้องเจอจริงๆ ก็ขอให้น้อยที่สุด)

ในกรณีนี้ทำได้สองแบบครับ

Object-Oriented approach

var emailSender = new EmailSender("ace@taskworld.com", "Hello ace")
emailSender.send()

ในกรณีนี้ ผมออกแบบ Class EmailSender ไม่ให้มี Empty Constructor ดังนั้นไม่ว่าใครใช้ก็ต้องใส่ข้อมูลให้ครบก่อนเสมอ

พอเราบังคับให้ Constructor ต้องมีครบ ก็จะไม่มีใครสามารถสร้าง EmailSender ที่พอกด emailSender.send() แล้วจะเกิด Exception ได้อีกแล้ว

Functional Approach

send(emailServer, "ace@taskworld.com", "Hello ace")

ง่ายและตรงไปตรงมา ผมส่ง Function บอกว่าต้องใส่ข้อมูลให้ครบ แค่นี้ก็เป็นการยากมากแล้วที่ Developer จะสร้าง Runtime error ได้

นี่คือเหตุผลนึงที่ผมชอบ Functional programming เนื่องจากมันไม่มี State mutation มีแค่คำสั่งกับ Data transformation ดังนั้นเราสามารถบอกได้ทันทีว่าเราต้องการอะไรบ้าง ส่งมาให้ครบ เราไม่สามารถสร้าง Invalid state ได้แต่แรกแล้ว

Common Popular Anti-Pattern

เรื่องที่น่ารำคาญใจสำหรับผมคือ ถึงแม้ Anti-pattern แบบนี้จะเป็นอะไรที่เข้าใจง่ายและหลีกเลี่ยงง่าย แต่เราก็พบ Anti-pattern แบบนี้ได้ทั่วไปมากๆ แม้แต่ระบบที่ออกแบบด้วยบริษัทใหญ่ๆ ก็ตาม

(ผมถึงได้บอกทุกคนตลอด อย่าเชื่อว่านี่มันเป็น Best practice ออกแบบมาดี เพียงเพราะบริษัทใหญ่ๆ อย่าง F…., G…, M… หรือใครก็ตามออกแบบมาให้เรา ตั้งคำถามด้วยว่าเขาทำมาดีหรือเปล่า)

ORM

user = new User()
user.email = "ace@taskworld.com"
user.name = "Chris"
user.password = Hash("WeirdPassword")
user.optionalTelephone = "0000000"
user.save() // *** Throws if some required fields missing ***

นี่คือ Orm ทั่วไปที่พบได้ในเกือบๆ ทุกภาษา

ถ้าในกรณีนี้ user นั้นต้องการ email, name, password แน่นอนในการสร้าง (คงไม่มี User คนไหนที่ไม่มีอ่ะนะ)

แทนที่เราจะบอกว่า user.save() คุณไปทำ Validation ด้วยนะ ใครส่งข้อมูลมาไม่ครบให้ Throw runtime exception นะ จะดีกว่ามั้ยถ้าเราออกแบบยังงี้

user = new User("ace@taskworld.com", "Chris", Hash("WeirdPassword")
user.optionalTelephone = "0000000"
user.save()

ถ้าเราออกแบบยังงี้ ไม่ว่าเราจะกด user.save() ตอนไหน ไม่ว่า Programmer จะมาซนยังไง ตราบใดที่เขาไม่เปลี่ยน Interface ของ Class user ก็ไม่มีทางเกิด Runtime exception จากการใส่ฟิลด์ที่ต้องการไม่ครบได้เลย

นี่เป็นหลักการที่เข้าใจง่าย แต่ก็พลาดกันบ่อย แม้แต่ผมเองก็ใช้เวลาหลายปีกว่าจะมองเห็นหลักการง่ายๆ ตรงนี้

สิ่งที่ผมอยากให้จำคือ ครั้งถัดไปที่เราใช้ Library หรือใช้ Class ของคนอื่น หรือใช้ Function ของคนอื่น แล้วมัน Throw runtime exception ขึ้นมาเพราะเรา Config อะไรบางอย่างไม่ครบ

อย่ามองแต่แค่โทษตัวเองว่าเราโง่เองไม่ยอมอ่านให้ครบก่อนใช้เพียงอย่างเดียว พี่เขาก็บอกแล้วใน Doc แต่ไม่อ่านอย่างเดียว

แต่อยากให้มองให้เห็นไปถึงว่าการดีไซน์คลาสที่เกิดสิ่งนี้ได้ มันเป็น Code smell แบบนึงด้วยครับ

--

--

Chris
Taskworld Tech

I am a product builder who specializes in programming. Strongly believe in humanist.