A Philosophy of Software Design ปรัชญาการออกแบบโปรแกรมให้เรียบง่าย

A Philosophy of software Design Cover

หนังสือ A Philosophy of Software Design ถือว่าดังในต่างประเทศพอสมควร เขียนโดยคุณ John Ousterhout ศาสตราจาร์ด้าน Computer Science จากมหาลัย Standford เป็นผู้เชี่ยวชาญด้าน Software Architecture และผู้สร้างภาษาโปรแกรมมิ่ง Tcl

John Ousterhout lecture
คุณ John Ousterhout Credit: Pinterest

เล่มนี้ค่อนข้างดีสำหรับคนที่ผ่านประสบการณ์มาบ้างเพราะเขาเล่าเป็น Concept สะส่วนใหญ่ถ้าเคยเจอมาบ้างจะเข้าใจง่ายเลย

สำหรับคนที่อ่าน Clean Code มาแล้วเริ่มรู้สึกว่า ทำไมทำตาม Clean Code แล้วโปรเจคมันกลับซับซ้อนขึ้น เล่มนี้เป็นตัว Balance ความสุดโต่งของลุง Bob ได้ดีเลย 55 เดียวบีมจะมาเล่าให้ฟัง

Complexity

“This book has one goal, is to help you deal with complexity” – A Philosophy of Software Design

ประเด็นสำคัญที่สุดของเล่มนี้คือการเข้าใจและรับมือกับความซับซ้อน เขาบอกว่าเจ้าความซับซ้อนนี่แหละคือตัวการของปัญหาทั้งปวง การลดความซับซ้อนลงเป็นสิ่งที่สำคัญที่สุดในการออกแบบโปรแกรมเลย

ปัญหาของโปรเจคที่ซับซ้อนเกินไปโดยที่ไม่จำเป็นทำให้

  1. โปรแกรมเมอร์ทำงานช้าลง และต้นทุนสูงขึ้น
  2. เกิดบัคได้ง่ายขึ้น
  3. ดึงดูดและรักษาคนเก่งได้ยากขึ้น

แต่จะหลีกเลี่ยง 100% เป็นเรื่องเป็นไปไม่ได้ ยิ่งโปรเจคใหญ่ขึ้นมันก็ซับซ้อนขึ้นเป็นเรื่องธรรมดาแต่เราสามารถช่วยได้โดยลดความซับซ้อนที่ไม่จำเป็นออกไปได้ โดยสองวิธี

  1. เขียนโค้ดให้อ่านง่าย ให้ Simple ที่สุด
  2. ซ่อนรายละเอียดที่ซับซ้อนไว้ข้างใน (Encapsulate)

นิยามความซับซ้อน

ก่อนจะแก้ปัญหาเราต้องเข้าใจว่าจริงๆปัญหาคืออะไร อะไรคือความซับซ้อน?

คุณ John นิยามไว้สั้นๆว่า “อะไรก็ตามที่เข้าใจยากและเปลี่ยนแปลงยาก”

คนส่วนใหญ่ตีความว่าโปรเจคใหญ่ ฟีเจอร์เยอะคือซับซ้อน แต่ไม่ใช่เลยตราบใดที่โค้ดอ่านเข้าใจทำงานด้วยง่ายถือว่าไม่ซับซ้อน ในขณะที่โปรเจคที่โค้ดไม่เยอะก็สามารถมีความซับซ้อนสูงมากได้

💡มีวิธีเช็คง่ายๆว่า Code เราซับซ้อนมั้ยคือลองให้คนอื่นอ่านดู เป็นใครก็ได้อย่าคิดว่าคนอ่านไม่เก่งถ้าอ่านของเราไม่รู้เรื่อง ถ้าคนอ่านบอกว่ามันซับซ้อน “แปลว่ามันซับซ้อน”

คนอ่านสามารถรับรู้ Code Quality ได้ดีกว่าคนเขียนเสมอ

อาการของโค้ดที่ซับซ้อน

ความซับซ้อนมักจะมีอาการบางอย่างที่เรารู้สึกได้เช่น

  1. Change Amplification เปลี่ยนโค้ดนิดเดียวต้องแก้หลายจุด
  2. Cognitive Load ต้องถือข้อมูลในหัวหลายอย่างเพื่อจะเข้าใจว่าโค้ดชุดนี้มันทำงานยังไง
  3. Unknown the Unknown โค้ดที่อ่านแล้วงงไม่รู้ว่าจะแก้ยังไง ต้องใช้อะไรก่อนรึเปล่า (อันนี้หนักสุด)

คนเขียนแซวว่าเขาเคยใช้ Framework ที่ทำให้เราเขียนสั้นลงมากๆบรรทัดน้อยลงเยอะ แต่​! อ่านแล้วไม่เข้าใจเลย 😂 (Cognitive Load)

