ล่าสุด ผมได้มีโอกาสไปพูดในงาน JavaScript Bangkok 2.0.0 ในหัวข้อ Redis cache decorator for Hexagonal architecture in NestJS แต่ในงานมีเวลาจำกัด และจอมีปัญหา เลยคิดว่าจะมาเขียนเล่าใน Blog อีกครั้ง เพื่อให้คนที่สนใจสามารถอ่านเพิ่มเติมได้
ปัญหา
เราจะมาเล่าถึงปัญหากันก่อน ปัจจุบันงานที่ทำผมทำอยู๋ใช้ MongoDB Atlas และทาง Business อยากจะลด Cost ครับ ซึ่งราคาจะเป็นตามรูปด้านล่างนี้ครับ

แน่นอนปัญหานี้เราน่าจะใช้ Cache มาช่วย เพื่อลด Hit ของ Database ครับ ปัจจุบันก็คงจะเลือกใช้เป็น Redis ครับ

Project ที่ผมทำอยู่เป็น NestJS และเป็น Hexagonal Architecture ครับ ซึ่งการทำ Cache สามารถใช้ Cache Manager ของ NestJS ได้เลย แต่ผมก็ติดปัญหาอยู่ตรงเมื่อเราต้องการทำ Invalidate cache ทันทีเมื่อมีการ Update ข้อมูลใหม่ ๆ ใน Database ครับ ซึ่ง Cache Manager ของ NestJS ก็ยังไม่มีตรงส่วนนี้ เราสามารถทำได้เพียงแค่ใช้ Time To Live (TTL) เท่านั้น ซึ่งหมายความว่ายังไม่ตอบโจทย์การใช้งานของผม
Credit: NestJS Cache
Decorator of TypeScript
ก่อนจะไปถึงวิธีแก้ปัญหา ผมขอเล่าถึง Decorator ใน TypeScript ก่อน เพราะจะเป็นสิ่งที่ช่วยเราในการแก้ไขปัญหา ใน TypeScript เรามีสิ่งที่เรียกว่า Decorator อยู่ ถ้าเปรียบเทียบให้เห็นภาพ ภาษาอื่น และ Framework อื่น ๆ จะมีสิ่งที่คล้าย ๆ กันอยู่ครับ เรามาลองดูกันว่ามีตัวไหนบ้าง
- Annotation in Java ยกตัวอย่างที่หลาย ๆ คนน่าจะเคยเจอกันมาในภาษา Java และใช้ Framework Spring Boot น่าจะคุ้นเคยกันดีกับ Annotation ครับ
Credit: Spring Annotations Cheat Sheet
- Attribute in C# หลายคนที่เคยเขียน C# แล้วใช้ NUnit เขียน Test ก็น่าจะเคยเห็น Attribute ผ่านหน้าผ่านตากันมาบ้างครับ
Credit: NUnit Attribute
คราวนี้เราลองมาดูตัวอย่าง Decorator ของ NestJS กันครับ

Hexagonal Architecture
- คิดโดย Alistair Cockburn ในปี 2005
- อีกชื่อคือ “Ports and Adapters Architecture.”
ในบทความนี้คงไม่ได้เล่าลงลึกมากครับ หลัก ๆ แล้วตัว Hexagonal Architecture นั้นจะแบ่ง Layer เป็นชั้น ๆ โดยจะทำให้ส่วนที่ผูกติดกับ Framework รวมถึงภายนอกอย่าง Database หรือ API นั้นแยกออกจากส่วนที่เป็น Business Logic ครับ ซึ่งจะเพิ่มความยืดหยุ่นกับ Code ของเราสามารถเปลี่ยนแปลงไปใช้ Framework และ Database อื่นได้ง่ายขึ้น รวมถึงสามารถทำ Unit Test ของ Business Logic ได้ง่ายขึ้นมากครับ โดยภายนอกและภายใน Application จะคุยกันผ่าน Port ครับ โดย Port นั้นจะเป็นเพียงแค่ Interface หลังจากนั้นเราค่อยเอาตัวที่จะเชื่อมต่อมา Implement จริงเป็น Concrete class อีกทีนึงครับ
Credit: Hexagonal Architecture, there are always two sides to every story
Credit: DDD, Hexagonal, Onion, Clean, CQRS, … How I put it all together
ยกตัวอย่างตามรูปด้านล่างนี้ครับ

ลองมาดู Code ตัวอย่างกันครับ

- Controller เรียก Use Case

- Use Case เรียก Repository

- Repository เป็นแค่ Interface ที่ดึงข้อมูล

- Real implementation ของ Repository จะทำการดึงข้อมูลจาก Database พร้อมกับแปลงข้อมูลเป็น Domain ที่ใช้งานใน Application ของเราครับ จะเห็นว่าข้อมูลที่เราส่งกลับไปให้ Application นั้นจะไม่ได้เป็นข้อมูลที่มาจาก Database โดยตรง และไม่มี Type ของ Database หลุดกลับออกไปใน Application ครับ
แก้ปัญหา
หลังจากปูพื้นกันมาแล้ว เรากลับมาเข้าเรื่องปัญหาของเราดีกว่าครับ เราจะแก้ปัญหากันอย่างไรได้บ้าง
First Solution
- เลือกเองใน use-case ว่าจะใช้ Cache หรือไม่
- แต่ Code ใน use-case จะมีส่วนที่ไม่เกี่ยวข้องกับ Business Logic อยู่
- ต้องไปไล่แก้ Test cases ทั้งหมด
- Code ที่เปลี่ยนมหาศาล

