En el desarrollo de software, es común acumular pequeñas herramientas para tareas repetitivas, como el procesamiento masivo de datos o la manipulación de archivos. Un error recurrente es centralizar estas utilidades en un único proyecto de consola, saturando el método Main con bloques de código comentados y llamadas difíciles de rastrear. Para resolver este desorden, se puede implementar un sistema basado en Reflexión (Reflection) que cargue y ejecute métodos de forma dinámica, desacoplando la lógica de la interfaz de ejecución.
Definición de la Estructura Base
Para que el cargador dinámico identifique qué clases contienen utilidades, definimos una clase abstracta que servirá como marcador y base funcional.
public abstract class UtilidadBase
{
protected ExecutionResult GenerarResultado(string mensaje, long tiempo)
{
return new ExecutionResult { Mensaje = mensaje, TiempoMs = tiempo };
}
}
Posteriormente, creamos un contenedor para los resultados, permitiendo estandarizar la salida de cualquier herramienta ejecutada.
public class ExecutionResult
{
public string Mensaje { get; set; }
public long TiempoMs { get; set; }
}
Carga Dinámica de Tipos y Métodos
Utilizando el espacio de nombres System.Reflection, podemos escanear el ensamblado actual para localizar todas las clases que heredan de UtilidadBase. Este enfoque permite que el sistema detecte nuevas herramientas automáticamente sin modificar el código del cargador.
private void CargarClasesDisponibles()
{
var assembly = Assembly.GetExecutingAssembly();
var tipos = assembly.GetTypes()
.Where(t => t.IsSubclassOf(typeof(UtilidadBase)))
.Select(t => new { Nombre = t.Name, FullName = t.FullName })
.ToList();
selectorClases.DataSource = tipos;
selectorClases.DisplayMember = "Nombre";
selectorClases.ValueMember = "FullName";
}
Una vez seleccionada una clase, el siguiente paso es filtrar sus métodos. Para mantener la integridad, solo expondremos aquellos que devuelvan un objeto de tipo ExecutionResult.
private void ActualizarMetodos(string nombreClase)
{
var tipo = Type.GetType(nombreClase);
var metodos = tipo.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
.Where(m => m.ReturnType == typeof(ExecutionResult))
.Select(m => m.Name)
.ToList();
selectorMetodos.DataSource = metodos;
}
Generación Automática de Parámetros e Inyeccción de Metadatos
Uno de los mayores retos es configurar los argumentos de entrada. Podemos inspeccionar los parámetros de un método mediante GetParameters() y generar controles de interfaz de usuario en tiempo real. Además, si habilitamos la generación de archivos de documentación XML en las propiedades del proyecto, podemos extraer los comentarios <summary> y <param> para usarlos como guías (tooltips o watermarks) en la UI.
private void ConstruirPanelParametros(MethodInfo metodo)
{
panelInputs.Controls.Clear();
var parametros = metodo.GetParameters();
foreach (var p in parametros)
{
Label lbl = new Label { Text = p.Name, AutoSize = true };
TextBox txt = new TextBox { Name = $"input_{p.Name}", Tag = p.ParameterType };
// Obtener metadatos del XML de documentación
txt.PlaceholderText = ObtenerDocumentacionParametro(metodo, p.Name);
panelInputs.Controls.Add(lbl);
panelInputs.Controls.Add(txt);
}
}
Ejecución Dinámica y Monitorización
Finalmente, para invocar la utilidad, recolectamos los valores de los controles, los convertimos al tipo de dato requerido por el parámetro y ejecutamos el método sobre una instancia de la clase seleccionada.
private void EjecutarUtilidad()
{
var tipoClase = Assembly.GetExecutingAssembly().GetType(selectorClases.SelectedValue.ToString());
var instancia = Activator.CreateInstance(tipoClase);
var metodo = tipoClase.GetMethod(selectorMetodos.SelectedItem.ToString());
var valores = new List<object>();
foreach (var p in metodo.GetParameters())
{
var ctrl = panelInputs.Controls.Find($"input_{p.Name}", true).FirstOrDefault() as TextBox;
var valorConvertido = Convert.ChangeType(ctrl.Text, p.ParameterType);
valores.Add(valorConvertido);
}
Stopwatch sw = Stopwatch.StartNew();
var resultado = (ExecutionResult)metodo.Invoke(instancia, valores.ToArray());
sw.Stop();
Console.WriteLine($"Resultado: {resultado.Mensaje} | Duración: {sw.ElapsedMilliseconds}ms");
}
Este patrón de diseño no solo mejora la organización del código, sino que transforma un conjunto de scripts aislados en una plataforma de herramientas extensible y fácil de mantener, donde agregar una nueva funcionalidad es tan sencillo como crear una nueva clase y heredar de la base.