อนึ่งว่าด้วย Dependency Injection

Kittiphat Srilomsak
Muze Innovation
Published in
2 min readAug 11, 2023
the plug-and-play — type of injection
Photo by Michel Didier Joomun on Unsplash

กาลครั้งหนึ่งนานมาแล้ว หัวหน้าผมเคยถามคำถาม Classic ว่า Interface กับ Abstract class ต่างกันยังไง ด้วยความวัยรุ่นคึกคนองผมจึงตอบไปว่า อันนึง provide implementation อีกอันไม่ provide ครับ พี่เขาจึงถามต่อว่าแล้วเมื่อไหร่ควรใช้อะไร เราก็ตอบต่อไปด้วยความกระตือรือล้นว่า ถ้าความสามารถของ Class คล้ายๆกันเราใช้ Abstract Class ครับ ผ่านมานับทศวรรษ ประสบการณ์สั่งสอนให้เราเบิกเนตรและเห็นว่า ความแตกต่างของทั้ง 2 options นี้ช่างแยบยลนัก

วันนี้จะเป็นการเหลา Dependency Injection ในมุมของกระผมเท่านั้นนะขอรับ ไม่อ้างอิงหนังสือใดๆ แน่นอนว่า Dependency Injection ข้อดีนั้นมีมากมายไม่ว่าจะเป็น

  • Decreased coupling between classes
  • Configurable Implementations
  • Testability

ซึ่งต้องบอกว่าพวกนี้มีคนเขียนไปหมดแล้วหล่ะ ไม่เหลาหรอก ไม่หนุก เรามาเหลาจากความเข้าใจเราดีกว่า

Abstract Class

เรามาเริ่มกันที่ Abstract Class ครั้งแรกที่เรารู้จัก Abstract Class เราย่อมเจอกันในภาษาระดับคุณพ่อของ OOP เช่น Java ตัวอย่างที่เราได้รับการเสี้ยมสอนมาก็เช่น Animals ที่จะมีน้องหมา และน้องแมวมา extends method walk ไป implement หรือไม่ก็ Vehicle แล้วก็ extends ไปเป็น Car, Truck อะไรเทือกนั้น มันเป็นการสอนในเชิงอุปมาอุปมัยที่เห็นภาพดี แต่นึกไม่ค่อยออกว่าเอาไปทำอะไรจนเห็นตัวอย่างที่แท้จริง ครั้งแรกที่กระผมเห็นพลังของ Abstract Class นั้นคือตอน เขียน PHP ครับ เจอ Code CRUDController น่าจะของ CodeIgniter (ใช่ครับเก่ามา คนเขียนแก่มาก) ที่มี Routing Function และมี Crud Operation ในตัวพร้อมให้คุณ​ extends/override/implement และนำไปใช้งาน พร้อมที่ทำงานเหมือนกันต่างกันที่ implementation บางส่วน วันนั้นเราจึงเห็นว่า โอ้ นี่มันคือเสน่ห์ของ Abstract Class สินะ ตัวอย่างนี้มันชัดเจนยิ่งกว่าเอา extends หมาจากสัตว์เป็นไหนๆ ในมุมของการใช้งานมันคือเราอยากจะเปิดช่องให้คนอื่นเอาไปทำต่อได้โดยไม่ต้องกังวลกับสิ่งใดบ้าง ถ้าเรามองมุมกลับอันนี้มันคือการ กลับข้างของ Dependency injection เหมือนกัน — วันนั้นเราจึงเริ่มมีหลักคิดแล้วว่า ถ้าเรายากตีกรอบด้วย implmenetation เราใช้ Abstract Class เมื่อเริ่มตีกรอบแล้วสิ่งที่จะตามมาคือความรับผิดชอบของผู้ตีกรอบครับ คุณต้องคิดละว่า คุณจะเปิดให้เขาทำอะไรให้คุณขนาดไหน

สำหรับ Developer ข้อคิสที่น่าสนใจคือ

  • ก่อนจะเขียน Abstract Class คุณต้องเข้าใจบริบทของโจทย์ที่คุณต้องการจะแก้ไขปัญหาก่อน
  • แล้วเริ่มแยกส่วนของปัญหาออกมาว่ามีอะไรที่มันซ้ำ อะไรที่เป็น pattern เขียนซ้ำแล้วซ้ำอีก และ อะไรที่มัน customize ได้
  • ถ้าสิ่งที่ต้อง Customize มันเป็น Jigsaw ที่อยู่ใน Engine ใหญ่ๆเนี่ย Abstract Class อาจจะเหมาะ

Interface

Interface หล่ะ, แน่นอนเราไม่พูดถึง User Interface เราพูดถึง Interface ใน Java หรือ Kotlin, Protocol ใน Swift. ไอ่นี่มันใช้ไง ทำไมมันมาเปลือยๆลอยๆ ถ้ามาจาก Java สมัยก่อนจะเจอ pattern ที่ argument ของ function รับ interface เปลือยๆมาเลย อันนี้ไอ่เราก็ไม่ได้ใส่ใจเขาบอกให้ implement ก็ implement ไป จนถึงวันที่เราต้องออกแบบระบบเราถึงเข้าใจว่า อ่ออออออออออออออออ มันเอาไว้ทำอย่างนี้…

