ทดลองใช้ GitHub Actions

GitHub Actions เป็นฟีเจอร์ด้าน Continuous Integration ของ Github ก็คือ เราสามารถลำดับของการกระทำบางอย่างกับโค๊ดหลังจากที่เกิดเหตุการบางอย่างขึ้นบน repository เช่น

  • มีโค๊ดใหม่ถูก push เข้าไปใน repository
  • มี tag ใหม่ถูกสร้างขึ้น
  • มีคนสร้าง pull request

ซึ่งเมื่อเกิดเหตุการดังกล่าว เราก็อาจจะอยากทำอะไรสักอย่างกับโค๊ดหรือโปรเจค เช่น

  • ทดลอง build โค๊ด เพื่อดูว่าเกิด build error หรือ warning
  • ทดลองรันโปรแกรมทดสอบ หาว่ารันได้อย่างที่ควรจะเป็นหรือไม่ มีพฤติกรรมที่ไม่เหมาะสมเกิดขึ้นหรือเปล่า
  • ทำ static analysis หาปัญหาที่อาจจะมีในโค๊ด
  • ส่งโค๊ดไปรันบนเซิร์ฟเวอร์
  • อะไรก็แล้วแต่เท่าที่จะจิตนาการณ์ได้

การกระทำที่ว่านี้ใน GitHub Actions จะเรียกว่า job ครับ

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

ยกตัวอย่าง workflow #1

เมื่อมีคน push code ขึ้นไปใน main branch ให้ทำการ

  1. คอมไพล์โปรแกรมบน Linux
  2. เตรียมข้อมูลสำหรับใช้รันชุดทดสอบ
  3. รันชุดทดสอบ
  4. ติด tag latest ที่ commit ที่ใหม่ที่สุดใน branch
  5. เตรียม docker image
  6. อัพโหลดโปรแกรมที่ release ของ github
  7. สั่งให้ development server ทำการสร้าง container ใหม่โดยรันโปรแกรมเวอร์ชั่นล่าสุด

ทั้งนี้ workflow สามารถทำงานหลาย ๆ job พร้อม ๆ กัน แต่เราสามารถกำหนดว่า job ไหนจะต้องทำหลังจาก job ไหน อย่างเช่นบน workflow ข้างบน ผมอาจจะทำ job #1 กับ #2 พร้อมกันได้ แต่ job #3 จะต้องรอ #1 กับ #2 ให้เสร็จก่อน เราก็กำหนด dependency ไปว่า #3 depend on #1 และ #2 เป็นต้น

แต่ละ job ใน workflow เราสามารถใช้ job ที่มีผู้สร้างเอาไว้อยู่แล้ว หรือกำหนดลำดับการทำงานของแต่ละ job ได้ด้วย เช่นการรันชุดทดสอบผมอาจจะกำหนด job นี้ว่า

  1. สร้าง environment variable จำวนหนึ่ง
  2. ติดโปรแกรม database server ใน virtual environment
  3. รัน database server ใน VE
  4. import test data ที่ใช้ทดสอบเข้าไป
  5. รันชุดทดสอบ
  6. อัพโหลดผลการทดสอบขึ้นไปบน server

ทั้งนี้ในหนึ่งโปรเจคอาจจะมีหลาย workflow ได้ เช่นอาจจะมี workflow หนึ่งสำหรับสร้าง Windows Build อีกตัวหนึ่งสำหรับสร้าง MacOS Build อะไรแบบนี้ GitHub สามารถรันหลาย ๆ workflow พร้อม ๆ กันได้ เพื่อลดเวลาที่ใช้โดยรวมครับ ตัว workflow นี่เราจะเขียนด้วยภาษา yaml ซึ่งจะต้องอยู่ใน directory .github/workflows ภายในโปรเจคเรา ข้างล่างเป็น workflow นึงที่ผมเขียนใช้กับโปรเจคที่ผมเขียนเล่น ๆ อยู่ตอนนี้ครับ

name: main
on: 
  push:
    branches:
      - main

jobs:
  build:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true
      - run: "Get-ChildItem Env:"
      - name: Create output dir
        run: mkdir out

      - name: Configure the project.
        run: cmake -D CMAKE_TOOLCHAIN_FILE=$env:VCPKG_INSTALLATION_ROOT\scripts\buildsystems\vcpkg.cmake $env:GITHUB_WORKSPACE
        working-directory: out

      - name: Build the project.
        run: cmake --build out --config Release

      - name: Copy font files.
        run: xcopy fonts out\Release\fonts\*

      - uses: EndBug/latest-tag@latest

      - uses: actions/upload-artifact@v2
        with:
          name: windows
          path: out/Release

      - name: "Create latest release file"
        run: |
          Get-ChildItem -Path out\Release | Compress-Archive -DestinationPath windows-latest.zip
      - name: Release
        uses: softprops/action-gh-release@v1
        with:
          body: |
            This is the latest, the bleeding edge. Might not work properly and might harm your system. Use with care.
          files: windows-latest.zip
          name: Latest
          prerelease: true
          tag_name: latest
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

