Devfavor

Back

การเขียน Action Pattern - เพื่อเพิ่มคุณภาพของโค้ดใน LaravelBlur image

❓ Action Pattern คืออะไร?#

ถ้าพูดถึง Code Pattern หรือ Design Pattern ในการเขียน Laravel แล้ว หลาย ๆ คนคงจะคุ้นเคยกับ Service Pattern หรือพวก Facade มากกว่า แต่เชื่อว่าหลายคนคงจะไม่คุ้นกับคำว่า Action Pattern สักเท่าไหร่ ซึ่งก็ไม่ใช่เรื่องแปลกอะไรเพราะใน document หลักของ Laravel แทบจะไม่พูดถึงคำนี้เลย แต่จริง ๆ แล้ว Action Pattern นั้นวนเวียนอยู่รอบตัวเราอยู่แล้ว เพียงแต่เราอาจจะแค่ยังไม่เคยทำความรู้จักกับมันจริง ๆ เท่านั้นเอง

Action Pattern คือ Design Pattern แบบหนึ่ง ที่มีแนวคิดให้ 1 Class มีหน้าที่เพียง 1 อย่างเท่านั้น (Single Responsibility) เพื่อลดความซับซ้อนของโค้ดให้สามารถบริหารจัดการได้ง่ายขึ้น โดย Class ที่เขียนให้รองรับ Action Pattern นั้นมักจะมีเพียง 1 method เท่านั้น เช่น handle() หรือ execute() เป็นต้น

✨ ประโยชน์ของ Action Pattern#

แม้ว่าโดย concept ของตัว Action Pattern นั้นจะบอกเพียงแค่ว่าให้ 1 Class มีหน้าที่เพียง 1 อย่างเท่านั้น แต่ในความเป็นจริงแล้ว เรามักจะใช้ประโยชน์จาก Action Pattern นี้เพื่อแยก business logic ออกมาจาก Laravel Lifecycle ปกติเสียมากกว่า เพื่อให้โค้ดในส่วนที่เป็น business logic นั้นสามารถปรับปรุงแก้ไขและดูแลรักษาได้โดยง่ายนั่นเอง

ซึ่งประโยชน์ของการแยก business logic ออกมานั้นสามารถสรุปออกมาได้เป็นหัวข้อคร่าว ๆ ดังนี้

Single Responsibility#

การเขียนโค้ดโดยยึดหลัก Single Responsibility Principle (SRP) ทำให้เราสามารถจัดระเบียบโค้ดได้ดีขึ้น ลดการผูกกันของโค้ดที่ไม่เกี่ยวข้องกัน เพื่อให้ developer สามารถแก้ไขหรือเพิ่มเติมได้ง่าย โดยที่เราจะสามารถมั่นใจได้ว่าสิ่งที่แก้ไขไปจะไม่ไปกระทบกับ business ส่วนอื่นที่ไม่เกี่ยวข้องกัน

Reusable#

ตัว Class ที่ถูกเขียนแบบ Action Pattern สามารถนำไปใช้ได้อย่างหลากหลาย ไม่ว่าจะเป็นใน Controller เองหรือจะใช้ใน Command, Queue หรือ Event ก็สามารถนำไปต่อยอดได้อย่างง่ายดาย

Test Friendly#

การแยก business logic ออกมาจาก Laravel Lifecycle ปกติทำให้เราสามารถเขียน test ได้แม่นยำขึ้น โดยสามารถเขียน test แยกการทำงานที่อาจเกิดขึ้นใน request เดียว ออกเป็นการ test งานย่อยแต่ละอย่างแทน ทำให้สามารถวิเคราะห์ปัญหาและแก้ไขได้อย่างตรงจุดและรวดเร็ว

Dev Friendly#

การเขียนโดยใช้ Action Pattern นอกจากจะดีในแง่ของระบบแล้วยังดีต่อตัวของ developer เองอีกด้วย เพราะไม่ว่าจะเป็น การ refactor code, การทำ code review หรือแม้แต่การ merge code ภายในทีม ตัว Action Pattern เองก็มีส่วนช่วยให้กิจกรรมเหล่านี้สามารถทำได้ง่าย ทำให้การบริหารจัดการกิจกรรมภายในทีมสามารถทำได้ง่ายยิ่งขึ้น

