Basic Programming กับระบบต่อสู้ใน Final Fantasy 12

final_fantasy_xii_box_art

Final Fantasy XII (12) นั้นมีระบบการต่อสู้ที่แตกต่างจากภาคอื่นๆอย่างสิ้นเชิง โดยส่วนตัวผมเพิ่งได้เล่นจริงๆจังๆจนจบก็ตอนที่ Remastered ลงบน PS4 นี่แหละครับในชื่อ Final Fantasy XII : The Zodiac Age ซึ่งแน่นอนวันนี้เราจะมาพูดถึงระบบต่อสู้ของเกมนี้กัน

Gambits เป็นชื่อของระบบต่อสู้ในเกม Final Fantasy 12 ซึ่งถ้าใครเป็น Programmer มาจะค่อนข้างคุ้นเคยมากเลยครับ เพราะนี่มันเป็น Basic Programming ชัดๆ

qhdvcpy0poow2saaenrr

จากรูปข้างบนนั้นระบบ Gambits นั้นจะเป็นการตั้ง programให้ตัวละครทำ action ต่างๆ ตามเงื่อนไข เช่น ถ้าศัตรูบินอยู่ให้โจมตี  ถ้าทีมเราพลังต่ำกว่า  30% ให้ใช้เวทย์มนต์ฟื้นหลัง ถ้าทีมเราติดพิษให้ใช้เวทย์แก้ผิด เป็นต้น หรือเราเรียกอีกอย่างนึงว่าการตั้งค่า AI (หรือ Bot?)

โดยที่ระบบนี้จะทำงานตามเงื่อนไขจาก บนลงล่าง หมายความว่าถ้าเหตุการณ์เข้าเงื่อนไขแรกก็ทำ action นั้นเลยโดยที่จะไม่ทำเงื่อนไข้ด้านล่างต่อ ซึ่งถือว่าเป็นการจบหนึ่งเทิร์นของตัวละครนั้นๆ

ที่นี้ถ้าเราลองเลือก 3 ตัวแรกมาลองแปลงเป็น Code ง่ายๆกัน มันก็น่าจะเป็นประมาณนี้ครับ

Screen Shot 2560-12-10 at 3.32.40 PM.png

 

if( enemy.IsFlying() ){
 me.Attack(enemy);
}
else if( team.IsHPbelow(30) ){
 let m = team.getIsHPbelow(30);
 me.Curaja(m);
}
else if(team.IsHPbelow(50) ){
 let m = team.getIsHPbelow(50);
 me.Curaga(m);
}

ซึ่งตีความลำดับของ Script ได้ว่า

  1. ถ้าศัตรูบินได้ ตีมันก่อนเลย
  2. ถ้าทีมเรามีคนพลังชีวิตต่ำกว่า 30% ให้ใช้เวทย์ Curaja
  3. ถ้าทีมเรามีคนพลังชีวิตต่ำกว่า 50% ให้ใช้เวทย์ Curaga (ซึ่งเป็นเวทย์ที่ฟื้นพลังชีวิตได้สูงกว่า Curaja)

ก็จะเห็นได้ว่าการเล่นเกม(บางเกม) ก็อาจจะช่วยให้เรารู้จัก Programming ได้นิดหน่อยนะครับสำหรับคนที่ไม่ได้มาสายนี้ แต่สำหรับคนที่เป็น Programmer/Developer ก็ยิ่งทำให้เข้าใจระบบของเกมได้ง่ายขึ้นทำให้เล่นได้ง่ายและสนุกไปกับมันมากขึ้น

 

เรื่องของตัว D (Dependency Inversion) ในหลักการ SOLID สำหรับคนเขียน OOP

Screen Shot 2560-08-27 at 7.02.07 PM

มาถึงตอนสุดท้ายของ SOLID กันแล้วนะครับ ตัวสุดท้ายตัว  D ซึ่งมาจาก Dependency Inversion Principle (DIP) โดยที่ส่วนตัวผมเองนั้นคิดว่าหลักการข้อนี้ค่อนข้างมีประโยชน์และน่าจะเป็นที่รู้จักกันเยอะระดับนึงอยู่แล้วในโลกของการพัฒนา Software ช่วงนี้

