FizzBuzz Problem without if (ปฐมบท)

จากบทความที่แล้ว FizzBuzz Problem ไปนะครับ ต่อไปผมลอง Refactor Code อีกรอบ โดยมีเงื่อนไขว่าห้ามใช้ IF ในส่วนของ Logic Fizz และ Buzz

ทำไมถึงต้องห้ามใช้ IF

เพราะ การใช้ IF ส่งผลให้เกิดความมักง่ายของ Developer ทำให้เกิดปัญหาในการแก้ไข หรือพัฒนา Module เพิ่มเติม ต้องไปแก้ Code จนเกินความจำเป็น และทำให้เกิด Defect(ฺBug) เพิ่มขึ้นด้วย ซึ่งมีแนวคิดทางวิชาการมาสนันสนุนแนวคิดนี้อย่าง The Open Close Principle (OCP) และ High Coupling & Low Cohesion (สามารถอ่านเพิ่มเติมได้ใน Blog ผมครับ ^__^)

หลังจากรู้ที่มาคร่าวๆแล้ว เรามาลองทำ FizzBuzz โดยไม่ใช้ IF กันนะครับ

ตอนนี้สิ่งที่ผมนึกออกมีไอเดียประมาณนี้นะครับ

  • แนวคิด: ทำตารางมา Map ว่ากรณีไหนได้ ผลลัพธ์อะไรบ้าง ?
    ข้อสรุป: ไม่ใช้ เพราะ ผมมองว่า ถ้ามีการเพิ่มกฏลงไป มันจะต้องแก้ Code และที่สำคัญต้องโดน Code เก่าด้วย
  • แนวคิด:ใช้ OOP มาช่วย โดยใช้แนวคิดของ Polymorphism
    ข้อสรุป: ใช้แนวคิดนี้ เพราะ The Open Close Principle(OCP) และ High Coupling & Low Cohesion

จากที่ผมเลือกแนวคิดที่ใช้ OOP มาช่วย ลองเอา Decision Tree ที่ผมเคยได้เขียนไว้ในบทความที่แล้วมาใช้ดูครับ

Decision Fizz Buzz Table
Decision Fizz Buzz Table

หากลองสังเกตุดีๆ เราสามารถยุบ เงื่อนไข IS FizzBuzz ได้นะครับ เพราะหาร 15 ลงตัว มัน คือ หาร 3 และ 5 ลงตัวครับ โดย Decision Tree ที่เราเขียนใหม่เหลือเท่านี้ (หมายความว่าเราสามารถใช้ เงื่อนไข IS Fizz กับ IS Buzz มาแทนได้นะครับ)

เนื่องจากต้องเขียนให้มันรองรับการเพิ่ม Rule เราต้องออกแบบ Rule ของเราก่อน โดยสร้าง Interface ขึ้นมา เพื่อกำหนดกรอบการทำงานคร่าวๆ ดังนี้ครับ

public interface Rule {
    //เอาไว้ตรวจสอบ Input ที่เข้ามาว่าตรงกับกฏ หรือไม่
    public boolean isInRule(Integer p_Input);
    //เอาไว้ คืนค่า ผลลัพธ์ของกฏนั้น
    public String result(Integer p_Input);
}

เมื่อได้ Interface แล้ว เรามาเพิ่มเงื่อนไขของเกมกันครับ

  • Class FizzRule: สำหรับเงื่อนไข หาร 3 ลงตัวครับ
public class FizzRule implements Rule {
    @Override
    public boolean isInRule(Integer p_Input) {
        return p_Input % 3 == 0;
    }
    @Override
    public String result(Integer p_Input) {
        return "Fizz";
    }
}
  • Class BuzzRule: สำหรับเงื่อนไข หาร 5 ลงตัวครับ
public class BuzzRule implements Rule {
    @Override
    public boolean isInRule(Integer p_Input) {
        return p_Input % 5 == 0;
    }
    @Override
    public String result(Integer p_Input) {
        return "Buzz";
    }
}
  • Class FizzBuzzRule: สำหรับเงื่อนไขหาร 3 และ 5 ลงตัว โดยใน Class ผม Reuse จาก Class FizzRule และ BuzzRule
public class FizzBuzzRule implements Rule {

    private FizzRule fizzRule;
    private BuzzRule buzzRule;

