Why Agoda use attribute-based Dependency Registration
IoC container based on Attributes
ในบทความนี้เราจะพูดถึง DI framework ที่ใช้ภายใน frontend project ภายใน Agoda กัน เนื่องจากปีที่ผ่านมาเราได้มีการปรับปรุงระบบภายในของเว็บ ww.agoda.com แบบยกเครื่องกันใหม่ เพื่อบริการที่ดีขึ้นสำหรับลูกค้าของเรา เนื่องจากระบบงานเดิมของเราเป็น Monolith Website Application ทำให้ส่งผลกระทบที่ทำให้เราไม่สามารถส่งมอง feature ใหม่ๆได้เร็วเพียงพอ และเราได้ตัดสินใจที่จะย้ายโครงสร้างจาก Monolith Architectureไป Modular Architecture สามารถอ่านต่อได้ที่ Breaking the Monolith โดย Vlad Batushkov จากงาน https://dotnetconfth.com/2020/
Dependency Injection คืออะไร ???
จากเอกสารของ Microsoft ได้อธิบายไว้ว่า dependency injection (DI) คือ Software design pattern ที่จะทำให้เราได้มาซึ่ง software ตามหลักการของ Inversion of Control (IoC) ระหว่าง Class และ Dependencies ของมัน ถึงจุดนี้อาจจะงงอีกว่า IoC คืออะไร ?? ถ้าจะให้ง่ายต่อความเข้าใจก็ไม่ต้องใส่ใจชื่อมัน แต่ให้นึกว่าเมื่อทำแล้วจะได้สิ่งต่างๆ ที่มีคุณสมบัติดังนี้
- แยกโค้ดส่วนที่เรียกใช้งาน กับส่วนที่เป็น implementation ออกจากกัน
- สนใจแค่ส่วนงานที่แต่ล่ะ Implementation ถูกออกแบบมาว่าจะให้ทำงานอะไร
- ให้แต่ล่ะ implementation ขึ้นต่อกันโดยผ่านทาง Contracts ไม่ใช้ Class
- ลดผลกระทบเมื่อมีการเปลี่ยน implementation class
การให้ได้มาซึ่งคุณสมบัติข้างต้นก็ทำได้หลายแบบดังนี้
- ใช้ Service locator pattern
- ใช้ dependency injection pattern (DI) ที่จะพูดในบทความนี้
- ใช้ contextualized lookup
- ใช้ template method design pattern
- ใช้ strategy design pattern
สำหรับ dependency injection (DI) สำหรับ Asp.net core เราทำกันอย่างไร
โดยปกติเราก็จะ register service ที่ Startup class ใน method ConfigureServices
ตาม code ด้านล่าง
public class Startup {
// ...
public void ConfigureServices(IServiceCollection services) {
services.AddTransient<IServiceA, ServiceA>();
services.AddSingleton<IServiceB, ServiceB>();
services.AddTransient<IServiceC, ServiceC>();
...
}
// ...
}
Dependency Lifetimes:
โดย Out of the box ของ DI ของ .NET core จะมี 3 แบบ
- Transient
- Scoped
- Singleton
Transient: สร้าง instance ทุกครั้งที่เรียกใช้
Scoped: สร้างทุกๆ web request หรือคิดง่ายๆคือ instance นี้จะมีตัวเดียวใน web request นั้น
Singleton: มี 1 instance ทั้ง application lifetime
หลังจากนั้นเราก็ Inject class ที่ต้องการ ในกรณี้ที่เป็น Controller ตาม Code ด้านล่างเลย
[ApiController]
[Route("[controller]")]
public class NoobController : ControllerBase{
private readonly IServiceA _serviceA;public NoobController(IServiceA serviceA){
_serviceA = serviceA;
}
}
IoC container ก็จะ inject service A มาให้เราใช้งานได้
ASP.NET DI From 0 To Hero
First move:
โดยส่วนมากเมื่อเราพัฒนา web application บน asp.net core เราก็จะทำการเขียน code และทำการ register service ที่ Startup class ซึ่งก็ไม่ได้มีอะไรผิดปกติ แต่ที่ agoda เรามี dev เป็น 200+ คน และต่างทีม ส่งที่เกิดขึ้นคือ Code เป็น 1000-2000 บรรทัดใน startup class
เราเจอว่าวิธีการนี้ไม่เหมาะกับ Project ใหญ่เพราะ code ยาวมาก และทำให้ merge conflicts บ่ายครั้งเพราะทุกคนต้องมาแก้ startup file และทำให้ deliver งานได้ช้า เราจะแก้ปัญหานี้ได้อย่างไงนะ
Extension methods:
ถึงตอนนี้ทุกคนก็อาจจะคิดว่า อ้าวถ้าอย่างงั้นทำไมเราไม่ไปท่า Extension method ล่ะ อาจจะแบ่งโดย Features ก็ได้เช่น Marketing Module ตาม code ด้านล่าง
ท่านี้ก็เชื่อว่าทุกคนน่าจะเคยใช้ เพราะทำให้แบ่ง code ไปตาม feature/module ของแต่ล่ะทีม ก็ดูเข้าท่าดีแต่เมื่อทำไปสักระยะ class นี้ก็ใหญ่ขึ้น เกิดปัญหาเดิมอีก แถมมี code ที่ทำ register service หลายที่ตามไปดูยากขึ้นอีก 1 step
อีกปัญหาที่เจอคือ เมื่อเรามี Services class หลายตัว เราจะรู้ได้อย่างไรว่า service แต่ล่ะตัวเรามี life time เป็นแบบไหน Transient, Scoped หรือ Singleton แล้วจะทำอย่างไรได้บ้างที่จะทำให้เราเขียน code ให้น้อยลงจากปัญหานี้
Hero step :
ทาง Agoda หลังจากเราเจอปัญหาทั้งหมดที่กล่าวมาข้างต้น เราจึงทำกาสร้าง Ioc Framework มาใช้เพื่อแก้ปัญหาโดยเราได้ Open source ให้ทุกคนได้ใช้ได้ หลังจากเราได้ใช้ภายในบริษัทมาสักระยะ
How to get start with Agoda.IoC
- เริ่มจาก install package Agoda.IoC.NetCore
Install-Package Agoda.IoC.NetCore
2. บอก Assembly ที่ต้องการให้ scan
public void ConfigureServices(IServiceCollection services)
{
services.AutoWireAssembly(new[]
{typeof(Startup).Assembly}, isMockMode);
}
3.ใส่ attribute ที่ class ที่ต้องการใส่เข้าไปใน IoC
จาก
services.AddSingleton<IServiceA , ServiceA>();
เป็น
// Simple registration
public interface IServiceA {}
[RegisterSingleton] /// replaces services.AddSingleton<IService, Service>();
public class ServiceA : IServiceA {}
ที่นี้เราก็จะแก้ปัญหา code ที่ยาวๆได้และรู้ว่า ServiceA มี lifetime แบบ Singleton
เนื้อหาต่อไปผมจะเป็นรายละเอียดกการใช้งาน library ตัวนี้แบบ basic ไปจนถึง Advance ว่าใช้ทำอะไรได้บ้าง โปรดติดตาม และ share ได้เลยครับถ้าเห็นว่ามีประโยชน์