🚀 ลงมือทำจริง ลดความอ้วนให้กับ Controller แบบดั้งเดิม#

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

สมมุติว่าเรามีไฟล์ Controller ดังนี้

app\Http\Controllers\CheckoutController.php

<?php

namespace App\Http\Controllers;

use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use App\Mail\OrderConfirmedMail;

class CheckoutController extends Controller
{
    public function store(Request $request)
    {
        /* 1. Validate */
        $validated = $request->validate([
            'customer_id' => ['required', 'exists:customers,id'],
            'items'       => ['required', 'array', 'min:1'],
            'items.*.id'  => ['required', 'exists:products,id'],
            'items.*.qty' => ['required', 'integer', 'min:1'],
        ]);

        /* 2. Business logic + DB transaction */
        /*
         *
         * Multiple lines of Business logic
         *
         */
        $order = Order::insert($validated);

        /* 3. Side-effect : broadcast */
        /*
         *
         * Multiple lines of Side-effect
         *
         */
        Mail::to($order->customer->email)->send(new OrderConfirmedMail($order));

        /** 4. Response */
        return response()->json($order, 201);
    }
}
php

จะเห็นว่า method store ของ CheckoutController มีจำนวนบรรทัดที่เยอะมากเพราะในการ Checkout และสร้าง Order มักจะมี business logic ที่ซับซ้อน

หากเราลองใช้แนวคิดของ Action Pattern เพื่อแยก business logic ออกมาจะทำให้ Controller ของเรามีหน้าที่แค่ รับ Request (และ validate data) และ Response กลับเพียงเท่านั้น ซึ่งเราสามารถแยก business logic ออกมาได้โดยการเพิ่มไฟล์ Action Class ดังนี้

app\Actions\CreateOrder.php

<?php

namespace App\Actions;

use App\Models\Order;

class CreateOrder
{
    public function handle($validated)
    {
        /* 2. Business logic + DB transaction */
        /*
         *
         * Multiple lines of Business logic
         *
         */
        $order = Order::insert($validated);

        return $order;
    }
}
php

app\Actions\SendOrderConfirmedMail.php

<?php

namespace App\Actions;

use Illuminate\Support\Facades\Mail;
use App\Mail\OrderConfirmedMail;

class SendOrderConfirmedMail
{
    public function handle($order)
    {
        /* 3. Side-effect : broadcast */
        /*
         *
         * Multiple lines of Side-effect
         *
         */
        Mail::to($order->customer->email)->send(new OrderConfirmedMail($order));
    }
}
php

และใน Controller จะเหลือเพียงแค่

app\Http\Controllers\CheckoutController.php

<?php

namespace App\Http\Controllers;

use App\Actions\CreateOrder;
use Illuminate\Http\Request;

class CheckoutController extends Controller
{
    public function store(Request $request)
    {
        /* 1. Validate */
        $validated = $request->validate([
            'customer_id' => ['required', 'exists:customers,id'],
            'items'       => ['required', 'array', 'min:1'],
            'items.*.id'  => ['required', 'exists:products,id'],
            'items.*.qty' => ['required', 'integer', 'min:1'],
        ]);

        /* 2. Business logic + DB transaction */
        $order = app(CreateOrder::class)->handle($request->validated());

        /* 3. Side-effect : broadcast */
        app(CreateOrder::class)->handle($order);

        /** 4. Response */
        return response()->json($order, 201);
    }
}
php

สังเกตว่าเราได้ย้ายในส่วนของ business logic ทั้งหมด (การสร้าง Order และการส่งอีเมล) ไปยัง Action แทน และให้ตัว Controller ทำหน้าที่เพียงแค่รับและส่ง Request และ Response เท่านั้น ทำให้การแก้ไข business logic ของการสร้าง Order และส่งอีเมลสามารถนำไป reuse ต่อได้ง่ายและยังสามารถปรับปรุงโค้ดต่อได้ง่ายอีกด้วย 🌟

🤔 Action Pattern VS Service Pattern เลือกใช้อย่างไร#

