Módulo 01: Fundamentos y Buenas Prácticas

Clean Architecture

Arquitectura Software

⏱️ Tiempo de lectura: ~15 min 📊 Nivel: Avanzado 🔄 Actualizado: Dic 2025
# Clean Architecture ## Introducción Clean Architecture es un patrón arquitectónico propuesto por Robert C. Martin (Uncle Bob) que busca crear sistemas que sean: - **Independientes de frameworks** - **Testeables** - **Independientes de la UI** - **Independientes de la base de datos** - **Independientes de cualquier agente externo** ## Diagrama de Capas ```mermaid graph TD subgraph "Frameworks & Drivers (External)" UI[UI / Web / DB] end subgraph "Interface Adapters" Controllers[Controllers / Presenters / Gateways] end subgraph "Application Business Rules" UseCases[Use Cases] end subgraph "Enterprise Business Rules" Entities[Entities] end UI --> Controllers Controllers --> UseCases UseCases --> Entities style Entities fill:#f9f,stroke:#333,stroke-width:2px style UseCases fill:#bbf,stroke:#333,stroke-width:2px style Controllers fill:#dfd,stroke:#333,stroke-width:2px style UI fill:#eef,stroke:#333,stroke-width:2px ``` ## Principios Fundamentales ### 1. La Regla de Dependencia > Las dependencias del código fuente deben apuntar solo hacia adentro, hacia políticas de más alto nivel. ```dart // ❌ INCORRECTO: La capa de dominio depende de la infraestructura class UserRepository { final HttpClient client; // ¡Dependencia externa! Future getUser(String id) async { final response = await client.get('/users/$id'); return User.fromJson(response.data); } } // ✅ CORRECTO: Inversión de dependencias abstract class UserRepository { Future getUser(String id); } class UserRepositoryImpl implements UserRepository { final HttpClient client; UserRepositoryImpl(this.client); @override Future getUser(String id) async { final response = await client.get('/users/$id'); return User.fromJson(response.data); } } ``` ### 2. Capas de Clean Architecture #### Capa de Dominio (Entities) Contiene la lógica de negocio empresarial pura. ```dart // domain/entities/user.dart class User { final String id; final String name; final String email; User({ required this.id, required this.name, required this.email, }); // Lógica de negocio pura bool get hasValidEmail => email.contains('@') && email.contains('.'); String get displayName => name.isEmpty ? email : name; } ``` #### Capa de Casos de Uso (Use Cases) Contiene la lógica de negocio de la aplicación. ```dart // domain/usecases/get_user.dart class GetUser { final UserRepository repository; GetUser(this.repository); Future> call(String userId) async { try { final user = await repository.getUser(userId); return Right(user); } on ServerException { return Left(ServerFailure()); } } } ``` #### Capa de Adaptadores (Data) Implementa las interfaces definidas en el dominio. ```dart // data/repositories/user_repository_impl.dart class UserRepositoryImpl implements UserRepository { final UserRemoteDataSource remoteDataSource; final UserLocalDataSource localDataSource; final NetworkInfo networkInfo; UserRepositoryImpl({ required this.remoteDataSource, required this.localDataSource, required this.networkInfo, }); @override Future getUser(String id) async { if (await networkInfo.isConnected) { final userModel = await remoteDataSource.getUser(id); await localDataSource.cacheUser(userModel); return userModel.toEntity(); } else { final cachedUser = await localDataSource.getCachedUser(id); return cachedUser.toEntity(); } } } ``` #### Capa de Presentación (UI) Maneja la interfaz de usuario y la lógica de presentación. ```dart // presentation/bloc/user_bloc.dart class UserBloc extends Bloc { final GetUser getUser; UserBloc({required this.getUser}) : super(UserInitial()) { on(_onGetUserRequested); } Future _onGetUserRequested( GetUserRequested event, Emitter emit, ) async { emit(UserLoading()); final result = await getUser(event.userId); result.fold( (failure) => emit(UserError(message: failure.message)), (user) => emit(UserLoaded(user: user)), ); } } ``` ## Estructura de Carpetas en Flutter ``` lib/ ├── core/ │ ├── error/ │ │ ├── exceptions.dart │ │ └── failures.dart │ ├── network/ │ │ └── network_info.dart │ └── usecases/ │ └── usecase.dart ├── features/ │ └── user/ │ ├── data/ │ │ ├── datasources/ │ │ │ ├── user_local_data_source.dart │ │ │ └── user_remote_data_source.dart │ │ ├── models/ │ │ │ └── user_model.dart │ │ └── repositories/ │ │ └── user_repository_impl.dart │ ├── domain/ │ │ ├── entities/ │ │ │ └── user.dart │ │ ├── repositories/ │ │ │ └── user_repository.dart │ │ └── usecases/ │ │ └── get_user.dart │ └── presentation/ │ ├── bloc/ │ │ ├── user_bloc.dart │ │ ├── user_event.dart │ │ └── user_state.dart │ ├── pages/ │ │ └── user_page.dart │ └── widgets/ │ └── user_card.dart └── main.dart ``` ## Beneficios en Flutter ### ✅ Testabilidad Mejorada ```dart // test/features/user/domain/usecases/get_user_test.dart void main() { late GetUser usecase; late MockUserRepository mockRepository; setUp(() { mockRepository = MockUserRepository(); usecase = GetUser(mockRepository); }); test('should get user from repository', () async { // Arrange final tUser = User(id: '1', name: 'Test', email: '[email protected]'); when(mockRepository.getUser('1')) .thenAnswer((_) async => tUser); // Act final result = await usecase('1'); // Assert expect(result, Right(tUser)); verify(mockRepository.getUser('1')); verifyNoMoreInteractions(mockRepository); }); } ``` ### ✅ Mantenibilidad El cambio de un framework (ej: cambiar de dio a http) solo afecta a la capa de datos: ```dart // Solo necesitas cambiar la implementación del data source class UserRemoteDataSourceImpl implements UserRemoteDataSource { // Cambiar de Dio a http package final http.Client client; // Antes: final Dio dio; UserRemoteDataSourceImpl(this.client); @override Future getUser(String id) async { final response = await client.get( Uri.parse('$baseUrl/users/$id'), ); if (response.statusCode == 200) { return UserModel.fromJson(json.decode(response.body)); } else { throw ServerException(); } } } ``` ### ✅ Escalabilidad Agregar nuevas features es simple y no afecta el código existente: ```dart // Nueva feature: Posts // Solo replica la estructura en features/posts/ features/ ├── user/ └── posts/ // Nueva feature ├── data/ ├── domain/ └── presentation/ ``` ## Mejores Prácticas 1. **Mantén las entidades puras**: Sin dependencias de Flutter 2. **Usa interfaces para todo**: Facilita el testing y el cambio de implementaciones 3. **Separa modelos de entidades**: Los modelos manejan serialización, las entidades lógica de negocio 4. **Inyección de dependencias**: Usa get_it o injectable 5. **Manejo de errores consistente**: Either o Result types ## ⚠️ Errores Comunes y Soluciones ### Error 1: Dependencia Invertida Incorrectamente **Código problemático:** ```dart // domain/usecases/get_user.dart import 'package:http/http.dart'; // ❌ Dominio depende de infraestructura! class GetUser { final Client client; GetUser(this.client); Future call(String id) async { final response = await client.get(Uri.parse('/users/$id')); return User.fromJson(jsonDecode(response.body)); } } ``` **Síntoma:** Acoplamiento fuerte entre capas, difícil de testear. **Causa:** La capa de dominio depende directamente de implementaciones de infraestructura. **Solución:** ```dart // domain/repositories/user_repository.dart abstract class UserRepository { Future getUser(String id); } // domain/usecases/get_user.dart class GetUser { final UserRepository repository; // ✅ Depende de abstracción GetUser(this.repository); Future> call(String id) async { try { final user = await repository.getUser(id); return Right(user); } catch (e) { return Left(ServerFailure()); } } } // data/repositories/user_repository_impl.dart class UserRepositoryImpl implements UserRepository { final Client client; // ✅ Solo en capa de datos // ... } ``` --- ### Error 2: Lógica de Negocio en Presentación **Código problemático:** ```dart // presentation/pages/user_page.dart class UserPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { if (state is UserLoaded) { final user = state.user; // ❌ Validación de negocio en UI! if (user.age < 18 && user.country=='US' ) { return ErrorWidget('Not allowed'); } return UserCard(user: user); } return LoadingWidget(); }, ); } } ``` **Síntoma:** Lógica de negocio duplicada, difícil de mantener. **Solución:** ```dart // domain/entities/user.dart class User { final int age; final String country; // ✅ Lógica de negocio en entidad bool get canAccessContent { if (country=='US' && age < 18) return false; if (country=='UK' && age < 16) return false; return true; } } // presentation/pages/user_page.dart Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { if (state is UserLoaded) { final user = state.user; // ✅ Solo usa la lógica, no la define if (!user.canAccessContent) { return ErrorWidget('Not allowed'); } return UserCard(user: user); } return LoadingWidget(); }, ); } ``` --- ### Error 3: Modelos y Entidades Mezclados **Código problemático:** ```dart // domain/entities/user.dart class User { final String id; final String name; // ❌ Serialización en entidad de dominio! factory User.fromJson(Map json) { return User( id: json['id'], name: json['name'], ); } Map toJson() { return {'id': id, 'name': name}; } } ``` **Síntoma:** Entidades acopladas al formato de datos externo. **Solución:** ```dart // domain/entities/user.dart class User { final String id; final String name; User({required this.id, required this.name}); // ✅ Solo lógica de negocio bool get hasValidName => name.isNotEmpty; } // data/models/user_model.dart class UserModel extends User { UserModel({required String id, required String name}) : super(id: id, name: name); // ✅ Serialización en modelo de datos factory UserModel.fromJson(Map json) { return UserModel( id: json['user_id'], // Puede mapear nombres diferentes name: json['full_name'], ); } Map toJson() { return {'user_id': id, 'full_name': name}; } // ✅ Conversión a entidad User toEntity() => User(id: id, name: name); } ``` ## Conclusión Clean Architecture en Flutter permite crear aplicaciones robustas, testeables y mantenibles. Aunque requiere más código inicial, los beneficios a largo plazo superan ampliamente el costo inicial. **Recuerda**: La arquitectura debe servir al proyecto, no al revés. Para proyectos pequeños, una versión simplificada puede ser suficiente. ## 📖 Recursos Adicionales ### 📄 Artículos Recomendados - [Clean Architecture in Flutter](https://resocoder.com/2019/08/27/flutter-tdd-clean-architecture-course-1-explanation-project-structure/) - Reso Coder - [Official Flutter Architecture Guide](https://docs.flutter.dev/development/data-and-backend/state-mgmt/options) - Flutter Docs - [Clean Architecture by Uncle Bob](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) - Robert C. Martin ### 🎥 Videos - [Flutter Clean Architecture Course](https://www.youtube.com/watch?v=KjE2IDphA_U) - Reso Coder - [Flutter Architecture Blueprints](https://www.youtube.com/watch?v=zI4Rl_S7rw8) - Flutter Team ### 💻 Repositorios de Ejemplo - [flutter_tdd_clean_architecture](https://github.com/ResoCoder/flutter-tdd-clean-architecture-course) - Reso Coder - [flutter_architecture_samples](https://github.com/brianegan/flutter_architecture_samples) - Brian Egan - [clean_architecture_flutter](https://github.com/ShadyBoukhary/flutter_clean_architecture) - Shady Boukhary ### 💬 Comunidades - [r/FlutterDev](https://reddit.com/r/FlutterDev) - Reddit Flutter Community - [Flutter Discord](https://discord.gg/flutter) - Official Flutter Discord - [Stack Overflow - flutter tag](https://stackoverflow.com/questions/tagged/flutter) ### 📚 Libros - "Clean Architecture" - Robert C. Martin - "Domain-Driven Design" - Eric Evans - "Patterns of Enterprise Application Architecture" - Martin Fowler

Este apartado profundiza en los conceptos clave, proporcionando ejemplos prácticos y mejores prácticas para su aplicación en proyectos reales.

Al comprender estos detalles, podrás diseñar soluciones más robustas, mantenibles y escalables en tus aplicaciones Flutter y Dart.

¿Te resultó útil esta lección?