วิธีแก้ที่ดีที่สุดคือความชัดเจน

ดังนั้นหน้าที่ที่สำคัญที่สุดของ Developer ไม่ใช่แค่เขียนโค้ดให้ใช้งานได้แต่ต้องเขียนโค้ดให้คนอื่นเข้าใจและเอาไปทำต่อได้

Strategic vs Tactical

Strategic Programming คือการลงทุนเขียนโค้ดหรือแก้ไขโดยให้คุณภาพโค้ดดีซึ่งใช้เวลานานกว่า แต่ดีกว่าการ Tactical Programming คือการแก้ไข แก้บัค หรือสร้างให้ใช้ได้เร็วที่สุด (คุ้นๆมั้ย 55)

พอนานๆเข้าถ้าเราแก้ปัญหาแบบ Tactical จะเกิด Technical Debt เต็มไปหมด ค่าใช้จ่ายสูงขึ้นจากการใช้เวลานานในการแก้ปัญหานานขึ้น และส่วนใหญ่คนที่สร้างมักจะออกไปก่อนจะถึงช่วง Maintain สร้างความปวดหัวให้คนที่มารับกรรมต่อ

เราควรตั้งเป้าหมายเป็น Strategic Programming ตลอด คนเขียนบอกถึงแม้ทุกบริษัทจะมีคนที่ใช้วิธี Tactical แบบสุดโต่งคือเน้นงานเสร็จเร็วที่สุดแต่ Quality ยังไงช่างมัน

เขาเรียกคนพวกนี้เรียกว่า Tactical Tornado (คนโคตรโค้ดพายุ 55) และตลกร้ายส่วนใหญ่จะได้รับคำชมเยอะจากฝั่งบริหารเยอะด้วย (แต่ Engineer ด้วยกันอาจจะมองบน) เพราะทำงานเสร็จเร็วแต่ระยะยาวจะทำให้โปรเจคพังพินาศ

ตัวอย่างคือบริษัท Facebook ที่เรียกตัวเองว่าเป็นบริษัทที่ “Move Fast Break Things!” แต่ต่อมา Motto นี้แหละที่กลับมาทำร้ายตัวเองเพราะคนเริ่มไม่อยากเข้าจากชื่อเสีย(ง) ของโค้ดที่เละเทะข้างใน จนตอนหลังต้องเปลี่ยนเป็น “Move Fast with Solid Infrastructure”

Module should be Deep

ชอบบทนี้ที่สุดในเล่มละ เขาบอกว่า Module ควรจะต้องลึก (Deep) คือซ่อนความซับซ้อนไว้ในที่เดียว และเปิดรายละเอียดใน Interface ให้น้อยที่สุด

Module อาจจะเป็นอะไรก็ได้ที่เก็บโค้ดไว้เป็นชุดๆเช่น Class, Function, Service หรืออื่นๆ

แนวคิดนี้ตรงข้ามกับหนังสือ Clean Code อย่างชัดเจนที่บอกว่า Class should be small ที่เน้นให้ทุกอย่างเล็กที่สุด หน้าที่น้อยที่สุดและใช้การแยกไฟล์, แยก Class, Inherite Class ย่อยๆไปเรื่อยๆ

จากประสบการณ์ส่วนตัวตอนอ่าน Clean Code ก็ไม่เห็นด้วยสักเท่าไหร่กับการแยกทุกอย่างให้เล็กที่สุดพออ่านเล่มนี้เลยค่อนข้างตรงกับที่คิด

คนเขียนเรียก Module ที่ข้างในไม่ค่อยมีอะไรว่า Shallow Module (ตื้น) เขาบอก Module ที่เล็กๆมักจะไม่ค่อยมีประโยชน์เพราะคนที่เอาไปใช้จะต้องรับรู้ข้อมูลที่จะต้องใช้เยอะเกินไป (Complexity)

Shallow Module

เขาบอกว่า Module ที่ตื้นไม่ช่วยลดความซับซ้อน (Complexity)

ตัวอย่าง Shallow Module คนเอาไปใช้จะต้องรู้ว่าต้องเรียกทั้ง 3 Class นี้ยังไงและเรียกลำดับไหนก่อนหลัง (Information Leaked)

(มีแถม Shallow Method ไปอีกอัน)

// Shallow Module

class FileOpener { 
  func openFile()
}

class FileCloser {
  func closeFile()
}

class FileReader {
  func readFile
}

// Shallow Method

func addUserToList(_ user: User) {
  userList.append(user)
}

Deep Module