DIP นั้นกล่าวว่า

Class ที่มีการเรียกใช้  Class อื่นๆนั้นไม่ควรที่จะมีการอ้างอิงถึงตัว Object ของ Class ที่ถูกเรียกตรงๆ แต่ควรที่จะเรียกผ่าน Interface หรือ Pointer ของ Base Class

เป้าหมายที่สำคัญของหลักการนี้เพื่อทำให้ Code นั้นมีการผูกติดกันให้น้อยที่สุดกับสิ่งที่เป็น Dependency ของ Class เรา

คำว่า Dependency ในกรณีนี้ก็คือ เช่น เรามี Class SmartPhone แล้วมี Data Member เป็น Object ที่สร้างจาก Class BluetoothDevice เช่น  Code ตัวอย่างนี้

class BluetoothDevice {
 public connect(){ /* do low level network tasks */}
 public scan(){}
} 

class SmartPhone {
 private bt : BluetoothDevice; // มีการถือ object BluetoothDevice ใน Class
 public init(){
   this.bt = new BluetoothDevice(); // มีการสร้าง Object ใน Class
 }
 public connectBluetooth(){
   this.bt.connect();
 }
}

จะเห็นว่า Code ข้างต้นนี้ออกแบบได้ถูกหลัก Single Responsibility แล้วเพราะเราทำการแยก Class ของ SmartPhone และ BluetoothDevice ออกจากกันตามหน้าที่ 🙂 ตามรูปด้านล่าง

Screen Shot 2560-08-28 at 11.10.51 PM

Refactor

แต่ถึงอย่างนั้นด้วย DIP เราจะทำให้ Class นี้มี Design ที่ดีขึ้นไปอีกโดยที่เราจะใช้ Interface เป็นตัวกลางในการ Reference เช่นรูปด้านล่างนี้

Screen Shot 2560-08-28 at 11.12.12 PM

ดังนั้น Code ที่ทำการ Refactor โดยการ Inversion แล้วก็จะมีหน้าตาแบบนี้แทนครับ

 interface IBluetooth {
   connect();
   scan();
 }
 class BluetoothDevice implements IBluetooth { // class นี้ทำการ implements interface
   public connect(){ 
     /* do low level network tasks */
     console.log('bluetooth connect');
   }
   public scan(){}
 } 
 class SmartPhone {
   private bt : IBluetooth; // อ้างถึง Bluetooth ผ่าน Interface ไม่ใช่ Object
   public init(bt : IBluetooth){ // มีการรับ Object ที่มี Interface IBluetooth เป็น Parameter
     this.bt = bt;
   }
   public connectBluetooth(){
     this.bt.connect();
   }
 }

จาก Code ด้านบนนี้จะเห็นว่า Class SmartPhone นั้นไม่ได้มีการอ้างอิงไปถึง Object ของ Class BluetoothDevice ตรงๆแล้วแต่จะใช้ Interface แทนครับ ทำให้การที่จะเรียกใช้ Method ที่ถูกต้องของ BluetoothDevice นั้นจะต้องมีการสร้าง Instance ของมันให้ถูก

.

.

Code ของฝั่งที่จะสร้าง Object หรือ Instance จาก Class SmartPhone จึงต้องเปลี่ยนไปเป็นเช่นนี้

let sp = new SmartPhone();
sp.init(new BluetoothDevice()); // ต้องสร้าง Object ของ BluetoothDevice ส่งไปให้
sp.connectBluetooth();

ซึ่งการทำการส่ง Object ที่เป็น Dependency ของอีก Class นึงเข้าไปแบบนี้คือการทำสิ่งที่เรียกว่า Dependency Injection ( DI ) นั่นเอง

.

Testable Code

ข้อดีที่เราจะได้รับจากการทำ DIP ก็คือการทำให้ Code ของเรานั้น Unit Test ได้ง่ายขึ้น

ลองคิดดูครับว่าถ้า Class BluetoothDevice ตัวจริงนั้นต้องไปต่อ Network จริงๆ แล้วเราจะเขียน Unit Test ของ Class SmartPhone ได้ยังไง

.

การทำ DIP นั้นจะทำให้เราสามารถ Inject Mock Dependency ลงไปได้เช่นดังตัวอย่างในรูปนี้