จุดเด่นอันหนึ่งของ GitHub Actions คือตัว job จะรันบน Virtual Environment ที่รัน OS ได้หลายแบบ (มี Windows, Linux และ MacOS ให้เลือกในตอนนี้) การที่่สามารถเลือก OS ได้นั้นทำให้เราสามารถที่จะสร้าง job ที่จำเป็นจะต้องใช้ OS เฉพาะทางเท่านั้นถึงจะทำได้ เช่นการ build Mac OS application หรือการทดสอบ web application บน windows server เป็นต้น ทำให้เรามีความสะดวกในการทำงานแบบหลายแพลตฟอร์ม ไม่จำเป็นจะต้องหาเครื่อง host มาทำงานดังกล่าวเอง

อ่านมาจนถึงตรงนี้แล้วหลายคนคงสงสัยว่า ฟีเจอร์เทพขนาดนี้ แล้วราคามันจะแพงหรือไม่ สำหรับโปรเจคที่เป็น Open Source เราสามารถใช้งาน GitHub Actions ได้ฟรีครับ ส่วนพวก Private Repository จะมีค่าใช้จ่าย (แพงอยู่เหมือนกัน) อันนี้ลองศึกษาหาข้อมูลเพิ่มดูครับ

ส่วนความไม่สะดวกของ GitHub Actions ตอนนี้น่าจะเป็นเรื่องที่เราไม่สามารถทดสอบตัว workflow ในเครื่องเราเองได้ ดังนั้นกว่าที่จะมี workflow ที่ใช้งานได้จริงก็อาจจะต้องรันบน server หลายครั้งอยู่ครับ อันนี้จะไม่สะดวกเท่าของ CircleCI ที่สามารถทดสอบในเครื่องตัวเองได้เลย

สำหรับคนที่สนใจลองศึกษาเพิ่มบนเว็บของ GitHub ดูครับ

Singleton ไม่ดีจริงเหรอ ?

อันนี้คือ เท่าที่อ่านๆ บนเน็ต มีแต่คนบอกว่า “มาเลิกใช้ Singleton กันเถอะ” ซึ่งอันที่จริงสำหรับคนที่มีประสพการณ์พอสมควรจะรู้ว่า ปริมาณของ Singleton เนี่ยเป็นตัวบอกถึงว่าโค๊ดเรากำลังจะมีปัญหา แต่ว่ามันถึงขั้นต้องส่งโค๊ดไปถ้ำกระบอกเพื่อเลิกการเสพย์ติด Singleton เลยงั้นเชียวหรือ

บางคนบอกว่ามันเป็น Anti-Pattern ด้วยซ้ำ (มันคืออะไรเหรอครับคำนี้ :o)

Singleton คืออะไร

อ้างอิงจากหนังสือ Design Patterns, Elements of Reusable Object-Oriented Software (GoF) ซึ่งถือว่าเป็นหนังสือคลาสสิคของ Design Pattern กล่าวไว้ว่า

Intent Ensure a class only has one instance, and provide a global point of access to it.

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

ซึ่งสาระสำคัญอยู่ตรงที่ 1 instance ครับ

Implementation

ในหนังสือเขาจะเขียนว่า คลาสที่เป็น Singleton นั้นจะอารมณ์ประมาณนี้

class Singleton {
public:
  static Singleton* Instance();
protected:
  Singleton();
private:
  static Singleton* _instance;
}

นี่ลอกมาเป๊ะ ๆ เลย

โดยในหนังสือเขาให้ตัวอย่างว่า ไอ้ฟังก์ชัน Singleton::Instance() จะมีหน้าตาประมาณนี้

Singleton* Singleton::Instance() {
  if(_instance == nullptr) {
    _instance = new Singleton;
  }
  return _instance;
}

ผมแก้นิดนึงให้เข้ากับ ศตวรรษที่ 21 หน่อย

ทีนี้ การทำ Lazy-Initialization ซึ่งเป็น Implicit Initialization กล่าวคือ ไอ้ตัวแปร _instance เนี่ยจะเป็น null มาตั้งแต่ต้น และไม่เคยถูก Init ขึ้นมาเลยจนกระทั่งฟังก์ชัน Instance() ถูกเรียกเป็นครั้งแรก ซึ่ง ผมมองว่าเป็นหลุมพรางที่หลาย ๆ คนตกลงไป

