Wstrzykiwanie zależności z Lazy

Wstrzykiwanie zależności z wykorzystaniem kontenerów jest bardzo wygodne, ale niesie też za sobą trochę problemów. Jednym z nich jest liczba oraz moment tworzenia obiektów. W tym wpisie pokażę ten problem oraz zaproponuję swoje rozwiązanie: wstrzykiwanie zależności z Lazy.

Problem

Poniżej przedstawiony jest dość standardowy kawałek kodu aplikacji ASP.NET MVC, w której wykorzystałem wstrzykiwanie zależności przez konstruktor w formie interfejsów.

public class UsersController : Controller
{
protected IUserLogic UserLogic { get; set; }
public UsersController(IUserLogic userLogic)
{
UserLogic = userLogic;
}
public ActionResult Create()
{
return View(new UserViewModel());
}
[HttpPost]
public ActionResult Create(UserViewModel viewModel)
{
if (ModelState.IsValid == false)
{
return View(viewModel);
}
var user = new User()
{
FirstName = viewModel.FirstName,
LastName = viewModel.LastName
};
var result = UserLogic.Add(user);
if (result.Success)
{
return Content("Added");
}
result.AddErrorToModelState(ModelState);
return View(viewModel);
}
}
public class UserLogic : IUserLogic
{
protected IUserRepository UserRepository { get; set; }
protected UserValidator Validator { get; set; }
private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();
public UserLogic(IUserRepository userRepository,
UserValidator validator)
{
UserRepository = userRepository;
Validator = validator;
_logger.Info("UserLogic.ctor");
}
public Result<User> Add(User user)
{
var validationResult = Validator.Validate(user);
if (validationResult.IsValid == false)
{
return Result.Failure<User>(validationResult.Errors);
}
UserRepository.Add(user);
return Result.Ok(user);
}
}
public class UserRepository : IUserRepository
{
private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();
protected DataContext Db { get; set; }
public UserRepository(DataContext db)
{
Db = db;
_logger.Info("UserRepository.ctor");
}
public void Add(User user)
{
Db.Users.Add(user);
Db.SaveChanges();
}
}

Jak widać w powyższym kodzie, kontroler w konstruktorze otrzymuje zależność do logiki biznesowej. Logika biznesowa następnie posiada zależność do repozytorium oraz walidatora, a repozytorium posiada jeszcze zależność do DataContext z entity framework.

Domyślnie kontenery działają w ten sposób, że przed utworzeniem instancji jakieś klasy, tworzą instancje klasy zależnej i tak dalej. W związku z tym w naszym przykładzie przed utworzeniem instancji kontrolera utworzone zostaną instancje logiki biznesowej, walidatora, repozytorium oraz kontekst Entity Framework. To wszystko zostanie utworzone za każdym razem, niezależnie od tego, czy obiekty będą używane, czy nie.

W niektórych sytuacjach takie zachowanie jest niepożądane. Wystarczy spojrzeć na akcję Create z kontrolera, która wyświetla tylko formularz, nie korzystając z logiki biznesowej. W tej sytuacji tworzenie tych wszystkich obiektów jest zbędne i niepotrzebnie obciąża działanie aplikacji – w tym przypadku serwera WWW, który mógłby obsłużyć dużo większy ruch.

Widziałem jedną aplikację WPF, która podczas startu (łącznie około 2,5 minuty) tworzyła instancje wszystkich widoków, nawet tych, których użytkownik w ogóle nie mógł widzieć z racji uprawnień. Użycie obiektów Lazy, o których opowiem niżej, spowodowało, że start aplikacji skrócił się do około 30 sekund.

Na potwierdzenie poniżej znajduje się log z testowej aplikacji, który wyświetla informacje o tworzonych obiektach przez kontener (użyłem Autofac) oraz o wywołaniach samych metod.

Activating: DependencyInjectionWithLazy.Models.DataContext
UserRepository.ctor
Activating: DependencyInjectionWithLazy.Models.UserRepository
Activating: DependencyInjectionWithLazy.Validators.UserValidator
UserLogic.ctor
Activating: DependencyInjectionWithLazy.Logics.UserLogic
Activating: DependencyInjectionWithLazy.Controllers.UsersController
Before call controller: Users.create (GET)
After call controller: Users.create (GET)

Darmowy kurs Visual Studio

Pracując z setkami programistów, zauważyłem, że większość osób nie pracuje efektywnie w Visual Studio. W skrajnych przypadkach korzystali z kopiowania z wykorzystaniem menu Edit. Wiem, że to dziwne, ale naprawdę niektórzy tak pracują. Dlatego postanowiłem stworzyć kurs Visual Studio – aby pomóc koleżankom i kolegom w efektywniejszej pracy.

Przygotowałem 30 lekcji e-mail, w których pokażę Ci, w jaki sposób pracować efektywniej i szybciej w Visual Studio. Poznasz dodatki, bez których nie wyobrażam sobie pracy w tym IDE.

Po więcej informacji zapraszam na dedykowaną stronę kursu: Darmowy Kurs Visual Studio.

Quiz C#

Ostatnio przygotowałem również quiz C#, w którym możesz sprawdzić swoją wiedzę. Podejmiesz wyzwanie?

Wstrzykiwanie zależności z Lazy

Jednym ze sposobów rozwiązania powyższego problemu jest wstrzykiwanie zależności z Lazy, który jest wspierany przez większość kontenerów. Obiekty Lazy przechowują informacje o tym, jak utworzyć jakiś obiekt, i tworzą go dopiero podczas pierwszego użycia. Gdy chcemy ręcznie skorzystać z Lazy, wystarczy że przekażemy delegat tworzący obiekt do konstruktora. Natomiast w przypadku Autofac nie musimy nic specjalnego robić po stronie konfiguracji samego kontenera.

Aby skorzystać z Lazy, wystarczy tylko zmienić parametry w konstruktorze z IUserLogic na Lazy<IUserLogic>. Następnie korzystamy z właściwości Value obiektu Lazy, która udostępnia nam właściwy obiekt i tworzy go podczas pierwszego skorzystania z Value. W swoim kodzie nie korzystam bezpośrednio z obiektu Lazy, tylko opakowuje to prostą właściwością (co najlepiej widać na kodzie).

Poniżej znajduje się zmieniony kod, który korzysta z wstrzykiwania zależności z Lazy:

public class UsersController : Controller
{
private Lazy<IUserLogic> _userLogic;
protected IUserLogic UserLogic
{
get { return _userLogic.Value; }
}
public UsersController(Lazy<IUserLogic> userLogic)
{
_userLogic = userLogic;
}
// GET: Users
public ActionResult Index()
{
return View();
}
public ActionResult Create()
{
return View(new UserViewModel());
}
[HttpPost]
public ActionResult Create(UserViewModel viewModel)
{
if (ModelState.IsValid == false)
{
return View(viewModel);
}
var user = new User()
{
FirstName = viewModel.FirstName,
LastName = viewModel.LastName
};
var result = UserLogic.Add(user);
if (result.Success)
{
return Content("Added");
}
result.AddErrorToModelState(ModelState);
return View(viewModel);
}
}
public class UserLogic : IUserLogic
{
private Lazy<IUserRepository> _userRepository;
protected IUserRepository UserRepository
{
get { return _userRepository.Value; }
}
private Lazy<UserValidator> _validator;
protected UserValidator Validator
{
get { return _validator.Value; }
}
private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();
public UserLogic(Lazy<IUserRepository> userRepository,
Lazy<UserValidator> validator)
{
_userRepository = userRepository;
_validator = validator;
_logger.Info("UserLogic.ctor");
}
public Result<User> Add(User user)
{
var validationResult = Validator.Validate(user);
if (validationResult.IsValid == false)
{
return Result.Failure<User>(validationResult.Errors);
}
UserRepository.Add(user);
return Result.Ok(user);
}
}
public class UserRepository : IUserRepository
{
private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();
private Lazy<DataContext> _db;
protected DataContext Db
{
get { return _db.Value; }
}
public UserRepository(Lazy<DataContext> db)
{
_db = db;
_logger.Info("UserRepository.ctor");
}
public void Add(User user)
{
Db.Users.Add(user);
Db.SaveChanges();
}
}

Jak widać, nie trzeba zbytnio zmieniać kodu, aby skorzystać z obiektów Lazy.

Na logu widać, że tworzone są tylko te obiekty, które są używane, oraz widać, że na przykład obiekt logiki jest tworzony już w trakcie wykonywania akcji. Pierwszy log pochodzi z wyświetlenia samego formularza (czyli tak samo, jak log wyżej), natomiast drugi log to próba zapisania formularza z błędem walidacji (nieustawione FirstName).

Activating: System.Lazy`1[DependencyInjectionWithLazy.Logics.IUserLogic]
Activating: DependencyInjectionWithLazy.Controllers.UsersController
Before call controller: Users.create (GET)
After call controller: Users.create (GET)
Activating: System.Lazy`1[DependencyInjectionWithLazy.Logics.IUserLogic]
Activating: DependencyInjectionWithLazy.Controllers.UsersController
Before call controller: Users.create (POST)
Activating: System.Lazy`1[DependencyInjectionWithLazy.Models.IUserRepository]
Activating: System.Lazy`1[DependencyInjectionWithLazy.Validators.UserValidator]
UserLogic.ctor
Activating: DependencyInjectionWithLazy.Logics.UserLogic
Before call: UserLogic.Add
Activating: DependencyInjectionWithLazy.Validators.UserValidator
After call: UserLogic.Add
After call controller: Users.create (POST)

Podsumowanie

Wstrzykiwanie zależości z Lazy potrafi zwiększyć wydajność aplikacji poprzez nietworzenie obiektów, które nie są wykorzystywane. Dlatego właśnie warto korzystać z wstrzykiwania zależności z Lazy.

Zachęcam do zapoznania się z przykładem na githubie. Kod znajduje się w dwóch branchach:

Kod zawiera też kilka różnych innych fajnych rzeczy, więc warto go pobrać i przejrzeć. 🙂

4 thoughts on “Wstrzykiwanie zależności z Lazy

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.