Refatorar usando interfaces

Concluído

Na lição anterior, você aprendeu sobre os desafios do código firmemente acoplado e como ele viola o Princípio Aberto/Fechado. Agora, vamos refatorar o exemplo Library e BorrowableBook para tornar o sistema mais flexível e fácil de manter.

Aqui está o código original que demonstra o problema:

public class BorrowableBook
{
    public string Title { get; set; }
    public bool IsAvailable { get; private set; } = true;

    public BorrowableBook(string title)
    {
        Title = title;
    }

    public void Borrow()
    {
        if (IsAvailable)
        {
            IsAvailable = false;
            Console.WriteLine($"You have borrowed \"{Title}\".");
        }
        else
        {
            Console.WriteLine($"\"{Title}\" is already borrowed.");
        }
    }
}

public class Library
{
    private BorrowableBook _book;

    public Library(BorrowableBook book)
    {
        _book = book;
    }

    public void BorrowBook()
    {
        if (_book.IsAvailable)
        {
            _book.Borrow();
        }
        else
        {
            Console.WriteLine("The book is not available.");
        }
    }
}

Esse design une firmemente a Library classe à BorrowableBook classe, dificultando a adição de novos tipos de itens emprestáveis (por exemplo, DVDs) sem modificar a Library classe. Para corrigir o acoplamento apertado, introduzimos uma interface para dissociar o comportamento de empréstimo.

Introdução às interfaces

Uma interface define um contrato de comportamento sem detalhar sua implementação. A introdução de uma interface abstrai a funcionalidade de empréstimo e aumenta a flexibilidade do sistema.

Aqui está a IBorrowable interface:

public interface IBorrowable
{
    bool IsAvailable { get; }
    void Borrow();
}

Esta interface introduz dois membros:

  • IsAvailable: Uma propriedade que indica se o item está disponível para empréstimo.
  • Borrow: Um método para emprestar o item.

Ao definir essa interface, separamos o comportamento de empréstimo de qualquer implementação específica.

Refatoração da classe BorrowableBook

Em seguida, atualizamos a BorrowableBook classe para implementar a IBorrowable interface:

public class BorrowableBook : IBorrowable
{
    public string Title { get; set; }
    public bool IsAvailable { get; private set; } = true;

    public BorrowableBook(string title)
    {
        Title = title;
    }

    public void Borrow()
    {
        if (IsAvailable)
        {
            IsAvailable = false;
            Console.WriteLine($"You have borrowed \"{Title}\".");
        }
        else
        {
            Console.WriteLine($"\"{Title}\" is already borrowed.");
        }
    }
}

Agora, a BorrowableBook classe adere à IBorrowable interface, tornando-a intercambiável com outras classes que implementam a mesma interface.

Refatoração da classe Library

Também atualizamos a Library classe para depender da IBorrowable interface em vez da classe concreta BorrowableBook :

public class Library
{
    private IBorrowable _item;

    public Library(IBorrowable item)
    {
        _item = item;
    }

    public void BorrowItem()
    {
        if (_item.IsAvailable)
        {
            _item.Borrow();
        }
        else
        {
            Console.WriteLine("The item is not available.");
        }
    }
}

Agora, a Library classe pode trabalhar com qualquer objeto que implemente a IBorrowable interface, tornando-a mais flexível e fácil de estender.

Usando a injeção de dependência

Imagine que está a configurar um sistema de entretenimento doméstico. Em vez de ligar permanentemente uma marca específica de colunas ao seu sistema estéreo, utiliza tomadas de colunas que podem aceitar vários tipos de fichas compatíveis. Este design permite-lhe substituir ou atualizar facilmente os altifalantes sem ter de alterar todo o sistema estéreo.

No software, a injeção de dependência funciona de forma semelhante. Ele permite que uma classe dependa de uma interface abstrata em vez de uma implementação específica. A injeção de dependência torna o sistema mais flexível e fácil de manter porque você pode "conectar" diferentes implementações sem modificar a própria classe.