แบบนี้คนเอาไปใช้ไม่จำเป็นต้องรู้ลำดับ หรือต้องทำอะไรบ้างแค่เรียกใช้แล้วจบ ได้ของที่ต้องการ

// Deep Module

class File {
  func readFile() {
    find
    open
    read
    close if finish 
  }
}

จินตนาการว่าถ้าโปรเจคมีแต่ Shallow Module เต็มไปหมดนอกจากจะไม่ช่วยเรื่องลด Complexity แล้วยังทำให้เพิ่มความซับซ้อนของโปรเจคขึ้นไปอีก เพราะจะมี Module ย่อยๆกระจายเต็มไปหมด

ดังนั้น Module ที่ดีควรจะต้องลึกและซ่อนรายละเอียดไว้เยอะๆ

Separate or Grouping

แยก Function? แยก Class? แยก File?

คำถามที่หลายๆคนสงสัยเวลาเขียนคือจะแยกโค้ดชุดนี้ไว้อีกที่หรือรวมกันดี ถ้าแยกต้องเยอะแค่ไหน

คนเขียนบอกว่าเราอาจจะนึกถึง Modularlity คือเราต้องแยกชิ้นส่วนแตกออกเป็น Component ย่อยๆลงไปเรื่อยๆ แต่การทำแบบนั้นมันทำให้ Complexity สูงขึ้นทันที (ไม่ได้ห้ามแต่มี Tradeoff)

การแยกชิ้นส่วนทำให้ Developer มองภาพรวมยากขึ้นดังนั้นเราต้องตัดสินใจให้ดีว่าควรแยกมั้ย

💡 ทริคในการตัดสินใจมีแค่นี้คือโค้ดที่จะแยกมี Dependency กันมั้ย

  • ถ้าไม่มี Dependency แยกดีแล้ว
  • ถ้ามี Dependency กันไม่ควรแยก

When grouping is good?

ให้สังเกตุง่ายๆว่าถ้า Code สองชุดมันมีความเกี่ยวข้องกัน พวกนี้ควรจะรวมอยู่ใกล้ๆกัน เช่น

  • ใช้ข้อมูลเดียวกัน
  • ถูกเรียกใช้ด้วยกัน
  • ใช้ Concept ใกล้เคียงกัน
  • ต้องอ่าน Code ทั้งสองที่ถึงจะเข้าใจว่ามันทำงานยังไง (กระโดดไปหลายๆไฟล์มันเหนื่อยนะ 55)

ตัวอย่าง Function ถ้าใน Clean Code จะบอกว่าไม่ควรมีเกิน 20 บรรทัด! แต่ John บอกว่าจะกี่บรรทัดก็ได้ความยาวไม่เกี่ยวถ้า Function นั้นที่ยาวทำให้ภาพรวมดูง่ายขึ้น (Simpler)

Function ที่ยาวๆไม่ได้แย่เสมอไปถ้าอ่านง่ายและมี Signature ที่เรียบง่าย 100 บรรทัดก็ไม่ได้แย่อะไร

คนเขียนไม่ได้แนะนำให้ทำแบบไหนเป็นพิเศษ เราต้องตัดสินเอง เพราะการไปสุดสักทาง เช่นรวมกันไว้ที่เดียวหมดก็ไม่ดี การแยกกระจายออกเยอะๆก็ไม่ดีเหมือนกัน

💡 แค่แนะนำว่าอย่าตัดสินเพราะแค่ “ความยาวบรรทัด” หรือแค่เพราะมันตรงกับ Concept ที่เราอ่านมา แต่ให้มองจากภาพรวมและความเหมาะสมเป็นหลัก เข้าปรัชญาเฉย

Depth is more important than length – A Philosophy of Software Design

Comments

คุณ John บอกว่าถ้าเขียน Comment ดีๆมันเพิ่มคุณค่าให้โปรเจคมาก อันนี้เห็นด้วย

ประโยคฮิต “โค้ดที่ดีเป็น Document ด้วยตัวมันเอง” ไม่จริงเสมอไปและเขาบอกด้วยว่าเราไม่ควรยึดติดกับแนวคิดนี้เลย เพราะเป็นแนวคิดที่ทำให้เราไม่กล้าเขียน Comment เลย

💡เทคนิคการเขียน Comment ให้ดีคือ “อธิบายสิ่งที่มองไม่เห็น WHY

  • เหตุผลของการตัดสินใจ (Decision)
  • สมมุติฐานของคนเขียน (Assumption)
  • สิ่งที่แลกมากับการทำแบบนี้ แต่เหตุผลที่ต้องทำคือ.. (Trade Off)
  • อะไรที่ควรระวัง (Non-obvious behavior)

สิ่งที่ไม่ควรเขียน What คือ

  • Code นี้ทำอะไร -> Code ควรเขียนให้อ่านรู้เรื่องด้วยตัวเอง