ถ้าอ่านดี ๆ ในบทนี้ หนังสือเขาเขียนแค่ว่า นี่เป็น Common-Implementation ไม่ใช่ The only implementation กล่าวคือ ตราบใดที่คลาสนี้มีแค่ instance เดียว จะเขียนแบบไหนก็เขียนได้ครับ

กรณีของ Singleton ผมจะชอบทำ Explicit Initialization มากกว่า คือแทนที่จะบอกว่า ใช้ได้เลย เดี๋ยวถ้า instance ยังไม่พร้อมเดี๋ยวสร้างใหม่เอง ก็เป็นว่าให้ user คุมไปเลยว่าจะสร้าง instance เมื่อไหร่ แล้วเราก็สามารถทำ Explicit Clean-up ได้ด้วย

ก็อารมณ์แบบนี้ครับ

class Singleton {
public:
  static Singleton* Instance() {return _instance;};
  static void Init();
  static void CleanUp();
protected:
  Singleton();
private:
  static Singleton* _instance;
}

มันก็แค่ฟังก์ชันอีกสองฟังก์ชันเท่านั้นเอง ว่าไหมครับ ถึงจุดนี้ผมก็ยังไม่ได้ละเมิดข้อตกลงที่ว่า ต้องมีแค่ instance เดียวนะ แค่บอกเฉย ๆ ว่ามันถูกสร้างเมื่อไหร่ และถูกทำลายเมื่อไหร่ เท่านั้นเอง

อ้อ ถ้า user ดันไปเรียกใช้ instance ก่อน init ก็ access-violation พังไปครับ อันนี้เรียกเสร่อ (ฮา) แต่ถ้าปราณีกันจะขว้าง exception กลับไปก็ดีเหมือนกัน

Instance ที่มันคืนมา ก็ไม่ได้จำเป็นว่าจะต้องมี type เดียวกับตัวคลาสด้วยซ้ำ

อันนี้เป็นอีกข้อนึงที่คนอ่านข้าม ทั้ง ๆ ที่มันก็อยู่หน้าข้าง ๆ ไอ้โค๊ดที่ผมก็อปมาเนี่ยแหละครับ

อย่างสมมติผมมีคลาส Singleton อันนึงชื่อ WindowManager (ลักษณะของคลาส Singleton อันนึงคือ มันชอบเป็น ผู้จัดการครับ ไม่รู้ทำไม) ผมสามารถสร้างโค๊ดในลักษณะนี้ได้ครับ

class WindowManager {
public:
  static WindowManager* Instance() {return _instance;};
  template<class T>
  static void Init();
  static void CleanUp();

protected:
  WindowManager();
private:
  static WindowManager* _instance;
};

class OSXWindowManager: public WindowManager {
//...
};

class LinuxWindowManager: public WindowManager {
//...
};

template<class T>
static void WindowManager::Init() {
  _instance = new T;
}

สังเกตตรงฟังก์ชัน WindowManager::Init() ที่เป็นเทมเพลตนะครับ คือตรงนี้เนี่ยเราระบุ type ตอน Init แบบ ให้ user เลือกเลยว่าอยากได้ class ไหน แต่เวลาเข้าถึงนี่เข้าได้จากที่เดียวนะ คือจาก WindowManager

ถ้าอ่านโค๊ดจากในหนังสือจะเห็นว่า ที่เขาวาง constructor ไว้เป็น protected ก็มีสาเหตุเพราะจะให้เราเลือกที่จะสร้าง subclass ก็ได้นะนี่แหละครับ แต่ก็มีข้อแม้ตรงที่ตัว subclass เองก็ควรเป็น protected ตามนะ ไม่งั้นเดี๋ยว user จะสร้าง instance เพิ่มได้ จะกลายเป็นละเมิดการออกแบบไป

หลายๆ คนน่าจะเห็นแล้วว่า โค๊ด WindowManager ข้างบนเนี่ย ดูแล้วเหมือนเป็นสองแพทเทิร์นรวมกัน ใช่ครับเราสามารถใช้ Singleton ร่วมกับ Abstract Factory ได้ด้วยครับ

Singleton เป็นตัวขวาง Dependency Injection??

จากการอ่านบนอินเตอร์เนทมา ผมพบว่ามีคนพูดถึงอันนี้เยอะอยู่ครับ คือเค้าว่ามันใช้คู่กับ DI ไม่ได้

แต่เอ ผมว่าผมเห็น Framework ที่ดังด้าน DI ตัวนึงอย่าง Spring เนี่ย ใช้ Singleton เพียบเลยนะ ?

