🎓 Caso de estudio:
Ejemplo de modularización de un programa ya hecho
Supongamos el siguiente enunciado:
Deberemos mostrar un tablero de ajedrez y sobre él, marcar con
*las celdas a las que se puede mover un alfil desde una posición dada por el usuario.Para indicar la posición de la casilla vamos a utilizar índices del 1 al 8 tanto para las filas como para las columnas, de tal manera que la casilla (1, 1) es la esquina superior izquierda del tablero y la casilla (8, 8) es la esquina inferior derecha.
Vamos imprimir colores en el terminal puedes usar las propiedades de consola:
Console.BackgroundColor = ConsoleColor.Color; Console.ForegroundColor = ConsoleColor.Color;Además, puedes imprimir en una posición específica en pantalla con.
Console.SetCursorPosition(Columna, Fila);Para más información sobre la clase Console
Ejemplo de ejecución:
Introduce fila: 4 Introduce columna: 4 12345678 1* * 2 * * 3 * * 4 A 5 * * 6 * * 7* * 8 *
Una propuesta de solución válida sería el siguiente código 'monolítico', largo y susceptible de ser modularizado puede ser el siguiente:
📌 Nota: Puedes descargarlo con desde el siguiente enlace.
public class Programa
{
public static void Main()
{
Console.Clear();
for (int i = 1; i <= 8; i++)
{
Console.SetCursorPosition(i, 0);
Console.Write(i);
Console.SetCursorPosition(0, i);
Console.Write(i);
}
for (int fila = 1; fila <= 8; fila++)
{
for (int columna = 1; columna <= 8; columna++)
{
bool esCasillaNegra;
if (fila % 2 == 0)
esCasillaNegra = columna % 2 != 0;
else
esCasillaNegra = columna % 2 == 0;
if (esCasillaNegra)
Console.BackgroundColor = ConsoleColor.DarkGray;
else
Console.BackgroundColor = ConsoleColor.White;
Console.SetCursorPosition(columna, fila);
Console.Write(" ");
}
}
Console.BackgroundColor = ConsoleColor.Black;
Console.Write("\n");
int columnaAlfil;
int filaAlfil;
Console.SetCursorPosition(10, 4);
Console.Write("Introduce fila: ");
filaAlfil = int.Parse(Console.ReadLine() ?? "1");
Console.SetCursorPosition(10, 5);
Console.Write("Introduce columna: ");
columnaAlfil = int.Parse(Console.ReadLine() ?? "1");
for (int fila = 1; fila <= 8; fila++)
{
for (int columna = 1; columna <= 8; columna++)
{
bool esDiagonalAlfil = Math.Abs(filaAlfil - fila) == Math.Abs(columnaAlfil - columna);
if (esDiagonalAlfil)
{
bool esCasillaNegra;
Console.SetCursorPosition(columna, fila);
if (fila % 2 == 0)
esCasillaNegra = columna % 2 != 0;
else
esCasillaNegra = columna % 2 == 0;
if (esCasillaNegra)
{
Console.BackgroundColor = ConsoleColor.DarkGray;
Console.ForegroundColor = ConsoleColor.White;
}
else
{
Console.BackgroundColor = ConsoleColor.White;
Console.ForegroundColor = ConsoleColor.Black;
}
Console.Write("*");
}
}
}
Console.BackgroundColor = ConsoleColor.Black;
Console.ForegroundColor = ConsoleColor.Gray;
Console.SetCursorPosition(0, 15);
Console.Write("\n");
}
}
Lo primero sería identificar los bloques de código relacionado y que hace una única funcionalidad y tratar de ponerles nombre y pasarles todos los datos que precisen por parámetro. Además, durante el proceso vamos a...
Importante
Este caso de estudio es simplemente para que veamos un ejemplo de una de las posibles formas de plantear la modularización de un código ya implementado. Quizá un programador experimentado es capaz de realizar ambos refinamientos de una sola vez, pero eso se logra con la práctica y la experiencia.
Este proceso es interesante abordarlo porque en la realidad la mayoría de casos de modularización se nos darán refactorizando código nuestro o de un compañero al que le estemos añadiendo nuevas características.
Paso 1 - Dibujo del tablero vacío:
Si analizamos los dos primeros bucles, cláramente se hace un proceso de dibujo del tablero vacío, tanto de la numeración, como de las casillas.
📌 Nota: Lee los comentarios en el código.
static void DibujaNumeracion(int dimensionTablero)
{
for (int i = 1; i <= dimensionTablero; i++)
{
Console.SetCursorPosition(i, 0);
Console.Write(i);
Console.SetCursorPosition(0, i);
Console.Write(i);
}
}
static void DibujaCuadricula(int dimensionTablero)
{
// Nos guardamos el color de fondo para restaurarlo al final
// aunque el nombre del identificador autodocumenta ahorrándonos este comentario.
ConsoleColor copiaRespaldoDeColorDeFondo = Console.BackgroundColor;
for (int fila = 1; fila <= dimensionTablero; fila++)
{
for (int columna = 1; columna <= dimensionTablero; columna++)
{
// Refactorizamos simplificando las expresiones
// y la lógica a partir del código dado.
bool esCasillaNegra = (fila % 2 == 0) ? columna % 2 != 0 : columna % 2 == 0;
Console.BackgroundColor = esCasillaNegra
? ConsoleColor.Black
: ConsoleColor.White;
Console.SetCursorPosition(columna, fila);
Console.Write(" "); // Dibuja una casilla del fondo en una posición.
}
}
// Restauramos el color.
Console.BackgroundColor = copiaRespaldoDeColorDeFondo;
}
public static void Main()
{
// Guardo los colores de la consola y la limpio.
ConsoleColor fondo = Console.BackgroundColor;
ConsoleColor primerPlano = Console.ForegroundColor;
Console.Clear();
// Pasamos la dimensión de tablero a través de una cte. en lugar de usar un número 'mágico'.
const int DIMENSION_TABLERO = 8;
DibujaTablero(DIMENSION_TABLERO);
...
}
Paso 2 - Pedir la casilla donde se encuentra el álfil:
El código a continuación del dibujo del tablero está pidiendo la fila y columna de la casilla según la numeración dibujada. En este primer refinamiento de modularización nos vamos a limitar a pasarlo a un módulo que devuelva ambos valores en forma de tupla ya que están relacionados.
static (int columna, int fila) CasillaAlfil()
{
Console.SetCursorPosition(10, 4);
Console.Write("Introduce fila: ");
int f = int.Parse(Console.ReadLine() ?? "1");
Console.SetCursorPosition(10, 5);
Console.Write("Introduce columna: ");
int c = int.Parse(Console.ReadLine() ?? "1");
return (c, f);
}
Importante
Fíjate que estamos dibujando en posiciones absolutas (10, 4) y (10, 5).
¿Cómo sabemos que no estamos dibujando encima del tablero?
Este módulo necesita saber donde está dibujando otro módulo del mismo nivel lo cual nos indica un claro acoplamiento. Para solucionarlo haremos le pasaremos la posición absoluta donde dibujar ...
static (int columna, int fila) CasillaAlfil(int columna, int fila)
{
Console.SetCursorPosition(columna, fila);
Console.Write("Introduce fila: ");
int f = int.Parse(Console.ReadLine() ?? "1");
Console.SetCursorPosition(columna, fila + 1);
Console.Write("Introduce columna: ");
int c = int.Parse(Console.ReadLine() ?? "1");
return (c, f);
}
public static void Main()
{
...
const int DIMENSION_TABLERO = 8;
DibujaTablero(DIMENSION_TABLERO);
// Realizamos la entrada de datos en función de la dimensión del tablero...
(int columnaAlfil, int filaAlfil) = CasillaAlfil(DIMENSION_TABLERO + 2, DIMENSION_TABLERO / 2);
...
}
Paso 3 - Dibujar las posiciones del álfil a partir de la casilla dada:
El último bucle en el Main dibujará las casillas alcanzables por el álfil a partir de la casilla dada.
static void DibujaDiagonalesAlfil(int columnaPieza, int filaPieza, int dimensionTablero)
{
for (int fila = 1; fila <= dimensionTablero; fila++)
{
for (int columna = 1; columna <= dimensionTablero; columna++)
{
// Asignamos la expresión a un identificados booleano para autodocumentarla.
bool estanEnLaMismaDiagonal = Math.Abs(filaPieza - fila) == Math.Abs(columnaPieza - columna);
if (estanEnLaMismaDiagonal)
{
bool esCasillaNegra = (fila % 2 == 0) ? columna % 2 != 0 : columna % 2 == 0; //Línea 11
if (esCasillaNegra)
{
Console.BackgroundColor = ConsoleColor.Black;
Console.ForegroundColor = ConsoleColor.White;
}
else
{
Console.BackgroundColor = ConsoleColor.White;
Console.ForegroundColor = ConsoleColor.Black;
}
Console.SetCursorPosition(columna, fila);
Console.Write("*");
}
}
}
}
Si nos fijamos en la línea 11, la expresión para saber si una casilla es negra o no, se repetía en DibujaCuadricula. Esto nos está diciendo que podemos aumentar la cohesión del código llevándola a un módulo común usado por ambos para evitar así repetir la lógica.
static bool EsCasillaNegra(int columna, int fila)
{
return (fila % 2 == 0) ? columna % 2 != 0 : columna % 2 == 0;
}
// Ahora en ambas líneas donde se repetía la expresión podremos poner directamente...
if (EsCasillaNegra(columna, fila)) ...
Paso 4 - Completando las llamadas en el programa principal:
Si nos fijamos en las líneas 10, 12 y 16 tendremos las llamadas resultado de la modularización.
public static void Main()
{
// Guardo los colores de la consola y la limpio.
ConsoleColor fondo = Console.BackgroundColor;
ConsoleColor primerPlano = Console.ForegroundColor;
Console.Clear();
const int DIMENSION_TABLERO = 8;
DibujaTablero(DIMENSION_TABLERO);//10
(int columnaAlfil, int filaAlfil) = CasillaAlfil(DIMENSION_TABLERO+2, DIMENSION_TABLERO/2);
Console.CursorVisible = false; // Ocultamos el cursor para que no se vea.
DibujaDiagonales(columnaAlfil, filaAlfil, DIMENSION_TABLERO);//16
Console.ReadKey(true); // Leemos una tecla sin eco para finalizar
Console.CursorVisible = true; // Restauro el cursor.
// Restauro los colores de la consola y la limpio.
Console.BackgroundColor = fondo;
Console.ForegroundColor = primerPlano;
Console.Clear();
}
Pero si nos fijamos, debemos saber donde coloca el tablero en la consola DibujaTablero para después los valores correctos a CasillaAlfil y eso es una forma de acoplamiento.
Así mismo, DibujaCuadricula y DibujaNumeracion que son módulos en el mismo nivel, están acoplados porque uno debe saber donde ha situado las cosas el otro para que encajen.
Por tanto en este segundo refinamiento, vamos a refactorizar el código para que las cosas se dibujen relativas a una posición en consola absoluta que se recibirá por parámetro.
📌 Nota: En este refinamiento el DEM no se modificará y solo pasaremos la información necesaria para evitar los acoplamientos descritos.
Paso 5 - Pasando posiciones absolutas de dibujo para eliminar acoplamientos:
📌 Nota: Puedes descargar el resultado final desde el siguiente enlace. Debes ejecutar el programa en una consola externa para que funcione correctamente.
static bool EsCasillaNegra(int columna, int fila)
{
return (fila % 2 == 0) ? columna % 2 != 0 : columna % 2 == 0;
}
// Recibirá columna y fila absolutas de dibujo para evitar acoplamiento yo dibujo desde
// donde me dicen. El no solaparse ahora es responsabilidad del módulo padre.
static void DibujaNumeracion(int columna, int fila, int dimensionTablero)
{
for (int i = 1; i <= dimensionTablero; i++)
{
// Añado los desplazamientos absolutos a las posiciones relativas anteriores
Console.SetCursorPosition(i + columna, fila);
Console.Write(i);
Console.SetCursorPosition(columna, i + fila);
Console.Write(i);
}
}
// Recibirá columna y fila absolutas de dibujo para evitar acoplamiento
// yo dibujo desde donde me dicen. El no solaparse ahora es responsabilidad del
// módulo padre.
static void DibujaCuadricula(int columna, int fila, int dimensionTablero)
{
ConsoleColor copiaRespaldoDeColorDeFondo = Console.BackgroundColor;
for (int fRelativa = 1; fRelativa <= dimensionTablero; fRelativa++)
{
for (int cRelativa = 1; cRelativa <= dimensionTablero; cRelativa++)
{
Console.BackgroundColor = EsCasillaNegra(cRelativa, fRelativa)
? ConsoleColor.Black
: ConsoleColor.White;
// Añado los desplazamientos absolutos a las posiciones relativas anteriores
Console.SetCursorPosition(cRelativa + columna, fRelativa + fila);
Console.Write(" ");
}
}
Console.BackgroundColor = copiaRespaldoDeColorDeFondo;
}
// Recibirá columna y fila absolutas de dibujo para evitar acoplamiento
// yo dibujo desde donde me dicen. El no solaparse ahora es responsabilidad del
// módulo padre.
static void DibujaTablero(int columna, int fila, int dimensionTablero)
{
// DibujaTablero ahora se encarga de que el dibujo de ambos módulos por debajo encaje.
DibujaNumeracion(columna, fila, dimensionTablero);
DibujaCuadricula(columna, fila, dimensionTablero);
}
// Recibirá columna y fila absolutas de dibujo para evitar acoplamiento
// yo dibujo desde donde me dicen. El que las posiciones del tablero coincidan
// con el tablero es responsabilidad del módulo padre.
static void DibujaDiagonalesAlfil(
int columnaTablero, int filaTablero, int columnaPieza, int filaPieza,
int dimensionTablero)
{
for (int fila = 1; fila <= dimensionTablero; fila++)
{
for (int columna = 1; columna <= dimensionTablero; columna++)
{
// Asignamos la expresióin a un identificados booleano para autodocumentarla.
bool estanEnLaMismaDiagonal = Math.Abs(filaPieza - fila) == Math.Abs(columnaPieza - columna);
if (estanEnLaMismaDiagonal)
{
if (EsCasillaNegra(columna, fila))
{
Console.BackgroundColor = ConsoleColor.Black;
Console.ForegroundColor = ConsoleColor.White;
}
else
{
Console.BackgroundColor = ConsoleColor.White;
Console.ForegroundColor = ConsoleColor.Black;
}
// Añado los desplazamientos absolutos a las posiciones relativas anteriores
Console.SetCursorPosition(columna + columnaTablero, fila + filaTablero);
Console.Write("*");
}
}
}
}
public static void Main()
{
// Guardo los colores de la consola y la limpio.
ConsoleColor fondo = Console.BackgroundColor;
ConsoleColor primerPlano = Console.ForegroundColor;
Console.Clear();
const int DIMENSION_TABLERO = 8;
// Defino las dimensiones de la consola como constantes
// y las establezco en la que me estoy ejecutando.
// Puede que solo funcione en Windows 10 y ejecutando en una consola externa.
const int COLUMNAS_CONSOLA = 80;
const int FILAS_CONSOLA = 24;
Console.SetWindowSize(COLUMNAS_CONSOLA, FILAS_CONSOLA);
// Decido que el tablero se va a dibujar ahora en el centro de la consola.
int columnaSuperiorIzquierdaTablero = COLUMNAS_CONSOLA / 2 - DIMENSION_TABLERO / 2;
int filaSuperiorIzquierdaTablero = FILAS_CONSOLA / 2 - DIMENSION_TABLERO / 2;
// El Main tiene la responsabilidad o es el encargado ahora de coordinar las posiciones
// absolutas de todos los dibujos de tal manera que no se solapen.
// Cada método solo sabe donde le ha dicho el Main que se dibuje.
DibujaTablero(
columnaSuperiorIzquierdaTablero, filaSuperiorIzquierdaTablero,
DIMENSION_TABLERO);
(int columnaAlfil, int filaAlfil) = CasillaAlfil(
columnaSuperiorIzquierdaTablero + DIMENSION_TABLERO + 2,
filaSuperiorIzquierdaTablero + DIMENSION_TABLERO / 2);
Console.CursorVisible = false; // Ocultamos el cursor para que no se vea.
DibujaDiagonalesAlfil(
columnaSuperiorIzquierdaTablero, filaSuperiorIzquierdaTablero,
columnaAlfil, filaAlfil, DIMENSION_TABLERO);
Console.ReadKey(true); // Leemos una tecla sin eco para finalizar
Console.CursorVisible = true; // Restauro el cursor.
// Restauro los colores de la consola y la limpio.
Console.BackgroundColor = fondo;
Console.ForegroundColor = primerPlano;
Console.Clear();
}