Good and Bad Comment

Comment ที่ดีต้องกระชับ อ่านง่าย ไม่กำกวม

/// Bad Comment

// This method is responsible for validating input data
func validate(...) { ... }

ตัวอย่างข้างบนบอกว่า validate input อ่านเสร็จแล้วคำถามเต็มไปหมด Input อะไร?, Validate ทำไม?

ลองดูตัวอย่างที่ดีข้างล่างที่บอกว่า ไม่เอาทำไม, ไม่เอาอะไร อ่านเข้าใจกว่าเยอะ

/// Good Comment

// Reject negative values here because downstream pricing
// assumes all inputs are non-negative.
func validate(...) { ... }

💡 อีกอย่างนึงที่มีประโยชน์มากคือ “Comment บอกปัญหาที่เคยเจอมาและทำไมถึงเลือกทำวิธีนี้”

// This check looks redundant, but it prevents corruption when
// requests are retried after a timeout.

Naming

เรื่องชื่อคือเรื่อง Classic มากไม่ว่าจะอ่านเล่มไหนก็เจอ และความเห็นไปทางเดียวกันหมดคือเราต้องให้ความสำคัญกับการตั้งชื่อให้มากๆๆ เขาบอกว่า Engineer ส่วนใหญ่ไม่ได้ให้ความสำคัญเรื่องนี้

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

“Bad names create Bugs” เราสามารถป้องกันบัคได้ตั้งแต่ต้นเลยแค่ตั้งชื่อให้ดี 😂

ปัญหาการตั้งชื่อเห็นได้บ่อยที่สุดเลยคือชื่อที่กำกวม, กว้างเกินไป (Too Generic) ตัวอย่าง

// Bad Naming (Too Generic)
func getCount()

พออ่านแล้วจะสงสัยว่า Count ของอะไร ทำให้เราต้องเข้าไปดูโค้ดข้างในอีกทำให้เสียเวลาไล่โค้ดไปอีก ลองดูแบบนี้

เห็นชัดเจนว่า Function นี้จะทำอะไร, จะเอา Count ของอะไร พอจะเดาออกว่ามันจะไปทำอะไรข้างใน

// Good Naming (Specific)
func getUserBankAccountCount()

หัวใจหลักการเขียนชื่อคือเขียนให้ชัดเจน (Precise) อย่างอื่นเป็นส่วนเสริมเทคนิคที่รู้ไว้เพื่อทำให้มันชัดเจนนี่แหละ เช่น

  • ใช้คำไม่เยอะ
  • ไม่ยาวเกินไป
  • ตัดส่วนเสริม (ed, list, string)

มีทริค💡ที่ดีจากเล่มนี้อันนึงเลยคือเขาบอกว่า “ชื่อยิ่งอยู่ห่างจากที่ใช้เท่าไหร่ ชื่อยิ่งควรยาวขึ้นเท่านั้น”

ตัวอย่างง่ายๆคือ for-loop บางคนก็ใช้ i แทนตัวที่ loop อยู่เพราะเอามาใช้แค่ใน loop บล๊อกเดียวเสร็จแล้วก็ไม่ได้ใช้ต่อ แบบนี้ทำให้ i ที่อ่านไม่รู้เรื่องก็ไม่ได้แย่อะไร แต่ถ้าเป็นตัวแปรที่มีไว้ใช้หลายๆที่ ก็ควรนั่งคิดชื่อให้มันหน่อย

เผื่อใครอยากดูตัวอย่างชัดๆวิธีเขียนชื่อให้ดีลองอ่านบทความ Clean Code ช่วง Naming ที่ผมเคยเขียนไว้ได้ครับ


ข้อคิดของเล่มนี้คือ “Complexity เป็นสิ่งที่หลีกเลี่ยงไม่ได้ แต่เราควรควบคุมมัน”

a philosophy of software design book

สังเกตว่าการ Approach ปัญหาของ Software ใช้การตัดสินเยอะมาก ไม่มีสูตรตายตัวน้อยไปก็ไม่ดีมากไปก็ไม่ควร หัวปวดเลยทั้งตอนทำงานจริงและตอนอ่าน 55

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

หวังว่าจะช่วยให้เพื่อนๆจัดการกับ Complexity ของโปรเจคที่เพื่อนๆทำอยู่ได้นะครับ ไว้เจอกันบทความหน้าครับ

ผมเขียนเรื่องแบบนี้เรื่อยๆ ถ้าเพื่อนๆชอบบทความที่อ่านช้าๆและมีสาระแบบนี้ ฝากกดติดตาม 🍌

Leave a Reply

Discover more from Beamtan's Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading