SOLID – Zasada Odwrócenia Zależności

W tym wpisie dowiesz się na przykładzie czym jest zasada odwrócenia zależności i jak z niej korzystać.

SOLID – Zasada Odwrócenia Zależności

Dotarliśmy do ostatniego artykułu opisującego zasady SOLID. Dzisiaj dowiesz się, czym jest zasada odwrócenia zależności (ang. Dependency Inversion Principle) na przykładzie.

SOLID

S – Zasada pojedynczej odpowiedzialności (Single Responsibility Principle – SRP)
OZasada otwarte – zamknięte – (Open-Closed Principle – OCP)
LZasada podstawiania Liskov  – (Liskov Substitution Principle – LSP)
I  – Zasada segregacji interfejsu – (Interface Segregation Principle – ISP)
DZasada odwracania zależności – (Dependency Inversion  Principle DIP)

Co kryje się pod tym tajemniczo brzmiącym zwrotem?

Zasada odwrócenia zależności

🔥
Zależności wysokopoziomowe nie powinny zależeć silnie od modułów niskopoziomowych. Jedne i drugie powinny zależeć od abstrakcji.

Tyle teoria. Głównym założeniem tej zasady jest to, aby posługiwać się interfejsami zamiast konkretnymi implementacjami. Dosadnie można rozumieć to następująco:

  • Żadna zmienna nie powinna zawierać wskaźnika lub referencji do konkretnej klasy.
  • Żadna klasa nie powinna być klasą pochodną konkretnej klasy.
  • Żadna metoda nie powinna przesłaniać zaimplementowanej metody żadnej ze swoich klas bazowych.

Oczywiście nie da się spełnić w pełni tych warunków. Zresztą jest to niemożliwe — gdzieś przecież musimy stworzyć instancję konkretnej klasy.

Dlatego należy tu jasno zaznaczyć, że zasady SOLID są wskazówkami do tego, w jaki sposób mamy pisać kod, aby posiadał jak najmniej zależności oraz powiązań pomiędzy tworzonymi strukturami. Powinniśmy, o ile jest to możliwe bazować na nich, a nie bezwzględnie i z premedytacją stosować się do nich w 100% — nie zawsze są najlepszymi rozwiązaniami w każdej sytuacji.

Spójrz na kod poniżej. Zastosowałem w nim DIP oraz wzorzec projektowy repozytorium.

Główny interfejs repozytorium.

public interface IRepository<TEntity> where TEntity : class
{
    TEntity GetById(int id);
    IEnumerable<TEntity> GetEntities();
    void Add(TEntity entity);
    void Remove(TEntity entity);
    void Update(TEntity entity);
    void Save();
}

Tutaj implementacja interfejsu repozytorium dla artykułów:

public interface IArticleRepository : IRepository<Article>
{}

Teraz konkretna klasa repozytorium artykułów. Modelem jest tutaj kontekst bazy danych, który także jest wstrzykiwany poprzez interfejs:

internal class ArticleRepository : IArticleRepository, IDisposable
{
    private readonly IErpDatabaseContext _dbContext;

    public ArticleRepository(IErpDatabaseContext erpDatabaseContext)
    {
        _dbContext = erpDatabaseContext;
    }

    public Article GetById(int id)
    {
        return _dbContext.Article.Find(id);
    }

    public IEnumerable<Article> GetEntities()
    {
        return _dbContext.Article.ToList();
    }

    public void Add(Article entity)
    {
        _dbContext.Article.Add(entity);
    }

    public void Remove(Article entity)
    {
        _dbContext.Article.Remove(entity);
    }

    public void Update(Article entity)
    {
        var a = _dbContext.Entry(entity).State = EntityState.Modified;
    }

    public void Save()
    {
        _dbContext.SaveChanges();
    }
}

Tutaj konkretne użycie repozytorium w klasie modelu widoku:

public class ArticleTableViewModel : ViewModelBase
{
    private readonly IArticleRepository _articleRepository;

    public ArticleTableViewModel(IArticleRepository articleRepository)
    {
        _articleRepository = articleRepository;
    }
    
    /\*
       Inne działania na repozytorium
    \*/
}

Spójrz teraz na konstruktor w klasie ArticleTableViewModel. Jego parametrem jest obiekt implementujący interfejs IArticleRepository.

Co zyskujemy?

Przede wszystkim nie działamy na konkretnych klasach, więc zmniejszamy zależności. Dzięki temu, że parametrem tym jest interfejs, możemy dostarczać różne implementacje repozytorium dla naszego modelu widoku i wcale nie musimy zmieniać jego kodu, gdy takie implementacje się pojawią! Coś wspaniałego!

Jest to rozwiązanie, które umożliwi testowanie kodu poprzez dostarczanie różnych dostawców danych dla tworzonego repozytorium. Warto zauważyć, że w klasie ArticleRepository możemy dostarczyć dowolną implementację IErpDatabaseContext.

Podsumowanie

Zasada odwrócenia zależności jest wykorzystywana w zasadzie każdym solidnym projekcie. W wyżej przedstawionej implementacja ArticleTableViewModel nie tworzymy żadnych konkretnych obiektów i nie korzystamy z klas konkretnych. Klasa ArticleTableViewModel korzysta z utworzonej gdzieś wyżej implementacji. Zwróć uwagę, że przy tak utworzonej klasie, możemy z niej korzystać w innych projektach, ponieważ korzystamy z kontraktów, jakimi są interfejsy.

Tu kończy się seria artykułów o SOLID. Mam szczerą nadzieję, że dostarczyła Ci wartości i pobudziła do głębszego analizowania tworzonego kodu.