FizzBuzz Problem without if (ปัจฉิมบท)

จากบทความที่แล้ว แสดงวิธีการเขียน Code กับโจทย์ปัญหา FizzBuzz โดยไม่ใช่ IF กันแล้วนะครับ แต่ผมยังทิ้งท้ายไว้อีกปัญหานึง คือ ถ้ามีเงื่อนไขเพิ่มขึ้นมาหละ โดยให้แสดง WOOF ้เพิ่ม ถ้าตัวเลขนั้นหาร 7 ได้ลงตัว (อ้างอิงจาก WIKI FIZZ BUZZ WOOF ) เราจะมีวิธีการแก้ปัญหาอย่างไรนะครับ โดยผมขอทวนวิธีการที่ใช้แก้ปัญหาใน Blog ตอนที่แล้วก่อนนะครับ

  1. แยกเงื่อนไข แต่ละข้อออกมาเป็น Class ของใครของมันเลย ตาม output ที่ได้ทั้ง 4 แบบ โดยการแตก Class นี้ เราจะใช้ Decision Tree ช่วยนะครับ ดังรูป
  1. ลองทำ Decision Tree อีกอันเพื่อหาความลองหาหน่วยย่อยที่สุดของ Class ที่นำมา Reuse ได้ โดยในตัวอย่างนี้จะเป้น Class FizzBuzzRule ที่นำเงื่อนไขของ Class FizzRule และ Class BuzzRule มาใช้งานต่อได้ครับ
  2. จากนั้นนำเงื่อนไข แต่ละอันมาเรียงตามที่ลำดับไว้ใน Decesion Tree ครับ ตามรูปด้านบนครับ
  3. ทำ Unittest เพิ่อทดลองลำดับการทำงานครับ

ดังนั้นเมื่อมี Requirement เพิ่มเข้ามา คือ แสดง WOOF เมื่อหาร 7 ลงตัว เราจะพบว่า Class เก่าๆในเงื่อนไขที่เราได้เคยทำไว้นั้น ไม่ต้องแก้ไขเลย แต่สามารถนำมาใช้งานต่อได้ครับ โดยลองเขียน Decision Tree เพื่อจัดเรียงความคิดได้ ดังนี้

FizzBuzzWoof Decision Tree
FizzBuzzWoof Decision Tree

จากนั้นเราลองมาเขียน Code กันเพิ่มเติมกันเลยครับ
ส่วนแรก เป็นส่วนของเงื่อนไขนะครับ ซึ่งถ้าเราดูจาก Decision Tree ที่เขียนไว้จะพบว่าเราต้องเพิ่ม Class ดังรูป

  • Class FizzBuzzWoofRule: สำหรับเงื่อนไข หาร 3, 5 และหาร 7 ลงตัวครับ
public class FizzBuzzWoofRule implements Rule {
    private FizzRule fizzRule;
    private BuzzRule buzzRule;
    private WoofRule woofRule;
    public FizzBuzzWoofRule() {
        fizzRule = new FizzRule();
        buzzRule = new BuzzRule();
        woofRule = new WoofRule();
    }
    @Override
    public boolean isInRule(Integer p_Input) {
        return (fizzRule.isInRule(p_Input) && buzzRule.isInRule(p_Input) && woofRule.isInRule(p_Input));
    }
    @Override
    public String result(Integer p_Input) {
        return fizzRule.result(p_Input) + buzzRule.result(p_Input) + woofRule.result(p_Input);
    }
}
  • Class BuzzWoofRule: สำหรับเงื่อนไข หาร 5 และหาร 7 ลงตัวครับ
public class BuzzWoofRule implements Rule {
    private BuzzRule buzzRule;
    private WoofRule woofRule;
    public BuzzWoofRule() {
        buzzRule = new BuzzRule();
        woofRule = new WoofRule();
    }
    @Override
    public boolean isInRule(Integer p_Input) {
        return (buzzRule.isInRule(p_Input) == true) && (woofRule.isInRule(p_Input) == true);
    }
    @Override
    public String result(Integer p_Input) {
        return buzzRule.result(p_Input) + woofRule.result(p_Input);
    }
}
  • Class FizzWoofRule: สำหรับเงื่อนไข หาร 3 และหาร 7 ลงตัวครับ
public class FizzWoofRule implements Rule {
    private FizzRule fizzRule;
    private WoofRule woofRule;
    public FizzWoofRule() {
        fizzRule = new FizzRule();
        woofRule = new WoofRule();
    }
    @Override
    public boolean isInRule(Integer p_Input) {
        return (fizzRule.isInRule(p_Input) == true) && (woofRule.isInRule(p_Input) == true);
    }
    @Override
    public String result(Integer p_Input) {
        return fizzRule.result(p_Input) + woofRule.result(p_Input);
    }
}
  • Class WoofRule: สำหรับเงื่อนไขหาร 7 ลงตัวครับ
