Módulo 01: Fundamentos y Buenas Prácticas

Solid

Principios Diseno

# Principios SOLID en Dart y Flutter Los principios SOLID son cinco principios de diseño orientado a objetos que hacen que el software sea más comprensible, flexible y mantenible. ## Resumen Visual ```mermaid graph TD SOLID[SOLID Principles] SRP[Single Responsibility] OCP[Open/Closed] LSP[Liskov Substitution] ISP[Interface Segregation] DIP[Dependency Inversion] SOLID --> SRP SOLID --> OCP SOLID --> LSP SOLID --> ISP SOLID --> DIP SRP --> SRP_Desc[One reason to change] OCP --> OCP_Desc[Open for extension, closed for mod] LSP --> LSP_Desc[Subtypes must be substitutable] ISP --> ISP_Desc[Client specific interfaces] DIP --> DIP_Desc[Depend on abstractions] style SOLID fill:#f9f,stroke:#333,stroke-width:2px style SRP fill:#bbf,stroke:#333,stroke-width:2px style OCP fill:#bbf,stroke:#333,stroke-width:2px style LSP fill:#bbf,stroke:#333,stroke-width:2px style ISP fill:#bbf,stroke:#333,stroke-width:2px style DIP fill:#bbf,stroke:#333,stroke-width:2px ``` ## S - Single Responsibility Principle (SRP) ### Principio de Responsabilidad Única > "Una clase debe tener una, y solo una, razón para cambiar" ### ❌ Violación del SRP ```dart class UserManager { // ❌ Múltiples responsabilidades en una clase // Responsabilidad 1: Validación bool validateEmail(String email) { return email.contains('@'); } // Responsabilidad 2: Persistencia Future saveToDatabase(User user) async { final db = await database; await db.insert('users', user.toMap()); } // Responsabilidad 3: Networking Future fetchFromApi(String id) async { final response = await http.get(Uri.parse('/users/$id')); return User.fromJson(json.decode(response.body)); } // Responsabilidad 4: Email Future sendWelcomeEmail(User user) async { await emailService.send(to: user.email, subject: 'Welcome!'); } } ``` ### ✅ Aplicando SRP ```dart // Responsabilidad 1: Validación class UserValidator { bool validateEmail(String email) { final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); return emailRegex.hasMatch(email); } bool validatePassword(String password) { return password.length >= 8; } } // Responsabilidad 2: Persistencia class UserLocalDataSource { final Database database; UserLocalDataSource(this.database); Future saveUser(UserModel user) async { await database.insert('users', user.toMap()); } Future getUser(String id) async { final maps = await database.query('users', where: 'id = ?', whereArgs: [id]); if (maps.isEmpty) return null; return UserModel.fromMap(maps.first); } } // Responsabilidad 3: Networking class UserRemoteDataSource { final http.Client client; UserRemoteDataSource(this.client); Future fetchUser(String id) async { final response = await client.get(Uri.parse('/users/$id')); if (response.statusCode == 200) { return UserModel.fromJson(json.decode(response.body)); } throw ServerException(); } } // Responsabilidad 4: Notificaciones class EmailService { Future sendWelcomeEmail(String email, String name) async { // Lógica de envío de email } } ``` ## O - Open/Closed Principle (OCP) ### Principio Abierto/Cerrado > "Las entidades de software deben estar abiertas para extensión, pero cerradas para modificación" ### ❌ Violación del OCP ```dart class PaymentProcessor { void processPayment(String type, double amount) { if (type == 'credit_card') { // Procesar tarjeta de crédito print('Processing credit card payment: \$amount'); } else if (type == 'paypal') { // Procesar PayPal print('Processing PayPal payment: \$amount'); } else if (type == 'bitcoin') { // ❌ Cada vez que agregamos un método de pago, modificamos esta clase print('Processing Bitcoin payment: \$amount'); } } } ``` ### ✅ Aplicando OCP ```dart // Abstracción abstract class PaymentMethod { Future process(double amount); String get name; } // Implementaciones concretas (extensiones) class CreditCardPayment implements PaymentMethod { final String cardNumber; final String cvv; CreditCardPayment({required this.cardNumber, required this.cvv}); @override String get name => 'Credit Card'; @override Future process(double amount) async { // Lógica específica de tarjeta de crédito print('Processing $name payment: \$$amount'); return PaymentResult.success(); } } class PayPalPayment implements PaymentMethod { final String email; PayPalPayment({required this.email}); @override String get name => 'PayPal'; @override Future process(double amount) async { // Lógica específica de PayPal print('Processing $name payment: \$$amount'); return PaymentResult.success(); } } // Nuevo método de pago (extensión sin modificar código existente) class BitcoinPayment implements PaymentMethod { final String walletAddress; BitcoinPayment({required this.walletAddress}); @override String get name => 'Bitcoin'; @override Future process(double amount) async { print('Processing $name payment: \$$amount'); return PaymentResult.success(); } } // Procesador que trabaja con la abstracción class PaymentProcessor { Future processPayment(PaymentMethod method, double amount) { return method.process(amount); } } // Uso final processor = PaymentProcessor(); await processor.processPayment(CreditCardPayment(cardNumber: '...', cvv: '...'), 100.0); await processor.processPayment(PayPalPayment(email: '[email protected]'), 50.0); await processor.processPayment(BitcoinPayment(walletAddress: '...'), 200.0); ``` ## L - Liskov Substitution Principle (LSP) ### Principio de Sustitución de Liskov > "Los objetos de una superclase deben ser reemplazables por objetos de sus subclases sin romper la aplicación" ### ❌ Violación del LSP ```dart class Rectangle { double width; double height; Rectangle(this.width, this.height); double get area => width * height; } class Square extends Rectangle { Square(double size) : super(size, size); // ❌ Viola LSP: cambiar el comportamiento esperado @override set width(double value) { super.width = value; super.height = value; // Cambio inesperado } @override set height(double value) { super.width = value; // Cambio inesperado super.height = value; } } // Problema void printArea(Rectangle rectangle) { rectangle.width = 5; rectangle.height = 4; print('Expected area: 20, Got: ${rectangle.area}'); // Si es Square, obtendremos 16 en lugar de 20! } ``` ### ✅ Aplicando LSP ```dart // Abstracción común abstract class Shape { double get area; } class Rectangle implements Shape { final double width; final double height; Rectangle(this.width, this.height); @override double get area => width * height; Rectangle copyWith({double? width, double? height}) { return Rectangle( width ?? this.width, height ?? this.height, ); } } class Square implements Shape { final double size; Square(this.size); @override double get area => size * size; Square copyWith({double? size}) { return Square(size ?? this.size); } } // Ahora podemos usar cualquier Shape sin problemas void printArea(Shape shape) { print('Area: ${shape.area}'); } printArea(Rectangle(5, 4)); // OK: 20 printArea(Square(4)); // OK: 16 ``` ## I - Interface Segregation Principle (ISP) ### Principio de Segregación de Interfaces > "Los clientes no deberían verse forzados a depender de interfaces que no utilizan" ### ❌ Violación del ISP ```dart // Interfaz demasiado grande abstract class Worker { void work(); void eat(); void sleep(); void attendMeeting(); void writeCode(); void designUI(); } // Robot worker no necesita eat() ni sleep() class RobotWorker implements Worker { @override void work() => print('Robot working'); @override void eat() => throw UnimplementedError('Robots don\'t eat'); // ❌ @override void sleep() => throw UnimplementedError('Robots don\'t sleep'); // ❌ @override void attendMeeting() => print('Robot attending meeting'); @override void writeCode() => print('Robot writing code'); @override void designUI() => throw UnimplementedError(); // ❌ } ``` ### ✅ Aplicando ISP ```dart // Interfaces segregadas abstract class Workable { void work(); } abstract class Eatable { void eat(); } abstract class Sleepable { void sleep(); } abstract class MeetingAttendable { void attendMeeting(); } abstract class Codeable { void writeCode(); } abstract class Designable { void designUI(); } // Cada trabajador implementa solo lo que necesita class HumanDeveloper implements Workable, Eatable, Sleepable, MeetingAttendable, Codeable { @override void work() => print('Human working'); @override void eat() => print('Human eating'); @override void sleep() => print('Human sleeping'); @override void attendMeeting() => print('Human attending meeting'); @override void writeCode() => print('Human writing code'); } class RobotWorker implements Workable, MeetingAttendable, Codeable { @override void work() => print('Robot working'); @override void attendMeeting() => print('Robot attending meeting'); @override void writeCode() => print('Robot writing code'); // ✅ No implementa eat(), sleep(), ni designUI() porque no las necesita } class Designer implements Workable, Eatable, Sleepable, Designable { @override void work() => print('Designer working'); @override void eat() => print('Designer eating'); @override void sleep() => print('Designer sleeping'); @override void designUI() => print('Designer designing UI'); } ``` ## D - Dependency Inversion Principle (DIP) ### Principio de Inversión de Dependencias > "Depende de abstracciones, no de concreciones" ### ❌ Violación del DIP ```dart // Acoplamiento fuerte a implementaciones concretas class UserService { final MySQLDatabase database; // ❌ Dependencia concreta UserService(this.database); Future getUser(String id) async { return await database.query('SELECT * FROM users WHERE id = ?', [id]); } } // Si queremos cambiar a PostgreSQL, tenemos que modificar UserService ``` ### ✅ Aplicando DIP ```dart // Abstracción abstract class Database { Future> query(String sql, List params); } // Implementaciones concretas class MySQLDatabase implements Database { @override Future> query(String sql, List params) async { // Implementación MySQL return {}; } } class PostgreSQLDatabase implements Database { @override Future> query(String sql, List params) async { // Implementación PostgreSQL return {}; } } // UserService depende de la abstracción class UserService { final Database database; // ✅ Dependencia abstracta UserService(this.database); Future getUser(String id) async { final data = await database.query('SELECT * FROM users WHERE id = ?', [id]); return User.fromMap(data); } } // Podemos inyectar cualquier implementación final mysqlService = UserService(MySQLDatabase()); final postgresService = UserService(PostgreSQLDatabase()); ``` ## SOLID en Flutter: Ejemplo Completo ```dart // ======= DOMAIN LAYER ======= // S: User tiene una sola responsabilidad (representar datos de usuario) class User { final String id; final String name; final String email; User({required this.id, required this.name, required this.email}); } // D: Dependemos de abstracción, no de implementación concreta abstract class UserRepository { Future getUser(String id); Future saveUser(User user); } // ======= DATA LAYER ======= // O: Podemos extender agregando nuevos data sources sin modificar el repositorio abstract class UserDataSource { Future getUser(String id); Future saveUser(User user); } class UserRemoteDataSource implements UserDataSource { final http.Client client; UserRemoteDataSource(this.client); @override Future getUser(String id) async { final response = await client.get(Uri.parse('/users/$id')); return User.fromJson(json.decode(response.body)); } @override Future saveUser(User user) async { await client.post( Uri.parse('/users'), body: json.encode(user.toJson()), ); } } // L: UserRepositoryImpl puede sustituir a UserRepository sin problemas class UserRepositoryImpl implements UserRepository { final UserDataSource remoteDataSource; final UserDataSource localDataSource; UserRepositoryImpl({ required this.remoteDataSource, required this.localDataSource, }); @override Future getUser(String id) async { try { final user = await remoteDataSource.getUser(id); await localDataSource.saveUser(user); // Cache return user; } catch (e) { return await localDataSource.getUser(id); } } @override Future saveUser(User user) async { await remoteDataSource.saveUser(user); await localDataSource.saveUser(user); } } // ======= PRESENTATION LAYER ======= // I: UserBloc solo depende de las operaciones que necesita class UserBloc extends Bloc { final UserRepository repository; // Solo necesita el repositorio UserBloc({required this.repository}) : super(UserInitial()); // Implementación del bloc... } ``` ## Beneficios de SOLID en Flutter 1. **Código más mantenible** - Cambios localizados 2. **Más testeable** - Fácil mockear dependencias 3. **Más escalable** - Fácil agregar funcionalidad 4. **Más legible** - Responsabilidades claras 5. **Menos acoplamiento** - Componentes independientes ## Conclusión Los principios SOLID no son reglas estrictas, son guías para escribir mejor código. Úsalos cuando aporten valor, no por usar. La clave está en el equilibrio entre simplicidad y flexibilidad.

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.