Tìm hiểu về Dependency Injection trong ASP.NET Core
Bài viết này chúng ta sẽ cùng tìm hiểu về những điều thú vị xung quanh depedency injection và unit testing. Mục tiêu của bài viết là demo cách giảm sự phụ thuộc để ứng dụng ASP.NET Core có thể test được. Điều này rất có giá trị cho các developer mới tìm hiểu về IoC, DI và unit testing.
Sau đó chúng ta sẽ có bài nói về unit testing và tìm hiểu rằng tại sao depedency injection lại rất quan trọng trong việc unit testing.
Vậy depedency injection là gì?
DI là một kiểu của Inversion of Control (IoC)
Inversion of Control chỉ là một khái niệm cho một nhóm các nguyên tắc thiết kế hướng tới việc loại bỏ sự phụ thuộc trong code. Nó làm việc bằng cách tự động tạo các instance của các thành phần phụ thuộc ở các module khác và đặt vào một nơi gọi là container.
Một container đơn giản nó là một nhà máy (factory) có trách nhiệm cung cấp các instance (thể hiện) của cá kiểu được yêu cầu.
DI là một kiểu đặc biệt của IoC cho phép các thành phần phụ thuộc (các thành phần khác, các service trong chương trình) được inject (tiêm) trực tiếp từ container vào một constructor (hàm khởi dựng) hoặc một thuộc tính công khai (public properties) của một class phụ thuộc vào chúng.
Ví dụ như sau:
public class CodeEditor
{
private SyntaxChecker syntaxChecker;
public CodeEditor()
{
this.syntaxChecker = new SyntaxChecker();
}
}
Thể hiện của kiểu SyntaxChecker được tạo ra trong constructor giống như việc phụ thuộc cứng giữa CodeEditor và SyntaxChecker bởi vì nó được tạo ra một cách trực tiếp bằng toán tử new.
Điều này là không thực sự lý tưởng
Sau này nếu tác giả của SyntaxChecker loại bỏ một constructor không tham số như vậy thì CodeEditor sẽ bị lỗi. Và sẽ có rất nhiều chỗ phải sửa nếu như SyntaxChecker được khởi tạo ở rất nhiều nơi nữa.
Và giờ hãy xem cách tiếp cận tốt hơn:
public class CodeEditor
{
private ISyntaxChecker syntaxChecker;
public CodeEditor(ISyntaxChecker syntaxChecker)
{
this.syntaxChecker = syntaxChecker;
}
}
Chúng ta tạo ra một sự thay đổi như trên. Đầu tiên chúng ta tạo một lớp trừu tượng cho SyntaxChecker sử dụng một interface. Điều này nghĩa là nó sẽ không tham chiếu đến một implement cụ thể nào và đơn giản chỉ là chúng ta tham chiếu CodeEditor đến một hợp đồng (interface) và nó có các điều khoản của nó.
Chúng ta có thể chấp nhận bất cứ kiểu nào của SyntaxChecker được triển khai các method được định nghĩa trong ISyntaxChecker, có thể là một JavaScriptSyntaxChecker hay CsharpSyntaxChecker, PythonSyntaxChecker. Tất cả các class này đều là triển khai cụ thể của interface ISyntaxChecker.
// a contract to define the behavior of a syntax checker
public interface ISyntaxChecker
{
bool IsValid();
bool GetLineCount();
bool GetErrorCount();
...
}
// a concrete SyntaxChecker implementation focused on JavaScript
public class JavaScriptSyntaxChecker : ISyntaxChecker
{
public JavaScriptSyntaxChecker()
{
}
public bool IsValid()
{
// implement JavaScript IsValid() method here...
}
... other methods defined in our interface
}
Điều thay đổi lớn cho CodeEditor chúng ta đã giải thoát khỏi sự phụ thuộc cứng cho SyntaxChecker. Chúng ta thay thế các lệnh new một SyntaxChecker trong constructor với một tham số là một thể hiện của ISyntaxChecker thông qua constructor. Vậy là tạo mới một CodeEditor có thể được thực hiện thông qua từng thể hiện của SyntaxChecker.
JavaScriptSyntaxChecker jsc = new JavaScriptSyntaxChecker(); // dependency
CodeEditor codeEditor = new CodeEditor(jsc);
CSharpSyntaxChecker cssc = new CSharpSyntaxChecker(); // dependency
CodeEditor codeEditor = new CodeEditor(cssc);
Đây là cơ bản của depedency injection. Không có gì ảo diệu đúng không nào?
Nhưng nó cho chúng ta hàng tá các lợi ích. Có 2 điều quan trọng là:
- Nó có thể quản lý được việc khởi tạo các thành phần phụ thuojc bên ngoài class và sử dụng chúng. Thường xuyên hơn, nó có thể làm được việc này ở một nơi tập trung như là IoC container hơn là lặp đi lặp lại trong ứng dụng.
- Có khả năng dễ dàng test mỗi class độc lập bởi vì chúng ta có thể truyền vào một đối tượng giả, hoặc đối tượng mẫu vào thông qua constructor thay vì sử dụng một implementation cứng. Điều này sẽ được chứng minh trong bài unit test.
Ở điểm này, tôi hy vọng bạn có thể hiểu được IoC và DI làm việc như thế nào với lớp trừu tượng (interfaces) để tạo ra một kiến trúc lỏng lẻo và các lợi ích nó mang lại.
Dependency Injection trong ASP.NET Core
Một tính năng mới trong ASP.NET Core là nó đã mang một IoC Container đơn giản gọn nhẹ để triển khai depedency injection lên sẵn trên nền tảng này.
Đây là một bước đi lớn so với các sản phẩm trước như WebForms, MVC, SignalR và Web API vì mỗi sản phẩm này đều phải sử dụng các thư viện bên thứ 3 như Ninject, Autofac, StructureMap… để triển khai DI.
Chú ý là ASP.NET Core hỗ trợ đầy đủ việc chuyển đổi giữa việc sử dụng IoC container có sẵn và sử dụng của bên thứ 3, ngĩa là bạn không thích dùng cái có sẵn của nó thì có thể đổi sang một trong số những IoC Container kia. Nhưng mình thấy là cái có sẵn của ASP.NET Core đã đầy đủ cho việc sử dụng DI ở một project nhỏ và vừa rồi.
Để bắt đầu chúng ta tạo một ứng dụng ASP.NET Core:
Project mới đã có sẵn IoC container nhưng nếu bạn cần thêm nó tách biệt ra thì bạn cần add thành phần Microsoft.Extensions.DependencyInjection từ NuGet package.
Khái niệm container
Container có sẵn trong ASP.NET Core được biểu diễn bởi IServiceProvider interface nó hỗ trợ mặc định inject vào constructor.
Các kiểu mà chúng ta đăng ký với container được biết là các service. Bạn register các kiểu của bạn với container trong method ConfigureServices của class Startup.
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
// registering our custom player repository
services.AddScoped<IPlayerRepository, PlayerRepository>();
// registering our custom game repository
services.AddScoped<IGameRepository, GameRepository>();
}
Tham số IserviceCollection là một collection lưu trữ danh sách các kiểu được đăng ký. Giống như các container khác, nó hỗ trợ khai báo dạng interface hoặc khởi tạo instance.
Chúng ta cũng phải xem xét đến vòng đời (lifetime) của các service mà chúng ta đăng ký. Vòng đời này của một service trong ứng dụng đơn giản là một thể hiện của service đó sẽ tồn tại bao lâu trong một request đến server.
Mặc định container hỗ trợ 3 loại vòng đời:
- ServiceLifeTime.Transient: Là một thể hiện duy nhất được trả về từ mỗi một lần request.
- ServiceLifeTime.Scoped: Trong ASP.NET Core thì một scope được tạo ra quanh mỗi một server request. Các thể hiện được đăng ký dạng Scoped sẽ chia sẻ bên trong cùng một request. Loại vòng đời này hữu dụng cho Entity Framework DbContext (Unit Of Work pattern) được chia sẻ đối tượng qua nhiều object nên bạn có thể chạy transaction trên nhiều đối tượng.
- ServiceLifetime.Singleton: Một thể hiện duy nhất được tạo và chia sẻ xuyên suốt thời gian chạy của ứng dụng.
Trong HomeController mình sử dụng cả inject constructor và inject parameter ở action Index. Không sử dụng toán tử new, nó đã có sẵn cho bạn sử dụng. Thật tuyệt phải không?
public class HomeController : Controller
{
private readonly IPlayerRepository _playerRepository;
public HomeController(IPlayerRepository playerRepository)
{
_playerRepository = playerRepository;
}
public IActionResult Index([FromServices] IGameRepository gameRepository)
{
var players = _playerRepository.GetAll(); // constructor injected
var games = gameRepository.GetTodaysGames(); // parameter injected
return View();
}
Bạn sẽ truyền hầu hết các thành phần phụ thuộc thông qua constructor nhưng trong ASP.NET Core thì việc inject qua parameter sẽ giúp inject một số đối tượng tốn bộ nhớ nếu dùng nhiều lần hoặc vì lý do nào đó không yêu cầu inject qua constructor.
Tổng kết
IoC và DI là các khái niệm trừu tượng khó hiểu chút nhưng tôi hy vọng bạn sẽ có thể hiểu được một vài ý tưởng về mục đích và lợi ích của chúng để có thể áp dụng nó trong ứng dụng của mình.
Bài viết tới mình sẽ nói đến unit test và chúng ta sẽ thấy được sức mạnh của việc áp dụng IoC và DI pattern.
Mã nguồn bài viết https://github.com/mmacneil/ASPNetCoreDIAndUnitTesting
Tác giả:
Chú ý: Tất cả các bài viết trên TEDU.COM.VN đều thuộc bản quyền TEDU, yêu cầu dẫn nguồn khi trích lại trên website khác.