- จะเห็นว่า Code ที่ใส่กรอบสีแดงไว้จะต้องไปไล่เพิ่มทุก Use Case ที่มีการใช้ Cache ครับ
- ดูแล้วยังไม่ตอบโจทย์เรา
Second Solution
- เลือกเองใน Repository ว่าจะใช้ Cache หรือไม่
- ไม่ต้องไปไล่แก้ Test cases
- Code ที่เปลี่ยนมหาศาล
- มีโอกาส Human Error เกิดขึ้น เพราะไม่มี Test ตรงส่วนนี้

- จะเห็นว่า Code ที่ใส่กรอบสีแดงไว้จะต้องไปไล่เพิ่มทุก Repository ที่มีการใช้ Cache ครับ
- ดูแล้วยังไม่ตอบโจทย์เราอีกแล้ว

คราวนี้เราลองมาใช้ความคิดต่ออีกหน่อย เห้ยจริง ๆ เราใช้ Decorator ของ NestJS เต็มบ้านเต็มเมืองใน Code ของเราอยู่แล้วนี่นา คิดออกมาได้เป็นไอเดียสุดท้ายครับ
Firnal Solution
- เอา Decorator แปะบนหัวของ methods in Mongo repository (Adapter)
- ไม่ต้องไปไล่แก้ Test cases ที่มีอยู่
- สามารถทำ Unit test สำหรับ Decorator ได้
- มี Code นิดหน่อยที่ต้องทำ
- ไม่มี Human Error กับ Test cases ใน Mongo repository
Decorator
กลับมาดูกันว่า Decorator ของ Method เราทำยังไงกับมันได้ครับ
จะเห็นว่าเราสามารทำอะไรก่อน และหลัง Method ของเราได้ครับ
ซึ่งมีคนคิดคล้ายเรา ๆ เลย ลองดูตัวอย่างด้านล่างนี้ครับ
จะเห็นว่าเราเก็บ Original method ไว้ และสุดท้ายคืน descriptor ตัวเดิมกลับออกไป
คราวนี้ลองดู Implement จริงครับ
Credit: How To Create a Custom Typescript Decorator
คราวนี้ลองมาดูไอเดียของเรากันครับ
Redis Cache for Repository

Invalidate Redis Cache for Repository

หลายคนมาถึงตรงนี้แล้วอาจจะงงว่า Combination key คืออะไร ลองดูตัวอย่างด้านล่างนี้ครับ

สรุปก็คือความเป็นไปได้ของ Cache key ของเราทั้งหมดกับ Repository นั้นครับ
Demo code

จะเห็นว่า Code เราเวลาจะต้องการจะทำ Cache ก็แค่แปะ CacheForRepository decorator เข้าไปใน Method ที่เราต้องการจะทำ Cache ได้เลยครับ โดยตัวอย่างนี้ผมให้ Focus แค่ส่วน Cache ก่อนนะครับ โดยอันนี้ Key ที่จะทำการ Cache คือ items:all นั้นเอง
คราวนี้ลองมาดูตัวอย่าง Cache อื่น ๆ กันบ้างครับ

ตัวอย่างนี้จะเป็น Key items:{id:?} เราสามารถแทนค่า ? ได้จาก itemId ที่ส่งเข้ามาใน Method นี้ครับ
ลองไปดูตัวอย่างถัดไปกันครับ

- Key
items:{status:?,color:?}เราสามารถแทนค่า ? ได้จาก status และ color ที่ส่งเข้ามาใน Method findByStatusAndColor ครับ - Key
items:{country:?,category:?}เราสามารถแทนค่า ? ได้จาก country และ category ที่ส่งเข้ามาใน Method findByCountryAndCategory ครับ
ต่อมาเรามาดูส่วนของ Invalidate Cache กันครับ

- เวลาเรา Invalidate Cache เราจะต้องกำหนดค่า keyCombinations ที่จะทำการ Invalidate ครับ ซึ่งจะมีรูปแบบคือ
[['id'], ['country', 'category'], ['status', 'color']]ซึ่งจะทำการ Invalidate ทุก Key ที่มีความเป็นไปได้ทั้งหมดที่อยู่ใน Array นี้ครับ - สำหรับ Method update จะไปทำการ Invalid cache ตัว Key ที่มีหน้าตาตามตัวอย่างจะประมาณนี้ครับ
items:all,items:{id:507f1f77bcf86cd799439011},items:{country:Thailand,category:Clothes}และitems:{status:Available,color:Red}ตามข้อมูลที่ Update เข้ามาครับ - แต่ id สำหรับ Method create จะไม่มีผล เพราะยังไม่เคยมี id นี้มาก่อน แต่เราก็ใส่ไปเพื่อกันความงงที่จะใส่บ้างไม่ใส่บ้างครับ
คราวนี้ลองมาดูตัว Decorator ที่เราเขียนกันครับ
ตัว Cache Decorator จะมีหน้าตาตาม Code ด้านล่างนี้ครับ

ตัว Invalidate Decorator จะมีหน้าตาตาม Code ด้านล่างนี้ครับ

สำหรับตัวอย่างนี้ผมไม่ได้เล่าทั้งหมดครับ แต่จะเป็น Point ที่สำคัญครับ จะมีตัวอย่างทั้งหมดใน Github ครับ
GitHub: redis-cache-decorator-for-hexagonal-architecture-in-nestjs
Slide: Redis cache decorator for hexagonal architecture in NestJS
จากทั้งหมดนี้จะเห็นว่าเราใช้เวลาคิดหน่อย แต่จะช่วยลดงาน และความซับซ้อนของ Code ไปได้เยอะเลยครับ
ที่ปล่อยบทความช้า เพราะอยากจะให้ทาง JavaScript Bangkok 2.0.0 ปล่อยตัว Video ที่ถ่ายไว้ในงานสู่สาธารณะก่อนครับ