February 11, 2020
7 min read

Refactor If-Else: มาเริ่มแก้ปัญหาที่ดูง่ายแต่ไม่ง่ายกัน

python
refactoring
software engineering

พอดีวันก่อนผมลองแก้ปัญหาโจทย์ โจทย์หนึ่งที่คล้ายกับเป็น Legacy Code และมีเงื่อนไข If-Else ที่น่าทึ่งพอสมควร จึงเกิดคำถามขึ้นมาว่า “เอ้ย? เราจะทำอะไรกับมันได้บ้างนะ?”

แน่นอนคำพูดของทุก ๆ คนก็คงหนีไม่พ้นคำว่า “ก็ Refactor มันซะสิ” ใช่ไหม? ผมเองก็คิดแบบนั้นเหมือนกัน … แต่มันก็มีคำถามว่า Refactor นี่มันจะต้องทำยังไงกันนะ

วันนี้เลยจะมาแชร์เทคนิคเล็ก ๆ เกี่ยวกับ “Refactor” ในแบบของผมกันครับ!

Threads, Like a complex condition, But you can make it beautiful

🔗Refactoring

ผมอิงจากหนังสือ Working effectively with Legacy Code เค้าให้คำแนะนำมาเป็นตารางนึงที่บอกว่า Changing Software นั้นมีทั้งหมด 4 แบบใหญ่ ๆ ด้วยกันคือ...

The Informit, from Sample PDF.

ซึ่งในวันนี้เราจะมาโฟกัสกับคำว่า Refactoring กัน ซึ่งจากตารางนี้เค้าได้บอกว่า สิ่งที่ควรเปลี่ยนแปลงนั้นควรมีแค่ Code Structure เท่านั้นนะ … ส่วนอย่างอื่นไม่ควรจะเกิดการเปลี่ยนแปลง

และเพื่อให้แน่ใจว่าการกระทำ (Functionality) ของ Software ยังเหมือนเดิม และไม่เกิดการกระทำใหม่ ๆ (New Functionality) ที่ไม่คาดคิดขึ้นมา … เราควรจะมี Test หรือมีอะไรก็ได้ที่ทำให้เรามั่นใจว่าระบบยังใช้งานได้ดีอยู่หลังจากเรา Refactor มันเสร็จ

สำหรับผมแล้วอย่างน้อยเราควรจะมี End to End (E2E) Test สำหรับใช้เพื่อบอกว่า การทำงานของ Software ยังทำงานได้ดีครบสมบูรณ์ตาม Business Requirement

งั้นมาเข้าเรื่องกันเลยดีกว่า …

🔗Let’s me show you some basic Refactoring Techniques

เรามาเริ่มจากอะไรที่ทำได้ง่าย ๆ กันก่อน อย่างเช่นเทคนิคแรกนั่นก็คือ…

🔗1. Decompose Conditional with Extract Method / Variable

เทคนิคแรกที่ง่ายที่สุด คือ เมื่อเราเจอ If-Else ที่ซับซ้อน มี AND และ OR อยู่ด้านใน หรือมีการทำงานที่ซับซ้อนอยู่ภายใน If-Else

ให้เราถอดมันออกมาเป็นตัวแปรหรือเป็นฟังก์ชันใหม่ด้วยชื่อที่เข้าใจได้ง่าย แค่นี้เงื่อนไข If-Else ของเราก็จะอ่านง่ายขึ้นมาเป็นกองเลย!

# -- Before -----------------------------------------
if date.after(SUMMER_START) and date.before(SUMMER_END):
    charge = quantity * winterRate + winterServiceCharge
else:
    charge = quantity * summerRate

# -- After ------------------------------------------
def winterCharge(quantity):
  return quantity * winterRate + winterServiceCharge

def summerCharge(quantity):
  return quantity * summerRate

def isSummer(date):
  return date.after(SUMMER_START) and date.before(SUMMER_END)

...

if isSummer(date):
    charge = summerCharge(quantity)
else:
    charge = winterCharge(quantity)

# read more: https://refactoring.guru/decompose-conditional

🔗2. Consolidate Conditional Expression

มาถึงเทคนิคที่ 2 เป็นการรวมเอา If-Else ที่ซ้ำ ๆ กันมารวมอยู่ด้วยกัน เพื่อลดความซับซ้อน และทำให้อ่านง่ายขึ้นอีกด้วย

Consolidate แปลเป็นไทยว่า “รวบรวม” นะ!

