# 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.