ความเห็นส่วนตัวนะครับ คือเรามักจะเห็น DI ในท่าที่ว่า

result = state.Function(operatorObject, parameter);

แต่ถ้าเราใช้วิธีเปลี่ยน type ของ operatorObject ก่อนเรียก Function ล่ะ ?

OperatorClass.Init<TestOperator>();
result = state.Function(parameter);
OperatorClass.CleanUp();

เนี่ย ก็เป็นการ inject แบบอ้อม ๆ แล้วหรือเปล่า ?

แน่นอนว่าวิธีนี้ไม่เหมาะกับการใช้ในโค๊ดจริง แต่เป็นประโยชน์มากตอนทำ Unit Test ครับ

หรือจะเขียนแบบ

result = state.Function(OperatorClass.Instance(), parameter);

ก็ได้ ก็ดูตามสถานการณ์ อย่างกรณีหลังเนี่ยคือบางที OperatorClass อื่น ๆ อาจจะไม่ต้องเป็น Singleton แต่เผอิญว่าไอ้คลาสนี้จำเป็น อะไรแบบนี้

อีกอย่างคือ เราใช้ singleton ในการ implement ตัว state ก็ได้เหมือนกันครับ ก็จะเหมือนกับท่าปรกติทั่วไปนี่แหละ

แล้วปัญหาจริง ๆ มันคืออะไรกันแน่ ?

ส่วนตัวผมพบว่า Singleton เนี่ย คือ … มันมักจะถูกใช้ผิดที่น่ะครับ ตามนิยามของ Singleton เนี่ย คือ การออกแบบคลาสที่มีได้แค่ instance เดียว ซึ่งผมว่ามันเหมาะกับคลาสที่ต้องมี instance เดียว ไม่ใช่เราต้องการให้คลาสนี้มีแค่ instance เดียว(ต่างกันนะครับ) เช่นพวกคลาสที่เก็บ state ของ Hardware ในระบบ ส่วนตัวผมมักจะใช้สร้างตัว Renderer เพราะมันมักจะเป็นโค๊ดที่ ถ้าสร้าง instance ขึ้นมามากกว่าหนึ่งตัว โปรแกรมจะพัง (อันนี้ต้องเปิดเอกสาร API ประกอบครับ)

ถ้าเราต้องการให้มีแค่ instance เดียว แต่ถ้ามีมากกว่านั้นก็ไม่มีใครตาย ก็ไม่ต้องสร้างมันเป็น Singleton หรอกครับ สร้าง instance ไปแปะไว้สักคลาสนึงก็ได้ ไม่ต้องไปลิมิตว่ามันต้องมีแค่ 1 instance นะ อะไรแบบนี้

ที่เห็นคนใช้เยอะถึงเยอะมาก ๆ โดยไม่จำเป็น … คือ Logging System ครับ เอาจริง ๆ จะสร้างสักตัว Instance ตราบใดที่มันไม่เขียนลงไฟล์หรือ Device เดียวกันมันก็ไม่เจ๊ง ว่าไหมครับ ??

สรุป

Singleton เนี่ย เป็นคลาสที่ถูกออกแบบมาให้มีได้แค่ 1 instance นิยามมันมีแค่นี้ เราจะเล่นท่าไหนก็ได้ตราบใดที่ไม่ได้ละเมิดกฎข้อนี้ครับ และที่สำคัญคือเราไม่ควรใช้ถ้าไม่จำเป็น เมื่อมันจำเป็นมันจะเป็นแพทเทิร์นที่ทรงพลังมาก เพราะมันเป็นการป้องกันไม่ให้ user ไปสร้างของที่ไม่ควรสร้าง (instance ที่สองนั่นเอง)

ทั้งนี้ Dependency Injection นั้น จะใช้คู่กับ Singleton ก็ได้ ตรงนี้ขึ้นอยู่กับการออกแบบ Class Interface และ Function Interface ครับ และถึงแม้ว่าเรายังไปเรียก Instance()->DoSomething() ตรง ๆ (ซึ่งตามหลักแล้ว มันก็ไปละเมิด DI อยู่ดี) เรายังพอ Inject Instance ก่อนที่ฟังก์ชันนั้นจะถูกเรียกได้เหมือนกัน สิ่งที่สำคัญคือเราต้องรู้ว่ากำลังทำอะไรอยู่ครับ

มาใช้ Code Beautifier กันเถอะ

วันนี้เห็นทีมข้าง ๆ เขาคุยกันเรื่องนี้ ก็เลยเอามาแชร์กันซะหน่อย