public class WoofRule implements Rule {
    @Override
    public boolean isInRule(Integer p_Input) {
        return p_Input % 7 == 0;
    }
    @Override
    public String result(Integer p_Input) {
        return "Woof";
    }
}

ส่วนที่สอง เป็น Class ที่จัดการกับเงื่อนไขกับ Class FizzBuzzManager โดยมีเพิ่มการเรียกเงื่อนไขที่เพิ่มเข้าไป 4 เงื่อนไขครับ โดยมีการเรียงลำดับการของกฏตาม Decision Tree อันแรกที่ได้วาดไว้ครับ ผู้อ่านสามารถสังเกตุได้จาก Comment ที่เพิ่มใน Code ครับ (ส่วน Added by .... End Added by)

public class FizzBuzzManager implements ruleManager {
    private List<Rule> ruleList;
    public FizzBuzzManager() {
        ruleList = new ArrayList<Rule>();
        //เพิ่มกฏได้ง่าย เพราะใส่ไว้ใน List
        //Added by new requirement 'Woof'
        ruleList.add(new FizzBuzzWoofRule());
        ruleList.add(new BuzzWoofRule());
        ruleList.add(new FizzWoofRule());
        ruleList.add(new WoofRule());
        //End Added by new requirement 'Woof'
        ruleList.add(new FizzBuzzRule());
        ruleList.add(new FizzRule());
        ruleList.add(new BuzzRule());
        ruleList.add(new DefaultRule());
    }
    @Override
    public String findActivateRule(Integer p_Input) {
        //เอาไว้วน Loop เพื่อดูว่าตัวเลขที่ป้อนเข้ามาตรงกับ Rule ไหนครัย
        for (Rule rule: ruleList) {
            if (rule.isInRule(p_Input)) {
                return rule.result(p_Input);
            }
        }
        return "Not found matching rules";
    }
}

ส่วนที่สาม เป็นส่วนของทำ Unittest นะครับ โดย Class FizzBuzzUT มีการเพิ่ม TestCase ลงไปจากเงื่อนไข ดังนี้

กรณีที่เป็นไปได้INPUTOUTPUTหมายเหตุ
หาร 3 ลงตัว3Fizzเงื่อนไขเก่า
6Fizz
หาร 5 ลงตัว5Buzz
10Buzz
หาร 3 และ 5 ลงตัว (หาร 15 ลงตัว)15FizzBuzz
30FizzBuzz
ต้องคืนค่า Input2323
901901
หาร 5 และ 7 ลงตัว35BuzzWoofส่วนที่เพิ่ม
5*7*5BuzzWoof
หาร 3 และ 7 ลงตัว21FizzWoof
42FizzWoof
หาร 7 ลงตัว7Woof
14Woof
หาร 3, หาร 5 และ หาร 7 ลงตัว3*5*7FizzBuzzWoof
105FizzBuzzWoof

และ Code ที่ได้จาก TestCase ครับ

import static org.junit.Assert.*;
import org.junit.Test;

public class FizzBuzzUT {
    @Test
    public void modThreeAndFiveIsFizzBuzz() {
        FizzBuzzManager fizzBuzz = new FizzBuzzManager();
        assertEquals("FizzBuzz", fizzBuzz.findActivateRule(15));
        assertEquals("FizzBuzz", fizzBuzz.findActivateRule(30));
    }
    @Test
    public void modThreeIsFizz() {
        FizzBuzzManager fizzBuzz = new FizzBuzzManager();
        assertEquals("Fizz", fizzBuzz.findActivateRule(3));
        assertEquals("Fizz", fizzBuzz.findActivateRule(6));
    }
    @Test
    public void modFiveIsFizz() {
        FizzBuzzManager fizzBuzz = new FizzBuzzManager();
        assertEquals("Buzz", fizzBuzz.findActivateRule(5));
        assertEquals("Buzz", fizzBuzz.findActivateRule(10));
    }
    @Test
    public void notFizzAndBuzz() {
        FizzBuzzManager fizzBuzz = new FizzBuzzManager();
        assertEquals("23", fizzBuzz.findActivateRule(23));
        //Modified by new requirement 'Woof'
        //assertEquals("308", fizzBuzz.findActivateRule(308));
        // >> เพราะ 308 หารด้วย 7 ลงตัว จึงเข้าเงื่อนไข Woof
        //End Modified new requirement 'Woof'

        //Added by new requirement 'Woof'
        assertEquals("901", fizzBuzz.findActivateRule(901));
        //End Added by new requirement 'Woof'

    }