    public FizzBuzzRule() {
        //Reuse rule
        fizzRule = new FizzRule();
        buzzRule = new BuzzRule();
    }
    @Override
    public boolean isInRule(Integer p_Input) {
        //Reuse กฏ โดยใช้เงื่อนไขจาก Class FizzRule และ BuzzRule
        return (fizzRule.isInRule(p_Input) == true) && (buzzRule.isInRule(p_Input) == true);
    }
    @Override
    public String result(Integer p_Input) {
        return fizzRule.result(p_Input) + buzzRule.result(p_Input);
    }
}
  • Class DefaultRule: สุดท้ายเอาไว้รองรับกับในกรณีที่ไม่ตรงกับเงื่อนไขใดๆข้างต้นครับ
public class DefaultRule implements Rule {
    @Override
    public boolean isInRule(Integer p_Input) {
        // TODO Auto-generated method stub
        return true;
    }
    @Override
    public String result(Integer p_Input) {
        // TODO Auto-generated method stub
        return p_Input.toString();
    }
}

เมื่อได้ Class ที่ Implement มาแล้ว เราก็เอา Class เอานี้มารวมกัน โดยเก็บเงื่อนไข หรือกฏเหล่านี้ไว้ใน List เพราะเวลามีกฏใหม่ๆเพิ่มเข้าสามารถเพิ่ม Class ได้เลย และที่สำคัญ คือ ไม่กระทบกระเทือน Code เก่าด้วย โดย Code ดังนี้ (ถ้าสังเกตุดูจะพบว่าผมเรียงกฏตาม Decision Tree อันแรกนะครับ)

import java.util.ArrayList;
import java.util.List;

public class FizzBuzzManager implements ruleManager {
    private List<Rule> ruleList;
    public FizzBuzzManager() {
        ruleList = new ArrayList<Rule>();
        //เพิ่มกฏได้ง่าย เพราะใส่ไว้ใน List
        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";
    }
}

และถ้าสังเกตุ Code พบว่าผมได้ implements interface ruleManager เพราะผมต้องการวางกรอบคร่าวๆ เผื่อมี Class อื่นนำรูปแบบการเขียนไปให้ต่อครับ

public interface ruleManager {
    public String findActivateRule(Integer p_Input);
}

เมื่อได้เงื่อนไข และตัวจัดการเงื่อนไขพร้อมแล้ว เราจะมาทำ Unit test กัน โดยผมใช้ Testcase จากบทความที่แล้วครับ โดย Unit test เขียนประมาณนี้นะครับ

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));
        assertEquals("308", fizzBuzz.findActivateRule(308));
    }
}

จากข้างต้นบอกว่าห้ามมี If จริงๆมันก็ตามโจทย์นะ ในส่วนของ FizzBuzz ไม่มี แต่ในส่วนของ findActivateRule มี 1 จุด ซึ่งจริงๆลดได้อีกนะใช้ Ternary Operator

แล้วถ้ามีกฏใหม่เพิ่มเข้ามา เราจะจัดการกับมันอย่างไร ?

เพราะ ถ้าลองดูใน WIKI: FIZZ BUZZ WOOF มีลูกเล่นเพิ่มเติม คือ ให้แสดง WOOF ถ้าตัวเลขนั้นหาร 7 ได้ลงตัวครับ  เดี๋ยวผมจะมาเขียนอธิบายต่อครั้งหน้าครับ เอาของแถมไปก่อนและกันครับ คือ FizzBuzz โดยใช้แนวคิดแรกครับ "ทำตารางมา Map ว่ากรณีไหนได้ ผลลัพธ์อะไรบ้าง" โดยผมขอใช้ภาษา python ในการ implement นะครับ(มันเขียนง่ายดี 555)

#!/usr/bin/python
# -*- coding: utf-8 -*-
def fizzbuzz(n):
    fizzes = [1, 0, 0]
    buzzes = [2, 0, 0, 0, 0]
    words = [None, 'Fizz', 'Buzz', 'FizzBuzz']


for i in range(1, n):
    words[0] = i
    print words[fizzes[i % 3] + buzzes[i % 5]]

ลองรันดู fizzbuzz(23) นะครับ เพราะ Code นี้จะเอาค่าที่ Map ไว้มาแทนใน Array words เพื่อแสดงผลลัพธ์ครับ โดยวิธีนี้ ถ้ามีการเปลี่ยนเงื่อนไข เราก็ต้อง Map ข้อมูลใน Array ทั้งสามใหม่ (Fizz, Buzz และ Words) ซึ่งมีโอกาสทำให้ Code เดิมที่ถูกต้องอยู่แล้วเกิด defect ได้

จากตัวอย่าง Python ไม่ต้อง Full OOP ก็ได้ใช้ Map Array เอาเป็นอีกวิธีนึงครับ