Code Beautifier หรือ Prettier หรือ Formatter เป็นเครื่องมือที่ใช้ในการจัดฟอร์แมทของโค๊ดให้อยู่ในรูปแบบที่สวยงาม … ก็ตามชื่อนั่นล่ะครับ ตัวโปรแกรมเองก็มีตั้งแต่แค่ regex บ้าน ๆ ธรรมดา ยันใช้คอมไพล์เลอร์ช่วยในการแบ่งส่วนประกอบในโค๊ด ตรงนี้ใครชอบอะไรชอบตัวไหนก็เอาตามที่ชอบเลยครับ

ส่วนตัวผมใช้ Clang-Format กับ Code Maid ครับ

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

เอาง่าย ๆ คือใช้ให้บ่อยที่สุดเท่าที่ทำได้นั่นเอง

ฟังดูเป็นคำแนะนำที่บ้าระห่ำมากใช่ไหมครับ ความจริงคือ เรื่องของ code format เนี่ยมันเป็นเรื่องเฉพาะคน โปรแกรมเมอร์แต่ละคนก็จะนิยมใช้ไม่เหมือนกัน บางคนชอบแท๊บ บางคนชอบอินเดนท์ละ 8 สเปซ บางคนชอบเอา brace ไว้บรรทัดเดียวกับ if

บางคนเขียนแบบนี้ก็มี

double
CalculateArea(double side)
   {
   return side * side;
   }

เคยเห็นไหมครับ ??

ดังนั้นวิธีที่ดีที่สุด เพื่อไม่ให้สมาชิกในทีมต่อยกันในเรื่องฟอร์แมทของโค๊ด ก็คือใช้ทูลจัดการมันซะเลย ถ้าคุณไม่ชอบก็ไปเปิด issue ให้คนทำทูลแก้ซะ (ซึ่งเขาไม่ทำหรอก เชื่อสิ) … ข้อดีอีกอย่างของการใช้ทูลในการจัดฟอร์แมทก็คือ ถ้าต่างคนต่างมีฟอร์แมทที่ตัวเองชอบ เราสามารถใช้ทูลในการแก้ให้โค๊ดอยู่ในฟอร์แมทที่เราชอบตอนที่เราเปิดโค๊ด แก้โค๊ด จากนั้นก็กดเซฟ โดยให้ทูลทำงานอีกครั้งตอนเซฟ กลับไปเป็นฟอร์แมทกลางที่ทุกคนตกลงกันซะ (แล้วรันอีกทีตอน commit code)

ทีนี้ก็ ข้อควรระวัง …

ข้อแรกเลยคือ เลือก tool ให้ถูกต้อง ใช้ตัวที่เป็นที่นิยมหน่อย ที่สำคัญคือ tool ที่ใช้ต้องไม่ทำให้โค๊ดพัง และไม่เปลี่ยนการกทำงานของโค๊ด ขยายความข้างหลังนิดนึง บางภาษาจะค่อนข้าง sensitive กับ whitespace มาก (เช่น Python) ในบางภาษาเครื่องหมายบางอย่างจะต้องอยู่ถูกที่ ไม่งั้นทำงานผิด (เช่น javascript) ถ้าใช้ tool ที่ไม่รู้เรื่องพวกนี้ ก็อาจจะทำให้โค๊ดพังได้ครับ

ข้อที่สองคือ ถ้าโค๊ดเราไม่เคยใช้ tool นี้มาก่อน และต้องการจะนำมาใช้แบบจริง ๆ จัง ๆ ต้องคุยกับคนในทีมให้รู้เรื่องก่อน ถ้ามาถึงเรา commit ตู้มเดียวเปลี่ยนโค๊ดทั้งโปรเจคนี่ แตกตื่นแน่นอนครับ ดังนั้นไปตกลงกันให้เรียบร้อยก่อน

ข้อที่สาม คือข้อที่เจอมาวันนี้ครับ นั่นคือ ถ้าเรายังไม่ตกลงกันว่าจะใช้ tool นี้จริงจัง แต่อยากจะนำมาใช้ ให้รันเฉพาะบนฟังก์ชันที่เราเขียนเท่านั้น อย่าไปรันบนโค๊ดเดิมที่เราไม่ได้แก้ครับ เพราะมันจะกลายเป็นการเปลี่ยนทั้งไฟล์ คนที่จะมาอ่านโค๊ดต่อจะไม่รู้ว่าคุณแก้โค๊ดตรงไหนน่ะครับ

… อ่านจบแล้ว ก็ ทดลองใช้ code formatter กันดูนะครับ ลองกับโค๊ดตัวเองก่อนนะครับ 🙂