[KBTG-GO#06] Refactoring with Go

รอบนี้ยาวเลยมาหลังสงกรานต์ หลังจากเคลียร์งานธุระต่างๆหมด หัวข้อมี ดังนี้

What is Code Smells ?

  • อะไรที่เราสามารถเอ๊ะ/แหม่งๆใน Code ได้ (Quick Spot) และเป็นจุดที่ทำให้เราต้องมาปรับปรุง หรือแก้ไข ก่อนที่ Code จุดนั้นทำให้เกิดปัญหาได้

Code Smells are symptoms of poor design or implementation choices

Martin Fowler

Type Of Code Smells

มีเยอะมาก แบ่งได้ 5 กลุ่ม

Bloaters - Don't have to be that big มันใหญ่ไป

  • Primitive Obsession พวก Primitive มันใช้ if เยอะ หรือ Data Structure ที่ซับซ้อนมากไป ทำให้เป็น Class / Struct ดีกว่า
  • Large Class
  • Data Clumps - data ที่ต้องไปด้วยกันเสมอ แต่เราดันแยกส่ง แทนที่ทำให้เป็น Class
  • Long Method - Code ยาวไปมีการทำงานหลายอย่างใน method เดียว
  • Long Parameter List - 1 method รับหลาย param ตัวเลขหลายคนบอก 3 / 5 ผมใช้ 5 นะ

Tool Abusers - Misuse

  • Alternative Classes with Different Interfaces - Method/Class ที่ทำหน้าที่เดียวกัน แต่ดันรับ Interfaces parameter คนละแบบ ไม่เป็น orthogonal design (อิสระจากกันชัดเจน)
  • Refused Bequest - Class ลูก Override Class แม่หมด ไม่รับมรดก แสดงว่า Design ผิดแล้ว
  • Switch Statements - ปัญหา อาจจะเป็น Default Case อาจจะทำให้เกิดเคสแปลกได้
  • Temporary Field - มีตัวแปรที่ไม่จำเป็น มันกิน mem และจัดการยาก เป็นไปได้ลด ถ้าๆไม่ได้ตั้งชื่อให้เหมาะสม

Change Preventers -ปรับเปลี่ยนยาก //จริงๆ Maintain ยาก

  • Parallel Inheritance Hierarchies
    - เมื่อก่อนนิยามไปแนวๆนี้มั้งที่เรียนมา จำๆได้ว่า C++ มันทำ multiple Inheritance ทำได้ Extend Class แม่ A / B ไปๆมามัน Conflict กันเอง แล้วไป override แทน เรื่องตลกร้าย คนที่มาใช้ override อีก ตอนแก้เลยยาก
    - ตอนนี้ when an inheritance tree depends on another inheritance tree by composition ผมวา่าคำนี้อธิบายดีสุด
    Note go ไม่มี inherited มีแต่ composite
  • Shotgun Surgery - ทุกอย่างกระจายหมด พอแก้ Feature นึง ปรากฏว่า เราต้องมาไล่แก้หลายจุด แบบปืน ยิงแล้วไป กระสุนแตกกระจาย เหมือนทำ Feature แปะหลายที่ แก้ไม่หมด
  • Divergent Change - Code ที่่เป็น ความหวังหมู่บ้าน กลับด้านของ Shotgun Surgery รวมศูนย์ Logic

Dispensable - ไม่จำเป็นต้องมี

  • Comments - เขียน Code ให้สื่อแทน เช่น เอาส่วนของ Code แยกเป็น method
  • Data Class - Class ที่เก็บ Data แต่จริงมันจะขัดกับบาง Pattern เช่น DTO
  • Lazy Class - บางอันไม่จำเป็นต้องแตก Class เป็น Untility เล็กๆได้
  • Dead Code - ไม่ได้ใช้
  • Speculative Generality - Generic เขียนเผื่ออนาคตเกิดไป ทำให้คนเอาไปใช้ต้องมีท่าแปลกๆ
  • Duplicate Code - Code ซ้ำ

Couplers - Too much depend on each other

  • Feature Envy - Feature ไปมันไปกองที่ Class อื่นมากเกินไป เหมือนใช้เพื่อนทำงาน
  • Inappropriate Intimacy - Access Private Method / Field ของ Class อื่นๆ มัน Couple มากเกินไป //ไปปรับ State ของเค้านะ
  • Incomplete Library Class - Library ยังทำไม่เสร็จ ใช้ได้บางเรื่องที่เหลือ อาจจะทำงานผิด หรือโยน Exception ออกมา
  • Message Chains - ส่ง Data ไปมา แล้ววนไปเรื่อย กว่าจะกลับมาทำเสร็จ //Ping-Pong meeting ที่น่าจะมีจุดจบ
  • Middle Man - โยนไม่ๆ คำสวยๆเป็น Proxy ส่งต่องานให้ Class อื่นๆทำ ตัวมันเองไม่มี Logic เลย

จริงต้องไปดู Coupling Cohesion

เหมือน Recap ป โท เลย ลองดูจาก Lecture 2110624_SDD_01.00.00.pdf ได้ น่าจะทำมา 6-7 ปี และ

Refactoring

Improving design without changing behaviour
หรือ Behavior-preserving transformation

อันนี้สรุปสั้นๆ ชัดดีชอบ
- แบบยาวๆ — Martin Fowler
  • (noun): a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior
  • (verb): to restructure software by applying a series of
    refactorings
    without changing its observable behavior.
    Note: series of refactorings - ท่าที่จัดการ Code Smells ที่ละท่าา เช่น พวกแยก Extract Method ทำจนเราพอใจ แต่ตอน Apply Re-run Test หรือ ไม่โดน Tools อย่าง Sonar มันด่า 5555

Refactoring != Rewrite

- When to Refactor?
  1. Code เดิมต้องมี Test และ Test มันทำงานได้ด้วย //Run ผ่าน
  2. ทำระหว่าง Development ควรทำระหว่าง Dev ไปเลย มันจะเหมาะกับแนวคิด TDD
    //ส่วนตัว ถ้ายังไม่ใช้ TDD ปรับยาก
    //หวังว่า Non-IT จะมาอ่านนะ บอกไปทำที่หลัง ที่หลังพี่ไม่มารับผิดชอบ กันเวลา 555 ในเลข 5 นั้นมีน้ำตา
- Why Refactor?
  • Economics - Business Value ทำแล้วลดเวลาปัญหาจุกจิ Scale ได้ / Technical Debt
    //บ่นต่อและ หลายที่ Business บอกว่าไร้สาระ อย่างที่ผมทำ ปีนี้บาง Product น่าจะได้รับ Effect หลังจากดียมาได้ 5-6 ปี
  • ส่วนอื่นๆที่ได้ นอกจาก Clean Code / Quality เป็นต้น

แล้ว ถ้าไม่ทำเกิดอะไรขึ้น ตามนี้เลย Design Stamina Hypothesis (martinfowler.com) มีคน proof มาแล้ว

มันช่วยให้ Business Scale ได้ไว้ ถ้ายอมลุงทุนด้วย Design + Refactoring + Test
- General Step for Refactoring
  • Find some code smell
  • Make sure you have solid set of tests for that section of code and passing
    สั้นๆ ทำ Test ส่วนตัวอาจจะยอมทำ Integration Test ของผมยอมทำแบบนั้นนะ ถ้างานมันรีบๆ ทำ1-2 เคส แล้วพอมี defect มาดู Coverage Report ถ้ามันครอบคลุม กล้า Refactor
  • Make a small code change by using Refactoring Techniques //NOT rewrite
  • Make sure the tests pass
  • Commit Code to add Check Point เผื่อพลาดจะได้ Revert ได้
  • Repeat the TDD cycle until the smell is gone
    ส่วนตัว ทำซ้ำ แต่อาจจะไม่ต้องใช้ TDD //ผมเขียนให้เห็นภาพ แล้วเอา Test ครอบที่หลัง

Refactoring Techniques

แต่ละ Code Smell จะมีเทคนิคที่ใช้จัดการต่างๆกัน แต่อันนี้จะใช้เน้น 4-5 ตัว

- Inline Function

แยก method จนตอนอ่านเรางงเอง ว่า Context มัน คือ อะไร เรียกว่ายังไงดี อาจจะใช้ท่า Extract Function มาไปจนมันเป็น Smell เอง
Note: แต่ Method ที่ทำ Inline ต้องไม่เป็น polymorphism นะ

  • Before
func getPrice(person Person) string {
   if isChildren(person) {
      return "free"
   }
   return "$15"
}
func isChildren(person Person) string {
   return person.age < 12
}
  • After
 func getPrice(person Person) string {
    if person.age < 12 {
       return "free"
    }
    return "$15"
 }

เอาจริงๆ ผมชอบตัวอย่างของ Inline Method (refactoring.guru) มากกว่านะ มันอ่านแล้วเอ๊ะ สัมพันธ์กันยังไง

- Extract Function

เมื่อไหร่ที่เรา Comment ในส่วนนั้น หรือ ของ C# ใส่ Region ครอบ แตกมันออกมา แยกให้อ่านง่าย แล้วพอทำมันเล็กลง เราจะเขียน unit test ได้ง่ายขึ้น

  • Before
func printReceipt(person Person) {
   amount := getPrice(person)
   
   //receipt details
   fmt.Println("name:", person.Name)
   fmt.Println("total amount:", amount)
}
  • After
func printReceipt(person Person) {
   amount := getPrice(person)
   receiptDetails(person, amount)
}

func receiptDetails(person, amount) {
   fmt.Println("name:", person.Name)
   fmt.Println("total amount:", amount)
}
- Replace Nested Conditional with Guard Clauses

เงื่อนไขที่ซ้บซ้อนมันอ่าน Code ยาก จัดใหม่ มอง if ไปอีกมุม และทำ Guard Clauses ลด Nested If ลด Cognative Load ตอนอ่าน

  • Before
func getPayAmount() int {
   result := 0   //Code smell tmp variable
   if isDead() {
      result = deadAmount()
   } else {
       if isSeparated() {
	  result = separatedAmount()
       } else {
          if isRetired() {
             result = retiredAmount()
          } else {
             result = normalPayAmount()
	  }
       }
   }
   return result
}
  • After
func getPayAmount() int {
	if isDead() {
	   return deadAmount()
	}
	if isSeparated() {
	   return separatedAmount()
	}
	if isRetired() {
	   return retiredAmount()
	}
	
	return normalPayAmount()
}
- Elimitnate tmp variable (Inline Temp)

จากตัวอย่าง Replace Nested Conditional with Guard Clauses เจอตัวแปร result ไม่จำเป็นต้องใช้ เราสามารถ return ได้เลย จริงๆ พวกตัวแปร tmp มันจะมีปัญหาอีกอันนึงด้วย มันหาจุดแยก method ยาก มันใช้ตัวแปรเดียวกัน

- Early return

เหมือนกันมอง if ไปอีกมุม จริงๆ Software Eng จะมี Metric cyclomatic complexity ยิ่ง จุดตัดสินใจเยอะ ค่ายิ่งสูง ตัว Early return จะมาช่วยลดอันนี้ด้วย

  • Before
func totalAmount(prices []int) int {
   if len(prices) != 0 {
      total := 0

      for i := 0; i < len(prices); i++ {
        total += prices[i]
      }
 		
      return total
   }
   return 0
}
  • After
func totalAmount(prices []int) int {
   if len(prices) == 0 {
      return 0
   }
   
   total := 0
   for i := 0; i < len(prices); i++ {
	  total += prices[i]
   }
   return total
}

ที่เรียนมา ใช้หมดนะ Extract Function / Replace Nested Conditional with Guard Clauses / Early return แต่มีตัวนึงที่ผมใช้เองน้อยมากๆ หรือ แทบไม่ได้ใช้ Inline Function

TDD

จริงแล้ว มันเทคนิคนึงใน Extrame Programming

- TDD mantra (State)
  • RED - Test ที่ Fail แต่มันบอกว่าเราต้องการ Test อะไร
  • GREEN - Code ตอนแรกที่ไม่มี Logic ทำให้ Test ผ่าน เขียน Logic ตาม Requirement ก่อน และทำ Test
  • Refactor ห้ามแก้ Code พร้อมกับ Test จะได้มีตัวกันแตก
    - Code - เพิ่ม Logic ตาม Requirement ก่อน + หา Smell และปรับ
    - Test - ปรับให้อ่านง่าย ลดความซับซ้อน เช่นจากเดิม Copy แปะ มาใช้ Testable

จากนั้นวน Loop ไป RED > GREEN > Refactor จนกว่าเราจะพอใจ หรือตี Constraint อื่นๆ เช่น เวลา

- Uncle Bob describes TDD with three rules:
  1. You are not allowed to write any production code unless it is to make a failing unit test pass.
    - ทำให้ Fail (RED) ก่อน แล้วเขียน Logic (production code) รับประกันว่ามี Test แน่ๆ
  2. You are not allowed to write any more of a unit test than is sufficient to fail, and compilation failures are failures.
    - เขียน Test ให้พอดี และ Fail (RED) ทำที่ละอัน จะได้แก้ที่ละจุด (Small Change)
  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
    - ไม่ต้องคิดเยอะ คิดเผื่อ

Refactoring Sample

อันนี้ไม่มีอะไรมากครับ เป็นตัวอย่าง Refactor จาก Sample Project เช่น

  • FizzBuzz - โจทย์ Classic ของ Test / TDD / Refactoring เลย

ผมเคยมีเขียนแบบ Java ด้วย ครบ 10 ปี พอดี 555 FizzBuzz Problem | naiwaen@DebuggingSoft สมัยนั้น่าจะ java 8 มาใหม่ๆ ตอนนี้ 21 และดีที่มาเรียนอันนี้ด้วย Blog ตอนนี้น่าจะแตกไปหลายปี 2 CSS พวก Syntax Highlight มันแตก ตอนนี้แก้เรียบร้อยแล้วอิอิ

  • Billing Report - Refactor + Structure แยก Report Logic / Render
  • Movie Rental - Go + Eliminate if with polymorphism
  • Data Storage API - Dependency Injection for Test Support //อยากคิด Step ได้แบบ Speaker จริงๆ
    v1. File Fixed Path Cannot Scale
    v2. Introduce DI to eliminate relationship DB -> Storage (วิธีการสร้าง เนื้อ DB V1 มัน lock สร้าง file + fixed path)
    v3. Parallel with os.createTemp
    v4. In-Memory with mattetti/filebuffer //ตอนแรกงงๆ ต้องไปไล่ Commit ถ้าแยก Branch ก็ดี
    v5. นอกจาก DB -> Storage ไปแล้ว ก็มาแก้ชั้น Handler -> Service -> DB แยก Interface แค่งงๆ ใช้ร่วมกันทั้ง Service / DB (น่าจะอันนี้ ถ้าพังไม่ผิด Go by Example: Struct Embedding)
        |-----Interace Storage--------||-Interface io.ReadWriteSeeker-|
Handler ---------> Service ---------> DB ---------> Storage 
- Code Smell อื่นๆ และ
  • Primitive Obsession แก้แล้ว พังเยอะ อาจจะทำใจด้วยว่า Test อาจจะไม่ช่วย หรือ ไม่ต้องถอดออกมาอีก Step Go Map
  • Push up dependency - ทำให้ Simple อะไรที่ Class / Method นั้นไม่ควรรับรู้ เราผลักมันออกไป
- go polymorphism

ขยายจากตัว Movie Rental ลด if โดยเอา โดย

  • แยกแต่ละ If ออกมาเป็น Class ใน Go Struct เพื่อสื่อ Movie กลุ่มต่างๆ
  • ทำข้อตกลงที่แต่ละ Struct ทำงานเหมือนกัน โดยใช้ Interface พยายามหามันว่าอะไรที่ควรมี หา Common (Abstraction)

Don't design with interfacesdiscover them.”

Rob Pike

สุดท้าย Loop ที่มี if มันจะหายไป เพราะมันถูกซ่อนไปและ แต่อีกมุมมันแอบคล้าย Strategy Pattern นะ

- ปิดท้าย

Refactor นอกจาก Code Smell แล้ว อาจจะเป็นพวก Style Guide ของภาษาด้วย เช่น

  • Struct ตัวแรกใหญ่ Public ตัวเล็ก Private ถ้าเจอ _ มันก็ private
  • Return + Initialize Variable
  • Get Set Func ไม่ต้องตั้งชื่อขึ้นต้นด้วย Get / Set ตามภาษาอื่นๆ ตั้งชื่อซ้ำกับ Struct ได้นะ
    - Struct ตัวเล็ก
    - Func ตัวใหญ่
    ถ้า Struct ตัวใหญ่ + Func ตัวใหญ่ มันมีอะไรแปลกๆและ ทำ Get / Set Encapsulate ทำไม
  • print format message (eg. Sprintf) - ทำให้อ่านรู้เรื่อง ไม่จำเป็นต้อง Replace ทุกจุด
  • ตัวแปร ถ้าไม่จำเป็นต้องเป็น Pointer มันไม่ต้องใช้นะ เพราะมันทำให้ Func มันไม่ Immutable ใครจะมาแก้ก็ได้ กระทบ Test
  • iota ทำให้ Enum ของ Go มีลำดับ Id
  • Run Test Parallel t.parallel ระวังปัญหา Race Condition Access Resource เดียวกัน เช่น ไฟล์ / DB แสดงว่าปกติ Test จะ Run ตามลำดับสินะ
    Ref: Does go test run unit tests concurrently? - Stack Overflow
  • io.WriteSeeker / io.ReadSeeker เป็น interface ให้ IO ใช้ Data Storage API ตัวอย่างของอันนี้

แต่เราควรปรับตาม Style Guide ของภาษาที่ตกลงกันในทีม

ตอนไหนควร Refactor

  • เงื่อนไขตัดสินใจ - เวลาที่มี / ความสำคัญ Code นั้นกับ Businesss / Impact
  • เริ่มยังไง - ทำ Test ก่อน อาจจะกว้างๆ Integration / Unit Test (White Box/ Gray Box)

use before reuse - pattern เช่น duplication เช่น มี 3 จุด จะให้เรารู้ว่ามี Common / abstraction แล้ว เริ่มลุยได้

Boy Scout Rule - Leave your code better than you found it

หนังสือแนะนำจาก Coaching Session

และจะบอกว่าเรียนจบ แต่ยังเล่น Code ตามไม่จบ 5555 หยุดไว้ที่ FizzBuzz เดี๋ยวต้องลองไปฝึกอีกรอบก่อน

Reference


Discover more from naiwaen@DebuggingSoft

Subscribe to get the latest posts to your email.