    //Added by new requirement 'Woof'
    @Test
    public void modFiveAndSevenIsBuzzWoof() {
        FizzBuzzManager fizzBuzz = new FizzBuzzManager();
        assertEquals("BuzzWoof", fizzBuzz.findActivateRule(35));
        assertEquals("BuzzWoof", fizzBuzz.findActivateRule(5 * 7 * 5));
    }

    @Test
    public void modThreeAndSevenIsFizzWoof() {
        FizzBuzzManager fizzBuzz = new FizzBuzzManager();
        assertEquals("FizzWoof", fizzBuzz.findActivateRule(21));
        assertEquals("FizzWoof", fizzBuzz.findActivateRule(42));
    }

    @Test
    public void modSevenIsWoof() {
        FizzBuzzManager fizzBuzz = new FizzBuzzManager();
        assertEquals("Woof", fizzBuzz.findActivateRule(7));
        assertEquals("Woof", fizzBuzz.findActivateRule(14));
    }

    @Test
    public void modThreeFiveAndSevenIsFizzBuzzWoof() {
        FizzBuzzManager fizzBuzz = new FizzBuzzManager();
        assertEquals("FizzBuzzWoof", fizzBuzz.findActivateRule(3 * 5 * 7));
        assertEquals("FizzBuzzWoof", fizzBuzz.findActivateRule(105));
    }
    //End Added by new requirement 'Woof'
}

ผลการทดสอบ Unittest ครับผ่านฉลุย ^___^

เมื่อลองมองดีๆ ทางที่เป็นไปได้ทั้งหมดจาก Decision Tree อันแรก แสดงให้เห็นว่าเงื่อนไขต่างๆ มีพื้นฐานจากจาก 3 เงื่อนไข ดังนี้ (ลองดูดีๆ จะเหมือนบทความที่แล้วเลยครับ)

FizzBuzzWoof Decision Tree Refactoring (Simplify If)
FizzBuzzWoof Decision Tree Refactoring (Simplify If)

แต่ผมไปเขียน Rule ตาม Decision Tree อันแรก จะเห็น Rule มันเยอะ ทั้งจริงๆ มันน่าจะปรับได้อีกนะ อาจจะต้องมาปรับ findActivateRule นิดนึง ลองไปทำกันดูครับ

แต่การเลือกทางนี้มันมี Trade-Off เหมือนกันนะ เพราะ findActivateRule มี Logic เข้ามาพันนิดนึง ซึ่งถ้ามี Requirement เพื่อ อาจจะทำให้มันใหญ่โตได้ในอนาคต

ปิดท้าย

หลายคนอาจจะสงสัยว่าทำไมผมถึงยกตัวอย่างเรื่อง FizzBuzz มาเทียบการออกแบบ Software ที่ต้อง Maintain ง่าย เพิ่ม Module ได้ง่าย และแก้ Code น้อย เพราะการออกแบบในรูปแบบนี้เห็นน้อยมาก ส่วนใหญ่จะใช้ IF กันเยอะมาก จากโจทย์ FizzBuzzWoof นี้ ผมได้แยกเงื่อนไข หรือกฏ แต่ละอันออกมาเป็น Class ย่อยๆครับ

ถึงตอนนี้ผู้อ่านอาจจะงงครับ งั้นผมขอยกตัวอย่างและกัน ครับ โดยสมมุติว่าเราออกแบบระบบตรวจสอบการลงทุนของกองทุนซึ่งต้องเป็นไปตามหนังสือเชิญชวนที่ชี้แจงไว้ โดยมีกฏ ข้อ A, B, C, D และ E แต่ถ้าหากอยู่ดีๆเราต้องเพิ่มกฏใหม่ขึ้นมา เพราะ กลต.(หน่วยงานที่ดูแลเรื่องการลงทุนในตลาดหลักทรัพย์) บังคับหละ โดยเพิ่มกฏข้อ F ลงไป ถ้าใช้แนวทางการเขียน Code แบบเดิมรับรองได้ว่า Code เก่า สำหรับกฏข้อที่ A, B , C, D และ E ต้องถูกแก้ไขไปด้วย เพื่อเพิ่มกฏข้อ F ลงไป แต่เราจะแน่ใจได้ไงว่า Code ที่เพิ่ม ไม่มีผลกระทบกับการทำงานของกฏเก่าๆ ครับ

ถ้าเราใช้ concept no if เข้ามาช่วยในการออกแบบระบบนี้ เราจะมี Class ประจำของแต่ละกฏ ถ้ามีกฏใหม่เพิ่มขึ้นมาเราเพิ่มแค่แก้ไข Class ของกฏใหม่ขึ้นมา โดยไม่จำเป็นต้องเข้าไปยุ่งกับ Process ของ Class ของกฏเดิมๆเลยครับ


Discover more from naiwaen@DebuggingSoft

Subscribe to get the latest posts to your email.