Screen Shot 2560-08-28 at 11.27.36 PM.png

class MockBluetoothDevice implements IBluetooth {
 public connect(){
   console.log('just mock connect');
 }
 public scan(){
   console.log('just mock scan');
 }
}

Code ตอนเขียน UniTest

let sptest = new SmartPhone();
sptest.init(new MockBluetoothDevice()); // ส่งตัวปลอม(Mock) ของ BluetoothDevice ไป
sptest.connectBluetooth(); // ไม่ต้องไปต่อ Network จริงๆ

ก็จะเห็นว่า DIP นั้นช่วยทำให้ Code เราเขียน Test ได้ง่ายขึ้น แต่ทั้งนี้ทั้งนั้นทุกวันนี้เราจะเห็นว่ามี Framework ที่ช่วยทำ Dependency Injection มากมายเลยนะครับ แต่ผมอยากให้ทุกคนทราบกันว่านี่คือเบื้องหลังของมันรวมถึงที่มาที่ไปด้วย

เอาล่ะ มาถึงตรงนี้เป็นอันว่าจบหลักสูตร SOLID กันแล้วนะครับ สำหรับคนที่คิดว่ามันเยอะไปสำหรับ 5 หลักนี้ ผมก็คงแนะนำให้ทบทวน 2 ตัว คือ S กับ D ละกันครับ 🙂 และหวังว่าซีรี่ย์นี่จะเป็นประโยชน์กับหลายๆคนนะครับ

SOLID The Series:

 

 

เรื่องของตัว I (Interface segregation) ในหลักการ SOLID สำหรับคนเขียน OOP

Screen Shot 2560-08-23 at 12.13.53 AM.png

มาถึงตอนที่ 4 ของซีรีย์ SOLID กันแล้วนะครับ กับตัว I ซึ่งตัวนี้ย่อมาจาก Interface segregation Principle (ISP)  ที่จะทำให้ Code ที่เขียนด้วยภาษา OOP นั้นดูดีมากขึ้นไปอีกขั้น 😉

ISP นั้นกล่าวถึงหลักการที่ว่า

Class ที่มา Implement เพื่อใช้งาน Interface นั้นๆไม่ควรที่จะต้องมา Implement Method ต่างๆให้ครบโดยที่อาจจะไม่ได้ใช้

ซึ่งถ้าตีความง่ายๆที่สุดเลยก็คือ เราควรอย่าไปกลัวกับการที่จะต้องสร้าง Interface ใหม่ เพราะโดนส่วนใหญ่ Developer อาจจะชอบเพิ่ม Method ใน Interface เดิม ซึ่งมันดูปลอดภัยมากกว่านั่นเอง

เราลองมาดูตัวอย่างกัน …

เริ่มแรกเลย

ผมมี Interface ชื่อ IPhone (ไม่ใช่ Appple iPhone นะครับ :D) ซึ่งมีด้วยกันทั้งหมด 6 Methods

interface IPhone {
        call();
        ring();
        installApp();
        addContact();
        connectWifi();
        connectMobile();
    }

ที่นีผมก็ลองสร้าง Class ชื่อ SmartPhone ที่จะมา Implement ตัว Interface IPhone ดูครับ