ซึ่งอาจจะสร้างเป็นฟังก์ชันใหม่ หรือ สร้างเป็นตัวแปรใหม่แบบในตัวอย่างด้านล่างครับ อย่าลืม! ชื่อต้องสื่อความหมายและเข้าใจง่ายด้วย

# -- Before -----------------------------------------
def disabilityAmount():
  if seniority < 2:
    return 0
  if monthsDisabled > 12:
    return 0
  if isPartTime:
    return 0

  # ... do something


# -- After ------------------------------------------
def disabilityAmount():
  isNotEligableForDisability = (seniority < 2) or (monthsDisabled > 12) or isPartTime

  if isNotEligableForDisability:
    return 0

  # ... do something

# read more: https://refactoring.guru/consolidate-conditional-expression

🔗3. Consolidate Duplicate Conditional Fragments

เทคนิคที่ 3 คือ การรวบรวมเอาสิ่งที่ซ้ำ ๆ กันใน​ If-Else ออกมาข้างนอกซะ ในเมื่อถ้าไม่ว่าจะเข้า If หรือ Else สุดท้ายก็ต้องทำอยู่ดี งั้นเอาออกมาไว้ข้างนอกซะเลย

อันนี้ค่อนข้างเข้าใจง่ายมาก และคิดว่าทุก ๆ คนน่าจะใช้กันอยู่เป็นประจำอยู่แล้วนะครับเพื่อลดความซ้ำซ้อนของ Code

# -- Before -----------------------------------------
if isSpecialDeal():
  total = price * 0.95
  send()
else:
  total = price * 0.98
  send()

# -- After ------------------------------------------
if isSpecialDeal():
  total = price * 0.95
else:
  total = price * 0.98
send()

# read more: https://refactoring.guru/consolidate-duplicate-conditional-fragments

🔗4. Replace Nested Conditional with Guard Pattern

เทคนิคที่ 4 คือ เทคนิคที่เรียกว่า Guard Pattern หรือ Guard Clauses เพื่อมาใช้ลด Nested If-Else ที่ซับซ้อน … เพราะยิ่งมี Nested If-Else ซ้อน ๆ กันมากขึ้น ก็จะยิ่งใช้เวลาทำความเข้าใจมากขึ้นเช่นกัน

สำหรับผมแล้ว การอธิบายเทคนิคนี้นั้น ให้ Code อธิบายตัวมันเองจะเข้าใจได้ง่ายกว่า…

# -- Before -----------------------------------------
def getPayAmount(self):
  if self.isExpired:
    result = expiredAmount()
  else:
    if self.isSeparated:
      result = separatedAmount()
    else:
      if self.isRetired:
        result = retiredAmount()
      else:
        extraPay = getExtraPay()
        result = normalPayAmount(extraPay)

  return result

# -- After ------------------------------------------
def getPayAmount(self):
  if self.isExpired:
    return expiredAmount()

  if self.isSeparated:
    return separatedAmount()

  if self.isRetired:
    return retiredAmount()

  extraPay = getExtraPay()
  return normalPayAmount()

# read more: https://refactoring.guru/replace-nested-conditional-with-guard-clauses

จาก Code ด้านบนทุกคนคงพอนึกออกว่า … มันเหมือนกับ Security Guard ที่ดักคนที่เข้ามาในฟังก์ชั่น ว่าถ้าตรงกับเงื่อนไขอะไรอันนึง ก็ให้ Return ออกไปเลย ทำให้ไม่มี Nested If-Else ที่ยากต่อการทำความเข้าใจ

หลายคนมักจะคุ้นเคยกับท่าแบบนี้ใน Controller นะครับ โดยมักจะใช้กับการ Validate Input, Auth Checking และถ้าไม่ผ่านก็ให้ Return HTTP Response กลับไปเลย

แต่ในความเป็นจริง เราสามารถใช้ได้กับทุกส่วนของ Software เลย เพราะมันก็คือเทคนิคแบบหนึ่งนั่นเอง!

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

🔗5. Replace Conditional with Polymorphism

เทคนิคที่ 5 จะเป็นการใช้ความรู้ของ Object Oriented Programming โดยจะใช้พลังของการสืบทอด (Inheritance) มาช่วยทำให้ Code นั้นอธิบายตัวของมันเอง