interface จริงๆแล้วถ้าแปลเป็นไทย อยากจะเรียกว่า “สัญญา” มันคือ สัญญากับฉันสิ ว่านายจะให้สิ่งเหล่านี้กับเรา เดี๋ยวอีตอนเราเรียกนาย เราจะเรียก function นี้และ function โน้น (หรือในบางภาษา) เรียกตัวแปรนี้ ตัวแปรโน้น — มันคือการออกแบบและแบ่งหน้าที่แบบชัดเจนมากๆ — ว่าฝั่งที่นำ interface ไปใช้ก็แค่จะเชื่อว่า นายจะทำตามสัญญา ส่วนฝั่งให้สัญญา (ผู้ที่ implement interface) ก็จะแค่ต้อง deliver ค่าหรือ function ตามสัญญา และด้วยเหตุผลนี้แหละ ทำให้ ไอ่คุณ interface นี้มันทำให้การเทสแบบแยกส่วนเกิดขึ้นได้ ซึ่งมันแปลว่า เราไม่สนใจว่านายจะ bug กี่ 100 ตัวตราบใดที่ test case ฝั่งเราผ่าน มันยากมากที่เราจะผิด (แต่ก็ไม่ใช้ว่าเกิดขึ้นไม่ได้นะ)

แล้วมันเกี่ยวกับ Dependency ตรงไหนนะ?

Dependency Injection

จริงๆแล้ว ไม่ว่าจะเป็นการ Implement Abstract class, หรือ การ provide interface ให้คนอื่นมา Impement ตามที่เราต้องการ มันคือการ ออกแบบโครงสร้างของ Source Code ของเราให้ Code เราสั้นกระชับและเปลี่ยนแปลงง่าย

มันคือการออกแบบว่า เราอยากจะ depend บนส่วนนี้ เราอยากจะคุยกับนายอย่างไร เราต้องการอะไรจากนาย

แล้ว Dependency Injection มีข้อเสียไหม ก็มีอยู่ตรงที่มันทำให้ Code คุณเยอะขึ้น และก่อนจะ init class ใดๆบางทีเราต้องหาทางส่ง dependencies เข้ามา ซึ่งปกติถ้าใช้ nest.js เราก็จะได้ Lib จัดการ nest.js มาซึ่งต้องบอกว่าของ nest.js ดีมากและ advance มากบอกได้กระทั่งว่า ให้ สร้าง instance ใหม่ตอนไหน เหมาะกับ scope ใด

แต่ถ้าเราใช้ TypeScript ทำ project light weight มากเช่น Koa บน Lambda แล้วไม่อยากเอา NestJS เข้ามา จะทำ implementation provider ยังไง …​ก็ต้องเขียนเองไปเลยดิครับ

อันนี้วิธีใช้ไม่ได้มีอะไรมาก เช่นปกติต้องสร้าง controller

const contentEncryptor: IContentEncryptor = new KMSContentEncryptor()
const mediaStorage: IMediaStorage = new S3Storage()
const encryptableStorage: IMediaStorage = new EncryptedStorage(contentEncryptor, mediaStorage)

const secCtrl = new SecretUploadController(encryptableStorage)
const mediaCtrl = new MediaUploadController(mediaStorage)

อันนี้คือ Code ปกติ (แต่นึกภาพว่า การ setup แบบนี้ถูกกระจายอยู่ในหลายๆ Lambda แต่ละ Lambda ใช้ service ไม่เท่ากัน) จะมี Code ซ้ำกระจัดกระจายอยู่เยอะเลย

ถ้าใช้ utils ข้างบนเราจะออกท่าประมาณนี้ได้

## Shared file
interface Services {
mediaStorage: IMediaStorage
encryptableStorage: IMediaStorage
}

const mod = impl<Services>()
const contentEncryptor: IContentEncryptor = new KMSContentEncryptor()
mod.using('mediaStorage', () => new S3Storage())
mod.using('encryptableStorage', (sv) => new EncryptableStorage(contentEncryptor, sv.mediaStorage))

## Now when using it.

const secCtrl = new SecretUploadController(mod.encryptableStorage)
const mediaCtrl = new MediaUploadController(mod.mediaStorage)

ตรง Service ที่ share ตรงกลางอันนี้เราเขียนไว้ให้มันเป็น Lazy initialization แปลว่าถ้าไม่มีคนมันก็จะไม่ถูกสร้าง เพราะฉะนั้นจะไม่มี implmentation ไหนต้อง init ถ้าไม่ถูกใช้

หวังว่าจักเป็นประโยชน์ไม่มากก็น้อย

--

--