O construtor em programação é como o técnico que conecta as tomadas de alto-falante durante a configuração do seu sistema estéreo. Para a Library classe, o construtor (public Library(IBorrowable item)) é onde a dependência é fornecida. O construtor permite que a classe trabalhe Library com qualquer implementação compatível, como BorrowableBook ou BorrowableDVD, sem a necessidade de alterar sua estrutura interna. Assim como a tomada de alto-falante permite flexibilidade na escolha de diferentes alto-falantes, o construtor facilita a flexibilidade no uso de vários itens emprestáveis.

Veja como funciona no código:

public class Library
{
    private IBorrowable _item;

    public Library(IBorrowable item) // Dependency is injected here
    {
        _item = item;
    }

    public void BorrowItem()
    {
        if (_item.IsAvailable)
        {
            _item.Borrow();
        }
        else
        {
            Console.WriteLine("The item is not available.");
        }
    }
}

Neste exemplo:

  • O construtor é onde a "conexão" (a IBorrowable dependência) é feita, semelhante a como o plugue do alto-falante se conecta ao "jack".
  • A IBorrowable interface funciona como o "conector estéreo", definindo o ponto de conexão padrão.
  • O BorrowableBook e BorrowableDVD são como diferentes tipos de alto-falantes estéreo com conectores exclusivos que se conectam ao "jack".

Usando a injeção de dependência, a Library classe pode trabalhar com qualquer implementação do IBorrowable. Esta abordagem prevê:

  • Flexibilidade: Você pode facilmente alternar ou adicionar novas implementações sem modificar a Library classe.
  • Testabilidade: Você pode "integrar" implementações simuladas para fins de teste.
  • Manutenabilidade: A Library classe não precisa saber os detalhes da implementação específica, facilitando a extensão e a manutenção.

Este design garante que a classe não está mais firmemente acoplada Library a implementações específicas, tornando o sistema mais modular e adaptável.

Adicionar novos itens emprestáveis

Com a interface em vigor, podemos facilmente adicionar novos tipos de itens emprestáveis sem modificar a Library classe. Por exemplo, aqui está uma BorrowableDVD classe:

public class BorrowableDVD : IBorrowable
{
    public string Title { get; set; }
    public bool IsAvailable { get; private set; } = true;

    public BorrowableDVD(string title)
    {
        Title = title;
    }

    public void Borrow()
    {
        if (IsAvailable)
        {
            IsAvailable = false;
            Console.WriteLine($"You have borrowed the DVD \"{Title}\".");
        }
        else
        {
            Console.WriteLine($"The DVD \"{Title}\" is already borrowed.");
        }
    }
}

A BorrowableDVD classe implementa a mesma IBorrowable interface, para que possa ser usada perfeitamente com a Library classe.

Testando o sistema

Aqui está um programa para demonstrar o sistema refatorado:

using System;

class Program
{
    static void Main()
    {
        // Create borrowable items
        IBorrowable book = new BorrowableBook("Adventure Works Cycles");
        IBorrowable dvd = new BorrowableDVD("Graphic Design Institute");

        // Create libraries
        Library bookLibrary = new Library(book);
        Library dvdLibrary = new Library(dvd);

        // Borrow items
        bookLibrary.BorrowItem();
        bookLibrary.BorrowItem(); // Try borrowing again

        Console.WriteLine();

        dvdLibrary.BorrowItem();
        dvdLibrary.BorrowItem(); // Try borrowing again
    }
}

A saída demonstra a flexibilidade do sistema refatorado:

You have borrowed "Adventure Works Cycles".
"Adventure Works Cycles" is already borrowed.

You have borrowed the DVD "Graphic Design Institute".
The DVD "Graphic Design Institute" is already borrowed.

Este exemplo refatorado demonstra como as interfaces reduzem dependências e melhoram a modularidade:

  • Separação de preocupações: A IBorrowable interface isola o comportamento de empréstimo, garantindo que a Library classe não dependa de implementações específicas.
  • Flexibilidade melhorada: Você pode adicionar novos tipos de itens emprestáveis (por exemplo, DVDs) sem modificar a Library classe.
  • Manutenção simplificada: O sistema é mais fácil de entender, testar e estender porque as responsabilidades são claramente divididas.