SOLID: Princípio do aberto/fechado, um exemplo prático com Java e Spring
Published: 2020-11-04 18:17:33


#### Você recebe o seguinte requisito de software[[1]](https://github.com/Creditas/challenge/blob/master/backend/code-challenges/README.pt-BR.md) para automatizar quais modalidades de empréstimo devem estar disponíveis a depender do perfil de um cliente:

Modelos de empréstimo disponíveis:
Empréstimo pessoal com taxa de juros a 4%
Empréstimo com garantia com taxa de juros a 3%
Consignado com taxa de juros a 2%

Abaixo seguem as regras de negócio relacionadas a concessão de empréstimo de acordo com o perfil da pessoa:

|                          | Empréstimo pessoal | Empréstimo c/ garantia | Consignado |
| ------------------------ | ------------------ | :--------------------: | ---------- |
| Salario <= 3000          | Sim                |       Sim\*\*\*        | Não        |
| Salario > 3000 e < 5000 | Sim                |        Sim\*\*         | Não        |
| Salário => 5000          | Sim                |         Sim\*          | Sim        |

- \* Clientes com menos de 30 anos
- \*\* Clientes que residem em SP
- \*\*\* Clientes com menos de 30 anos que residem em SP

Um input:

```json
{
  "customer": {
    "name": "Erikaya",
    "cpf": "123.456.789-10",
    "age": 29,
    "location": "BH",
    "income": 3000
  }
}
```


Deverá responder um output:

```json
{
  "customer": "Erikaya",
  "loans": [
    {
      "type": "personal",
      "taxes": 1
    }
  ]
}
```

#### Você rapidamente identifica as entidades e constrói um solução

##### Entidades*

``` java
public class Customer {

    private String name;
    private String cpf;
    private int age;
    private String location;
    private int income;
    
}
```

``` java
public enum LoanType {

    PERSONAL,
    GUARANTEED,
    CONSIGNMENT;
    
}
```

``` java
public class Loan {

    private LoanType loanType;
    private Integer taxes;
    
}
```
*package, imports, construtores, getters, setters, equals e hashcode omitidos por brevidade

##### Solução

Controller:
``` java
@RestController
public class LoanController {

    private final LoanService loanService;

    public LoanController(LoanService loanService) {
        this.loanService = loanService;
    }

    @PostMapping(value = "/api/loan/availability")
    public ResponseEntity<CustomerLoanAvailabilityDTO> findAvailableLoansByCustomer(@RequestBody Customer customer) {
        Set<Loan> availableLoanTypes = loanService.findAvailableLoansByCustomer(customer);
        return ResponseEntity.ok().body(new CustomerLoanAvailabilityDTO(customer, availableLoanTypes));
    }
}

```

Service:
``` java
@Service
public class LoanService {

    public Set<Loan> findAvailableLoansByCustomer(Customer customer) {

        Set<Loan> availableLoans = new HashSet<>();
        Loan personalLoan = new Loan(LoanType.PERSONAL, 4);
        Loan guaranteedLoan = new Loan(LoanType.GUARANTEED, 3);
        Loan consignmentLoan = new Loan(LoanType.CONSIGNMENT, 2);

        availableLoans.add(personalLoan);

        if (customer.getIncome() <= 3000) {
            if (isAgeLessThan30(customer) && isLocationSp(customer)) {
                availableLoans.add(guaranteedLoan);
            }
        }
        else if (customer.getIncome() > 3000 && customer.getIncome() < 5000) {
            if (isLocationSp(customer)) {
                availableLoans.add(guaranteedLoan);
            }
        }
        else  if (customer.getIncome() >= 5000) {
            if (isAgeLessThan30(customer)) {
                availableLoans.add(guaranteedLoan);
            }
        }
        
        if (customer.getIncome() >= 5000) {
            availableLoans.add(consignmentLoan);
        }

        return availableLoans;
    }

    private boolean isAgeLessThan30(Customer customer) {
        return customer.getAge() < 30;
    }

    private boolean isLocationSp(Customer customer) {
        return customer.getLocation().equalsIgnoreCase("SP");
    }

}
```