ใน document ของ Laravel เองจะมีการพูดถึง Service Pattern อยู่แล้วและในการบริหารจัดการโค้ดก็สามารถใช้ Service Pattern ได้อยู่แล้วเช่นกัน แล้วทีนี้เราจะเลือกอย่างไรว่าเมื่อไหร่ควรใช้ Service Pattern และเมื่อไหร่ควรใช้ Action Pattern มาลองพิจารณาจากประเด็นเหล่านี้กัน

แนวคิดในการ Design#

  • Action Pattern - จะมอง Class ในแง่ของ Behavioral หรือ พฤติกรรม มากกว่า ทำให้ชื่อ Class มักเป็นกิริยา เช่น SendMailAction หรือ CreateOrderAction เป็นต้น
  • Service Pattern - จะมอง Class ในแง่ของ Structural หรือ โครงสร้าง มากกว่า ทำให้ชื่อ Class มักเป็นคำนามแต่แยกการกระทำด้วย method ข้างใน เช่น Order::create() เป็นต้น

ขนาดของทีม#

  • Action Pattern - จะมีจำนวนไฟล์เยอะขึ้น แยกการทำงานออกเป็นไฟล์ที่ชัดเจน ลดการ conflict ของ developer ในการแก้ไข
  • Service Pattern - จะมีจำนวนไฟล์น้อยกว่า แต่ใน 1 ไฟล์จะมีความสัมพันธ์กันของ method

คุณสมบัติของ OOP#

  • Action Pattern - จะมี method เดียวในการเขียนฟังก์ชันการทำงาน
  • Service Pattern - จะสามารถดึงคุณสมบัติของการเขียนโปรแกรมแบบ OOP มาใช้ได้อย่างมีประสิทธิภาพกว่า

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

🐥 ติดปีกให้ Action Pattern ด้วย lorisleiva/laravel-actions#

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

https://www.laravelactions.com/

โดยที่ plugin ตัวนี้จะช่วยให้ Class ที่เราเขียนขึ้นสามารถนำไป reuse ใช้งานได้ง่ายยิ่งขึ้น ไม่ว่าจะเป็น Controller, Job หรือ Listener ก็ตาม

ตัวอย่างโค้ดคร่าว ๆ จากในเว็บไซต์มีดังนี้

app\Actions\PublishANewArticle

class PublishANewArticle
{
    use AsAction;

    public function handle(User $author, string $title, string $body): Article
    {
        return $author->articles()->create([
            'title' => $title,
            'body' => $body,
        ]);
    }

    public function asController(Request $request): ArticleResource
    {
        $article = $this->handle(
            $request->user(),
            $request->get('title'),
            $request->get('body'),
        );

        return new ArticleResource($article);
    }

    public function asListener(NewProductReleased $event): void
    {
        $this->handle(
            $event->product->manager,
            $event->product->name . ' Released!',
            $event->product->description,
        );
    }
}
php

จะเห็นว่าหากเราวางแนวคิดของ Action Pattern ไว้ตั้งแต่แรก เราสามารถเขียน 1 Class ที่ทำงานเพียงอย่างเดียวแล้วนำไป reuse ใช้ได้โดยง่าย โดยที่ไม่ต้องมีไฟล์ Controller หรือ Listener มา wrap การทำงานเพิ่มไปอีก ทำให้จำนวนไฟล์และโค้ดที่เราเขียนสะอาดมากยิ่งขึ้น (แต่ไม่แนะนำกับระบบใหญ่ ๆ ที่ควรจะวางโครงสร้างให้ดี ไม่เช่นนั้นอาจจะเจอปัญหาในการ refactor ทีหลังได้)

สำหรับบทความนี้แอดมินขอแนะนำแนวคิดและไอเดียของการใช้ Action Pattern ใน Laravel ไว้เพียงเท่านี้ก่อน หากมีโอกาสจะมาแนะนำวิธีการใช้งาน plugin เพิ่มเติมในโอกาสต่อไป 🛠️

ขอให้สนุกกับการเรียนรู้และการเขียนโค้ด 🥰

Code, coffee, and calm — the holy trinity of a good day. ☕

การเขียน Action Pattern - เพื่อเพิ่มคุณภาพของโค้ดใน Laravel
Author Coffee Stack
Published at July 22, 2025