Módulo 01: Fundamentos y Buenas Prácticas

Separation Of Concerns

Arquitectura Software

# Separation of Concerns (Separación de Responsabilidades) ## ¿Qué es Separation of Concerns? La Separación de Responsabilidades es un principio de diseño que establece que diferentes partes de un sistema deben manejar diferentes aspectos del comportamiento del sistema. Cada componente debe tener una única responsabilidad bien definida. ## Principio Fundamental > "Un componente debe tener una única razón para cambiar" ## Visualización del Concepto ```mermaid graph TD subgraph "❌ Monolithic / Spaghetti Code" M[UI + Logic + Data + Parsing] M --> DB[(Database)] end subgraph "✅ Separated Concerns" UI[UI Layer] --> Logic[Business Logic] Logic --> Data[Data Layer] Data --> DB2[(Database)] end style M fill:#f99,stroke:#333,stroke-width:2px style UI fill:#bbf,stroke:#333,stroke-width:2px style Logic fill:#dfd,stroke:#333,stroke-width:2px style Data fill:#fdb,stroke:#333,stroke-width:2px ``` ## ¿Por qué es Importante en Flutter? ### ❌ Sin Separación de Responsabilidades ```dart class UserScreen extends StatefulWidget { @override _UserScreenState createState() => _UserScreenState(); } class _UserScreenState extends State { User? user; bool isLoading = false; String? error; @override void initState() { super.initState(); _loadUser(); } Future _loadUser() async { setState(() => isLoading = true); try { // ❌ Lógica de red mezclada con UI final response = await http.get( Uri.parse('https://api.example.com/users/1'), ); if (response.statusCode == 200) { // ❌ Parsing mezclado con UI final json = jsonDecode(response.body); setState(() { user = User( id: json['id'], name: json['name'], email: json['email'], ); isLoading = false; }); } else { setState(() { error = 'Error loading user'; isLoading = false; }); } } catch (e) { setState(() { error = e.toString(); isLoading = false; }); } } @override Widget build(BuildContext context) { if (isLoading) { return Center(child: CircularProgressIndicator()); } if (error != null) { return Center(child: Text(error!)); } return Column( children: [ Text(user?.name ?? ''), Text(user?.email ?? ''), ], ); } } ``` **Problemas:** - UI, lógica de negocio y acceso a datos en un solo archivo - Imposible de testear unitariamente - Difícil de mantener - No reutilizable ### ✅ Con Separación de Responsabilidades #### 1. Capa de Datos (Data Layer) ```dart // data/datasources/user_remote_data_source.dart abstract class UserRemoteDataSource { Future getUser(String id); } class UserRemoteDataSourceImpl implements UserRemoteDataSource { final http.Client client; UserRemoteDataSourceImpl(this.client); @override Future getUser(String id) async { final response = await client.get( Uri.parse('https://api.example.com/users/$id'), ); if (response.statusCode == 200) { return UserModel.fromJson(jsonDecode(response.body)); } else { throw ServerException(); } } } // data/models/user_model.dart class UserModel extends User { UserModel({ required String id, required String name, required String email, }) : super(id: id, name: name, email: email); factory UserModel.fromJson(Map json) { return UserModel( id: json['id'], name: json['name'], email: json['email'], ); } Map toJson() { return { 'id': id, 'name': name, 'email': email, }; } } ``` #### 2. Capa de Dominio (Domain Layer) ```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, }); } // domain/repositories/user_repository.dart abstract class UserRepository { Future> getUser(String id); } // domain/usecases/get_user.dart class GetUser { final UserRepository repository; GetUser(this.repository); Future> call(String id) { return repository.getUser(id); } } ``` #### 3. Capa de Presentación (Presentation Layer) ##### BLoC (Business Logic Component) ```dart // presentation/bloc/user_bloc.dart class UserBloc extends Bloc { final GetUser getUser; UserBloc({required this.getUser}) : super(UserInitial()) { on(_onLoadUser); } Future _onLoadUser( LoadUser event, Emitter emit, ) async { emit(UserLoading()); final result = await getUser(event.userId); result.fold( (failure) => emit(UserError(message: _mapFailureToMessage(failure))), (user) => emit(UserLoaded(user: user)), ); } String _mapFailureToMessage(Failure failure) { switch (failure.runtimeType) { case ServerFailure: return 'Server error. Please try again later.'; case NetworkFailure: return 'No internet connection.'; default: return 'Unexpected error occurred.'; } } } // presentation/bloc/user_event.dart abstract class UserEvent {} class LoadUser extends UserEvent { final String userId; LoadUser(this.userId); } // presentation/bloc/user_state.dart abstract class UserState {} class UserInitial extends UserState {} class UserLoading extends UserState {} class UserLoaded extends UserState { final User user; UserLoaded({required this.user}); } class UserError extends UserState { final String message; UserError({required this.message}); } ``` ##### Vista (UI) ```dart // presentation/pages/user_page.dart class UserPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('User Profile')), body: BlocBuilder( builder: (context, state) { if (state is UserLoading) { return Center(child: CircularProgressIndicator()); } if (state is UserError) { return Center(child: Text(state.message)); } if (state is UserLoaded) { return UserProfileWidget(user: state.user); } return SizedBox.shrink(); }, ), ); } } // presentation/widgets/user_profile_widget.dart class UserProfileWidget extends StatelessWidget { final User user; const UserProfileWidget({required this.user}); @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( user.name, style: Theme.of(context).textTheme.headline5, ), SizedBox(height: 8), Text( user.email, style: Theme.of(context).textTheme.bodyText1, ), ], ), ); } } ``` ## Responsabilidades Separadas | Capa | Responsabilidad | No Debe Contener | |------|----------------|------------------| | **Data** | Obtener/guardar datos de fuentes externas | Widgets, lógica de negocio | | **Domain** | Reglas de negocio, entidades | Dependencias de Flutter, detalles de implementación | | **Presentation** | Mostrar UI, manejar interacciones | Llamadas directas a API, lógica de negocio | ## Beneficios ### 1. **Testeable** ```dart // Cada capa se puede testear independientemente test('should emit UserLoaded when data is retrieved successfully', () async { // Arrange when(mockGetUser(any)) .thenAnswer((_) async => Right(tUser)); // Act bloc.add(LoadUser('1')); // Assert await expectLater( bloc.stream, emitsInOrder([ UserLoading(), UserLoaded(user: tUser), ]), ); }); ``` ### 2. **Reutilizable** ```dart // El mismo GetUser usecase puede usarse en diferentes widgets class UserListPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (_) => UserListBloc( getUser: sl(), // ← Mismo usecase reutilizado ), child: UserListView(), ); } } ``` ### 3. **Mantenible** ```dart // Cambiar de http a dio solo afecta UserRemoteDataSourceImpl // No se toca ni el dominio ni la presentación class UserRemoteDataSourceImpl implements UserRemoteDataSource { final Dio dio; // Cambio fácil desde http.Client @override Future getUser(String id) async { final response = await dio.get('/users/$id'); return UserModel.fromJson(response.data); } } ``` ## Patrones de Separación en Flutter ### MVC (Model-View-Controller) ``` Model: Datos y lógica de negocio View: Widgets de Flutter Controller: Coordina modelo y vista ``` ### MVVM (Model-View-ViewModel) ``` Model: Datos y repositorios View: Widgets de Flutter ViewModel: Estado y lógica de presentación ``` ### BLoC/Cubit ``` Bloc: Lógica de negocios UI: Widgets que escuchan el Bloc Repository: Acceso a datos ``` ## Anti-Patrones Comunes ### ❌ God Objects ```dart // ❌ Una clase que hace todo class UserManager { fetchUser() {} saveUser() {} validateUser() {} displayUser() {} sendEmail() {} // ... 20 métodos más } ``` ### ❌ Tight Coupling ```dart // ❌ Acoplamiento fuerte class UserWidget extends StatelessWidget { Widget build(BuildContext context) { final user = FirebaseDatabase.instance.ref('users/1').get(); // ¡Dependencia directa! return Text(user.name); } } ``` ## Conclusión La Separación de Responsabilidades no es solo una buena práctica, es **esencial** para crear aplicaciones Flutter escalables y mantenibles. Cada capa debe: 1. **Tener una responsabilidad clara** 2. **No depender de detalles de implementación** 3. **Ser fácilmente testeable** 4. **Poder cambiar independientemente** > "El código es leído mucho más veces de las que es escrito. Organízalo para facilitar su lectura y mantenimiento."

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.