#### O problema dessa solução
O nosso código funciona, nossa classe LoanService que contém as regras de negócio está com cerca de 40 linhas, nada mal se comparado a sistemas legado, mas atualmente existem somente três regras de empréstimo, e se o número de regras aumentar? Na medida que o número de regras aumentar a dificuldade de compreensão do código irá crescer de forma diretamente proporcional, causando um alto custo de manutenção do nosso software. Ficará mais difícil para mudar uma regra existente, ou adicionar uma nova. Daí que urge a necessidade de usarmos o principio do aberto/fechado.

#### Principio do aberto/fechado
O princípio do aberto/fechado diz que "entidades de software (classes, módulos, funções) devem ser abertas para extensão, porém fechadas para modificação"[[2]](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle). Dessa forma é possível adicionar novos comportamentos para  o sistema sem alterar a nossa lógica.

#### Utilizando o principio do aberto e fechado na prática
Primeiramente iremos criar uma abstração, em forma de interface, para regra de empréstimo:
``` java
public interface LoanAvailability {

    Loan getLoan();
    boolean isLoanAvailable(Customer customer);
}
```
Em seguida vamos programar a nossa solução voltada para essa interface:

``` java
public class LoanService {

    private final Set<LoanAvailability> loanRules;

    public LoanService(Set<LoanAvailability> loanAvailabilities) {
        this.loanRules = loanAvailabilities;
    }

    public Set<Loan> findAvailableLoansByCustomer(Customer customer) {

        Set<Loan> availableLoans = new HashSet<>();
        for (LoanAvailability loanAvailability : loanRules) {
            if (loanAvailability.isLoanAvailable(customer)) {
                Loan loan = loanAvailability.getLoan();
                availableLoans.add(loan);
            }
        }
        return availableLoans;
    }

}
```
Podemos deixar o nosso código ainda mais pragmático utilizando a Streams API do Java 8:
``` java
@Service
public class LoanService {

    private final Set<LoanAvailability> loanRules;

    public LoanService(Set<LoanAvailability> loanAvailabilities) {
        this.loanRules = loanAvailabilities;
    }

    public Set<Loan> findAvailableLoansByCustomer(Customer customer) {

        return loanRules.stream()
                .filter(loanAvailability -> loanAvailability.isLoanAvailable(customer))
                .map(LoanAvailability::getLoan)
                .collect(Collectors.toSet());
    }

}
```
O nosso código agora está recebendo as regras de empréstimo por meio de injeção de dependência. Agora toda Bean que implemente a interface ``LoanAvailability`` será instanciada e adicionanda ao conjunto ``loanRules``.
&nbsp;


Agora podemos implementar as nossas regras de negócio separadamente, implementado a interface LoanAvailability, que contém o contrato de como deve ser um regra de disponibilização de emprestimo:

``` java

@Component
public class PersonalLoanAvailability implements LoanAvailability {
    @Override
    public Loan getLoan() {
        return new Loan(LoanType.PERSONAL, 4);
    }

    @Override
    public boolean isLoanAvailable(Customer customer) {
        return true;
    }
}


@Component
public class ConsignmentLoanAvailability implements LoanAvailability {

    @Override
    public Loan getLoan() {
        return new Loan(LoanType.CONSIGNMENT, 2);
    }

    @Override
    public boolean isLoanAvailable(Customer customer) {
        return customer.getIncome() >= 5000;
    }

}

@Component
public class GuaranteedLoanAvailability implements LoanAvailability {

    @Override
    public Loan getLoan() {
        return new Loan(LoanType.GUARANTEED, 3);
    }

    @Override
    public boolean isLoanAvailable(Customer customer) {

        if (customer.getIncome() <= 3000) {
            return isAgeLessThan30(customer) && isLocationSp(customer);
        }
        else if (customer.getIncome() > 3000 && customer.getIncome() <= 5000) {
            return isLocationSp(customer);
        }
        else  if (customer.getIncome() > 5000) {
            return isAgeLessThan30(customer);
        }
        return false;
    }

    private boolean isAgeLessThan30(Customer customer) {
        return customer.getAge() < 30;
    }

    private boolean isLocationSp(Customer customer) {
        return customer.getLocation().equalsIgnoreCase("SP");
    }
}

```
Assim é possível adicionar quantas novas regras de empréstimo forem solicitadas sem alterarmos o método ``findAvailableLoansByCustomer ``. Ou seja nossa solução está fechada para modificação e aberta para extensão.

#### Referências:
[[1]  Desafio de Código da Creditas](https://github.com/Creditas/challenge/blob/master/backend/code-challenges/README.pt-BR.md)

[[2] Open–closed principle](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle)