08 February 2016

เมื่อเจ้าของบล็อกได้รู้จักกับ AOP และได้ลองใช้ AspectJ

Updated on


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

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

เกริ่นเรื่อง

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

        ในระหว่างนั่งปรับโค๊ดช่วงนึงก็พบว่า "อยากจะเพิ่มคำสั่งชุดนึงเข้าไป โดยให้คำสั่งนั้นถูกเรียกใช้งานก่อนที่จะเริ่มคำสั่งเดิมใน Method นั้นๆ" ถ้ายังนึกไม่ออกก็ดูภาพข้างล่างนี้ละกันนะ


        ซึ่งวิธีที่ง่ายที่สุดและชอบทำกันก็คงจะเป็นการก๊อปคำสั่งที่ว่าแล้วแปะให้ครบทุก Method ที่ต้องการ (ง่ายดี) แต่ทว่ามันไม่ใช่แค่ Class เดียว นี่สิ... (คิดแล้วก็เกือบๆ 50 ที่)

        คำถามก็เกิดขึ้นมาทันทีว่า ทำอย่างไรให้มันง่ายและดีกว่านี้ เพราะจะไปนั่งก๊อปแปะให้ครบก็ดูไม่ค่อยน่ารักซักเท่าไร จนกระทั่งพี่ที่ออฟฟิศบอกว่า "ลองไปดูเรื่อง Cross-cutting Concerns สิ"

        "พึ่งจะเคยได้ยินคำนี้เป็นครั้งแรกก็จากพี่นี่แหละครับ..."

         ก็เลยเป็นจุดเริ่มต้นที่ทำให้เจ้าของบล็อกก็เลยนั่งหาข้อมูลและทำความเข้าใจเกี่ยวกับเรื่องนี้นี่แหละครับ

Cross-cutting Concerns?

        ถ้าจะให้อธิบายตามนิยามของมันก็เกรงว่าจะเข้าใจ (โคตร) ยาก ซึ่งจริงๆแล้ว มันก็คือไอ้ปัญหาที่เจ้าของบล็อกเล่าเกริ่นไว้นั่นแหละครับ

        • มีโค๊ดที่ต้องการแทรกเข้าไปใน Method ของโค๊ดชุดเก่า
        • โค๊ดดังกล่าวไม่ได้ส่งผลกับโค๊ดชุดเก่าโดยตรง
        • ต้องการแทรกในโค๊ดเก่าหลายๆ Method แต่ก็ไม่ได้แทรกเข้าไปในทุกๆ Method
        • ไม่อยากให้โค๊ดที่ต้องการแทรกเข้าไปส่งผลกับใน Method นั้นๆจนทำให้มันวุ่นวายยุ่งเหยิงและเสียเวลาเพื่อแก้ไขมันโดยใช่เหตุ


        ความรู้สึกที่ว่ามันก็ประมาณภาพข้างบนนี้แหละ คำสั่งที่อยากจะเพิ่มดันต้องเพิ่มในหลายๆคลาส และอยากจะเพิ่มแค่ในบาง Method เท่านั้นเอง น่าจะเข้าใจกันเนอะ?

        ซึ่งปัญหานี้แหละจึงทำให้เกิด AOP ขึ้นมาเพื่อจัดการกับเรื่อง Cross-cutting Concerns

AOP คืออะไร?

        AOP มันย่อมาจากคำว่า Aspect-oriented Programming ซึ่งถ้าแปลเป็นไทยก็จะเป็น "การโปรแกรมเชิงลักษณะ" ซึ่งไม่ต้องไปเข้าใจความหมายภาษาไทยหรอก เพราะคำมันสื่อความหมายเข้าใจยากไปหน่อย...

        ซึ่ง AOP นั้นเป็นรูปแบบในการแก้ปัญหา Corss-cutting Concerns ซึ่งมี Concept ง่ายๆก็คือ "มีไว้เพื่อแทรกชุดคำสั่งไว้ในโค๊ดชุดเดิมได้ง่ายๆ" ถ้าอยากได้คำอธิบายแบบวิชาการหน่อยก็ลองเอาไปหาข้อมูลดูครับ (เพราะเจ้าของบล็อกนั่งอ่านแล้วก็ไม่ค่อยเข้าใจ คำศัพท์เฉพาะเยอะเหลือเกิน)

        ลองนึกภาพครับว่าผู้ที่หลงเข้ามาอ่านสามารถแทรกโค๊ดชุดหนึ่งเข้าไปในของเก่า โดยที่ไม่ต้องแก้ไขโค๊ดชุดเก่าเลย เออ ฟังดูเข้าท่าดีเนอะ

