SOLID : Conceitos
S = Single Responsibility Principle
Digamos que seu chefe lhe ordenou a criar um sistema que salve dados simples de cliente apenas com nome e idade para saber a média de idade do seu público, seria necessário pegar os dados e ter uma maneira de os persistir quando possível, então para isso é criada uma classe simples que servirá como ferramenta para executar essa ação:
class Person {
private final Integer id;
private final String name;
private final int age;
private final Connection connection;
public Person(@Nullable Integer id, String name, int age, Connection connection) {
this.id = id;
this.name = name;
this.age = age;
this.connection = connection;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void save(Person person) throws SQLException {
String sql = "INSERT INTO pessoas (nome, age) VALUES (?, ?)";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, person.getName());
stmt.setInt(2, person.getAge());
stmt.executeUpdate();
}
}
}
No exemplo acima temos uma classe que executaria a ação requerida, porém, isso quebraria o Single Responsibility Principle, esse princípio nos diz que uma classe ou função não deve ter mais de uma responsabilidade.
Por exemplo, uma classe que auxilia na persistência de dados no banco não deve ter a responsabilidade de também receber atributos de um objeto para criá-lo, ou seja, a classe não deve ser um objeto por exemplo que represente uma Pessoa e também se conectar com o banco Clientes, para isso precisaríamos ter duas classes com suas respectivas responsabilidades.
class Person {
private final Integer id;
private final String name;
private final int age;
public Person(@Nullable Integer id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class PersonRepository {
private Connection connection;
public PersonRepository(Connection connection) {
this.connection = connection;
}
O = Open-closed Principle
Esse princípio nos diz que uma classe deve ser aberta a extensões e fechada a modificações, por exemplo, temos uma classe ProductController e ela obtém uma função chamada searchProductByType(), ele pesquisa produtos do tipo que o usuário quer:
public void searchProduct(PRODUCT_TYPE type){
switch (type){
case CONSOLE -> {searchConsoleModels();}
case BEST_GAME_BY_CONSOLE -> { searchBestGamesByConsole();}
case TV -> {searchTVModels();}
}
}
Toda vez que precisasse pesquisar algum tipo novo de produto seria necessário adicionar mais uma condição na função, o que fugiria desse princípio, o melhor seria criar uma interface(dependendo do contexto) e implementa-la nos controllers dos produtos, assim cada classe terá o seu próprio comportamento necessário e não irá precisar criar funções e condições:
interface ProductSearcher{
JsonNode searchAllProduct();
JsonNode searchByYear();
}
class ConsoleController implements ProductSearcher{
@Override
public JsonNode searchAllProduct(){
//code
return null;
}
@Override
public JsonNode searchByYear() {
return null;
}
public JsonNode searchBestGamesByConsole(){
//code
return null;
}
}
class TVController implements ProductSearcher{
@Override
public JsonNode searchAllProduct(){
//code
return null;
}
@Override
public JsonNode searchByYear() {
return null;
}
public JsonNode searchByResolution(){
//code
return null;
}
}
Com essa abordagem, se precisarmos adicionar um novo produto, basta criar uma nova classe que implemente ProductSearcher, sem alterar código existente.
L = Liskov Substitution Principle
Em outras palavras, uma subclasse deve poder substituir sua superclasse sem quebrar o comportamento esperado do código. Por exemplo: Temos uma classe "Telefone(Phone)" e a mesma contém os métodos call(), answer() e , recordMessage(). A classe Telefone tem duas classes filhas: Telefone de disco(OldPhone) e Celular(CellPhone).
abstract class Phone{
public void call() {
//code
}
public void answer() {
//code
}
public void recordMessage(){
//code
}
}
class CellPhone extends Phone{
public void vibrate(){
//code
}
}
class OldPhone extends Phone{
@Override
public void recordMessage() {
throw new RuntimeException("Telefone antigo não tem armazenamento para gravar a mensagem");
}
public void rewindDisc(){
//code
}
}
A classe Celular(CellPhone) é um tipo normal de telefone, porém Telefone de disco (OldPhone) é um tipo antigo de telefone e isso nos ocasionaria em um problema, já que o mesmo pode usar o método recordMessage() sendo um telefone antigo, então ele não poderia herdar a classe Telefone da maneira em que ela está agora, poderíamos criar uma classe Phone que teria apenas as funções call() e answer(), e uma outra classe TelefoneComGravador que herdaria a classe Telefone e a própria classe nova teria a função recordMessage(), enquanto o Telefone com disco(OldPhone) iria apenas herdar a classe Telefone(Phone) que teria apenas as funções call() e answer().
abstract class Phone{
public void call() {
//code
}
public void answer() {
//code
}
}
class PhoneWithStorage extends Phone{
public void recordMessage(){
//code
}
}
class CellPhone extends PhoneWithStorage{
public void vibrate(){
//code
}
}
class OldPhone extends Phone{
public void rewindDisc(){
//code
}
}
I = Interface Segregation Principle
Temos uma Interface chamada BirdActions a mesma tem as funções peck(), walk(), stop(), fly() e land(). Usamos essa nossa interface em duas classes, a classe Coruja e a classe Pinguim
interface BirdActions{
default void peck(){
System.out.println("Bicou");
}
default void walk(){
System.out.println("Deu um passo");
};
default void stop(){
System.out.println("Parou");
};
default void fly(){
System.out.println("Voou");
};
default void land(){
System.out.println("Aterrissou");
};
}
abstract class Bird{
private String name;
private String scientificName;
private Double size;
public Bird(String name, String scientificName, Double size) {
this.name = name;
this.scientificName = scientificName;
this.size = size;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getScientificName() {
return scientificName;
}
public void setScientificName(String scientificName) {
this.scientificName = scientificName;
}
public Double getSize() {
return size;
}
public void setSize(Double size) {
this.size = size;
}
}
class Coruja extends Bird implements BirdActions {
public Coruja(String name, String scientificName, Double size) {
super(name, scientificName, size);
}
}
class Pinguim extends Bird implements BirdActions {
public Pinguim(String name, String scientificName, Double size) {
super(name, scientificName, size);
}
}
Ambas as classes terão que implementar as funções da interface BirdActions, mas temos um problema, a classe Pinguim irá receber as funções fly() e land() sendo que elas nunca serão usadas, afinal pinguins não voam (ainda). Para resolvermos esse problema nós teremos que segregar a interface, assim movendo as funções para outra interface, a solução seria mover o fly() e o land() para uma interface própria, então criamos a interface FlyableBird que irá receber as nossas funções.
interface BirdActions{
default void peck(){
System.out.println("Bicou");
}
default void walk(){
System.out.println("Deu um passo");
}
default void stop(){
System.out.println("Parou");
}
}
interface FlyableBird{
default void fly(){
System.out.println("Voou");
}
default void land(){
System.out.println("Aterrissou");
}
}
A classe Pinguim continuará implementando a interface BirdActions assim recebendo apenas as suas “ações” necessárias, enquanto a classe Coruja irá implementar as duas interfaces, BirdActions e FlyableBird, assim podendo implementar as funções de cada uma delas que de fato serão usadas.
class Coruja extends Bird implements BirdActions, FlyableBird {
class Pinguim extends Bird implements BirdActions {
D = Dependency Inversion Principle
- Depende de abstrações e não de implementações.
- Inverter as dependências
O Princípio da Inversão de Dependência (DIP - Dependency Inversion Principle) incentiva o código a depender de abstrações (interfaces ou classes abstratas) em vez de implementações concretas.
Desacoplamento e Boas Práticas:
Interfaces permitem programar para abstrações e não para implementações. Isso facilita a manutenção e a injeção de dependências. Usando interface, podemos trocar a implementação sem mexer na Service:
public abstract class People {
protected Integer id;
protected String name;
protected String lastName;
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public String getLastName() {
return lastName;
}
public class Student extends People {
private AcademicRecord academicRecord;
public Student(Integer id, String name, String lastName, AcademicRecord academicRecord) {
this.id = id;
this.name = name;
this.lastName = lastName;
this.academicRecord = academicRecord;
}
public AcademicRecord getAcademicRecord() {
return academicRecord;
}
public void setAcademicRecord(AcademicRecord academicRecord) {
this.academicRecord = academicRecord;
}
}
public class Teacher extends People{
private String discipline;
public Teacher(Integer id, String name, String lastName, String discipline) {
this.id = id;
this.name = name;
this.lastName = lastName;
this.discipline = discipline;
}
public String getDiscipline() {
return discipline;
}
public void setDiscipline(String discipline) {
this.discipline = discipline;
}
}
}
}
public interface Controller<T>{
boolean save(T o);
}
public class StudentController implements Controller<Student> {
@Override
public boolean save(Student studanty) {
System.out.printf("Registrando aluno %s %s...%n", studanty.getName(), studanty.getLastName());
return true;
}
}
public class TeacherController implements Controller<Teacher>{
@Override
public boolean save(Teacher teacher) {
System.out.printf("Registrando professor %s %s...%n", teacher.getName(), teacher.getLastName());
return true;
}
}
class Teste {
public static void main(String[] args) {
Service<Student> studentController = new Service<>(new StudentController());
Service<Teacher> teacherServiceController = new Service<>(new TeacherController());
boolean saveStudent = studentController.save(new Student(0, "Theodoro", "Smith", null));
boolean saveTeacher = teacherServiceController.save(new Teacher(0, "Rebeca", "Diaz", "Matematica"));
if (saveStudent){
System.out.println("O estudante foi registrado");
}
if (saveTeacher){
System.out.println("O professor(a) foi registrado");
}
}
}
public class Service<T> {
private final Controller<T> controller;
public Service(Controller<T> controller) {
this.controller = controller;
}
public boolean save( T entiy){
return controller.save(entiy);
}
}
Assim podemos trocar StudentController por TeacherController sem alterar Service, tornando o código mais flexível e escalável.