# -- Before -----------------------------------------
class Bird:
    # ...
    def getSpeed(self):
        if self.type == EUROPEAN:
            return self.getBaseSpeed()
        elif self.type == AFRICAN:
            return self.getBaseSpeed() - self.getLoadFactor() * self.numberOfCoconuts
        elif self.type == NORWEGIAN_BLUE:
            return 0 if self.isNailed else self.getBaseSpeed(self.voltage)
        else:
            raise Exception("Should be unreachable")

# -- After ------------------------------------------
class Bird:
    # ...
    def getSpeed(self):
        pass

class European(Bird):
    def getSpeed(self):
        return self.getBaseSpeed()


class African(Bird):
    def getSpeed(self):
        return self.getBaseSpeed() - self.getLoadFactor() * self.numberOfCoconuts


class NorwegianBlue(Bird):
    def getSpeed(self):
        return 0 if self.isNailed else self.getBaseSpeed(self.voltage)

# Somewhere in client code
speed = bird.getSpeed()

# read more: https://refactoring.guru/replace-conditional-with-polymorphism

โดยเทคนิคนี้จะอธิบายง่าย ๆ คือเราจะมองหา Code ที่ทำงานเหมือน ๆ กัน เช่น สินค้าที่มีการคำนวนราคาแยกตามประเภทหรือชื่อสินค้า .. รูปทรงเรขาคณิตที่มีการคำนวนพื้นที่แตกต่างตามรูปทรง หรือ การคำนวนอัตราดอกเบี้ยตามรูปแบบของประเภทของสมุดบัญชีธนาคาร เป็นต้น

ฉะนั้น ให้ลองนำรูปแบบต่าง ๆ เหล่านั้น มาสร้างขึ้นมาเป็น Class ใหม่ที่มีการทำงาน การคำนวนเป็นของตัวเองไปเลย!

นอกจากนี้ ผมอยากให้เข้าไปอ่านบล็อคของพี่สมเกียรติเกี่ยวกับ Polymorphism เพื่อเพิ่มความเข้าใจให้กับเทคนิคนี้ยิ่งขึ้นด้วยครับ

🔗6. Simplify complex condition with basic Logic knowledge

เทคนิคที่ 6 เป็นเทคนิคที่ค่อนข้าง Simple มาก ๆ เลยและคิดว่าหลายคนก็สามารถใช้กันได้ง่าย ๆ อยู่แล้ว

เพราะเงื่อนไข If-Else ทุกชนิดก็มีพื้นฐานมาจากความรู้ตรรกศาสตร์ (Logic) ดั้งเดิม ดังนั้นทำให้เราสามารถนำความรู้ สมบัติ เอกลักษณ์ของตรรกศาสตร์ทุกข้อเพื่อนำมา Simplify เงื่อนไขของเราได้นั่นเอง

เช่น สมบัติการสลับที่ สมบัติการรวมกลุ่ม สมบัติการเปลี่ยนกลุ่ม ฯลฯ และยังทำงานได้เหมือนเดิมทุกอย่าง

# -- Before -----------------------------------------
if item.sell_in < 0:
    if item.name != "Aged Brie":
        if item.name != "Backstage passes to a TAFKAL80ETC concert":
            if item.quality > 0:
                if item.name != "Sulfuras, Hand of Ragnaros":
                    item.quality = item.quality - 1
    else:
        item.quality = item.quality + 1

# -- After ------------------------------------------
if item.sell_in < 0:
    if (
        item.quality > 0
        and item.name != "Aged Brie"
        and item.name != "Backstage passes to a TAFKAL80ETC concert"
        and item.name != "Sulfuras, Hand of Ragnaros"
    ):
        # you can group it and swap their position also.
        item.quality = item.quality - 1
    else:
        item.quality = item.quality + 1

หรือแม้การเพิ่มนิเสธ (NOT) เข้าไปใน If-Else เราเองก็ควรจะรู้ว่า AND และ OR ในเงื่อนไขของเราควรเปลี่ยนไปอย่างไรเช่นกันครับ

# -- Before -----------------------------------------
if not (number >= 1 and number <= 100): # ~(p v q)
  print('Out of the range')
else:
  print('In Between 1 and 100!')

# -- After ------------------------------------------
if (not number >= 1) or (not number <= 100): # ~p ^ ~q
  print('Out of the range')
else:
  print('In Between 1 and 100!')

🔗Look at this code.

จนถึงตรงนี้ทุกคนคงพอจะเห็นภาพคร่าว ๆ กันออกแล้วว่าเราจะ Refactor ตัวเงื่อนไข If-Else ของเราได้อย่างไรบ้าง … ผมขอแนะนำโจทย์เกี่ยวกับ If-Else มาหนึ่งข้อ เผื่อใครอยากจะลองฝึก Refactor กันดู

