DEV Community

Cover image for 🚀 Revisando o S.O.L.I.D na Prática com Flutter e Dart
Jefferson Rodrigues
Jefferson Rodrigues

Posted on

🚀 Revisando o S.O.L.I.D na Prática com Flutter e Dart

🚀 Revisando o S.O.L.I.D na Prática com Flutter e Dart

🎯 Introdução

Resolvi revisar os meus conhecimentos sobre temas essenciais da programação. Iniciei pelo SOLID porque é algo que deve ser revisitado de vez em quando para garantir que estamos entendendo não só o conceito, mas a prática também.

Com isso, podemos escrever códigos mais limpos, escaláveis e de fácil entendimento.


🛠️ Metodologia

Usei como base dois repositórios meus de quando eu estava estudando Clean Architecture. Um deles trata-se de um app simples: buscar conselhos em uma API e retornar para o usuário.

Neste projeto, aplico cada um dos cinco princípios do SOLID. Abaixo, detalho como cada um deles se comporta no código.


🟢 S — Single Responsibility Principle (SRP)

Princípio da Responsabilidade Única: Uma classe deve ter um, e apenas um, motivo para mudar.

No exemplo abaixo, a classe abstrata para o data source define apenas o contrato de busca remota. Ela não se preocupa com lógica de negócio ou como os dados serão exibidos.

abstract class AdviceRemoteDataSource {
  Future<Advice>? getAdvice();
}

Enter fullscreen mode Exit fullscreen mode

Outro exemplo claro é o tratamento de erros. Criamos uma estrutura base e especializamos as falhas:

abstract class Failure extends Equatable {
  const Failure([List properties = const <dynamic>[]]) : super();
}

// Responsável apenas por erros de servidor
class ServerFailure extends Failure {
  @override
  List<Object?> get props => [];
}

// Responsável apenas por erros de cache local
class CacheFailure extends Failure {
  @override
  List<Object?> get props => [];
}

Enter fullscreen mode Exit fullscreen mode

🔵 O — Open/Closed Principle (OCP)

Aberto para Extensão, Fechado para Modificação: Você deve ser capaz de estender o comportamento de uma classe sem modificar o seu código-fonte original.

A classe AdviceRepository está "fechada" (o contrato está definido). Se precisarmos de uma nova forma de obter dados, apenas implementamos o contrato em uma nova classe, sem tocar na interface original.

abstract class AdviceRepository {
  Future<Either<Failure, Advice?>>? getAdvice();
}

class AdviceRepositoryImpl implements AdviceRepository {
  final AdviceRemoteDataSource remoteDataSource;

  AdviceRepositoryImpl({required this.remoteDataSource});

  @override
  Future<Either<Failure, Advice?>>? getAdvice() async {
    try {
      final remoteAdvice = await remoteDataSource.getAdvice();
      return Right(remoteAdvice);
    } on ServerException {
      return Left(ServerFailure());
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

🟡 L — Liskov Substitution Principle (LSP)

Princípio da Substituição de Liskov: Objetos de uma superclasse devem ser substituíveis por objetos de suas subclasses sem quebrar a aplicação.

No Flutter, isso é muito comum com os DataSources. A implementação AdviceRemoteDataSourceImpl pode substituir perfeitamente a abstração AdviceRemoteDataSource em qualquer lugar que ela seja exigida.

class AdviceRemoteDataSourceImpl implements AdviceRemoteDataSource {
  final http.Client client;
  AdviceRemoteDataSourceImpl({required this.client});

  @override
  Future<Advice>? getAdvice() async {
    // Implementação real da chamada HTTP
    // ...
  }
}

Enter fullscreen mode Exit fullscreen mode

💡 Obs.: Uma melhoria seria criar um AdviceDataSource genérico, permitindo trocar o Remote pelo Local de forma transparente.


🟠 I — Interface Segregation Principle (ISP)

Segregação de Interface: Uma classe não deve ser forçada a implementar interfaces e métodos que não utiliza.

Em vez de criar uma interface gigante "TudoNoApp", dividimos em interfaces específicas. No exemplo, o repositório depende apenas do que é essencial para ele: o DataSource de conselhos.

// Interface específica e focada no domínio de conselhos
abstract class AdviceRemoteDataSource {
  Future<Advice>? getAdvice();
}

Enter fullscreen mode Exit fullscreen mode

🔴 D — Dependency Inversion Principle (DIP)

Inversão de Dependência: Dependa de abstrações, não de implementações concretas.

Aqui é onde o GetIt ou Provider brilham. O AdviceRepositoryImpl não conhece a implementação do DataSource, ele conhece apenas o contrato. A "mágica" acontece no nosso Service Locator:

void setupLocator() {
  // Registramos a abstração apontando para a implementação
  getIt.registerLazySingleton<AdviceRepository>(
    () => AdviceRepositoryImpl(remoteDataSource: getIt()),
  );

  getIt.registerLazySingleton<AdviceRemoteDataSource>(
      () => AdviceRemoteDataSourceImpl(client: getIt()));

  getIt.registerLazySingleton(() => http.Client());
}

Enter fullscreen mode Exit fullscreen mode

📊 Resumo Visual

Sigla Princípio Resumo em uma frase
S Responsabilidade Única Cada classe faz apenas uma coisa.
O Aberto/Fechado Estenda o comportamento sem alterar o original.
L Subst. de Liskov Subclasses devem honrar o contrato da base.
I Segregação de Interface Crie interfaces pequenas e específicas.
D Inversão de Dependência Dependa de contratos, não de classes concretas.

🏁 Conclusão

Utilizar esses conceitos na prática vai além de construir "códigos bonitos". Trata-se de garantir a manutenibilidade. Um código bem estruturado permite que ele viva mais e que as manutenções futuras não sejam um sofrimento.

Lembre-se: o seu código fala muito sobre você! 🚀

Ficou com alguma dúvida sobre como aplicar isso no seu projeto Flutter? Comenta aqui embaixo e vamos trocar uma ideia!

Top comments (0)