คำศัพท์เบื้องต้นสำหรับ AOP ที่ควรรู้

        โค๊ดชุดใหม่ที่จะแทรกเข้าไปในโค๊ดชุดเก่า เจ้าของบล็อกขอเรียกมันว่า Aspect Code นะครับ

        Joint Point จุดที่สามารถแทรก Aspect Code เข้าไปได้

        Point Cut คือการระบุว่า Class ไหน และ Method ไหน จะมีการเพิ่ม Aspect Code เข้าไป

        Advice ตำแหน่งของ Joint Point ที่จะเพิ่ม Aspect Code เข้าไป สามารถกำหนดได้ เช่น Before, After หรือ Around เป็นต้น

Advice Type : จะเพิ่ม Aspect Code ตรงไหนได้บ้าง

        ผู้ที่หลงเข้ามาอ่านสามารถแทรก Aspect Code ลงใน Point Cut ที่ต้องการได้ เพื่อให้เข้าใจง่ายขึ้น ลองมาดูกันก่อนว่าเวลา Method ตัวหนึ่งถูกเรียกใช้งาน มีอะไรเกิดขึ้นบ้าง


        เมื่อ Method ถูกเรียก (Call) จากที่ใดก็ตามในโปรแกรม Method ก็จะทำคำสั่ง (Execute) ของมันเอง เมื่อทำงานเสร็จแล้วก็จะส่งข้อมูลกลับไป (Return) ที่ตำแหน่งที่โปรแกรมเรียกใช้งานแล้วก็จบการทำงาน (End) ของ Method แต่ถ้าระหว่างที่ Method ทำคำสั่งของมันอยู่แล้วเกิด Error ขึ้นมา (Exception) ก็จะโยน Exception ออกมาแทนแล้วก็จบการทำงาน (End)

        เข้าใจไม่ยากเนอะ?

        ทีนี้ถ้าลองใส่ Joint Point ลงไปในภาพเป็นจุดวงกลมสีแดง ก็จะได้แบบนี้


        เวลาอยากจะให้ Aspect Code ทำงานที่ Joint Point ไหน ก็จะกำหนดจาก Advice นั่นเอง ซึ่ง Advice จะมีทั้งหมดดังนี้

Before

        ก่อนที่ Method จะเริ่มทำงาน

After

        หลังจากที่ Method ทำคำสั่งจบ (ไม่ว่าจะเป็นจบด้วยดีหรือว่าเกิด Error กลางคัน)

Around 

        แทนที่คำสั่งของเดิม

AfterReturning

        หลังจาก Method ทำงานเสร็จแล้วและกำลังส่งข้อมูลกลับไป

AfterThrowing

        หลังจาก Method ทำงานแล้วเกิด Error กลางคันทำให้ต้องส่ง Exception กลับไป


AspectJ : แอนดรอยด์ก็ใช้ AOP ได้เหมือนกันนะ

        การจะใช้แนวคิด AOP กับภาษา Java ก็คงไม่พ้น AspectJ ซึ่งบน Eclipse จะมีให้ในตัวแล้ว สามารถใช้งานได้เลย แต่ทว่าบน Android Studio ที่ใช้ IntelliJ IDEA มันดันไม่มีนี่สิ ฮาๆ แต่ไม่ต้องห่วงเพราะมีนักพัฒนาทำ AspectJ Plugin ให้เรียกใช้งานผ่าน Gradle ได้เลย

        ส่วนวิธีการเรียกใช้งาน AspectJ ก็แค่เพียง...

        เปิด build.gradle ของ Root Project แล้วใน dependencies ของ buildscript ให้ใส่ลงไปว่า

classpath 'me.leolin:android-aspectj-plugin:1.0.7'

        ถ้านึกไม่ออกว่าใส่ตรงไหน ก็ประมาณนี้ครับ

buildscript {
    repositories {
        ...
    }
    dependencies {
        ...
        classpath 'me.leolin:android-aspectj-plugin:1.0.7'
    }
}

allprojects {
    repositories {
        ...
    }
}

task clean(type: Delete) {
    ...
}


        เลขเวอร์ชันคอยเช็คและอัพเดทเอาเองนะ

        Module ไหนที่อยากจะใช้ AspectJ ก็ให้เปิด build.gradle ของตัวนั้นมา แล้วระบุลงไปว่า

apply plugin: 'me.leolin.gradle-android-aspectj'

        นึกไม่ออกก็ประมาณนี้

apply plugin: 'com.android.application'
apply plugin: 'me.leolin.gradle-android-aspectj'

android {
    ...

    defaultConfig {
        ...
    }
    buildTypes {
        release {
            ...
        }
    }
}