class SmartPhone implements IPhone {
 public call(){ /* do something /* }
 public ring(){/* do something /* }
 public installApp(){/* do something /* }
 public addContact(){/* do something /* }
 public connectWifi(){/* do something /* }
 public connectMobile(){/* do something /* }
}

เนื่องจากว่า SmartPhone นั้นมีความสามารถครบก็ดูโอเคดีครับที่เราจะออกแบบ Interface IPhone แบบนี้

แต่ถ้าผมมีอีก Class ชื่อ FeaturePhone ที่ต้อง Implement IPhone แต่ไม่สามารถ install App หรือ ต่อ wifi ได้ล่ะ ด้วยมันก็จะเป็นแบบนี้

class FeaturePhone implements IPhone {
 public call(){}
 public ring(){}
 public installApp(){
     throw new Error('Feature Phone does not support apps..');
 }
 public addContact(){}
 public connectWifi(){
     throw new Error('Feature Phone does not support using Wifi..');
 }
 public connectMobile(){}
 }

และถ้าเรามีอีก Class ชื่อ BasicTablet ที่มันทำอะไรไม่ค่อยได้นอกว่า install App กับต่อ wifi  มันก็จะเป็นแบบนี้

 BasicTablet implements IPhone {
 public call(){
    throw new Error('Basic Tablet does not support call..');
 }
 public ring(){
    throw new Error('Basic Tablet does not support ring..');
 }
 public installApp(){}
 public addContact(){
    throw new Error('Basic Tablet does not support contacts..');
 }
 public connectWifi(){}
 public connectMobile(){
    throw new Error('Basic Tablet does not support using mobile..');
 }
}

ซึ่งจากตัวอย่างทั้ง 2 Class นั้นก็จะเห็นได้ว่า การออกแบบ Interface แบบนี้ดูแล้วมันค่อนข้างกว้างไป

.

.

เริ่มการ Refactor

แน่นอนว่า Interface IPhone ที่สร้างไว้ตั้งแต่ต้นนั้นมันกว้างไป เทคนิคที่จะใช้ครั้งนี้คือ Extract Interface ซึ่งก็คือทำการแบบ Interface IPhone ออกมาเป็น Interface ย่อยๆตามกลุ่มของ Method ที่เรามี

ซึ่งถ้าเราสังเกตุดูจาก Interface IPhone นั้นจะมี Method อยู่ 3 กลุ่มคือ

  1. โทร พื้นฐาน
  2. การใช้งาน App
  3. การเชื่อมต่อ

ดังนั้นหลักจากที่ทำการ Extract Method แล้วจะได้ผลแบบนี้ครับ

interface IPhone {
  call();
  ring();
  addContact();
}

interface ISmartFeatures {
  installApp();
}

interface IMobileConnect {
  connectMobile();
  connectWifi();
}

ที่นี้ Class SmartPhone ของเราก็ต้องแก้ Code ให้มาเป็นแบบนี้แทน

class SmartPhone implements IPhone, ISmartFeatures, IMobileConnect {

  public call(){}
  public ring(){}
  public installApp(){}
  public addContact(){}
  public connectWifi(){}
  public connectMobile(){}

}

ส่วน Class FeaturePhone ก็จะเป็นแบบนี้ ซึ่งก็ยังต้องมีการ handle Method ที่ไม่ Support อยู่แต่ว่าก็จะน้อยลงกว่าตอนแรก

class FeaturePhone implements IPhone, IMobileConnect {

  public call(){}
  public ring(){}
  public addContact(){}
  public connectMobile(){}
  public connectWifi() {
    throw new Error('Feature Phone does not support using Wifi..');
  }

}

ส่วน Class BasicTablet นั้นก็จะคล้ายๆกับ FeaturePhone ก็คือ

class BasicTablet implements ISmartFeatures, IMobileConnect {

  public installApp(){}
  public connectWifi(){}
  public addContact(){}
  public connectMobile(){}
  public connectMobile(){ 
    throw new Error('Basic Tablet does not support using mobile..'); 
  }

}

สรุป

เพื่อนๆก็จะเห็นว่าการทำ Interface Segregation นั้นจะทำให้เราสามารถออกแบบ Interface ได้ดียิ่งขึ้นและไม่เป็นการทำให้ Client ของ Interface นั้นต้องมาเขียน  Code  เพิ่มโดยที่ไม่ได้ Handle อะไรเลย รวมถึงทำให้ Class และ Interface ของเราดูแลได้ง่ายขึ้นและเจาะจงการการทำงานแต่ละเรื่องมากขึ้น

SOLID The Series:

เรื่องของตัว L (Liskov Substitution) ในหลักการ SOLID สำหรับคนเขียน OOP

Screen Shot 2560-08-23 at 12.09.41 AM.png

มาถึงหลักการข้อ 3 ของ SOLID กันแล้วนะครับ สำหรับตัว L ซึ่งย่อมากจาก Liskov Substitution Principle (LSP)  ซึ่งกล่าวด้วยความ Abstact อีกแล้วว่า

Function ที่ใช้ reference หรือ Object ของ คลาสแม่ (Base Class) นั้นควรจะต้องสามารถใช้ Object ของคลาสลูกมันได้โดยไม่ต้องรู้รายละเอียดหรือข้อจำกัดของมัน

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

…เอาล่ะ เราเป็น Developer เราก็มาลองดู Code กันดีกว่าครับ

*เช่นเดิม Code เขียนด้วย TypeScript

ก่อนอื่นเลยผมมี Class Phone ซึ่งมี  2 Methods คือ call() กับ installApp()

    class Phone {
        public call(){
            console.log('Make call...');
        }
        public installApp(){}
    }

ผมก็ได้ทำการเพิ่ม Class มาอีก 2 Classes คือ SmartPhone กับ FeaturePhone โดยการสืบทอดมาจาก Phone

แต่บังเอิญว่า FeaturePhone ดัน install App ไม่ได้เลยต้องทำการ Throw Exception ออกไป

    class SmartPhone extends Phone {
        public installApp(){
            console.log('SmartPhone install the app...');
        }
    }

    class FeaturePhone extends Phone {
        public installApp(){
            throw new Error('Feature Phone does not support install app...');
        }
    }

ถ้าผมที่ Code ฝั่งที่เรียกใช้ Phone จะเป็นแบบนี้ครับ

let phone = new Phone();
phone.installApp(); // ก็  Work ดี

let smPhone = new SmartPhone();
smPhone.installApp(); // ก็ Work ดี

let ftPhone = new FeaturePhone();
ftPhone.installApp(); // พัง เกิด Exception

ซึ่งจะเห็นได้ว่า ผมต้องมาแก้เป็นแบบนี้ เป็นกรณีพิเศษ เช่นต้องใช้ try/catch

try {
    ftPhone.installApp();
}

และนี่แหละคือการผิดหลักการออกแบบที่ดีของหลักการ LSP เพราะเราไม่สามารถเรียกใช้ Class ลูก (FeaturePhone) ได้แบบที่เหมือนเรียก Class แม่ (Phone) ซึ่งการที่เราจะรู้ได้ว่ามัน Throw Exception นั้นก็ต้องไปดู  Code ที่ถูก Implement ใน Class นั้น ซึ่งถ้าเป็น  Code ที่อยู่ใน Libary หรือพัฒนาโดยคนอื่นล่ะ มันก็คงจะดูไม่ได้ง่ายๆใช่มั้ยครับ

การจะแก้ไขปัญหานี้ที่ถูกต้องตามหลัก OO คือต้อง Refactor โดยใช้เทคนิคชื่อ Push Down Method ครับ (ก็คือย้าย Method ที่ไม่จำเป็นใน Class แม่ลงไปที่ Class ลูก)
ฉะนั้นผมจะได้ Class ใหม่แบบนี้แทน

    class Phone {
        public call(){
            console.log('Phone install the app...');
        }
    }
    
    class SmartPhone extends Phone {
        public installApp(){
            console.log('SmartPhone install the app...');
        }
    }

    class FeaturePhone extends Phone {
        // additional FeaturePhone feature e.g. multi sim cards
    }

สิ่งที่เปลี่ยนไปคือ

  1. Method installApp() ถูกย้ายลงมาที่ SmartPhone
  2. FeaturePhone จะไม่มี Method installApp() ให้เรียกแล้ว
  3. ทำให้แก้ปัญหาที่ Code ที่ใช้เรียก ftPhone.installApp() แล้วต้องใช้ try/catch ไปได้! 🙂

SOLID The Series:

 

เรื่องของตัว O (Open-Closed) ในหลักการ SOLID สำหรับคนเขียน OOP

Screen Shot 2560-08-23 at 12.08.07 AM.png

หลังจากที่พูดถึง S (หรือ Single Responsibility) ไปแล้วหลักการต่อมาก็คือ O ซึ่งย่อมาจาก  Open-Closed Principle ซึ่งเป็นหลักการที่ 2 ของ SOLID ที่มีใจความสำคัญแบบ Abstractๆ ว่า

เราควรออกแบบระบบให้ง่ายต่อการเพิ่มเติมอะไรใหม่ๆ โดยที่ไม่ต้องแก้ไขระบบเก่า (ถ้าไม่จำเป็น)

Open-Closed Principle (OCP) จะกล่าวถึงการออกแบบ Class หรือทั้งระบบให้ยืดหยุ่นมากขึ้น โดยที่เราใช้ความสามารถของ OOP มาช่วย โดยยกตัวอย่างเช่น ถ้าเราต้องเพิ่ม Feature ใหม่ให้ Class เรา จุดที่ควรจะแก้ไข Code นั้นควรจะเป็นจุดที่เหมาะสมและไม่ควรกระทบกับสิ่งที่ไม่ควรจะแก้ไข

อธิบายเพิ่มก็ยัง งง อีก…งั้นเรามาดูตัวอย่างกันดีกว่าครับ

เช่นเดิม ผมมี Class SmartPhone ซึ่งมี Constructor ที่มีการ Set ค่าต่างๆให้กับ Object ที่กำลังถูกสร้างตามชื่อ Model เช่น S10 หรือ 8s

*Code ตัวอย่างเป็น TypeScript

    
class SmartPhone {
    private phoneModel : string;
    private phoneFullName : string;
    private screenWidth : number;
    private screenHeigth : number;
    constructor(model : string){
       if(model ==='S10'){
          this.phoneModel ='S10';
          this.screenWidth =5;
          this.screenHeigth =10;
          this.phoneFullName ='Samsung Galaxy S10';
        }
        else if(model ==='8s'){
          this.phoneModel ='8s';
          this.screenWidth =4;
          this.screenHeigth =3;
          this.phoneFullName ='Apple iPhone 8s';
        }
    }
}

จาก Code นี้ถ้าผมทำการเพิ่ม Model ใหม่สิ่งที่ผมต้องทำคือผมต้องผมแก้ Contructor ของ Class SmartPhone โดยการเพิ่ม else if  แล้วทำการเพิ่ม Code ของ Model Nexus 9 เข้าไป

constructor(model : string){
       if(model ==='S10'){
          this.phoneModel ='S10';
          this.screenWidth =5;
          this.screenHeigth =10;
          this.phoneFullName ='Samsung Galaxy S10';
        }
        else if(model ==='8s'){
          this.phoneModel ='8s';
          this.screenWidth =4;
          this.screenHeigth =3;
          this.phoneFullName ='Apple iPhone 8s';
        }
        else if(model ==='Nexus 9'){
          this.phoneModel ='Nx9';
          this.screenWidth =5;
          this.screenHeigth =5;
          this.phoneFullName ='Google Nexus 9';
        }
}
       
การทำแบบนี้ไม่ผิดครับ แต่เนื่องจากเราเขียนภาษา OO และต้องการทำให้ Design ของเราเป็นไปตามหลักการ OCP ดังนั้นเราต้องทำการ Refactor Code กันครับ
ลองคิดดูก่อนนะครับว่าจะ Refactor กันยังไงดี…
.
.
.
เอาล่ะมาเริ่มกัน
เทคนิคที่เราจะใช้ในการเพิ่ม Code Quality ครั้งนี้คือการใช้ Inherithance หรือการสืบทอดนั่นเอง ดังนั้นเรามาสร้างเป็น 3 Classes โดยที่ SmartPhone จะเป็น Base Class
   class SmartPhone { // Base Class
        protected phoneModel : string;
        protected phoneFullName : string;
        protected screenWidth : number;
        protected screenHeigth : number;
        constructor(){
            this.init();
        }
        protected init(){};
    }

    class S10Phone extends SmartPhone{// Inherited จาก Base Class
        protected init(){
            this.phoneModel = 'S10';
            this.screenWidth = 5;
            this.screenHeigth = 10;
            this.phoneFullName = 'Samsung Galasy S10';
        } 
    }

    class i8sPhone extends SmartPhone{// Inherited จาก Base Class
        protected init(){
            this.phoneModel = '8s';
            this.screenWidth = 4;
            this.screenHeigth = 3;
            this.phoneFullName = 'Apple iPhone 8s';
        } 
    }

    class Nx9Phone extends SmartPhone{ // Inherited จาก Base Class
        protected init(){
              this.phoneModel ='Nx9';
              this.screenWidth =5;
              this.screenHeigth =5;
              this.phoneFullName ='Google Nexus 9';
        }
    }

ซึ่งการทำแบบนี้จะเห็นว่าถ้าเราต้องการเพิ่ม model ใหม่ๆเข้าไปเราจะไม่ต้องมาแก้ไข้ base class เลยครับแค่ทำการ Inheritance กับสร้าง Class ใหม่แทน ซึ่งนี่ก็คือหลักการของ OCP ครับ 🙂

SOLID The Series:

เรื่องของตัว S (Single Responsibility) ในหลักการ SOLID

Screen Shot 2560-08-23 at 12.04.58 AM.png

หลังจากที่ผมได้เกริ่นเกี่ยวกับหลักการ SOLID ไปแล้วนั้น วันนี้เราจะมาลองดูกันกับตัวแรกของเรา ซึ่งก็คือตัว S ซึ่งย่อมากจาก Single Responsibility หรือแปลว่า หลักการทำหน้าที่เดียวพอ ซึ่งผมจะขอใช้ตัวอย่างด้วย TypeScript ครับ

Single Responsibility Principle (SRP) นั้นกล่าวถึงการออกแบบ Class ในภาษา Object Oriented เพื่อที่จะทำให้ Class นั้นดูแลและจัดการได้สะดวกมากขึ้น

เริ่มต้น ผมมี Class ตัวอย่างที่ชื่อว่า SmartPhone

class SmartPhone {
   // properties
   private apps : Array<string>;
   private contacts : Array<string>;
   private phoneNumber : string;
   private phoneModel : string;
   private screenWidth : number;
   private screenHeigth : number;

   // methods
   public call( telNum : number){
   }
   public ring(){
   }
   public installApp(appName : string){
      this.apps.push(appName);
   }
   public launchApp(appName : string){
      let app = this.apps.filter(element => element === appName);
   }
   public addContact(contact : string){
      this.contacts.push(contact);
   }
   public getScreenPixel() : number {
      return this.screenWidth * this.screenHeigth;
   }
   public getModel() : string {
      return this.phoneModel;
   }
}

ซึ่งจาก Code ตัวอย่างนี้จะเห็นได้ว่า Class SmartPhone นี้ก็ออกแบบมาตามหลัก OO ระดับหนึ่ง ซึ่งถ้าเรามาดูรายละเอียดดีๆนั้นจะเห็นว่ามันมี 3 ส่วนด้วยกัน

  1. การจัดการ Apps
  2. การจัดการ Contact
  3. ข้อมูลของตัว SmartPhone เอง

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

สิ่งที่ต้องทำก็คือจะใช้เทคนิคการ  Refactor ที่ชื่อว่า Extract Class ก็คือเป็นการแยก Code ในส่วนของ Properties และ Methods ที่น่าจะรวมกันเป็น Class ใหม่ได้ออกมา

ดังนั้นเราเราจะได้ Class ใหม่มาเพิ่มอีก 3 Class คือ Apps และ Contacts

  1. Class Apps จะเอาการเก็บข้อมูล Array และ Methods การ install กับ launch มาไว้ใน Class นี้
class Apps {
   private apps : Array<string>;
   public install(appName : string){
      this.apps.push(appName);
   }
   public launch(appName : string){
      let app = this.apps.filter(element => element === appName);
   }
}

2. Class Contacts จะเอาไว้จัดการเกี่ยวกับ Contact List

class Contacts {
   private contacts : Array<string>;
   public add(contact : string){
      this.contacts.push(contact);
   }
}

3. Class PhoneInfo เราจะเอามาเก็บข้อมูลทั่วไปเช่น เบอร์โทร ชื่อรุ่น ขนาดหน้าจอ

class PhoneInfo {
   private phoneNumber : string;
   private phoneModel : string;
   private screenWidth : number;
   private screenHeigth : number;
   constructor(model : string){
      this.phoneModel = model;
      this.screenWidth =5;
      this.screenHeigth =10;
   }

   public getModel() : string {
      return this.phoneModel;
   }

   public getScreenPixel() : number {
      return this.screenWidth *this.screenHeigth;
   }
}

ซึ่งเมื่อมาถึงตรงนี้แล้วเราก็ต้องทำการแก้ Class หลักของเราก็คือ Class SmartPhone ให้มาเรียกใช้งานทั้ง 3 Classes นี้

// Smart Phone V2
class SmartPhone {
   private apps : Apps; // เรียกใช้ Class Apps
   private info : PhoneInfo; // เรียกใช้ Class PhoneInfo
   private contacts : Contacts; // เรียกใช้ Class Contacts

   constructor(){
      this.apps =new Apps();
      this.info =new PhoneInfo('S10');
      this.contacts =new Contacts();
   }

   public call( telNum : number){
   }

   public ring(){
   }

   public installApp(appName : string){
       this.apps.install(appName); // เรียกใช้ Method ของ Class Apps
   }

   public launchApp(appName : string){
      this.apps.launch(appName);// เรียกใช้ Method ของ Class Apps
   }

   public addContact(contact : string){
      this.contacts.add(contact);// เรียกใช้ Method ของ Class Contacts
   }

   public getScreenPixel() : number {
      return this.info.getScreenPixel();// เรียกใช้ Method ของ Class PhoneInfo
   }

   public getModel() : string {
       return this.info.getModel();// เรียกใช้ Method ของ Class PhoneInfo
   }
}

หลังจากทำการ Refactor เสร็จก็จะเห็นได้ว่า Class SmartPhone Verion 2 นั้นจะเป็นระเบียบมากขึ้น ไม่ต้องมาเก็บตัวแปรที่เป็น String หรือ Array ที่ไม่เกี่ยวข้องอะไรกับตัว Class นี้เองเลย แต่จะย้ายไปเก็บใน Class ย่อยๆแทนเพื่อที่จะให้ Code นั้นเหมาะสมกับ Class แต่ละ Class จริงๆ

เท่านี้เพื่อนๆก็จบหลักสูตร Single Responsibility Principle กันแล้วครับ หวังว่าจะเป็นประโยชน์กับทุกคน แล้วเจอกันตอนหน้าด้วยหลักการตัว O 🙂

หลักการ SOLID สำหรับคนที่เขียน OOP

SOLID เป็นหลักการ 5 ข้อที่จะช่วยให้ Developer ที่เขียนภาษา Object Oriented เช่นพวก C#, Java, C++) นั้นสามารถเขียน Code ได้ดูเป็นระบบระเบียบมากขึ้น รวมถึงสามารถที่จะดูแลและแก้ไขได้ง่ายและสุดท้ายทำให้พวกเราดูเป็น Profressional กันด้วย 🙂

ถึงแม้ว่าหลักการของ SOLID นั้นมีมาก็หลาย 10 ปีแล้วแต่ผมคิดว่าสำหรับนักพัฒนาที่เขียนภาษา OO ก็เป็นสิ่งที่ยังควรรู้ รวมถึงคนที่เขียนภาษาอื่นๆเช่นพวก Script หรือ Dynamic languages ก็อาจจะเอาไปประยุกต์ใช้ได้เหมือนกัน

เอาล่ะเรามาดูกันว่าหลักการ 5 ข้อนั้นย่ามาจากอะไรบ้าง

S – Single Responsibility Principle

กล่าวถึง Class หนึ่งๆนั้นควรจะมีแค่หน้าที่ ความรับผิดชอบเดียวเท่านั้น

O – Open Closed Principle

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

L – Liskov substitution Principle

กล่าวถึง Object ของ Class ที่ระบบได้เรียกใช้นั้นควรจะสามารถเรียกใช้ Class ลูกๆได้โดยที่ไม่ต้องแก้ไขระบบเลย (ข้อนี้อาจจะเข้าใจยากนิดนึงนะครับ อาจจะต้องลองดูตัวอย่าง)

I – Interface segregation Principle

กล่าวถึงการแบ่งย่อย Interface ให้เป็นสัดสวนตามหน้าที่ของมัน ดีกว่ามี Interface เดียวที่มีหลายๆ Methods

D – Dependency Inversion Principle

กล่างถึงการที่ Class ต่างๆติดต่อกับอีก Class อื่นๆนั้น ควรจะติดต่อกันผ่าน Interface มากกว่า Object ของ Class นั้นๆตรงๆ

 

 

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

ref : https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)