คำว่า 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 ก็เริ่มจากเพิ่ม Classpath เข้าไปใน build.gradle ของโปรเจคก่อน

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

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

// build.gradle (Project)
/* ... */
buildscript {
    /* ... */
    dependencies {
        /* ... */
        classpath 'me.leolin:android-aspectj-plugin:1.0.7'
    }
}

อยากให้ Module ไหนใช้ AspectJ ก็เพิ่ม Plugin เข้าไปใน build.gradle ได้เลย

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

แล้วสั่ง Build Gradle ทีนึงเป็นอันพร้อมใช้งาน

วิธีใช้งาน AspectJ

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

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

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

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

@Aspect 
class AspectLog { 
    /* ... */
}

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

ใส่ Aspect Code

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

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

@Aspect 
class AspectLog { 
	@Before("execution(* com.akexorcist.aspectjbasic.MainActivity.*(..))") 
    fun callLog(joinPoint: JoinPoint) { 
    	Log.e("Check", "Yeah")
    } 
}

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

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

class MainActivity: AppCompatActivity() {
	override fun onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

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

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

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

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

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

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

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

object AwesomeService {
    fun setUserData(data: UserData) { /* ... */ }
    
    fun setUsername(name: String) { /* ... */ }
    
    fun getUsername(): String { /* ... */ }
    
    fun getUsername(id: String): String { /* ... */ }

    fun getAddress(id: String, position: Int): String { /* ... */ }
    
    fun getPhoneNumber(id: String): String { /* ... */ }
}

อยากจะแทรก 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)")

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

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

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

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

class MainActivity: AppCompatActivity() { 
    /* ... */
    fun onUserInfoREsult(response: String) {
        /* ... */
    }
    
    fun onCustomerChangeResult(response: String) {
        /* ... */
    }
}

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

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

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

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

สรุป

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

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