Entregable: Código refactorizado en Rust + README explicativo.
La aplicación original tenía clases concretas (Guitar, Piano, ...) y una clase principal MusicStore que usaba condicionales (strings con match / if) para decidir qué instrumento instanciar y reproducir. Cada vez que se agregaba un nuevo instrumento había que modificar MusicStore y otras partes del código.
- Uso excesivo de condicionales para seleccionar tipos.
- Alto acoplamiento entre
MusicStorey las clases concretas. - Violación del principio Open/Closed: para añadir un tipo nuevo hay que cambiar código existente.
- Falta de abstracción común para los instrumentos.
- Dificultad para pruebas unitarias.
- Eliminar condicionales fuera de la fábrica.
- Introducir la abstracción
Instrumentcomo trait. - Implementar un Factory con registro (registro dinámico de constructores) para soportar Open/Closed.
MusicStoredependerá de una abstracción (InstrumentFactory) e inyectará la dependencia.- Facilitar pruebas mediante el uso de mocks/fakes implementando
Instrument.
- Factory Method / Registry-based Factory: Se expone un trait
InstrumentFactoryy una implementaciónRegistryFactoryque mantiene una tabla de constructores. Esto permite registrar nuevos instrumentos sin tocarMusicStore.
- Single Responsibility: Cada struct/trait tiene una responsabilidad clara:
Instrument(contrato),Guitar/Piano(implementación),RegistryFactory(creación/registro),MusicStore(uso de instrumentos). - Open/Closed:
RegistryFactorypermite añadir nuevas implementaciones registrando constructores, no cambiandoMusicStore. - Liskov: Las implementaciones de
Instrumentrespetan el contratoplay(). - Inversion of Dependencies:
MusicStoredepende de la abstracciónInstrumentFactoryen vez de clases concretas. - Dependency Inversion & Dependency Injection:
MusicStorerecibe el factory por constructor.
- Crear un nuevo proyecto con
cargo new music_store. - Reemplazar
src/main.rspor el contenido provisto. cargo runpara ver el ejemplo.cargo testpara ejecutar pruebas.
struct Guitar;
impl Guitar {
fn play(&self) {
println!("Strumming the guitar 🎸");
}
}
struct Piano;
impl Piano {
fn play(&self) {
println!("Playing the piano 🎹");
}
}
struct Drums;
impl Drums {
fn play(&self) {
println!("Beating the drums 🥁");
}
}
// Clase principal que decide qué instrumento crear
// Notar el uso de condicionales, violando Open/Closed
struct MusicStore;
impl MusicStore {
fn play_instrument(&self, kind: &str) {
if kind == "guitar" {
let g = Guitar;
g.play();
} else if kind == "piano" {
let p = Piano;
p.play();
} else if kind == "drums" {
let d = Drums;
d.play();
} else {
println!("Instrument not found ❌");
}
}
}
fn main() {
let store = MusicStore;
store.play_instrument("guitar");
store.play_instrument("piano");
store.play_instrument("drums");
store.play_instrument("violin"); // no existe, rompe la extensibilidad
}Problema Descripción ❌ Uso excesivo de condicionales Cada nuevo instrumento requiere agregar otro else if. ❌ Alto acoplamiento MusicStore depende directamente de Guitar, Piano, Drums. ❌ Violación de Open/Closed No se pueden agregar nuevos instrumentos sin modificar el código existente. ❌ Falta de abstracción No existe un trait común (Instrument) que unifique las implementaciones. ❌ Dificultad para pruebas No es posible simular instrumentos o usar inyección de dependencias.
A continuación se incluye una implementación completa en un solo archivo (src/main.rs) para facilitar la prueba.
Archivo: src/main.rs
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
// -------------------------
// Trait: Instrument (abstracción)
// -------------------------
pub trait Instrument: Send + Sync {
fn play(&self) -> String;
}
// -------------------------
// Implementaciones concretas
// -------------------------
pub struct Guitar {
pub model: String,
}
impl Guitar {
pub fn new(model: impl Into<String>) -> Self {
Guitar { model: model.into() }
}
}
impl Instrument for Guitar {
fn play(&self) -> String {
format!("Strumming the {} guitar 🎸", self.model)
}
}
pub struct Piano {
pub brand: String,
}
impl Piano {
pub fn new(brand: impl Into<String>) -> Self {
Piano { brand: brand.into() }
}
}
impl Instrument for Piano {
fn play(&self) -> String {
format!("Playing a {} piano 🎹", self.brand)
}
}
pub struct Drums {
pub kit_name: String,
}
impl Drums {
pub fn new(kit_name: impl Into<String>) -> Self {
Drums { kit_name: kit_name.into() }
}
}
impl Instrument for Drums {
fn play(&self) -> String {
format!("Beating the {} drums 🥁", self.kit_name)
}
}
// -------------------------
// Factory abstraction
// -------------------------
// Un constructor devuelve un Box<dyn Instrument>
type Constructor = dyn Fn() -> Box<dyn Instrument> + Send + Sync;
pub trait InstrumentFactory: Send + Sync {
fn create(&self, key: &str) -> Option<Box<dyn Instrument>>;
fn register(&self, key: &str, constructor: Box<Constructor>);
}
// -------------------------
// RegistryFactory: permite registrar constructores dinámicamente
// cumple Open/Closed — para agregar instrumentos se registra uno nuevo
// -------------------------
pub struct RegistryFactory {
registry: Mutex<HashMap<String, Box<Constructor>>>,
}
impl RegistryFactory {
pub fn new() -> Self {
RegistryFactory { registry: Mutex::new(HashMap::new()) }
}
}
impl InstrumentFactory for RegistryFactory {
fn create(&self, key: &str) -> Option<Box<dyn Instrument>> {
let registry = self.registry.lock().unwrap();
registry.get(key).map(|ctor| ctor())
}
fn register(&self, key: &str, constructor: Box<Constructor>) {
let mut registry = self.registry.lock().unwrap();
registry.insert(key.to_string(), constructor);
}
}
// -------------------------
// MusicStore: consumidor de InstrumentFactory
// -------------------------
pub struct MusicStore {
factory: Arc<dyn InstrumentFactory>,
}
impl MusicStore {
pub fn new(factory: Arc<dyn InstrumentFactory>) -> Self {
MusicStore { factory }
}
pub fn play_instrument(&self, kind: &str) -> Result<String, String> {
match self.factory.create(kind) {
Some(instr) => Ok(instr.play()),
None => Err(format!("Instrument '{}' not found in factory", kind)),
}
}
}
// -------------------------
// Ejemplo de uso en main
// -------------------------
fn main() {
let factory = Arc::new(RegistryFactory::new());
// Registramos constructores para cada tipo de instrumento
{ // scope para ejemplo de closures
let f = factory.clone();
f.register("guitar", Box::new(|| Box::new(Guitar::new("Fender Stratocaster"))));
f.register("piano", Box::new(|| Box::new(Piano::new("Yamaha"))));
f.register("drums", Box::new(|| Box::new(Drums::new("Pearl Export"))));
}
let store = MusicStore::new(factory.clone());
// Usos
for kind in ["guitar", "piano", "drums", "violin"].iter() {
match store.play_instrument(kind) {
Ok(output) => println!("{} -> {}", kind, output),
Err(err) => println!("{} -> ERROR: {}", kind, err),
}
}
// Mostrar cómo un nuevo instrumento puede añadirse sin tocar MusicStore
factory.register("ukulele", Box::new(|| Box::new(Guitar::new("Kala Ukelele"))));
println!("ukulele -> {}", store.play_instrument("ukulele").unwrap());
}
// -------------------------
// Pruebas unitarias
// -------------------------
#[cfg(test)]
mod tests {
use super::*;
struct FakeInstrument;
impl Instrument for FakeInstrument {
fn play(&self) -> String { "fake play".to_string() }
}
#[test]
fn test_music_store_uses_factory() {
let factory = Arc::new(RegistryFactory::new());
factory.register("fake", Box::new(|| Box::new(FakeInstrument)));
let store = MusicStore::new(factory);
let out = store.play_instrument("fake").unwrap();
assert_eq!(out, "fake play");
}
#[test]
fn test_missing_instrument() {
let factory = Arc::new(RegistryFactory::new());
let store = MusicStore::new(factory);
let res = store.play_instrument("nope");
assert!(res.is_err());
}
}- La
RegistryFactorycentraliza la creación y mantiene un registro de constructores. Esto elimina condicionales enMusicStorey permite registrar nuevos instrumentos en tiempo de inicialización o por plugins. - Para proyectos más grandes se puede dividir en varios módulos (
instruments::guitar,factory::registry,store::music_store) y agregarCargo.tomlcon metadata.