dependencies {
    ...
}


        สามารถใช้กับ Library Module ได้ด้วยนะ
     
        เสร็จแล้วก็สั่ง Build Gradle ทีนึงเป็นอันเสร็จ

วิธีใช้งาน AspectJ

        เจ้าของบล็อกอธิบายแค่ในส่วนที่เจ้าของบล็อกใช้งานนะครับ ส่วนอื่นๆนอกเหนือจากนั้นขอข้ามๆไปนะ

        ถ้าจะยกตัวอย่างที่ทำให้เห็นภาพได้ง่ายที่สุดก็คงจะเป็น "อยากจะให้แสดง Log ใน Method ที่ต้องการ" ซึ่งการไปพิมพ์คำสั่ง Log ไว้ใน Method ที่ต้องการมันก็คงจะดูง่ายกว่าเนอะ แต่มาลองใช้ AOP แทนดีกว่า

สร้าง Class สำหรับ Aspect

        สมมติชื่อ Class เป็น AspectLog ละกันนะ

package com.akexorcist.aspectjbasic;

import org.aspectj.lang.annotation.Aspect;

@Aspect
public class AspectLog {
    
}

        ที่สำคัญก็คือใส่ Annotation ไว้บน Class นั้นๆด้วยว่า @Aspect เพื่อระบุว่า Class ตัวนี้เป็น Aspect Code นั่นเอง

ใส่ Aspect Code

       Method ที่สร้างขึ้นในนี้ก็คือ Aspect Code ทั้งหมด โดยสามารถกำหนด Advice Type และ Point Cut ได้โดยใช้ Annotation อีกนั่นแหละ

        ยกตัวอย่างเช่น

package com.akexorcist.aspectjbasic;

import android.util.Log;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class AspectLog {

    @Before("execution(* com.akexorcist.aspectjbasic.MainActivity.*(..))")
    public void callLog(JoinPoint joinPoint) {
        Log.e("Check", "Yeah");
    }

}

        เห็น Annotation ข้างบน Method callLog มั้ย? นั่นล่ะที่จะกำหนด Point Cut ที่จะแทรก Aspect Code เข้าไป ในกรณีนี้ก็คือ ทุก Method ที่ประกาศไว้ใน MainActivity ของโปรเจคตัวนี้จะถูกคำสั่ง callLog แทรกเข้าไปก่อนที่ Method จะเริ่มทำงาน

        ส่วน MainActivity เจ้าของบล็อกไม่ได้เพิ่มอะไรเข้าไปนะ มีแค่ onCreate อยู่ตัวเดียวในนี้

package com.akexorcist.aspectjbasic;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

        พอลองทดสอบดูก็จะเห็นคำว่า Yeah โผล่ขึ้นมาใน LogCat


        ไงล่ะ!! อย่างกับมายากลเลยได้ใช่มั้ยล่ะ!!

        แต่ถ้า Log ไม่แสดงแบบตัวอย่างนี้ก็อาจจะเพราะว่าระบุ Point Cut ผิด ดังนั้นมาดูกันว่ามันกำหนดยังไง

การกำหนด Point Cut ของ AspectJ


        • Advice Type
        • Method Designator
        • Return Type
        • Package
        • Class
        • Method
     
        จะเห็นว่าบางอันเจ้าของบล็อกใส่เป็นเครื่องหมาย * ไว้ หมายความว่า "อะไรก็ได้" นั่นเอง ดังนั้น Point Cut ของตัวอย่างนี้ก็จะระบุไว้ว่า Return Type อะไรก็ได้ และ Method ชื่ออะไรก็ได้ และ Argument จะมีหรือไม่มีก็ได้ แต่เป็นคลาสที่ชื่อว่า MainActivity เท่านั้น

        ถึงจุดนี้ก็น่าจะงงกันบ้าง อาจจะยังไม่เห็นภาพซักเท่าไรนัก ดังนั้นขอยกตัวอย่างเพิ่มอีกหน่อยนะ

        สมมติว่าเจ้าของบล็อกมี Network Class ชื่อว่า AwesomeService ซึ่งมี Method ต่างๆที่เอาไว้ดึงข้อมูลจากฐานข้อมูล (สมมติๆ)

package com.akexorcist.aspectjbasic;

public class AwesomeService {

    public static void setUserData(UserData userData) {
        ...
    }

    public static void setUsername(String name) {
        ...
    }

    public static UserData getUserData(String id, String date, String time) {
        ...
    }

    public static String getUsername(String id) {
        ...
    }

    public static String getUsername() {
        ...
    }

    public static String getAddress(String id, int position) {
        ...
    }

    public static String getPhoneNumber(String id) {
        ...
    }

}


        อยากจะแทรก Aspect Code เข้าไปทุกๆ Method ที่มีอยู่ในนี้

@Before("execution(* com.akexorcist.aspectjbasic.AwesomeService.*(..))")

        เฉพาะ Method ที่ชื่อ getAddress

@Before("execution(* com.akexorcist.aspectjbasic.AwesomeService.getAddress(..))")

        เฉพาะ Method ที่ขึ้นต้นด้วยคำว่า get

@Before("execution(* com.akexorcist.aspectjbasic.AwesomeService.get*(..))")

        เฉพาะ Method ที่ลงท้ายด้วยคำว่า Username

@Before("execution(* com.akexorcist.aspectjbasic.AwesomeService.*Username(..))")

        ช่างมันละ ขอเป็น Class ไหนและ Method ไหนก็ได้ที่อยู่ใน com.akexorcist.aspectjbasic ก็พอ

@Before("execution(* com.akexorcist.aspectjbasic.*.*(..))")

        เฉพาะ Method ที่มี Argument เป็น String เท่านั้น

@Before("execution(* com.akexorcist.aspectjbasic.AwesomeService.*(String))")

        เฉพาะ Method ที่มี Argument ตัวแรกสุดเป็น Integer เท่านั้น ไม่สนใจว่าจะมี Argument ตัวอื่นด้วยหรือป่าว (จะมี Argument กี่ตัวก็ได้ ขอแค่ตัวแรกสุดเป็น Integer)

@Before("execution(* com.akexorcist.aspectjbasic.AwesomeService.*(int, ..))")

        เฉพาะ Method ที่ชื่อ getUsername ที่ไม่มี Argument

@Before("execution(* com.akexorcist.aspectjbasic.AwesomeService.getUsername())")

        เฉพาะ Method ที่มี Return Type เป็น UserData

@Before("execution(UserData com.akexorcist.aspectjbasic.AwesomeService.*(..))")

        เฉพาะ Method ที่มี Argument ตัวเดียวและเป็น String และ Return Type เป็น String

@Before("execution(String com.akexorcist.aspectjbasic.AwesomeService.*(String))")

        เฉพาะ Method ที่มี Argument เป็น String และเป็น Argument ที่ชื่อว่า name

@Before("execution(String com.akexorcist.aspectjbasic.AwesomeService.*(String)) && args(name)")

        น่าจะพอเข้าใจแล้วเนอะ?

        สำหรับ Advice Type ก็จะมีตามที่อธิบายไว้ทั้งหมดเลย

        • @Before
        • @After
        • @Around
        • @AfterReturning
        • @AfterThrowing

ตัวอย่างการใช้งาน

        สมมติว่าเจ้าของบล็อกอยากจะดัก Method ตัวหนึ่งที่มีข้อมูลจาก Web Service ส่งกลับมาให้ แล้วอยาก Logging เก็บไว้ในเครื่อง ก็สามารถดึง Argument จาก Method ที่ต้องการมา Logging ได้เลย

        ยกตัวอย่าง Method ที่ทำงานเมื่อมี Response ส่งกลับมาจาก Web Service

public class MainActivity extends AppCompatActivity {

    ...

    public void onUserInfoResult(String response) {
        ...
    }

    public void onCustomerChangeResult(String response) {
        ...
    }
}

        ถ้าอยากจะดัก String จาก Response มา Logging ก็จะทำแบบนี้

@Before("execution(* com.akexorcist.multidextest.MainActivity.on*Result(String)) &&args(response)")
public void aspectIt(JoinPoint joinPoint, String response) {
    ... เอา response ไป Logging ...
}

        กรณีนี้คือแทรก Aspect Code เข้าไปโดยที่โค๊ดนั้นๆไม่ส่งผลกับการทำงานของโค๊ดเก่า แต่ในความเป็นจริงมันสามารถดัก Argument แล้วแก้ไขก่อนที่จะโยนเข้า Method นั้นได้นะ หรือเข้าไปจัดการกับ Return ของ Method ก่อนที่จะส่งค่ากลับไปก็ได้เช่นกัน

        ซึ่งก็อยู่ที่ว่าจะเอาไปใช้งานกันยังไงนะครับ

สรุป

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

แหล่งอ้างอิงข้อมูล

        • Spring framework AOP
        • Weaving aspects in PHP with the help of Go! AOP library
        • AOP :: Aspect-Oriented Programming
        • AOP คืออะไร?
        • AOP คืออะไร
        • การเขียนโปรแกรมแบบ AOP ด้วย AspectJ
        • @AspectJ cheat sheet
        • AOP : ตอนที่ 1 AOP คืออะไรและใช้ทำอะไร
        • Aspect-oriented programming@Wikipedia