โจทย์นี้ชื่อว่า Gilded Rose ซึ่งมีเนื้อหาคร่าว ๆ อยู่ว่า ของแต่ละชิ้นจะมีชื่อ, คุณภาพ, จำนวนวันที่เหลือก่อนหมดอายุ … ซึ่งของแต่ละชิ้น จะมีเงื่อนไขการเพิ่มลดของคุณภาพที่แตกต่างกันอยู่ (ผมเลือกภาษา Python มาให้ดูเพราะคิดว่าอ่านแล้วเข้าใจง่ายที่สุด)

อยากให้ลองนึกดูว่า “ถ้าเป็นเรา จะเริ่มต้น Refactor มันยังไงดีนะ!?” และ “ภาพสุดท้ายนั้นอยากให้มันออกมาเป็นแบบไหน?”

class GildedRose(object):
    def __init__(self, items):
        self.items = items

    def update_quality(self):
        for item in self.items:
            if item.name != "Aged Brie" and item.name != "Backstage passes to a TAFKAL80ETC concert":
                if item.quality > 0:
                    if item.name != "Sulfuras, Hand of Ragnaros":
                        item.quality = item.quality - 1
            else:
                if item.quality < 50:
                    item.quality = item.quality + 1
                    if item.name == "Backstage passes to a TAFKAL80ETC concert":
                        if item.sell_in < 11:
                            if item.quality < 50:
                                item.quality = item.quality + 1
                        if item.sell_in < 6:
                            if item.quality < 50:
                                item.quality = item.quality + 1

            if item.name != "Sulfuras, Hand of Ragnaros":
                item.sell_in = item.sell_in - 1

            if item.sell_in < 0:
                if item.name != "Aged Brie":
                    if item.name != "Backstage passes to a TAFKAL80ETC concert":
                        if item.quality > 0:
                            if item.name != "Sulfuras, Hand of Ragnaros":
                                item.quality = item.quality - 1
                    else:
                        item.quality = 0
                else:
                    if item.quality < 50:
                        item.quality = item.quality + 1


class Item:
    def __init__(self, name, sell_in, quality):
        self.name = name
        self.sell_in = sell_in
        self.quality = quality

    def __repr__(self):
        return "%s, %s, %s" % (self.name, self.sell_in, self.quality)

จากโจทย์ผมจะลองเล่าให้ฟังว่าในมุมของผม ผมเห็นอะไรบ้างใน Code ชุดนี้บ้าง…

  1. ผมเห็น Nested If-else ที่ค่อนข้างลึก
  2. ผมเห็น Duplicate Code ในหลาย ๆ จุด ซึ่งบางจุดซ้ำกันถึง 2–3 บรรทัดต่อกันเลย
  3. ผมเห็น Conditions ที่ซ้ำ ๆ กันอยู่หลายอัน
  4. ผมเห็นและรู้สึกไม่ชอบที่มี Conditions ที่เป็น != (NOT) ค่อนข้างเยอะ
  5. ผมเห็นว่ามี class Item และคิดว่าภาพสุดท้ายที่อยากให้ Code เดินทางไปคือ ของแต่ละชิ้นนั้น มีการอัพเดทเป็นเฉพาะของตัวเอง
  6. แต่มันเป็นไปไม่ได้เลยที่จะรู้ว่าของแต่ละชิ้นตอนนี้มี Behavior ยังไง จะลบทิ้งเขียนใหม่ก็ค่อนข้างยาก และเดาไม่ออกเลยว่าเมื่อไรจะ Refactor เสร็จ
  7. ฉะนั้น มีอะไรที่เราพอทำได้บ้างตอนนี้แล้วมันดีขึ้นได้บ้างนะ !?
  8. โอเค… งั้นเริ่มจาก Extract Code การเพิ่ม Quality ที่ซ้ำ ๆ กันออกมาเป็นฟังก์ชัน increase_quality(item)ดีกว่า!
  9. … Keep Refactoring …

ส่วนตัวผมได้ทำโจทย์นี้มาแล้วซ้ำ ๆ 3–4 รอบแล้ว และจบไม่เหมือนเดิมซักครั้งเลย มันค่อนข้างดีขึ้นเรื่อย ๆ หรือลองแก้โจทย์ปัญหาจากทาง OOP เป็นทางของ Functional Programming บ้างก็ได้

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

