S.O.L.I.D no FLutter
S - Princípio da Responsabilidade Única (Single Responsibility Principle - SRP): Este princípio afirma que uma classe deve ter apenas um motivo para mudar. Caso tenha mais de uma responsabilidade, deve ser dividida em duas ou mais classes.
O - Princípio do Aberto e Fechado (Open/Closed Principle - OCP): Este princípio diz que uma classe deve estar aberta a extensões, mas fechada para alterações.
L - Princípio da Substituição de Liskov (Liskov Substitution Principle - LSP): Este princípio define que uma classe que herda de outra deve ser capaz de substituí-la sem afetar o funcionamento do programa.
I - Princípio da Segregação de Interface (Interface Segregation Principle - ISP): Este princípio diz que uma classe não deve ser forçada a implementar interfaces que não utiliza.
D - Princípio da Inversão de Dependência (Dependency Inversion Principle - DIP): Este princípio diz que classes de alto nível não devem depender de classes de baixo nível. Ambas devem depender de abstrações.
A arquitetura que eu escolhi para exemplificar é a Clean Architecture, geralmente utilizada com o gerenciador de estados BLoC/Cubit
lib/
├── features/
│ └── products/
│ ├── data/
│ │ ├── datasources/ (Fontes de dados: API, local)
│ │ ├── models/ (Modelos de dados para serialização)
│ │ └── repositories/ (Implementação do repositório)
│ ├── domain/
│ │ ├── entities/ (Objetos de negócio puros)
│ │ ├── repositories/ (Contratos/Interfaces dos repositórios)
│ │ └── usecases/ (Casos de uso/Regras de negócio)
│
└── presentation/
│ ├── bloc/ (ou cubit, provider, etc.)
│ └── pages/ (Widgets da UI)
└── core/
└── ... (utils, error handling)
Para exemplificar o primeiro princípio (S), implementei uma classe ProductRepositoryImpl
que implementa a interface ProductRepository
. A única função dessa classe é retornar os dados do banco. Além disso, o código também obedece ao princípio da segregação de interface (I), pois a classe implementa somente a interface ProductRepository
, que é específica para a sua necessidade.
Expondo um panorama geral da arquitetura, a camada Domain tem a única responsabilidade de conter as entidades e as regras de negócio (casos de uso). Já a camada Data tem a responsabilidade de implementar os contratos (interfaces) definidos pelo Domain, buscando os dados de fontes externas. A camada de Apresentação (UI) tem a única responsabilidade de apresentar as informações ao usuário. Ou seja, a Clean Architecture já força a utilização do primeiro princípio do SOLID.
class ProductRepositoryImpl implements IProductRepository {
final ProductRemoteDataSource remoteDataSource;
ProductRepositoryImpl(this.remoteDataSource);
@override Future<List<ProductEntity>> getProducts() async {
return await remoteDataSource.fetchProducts();
}
}
Para o segundo principio (S) escrevi a classe abstrata a seguir:
// data/datasources/product_datasource.dart
abstract class IProductDataSource {
Future<List<ProductModel>> fetchProducts();
}
E implementei nas seguintes classes concretas:
// data/datasources/product_api_datasource.dart
class ProductApiDataSource implements IProductDataSource { ... }
// data/datasources/product_local_datasource.dart
class ProductLocalDataSource implements IProductDataSource { ... }
// NOVA FONTE DE DADOS (extensão)
class ProductFirebaseDataSource implements IProductDataSource { ... }
Com uma implementação de classe que depende de uma abstração, não importa a fonte de dados (seja Firebase, SQLite ou uma API), pois sua única função é implementar a funcionalidade getProducts()
.
Além de cumprir com o princípio do Aberto/Fechado (O), esse exemplo também obedece ao princípio da Inversão de Dependência (D).
// data/repositories/product_repository_impl.dart
class ProductRepositoryImpl implements IProductRepository {
final IProductDataSource dataSource; // Depende da abstração
ProductRepositoryImpl(this.dataSource);
@override
Future<List<ProductEntity>> getProducts() async {
// Apenas chama o método da abstração, sem se preocupar com a implementação.
final productModels = await dataSource.fetchProducts();
return productModels.map((model) => model.toEntity()).toList();
}
}
Portanto, para concluir, creio que esses exemplos já demonstram o poder do S.O.L.I.D. Ao depender de abstrações, a manutenção futura será muito mais facilitada. Desse modo, mantemos nosso código escalável, ainda que tenhamos que mudar nossa fonte de dados.
Ademais, a Inversão de Dependência facilitará a implementação de testes futuros, pois nos permite injetar dependências falsas (mocks) diretamente na instância da classe, tornando nossa aplicação mais confiável. Outro fator importante é o Princípio da Responsabilidade Única, que também facilita a manutenção do app e, consequentemente, a implementação de testes unitários.