ลองเข้าไปดูใน Repository นี้นะครับ ตัว Code ตั้งต้นมีอยู่หลายภาษา ที่ผมเลือกมาด้านบนนั้นเป็น Python อย่างที่กล่าวไว้ด้านบน ลอง Fork/Clone มาฝึกกันได้นะครับ


🔗No way to 100% perfect.

ผมอยากให้คิดว่ามันเป็นการฝึกฝน! ลองผิด ลองถูก ลองหาวิธีที่เราคิดว่ามันใช่ ณ ตอนนั้นก็เพียงพอแล้ว …

ให้ฝึกไปเรื่อย ๆ ครับ ในวันแรกเราไม่มีทางรู้หรอกว่าท่าที่เราเลือกใช้ Refactor นั้นมันเหมาะสมกับปัญหาตรงหน้าหรือเปล่า

สำหรับผมขอแค่มี Git และมี Tests ที่ครอบคลุมก็เพียงพอแล้ว ทุกครั้งที่ Refactor ก็ลองรัน​ Tests ซักครั้งหนึ่ง แล้ว Commit ซักทีหนึ่ง พอทำไปถึงจุดนึงแล้วเกิดมันไม่ใช่หรือรู้สึกว่ามาผิดทาง ก็ Git Revert มันทิ้งก็ได้ … ใช่ไหมล่ะ แล้วเราจะมี Git ไว้ทำไมล่ะจริงไหมครับ?

ฉะนั้น ทำไปเถอะครับ ลองเริ่มต้นกับอะไรเล็ก ๆ สำหรับผมแค่ได้ Refactor ซักนิด Code เรามันก็ดีขึ้นแล้วล่ะครับ

You have Git & Tests as a support wheel. So just do it!

🔗Before we go

ใครที่อ่านมาถึงตรงนี้ก็ขอขอบคุณมาก ๆ ครับ หวังว่าจะได้ความรู้หรือไอเดียอะไรใหม่ ๆ กันไปบ้างนะครับ!

ผมคิดว่าการเขียน Code ให้ทำงานได้มันไม่ได้ยากมาก .. ถ้าเทียบกับการเขียน Code ให้สะอาด อ่านง่าย คนอื่นเข้าใจได้และเข้าใจ Code โดยไม่ต้องกดหา Git Blame ว่าใครนะ แม่งเป็นคนเขียนบรรทัดนี้!

หยอกเล่นน่ะครับ… 😅

เอาจริง ๆ ผมหวังว่าหลังจากนี้ ทุกคนจะจัดการกับเงื่อนไข If-Else ที่ซับซ้อนได้ดีขึ้นกว่าเมื่อก่อน … เนื้อหาบล็อคนี้มันอาจจะไม่ได้ลึกมาก และหวังว่ามันไม่ได้ยากเกินไปสำหรับการเริ่มต้นทำอะไรบางอย่างดู

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

ไว้ครั้งหน้า จะหาเรื่องมาเล่าให้ฟังอีกนะครับ

🔗Read More…

สำหรับใครสนใจอยากอ่านเพิ่มเติม ผมได้แนบสิ่งที่น่าสนใจไว้ด้านล่างนะครับ

เป็นเว็บไซต์ที่ผมใช้อ้างอิงเทคนิคทุกเทคนิคที่ผมมาเล่าให้ฟังในวันนี้ครับ … ส่วนตัวผมคิดว่าเค้าค่อนข้างอธิบายได้เข้าใจง่ายมาก ๆ ลองอ่านเพิ่มเติมดูครับ มีทั้งเรื่องของการ Refactoring, Code Smell, เทคนิคต่าง ๆ รวมถึงแนะนำ Design Pattern ที่มีประโยชน์หลาย ๆ แบบด้วยครับ

เรื่องของ Code Smell ของ If-Else Condition ที่เรียกว่า “Arrow Code” … ซึ่งเค้าอธิบายว่าเค้ากำลังจะทำให้ Arrow แบนลง … เป็นยังไงแนะนำให้ลองอ่านดูครับ

หนังสือที่ผมเองพึ่งเริ่มอ่าน และช่วยให้ผมเริ่มสนุกและมีทางออกกับการทำงานด้วย . Legacy Code … เป็นหนังสือที่อยากแนะนำให้ทุกคนลองอ่านดูนะครับ (สำหรับใครมี SafariBook แล้ว Search หาได้ข้างในเลย!)

The views and opinions expressed in this article are purely mine and do not necessarily reflect the positions of any companies for which I have worked in the past, present, or future.