¿Tienes todos los patitos en línea?

Sé lo que estás pensando sobre tener o no los patitos en línea. En este texto trataremos de resolver el problema… desde un punto de vista estrictamente informático. Por el camino, aprenderemos sobre la programación por contrato.

El problema a mode…


This content originally appeared on DEV Community and was authored by Baltasar García Perez-Schofield

Patitos en línea, pero literal

Sé lo que estás pensando sobre tener o no los patitos en línea. En este texto trataremos de resolver el problema... desde un punto de vista estrictamente informático. Por el camino, aprenderemos sobre la programación por contrato.

El problema a modelar es el de Mamá Pato manteniendo a sus patitos en línea o en fila. Para ello, tanto mamá pato como sus patitos mantendrá una variable entera que indicará la dirección, es decir, los grados a los que apunta el pato.

La dirección se indicará en grados

Si los patitos se mantienen mirando en la misma dirección (con una cierta tolerancia, claro), entonces Mamá Pato no hace nada. En caso contrario, golpea a picotazos al patito rebelde.

Bueno, como queremos dar una imagen de amor fraternal, lo dejaremos que en que Mamá Pato motiva al patito.

Quitémonos de encima algunas definiciones. La programación por contrato se basa en precondiciones, postcondiciones e invariantes de clase. La forma más directa de implantar estas condiciones es mediante aserciones o asserts. Estas aserciones se componen principalmente de una condición que debe cumplirse, y un mensaje que solo se muestra por consola si dicha condición no se cumple. En este último caso, además, la ejecución se detiene abrupta y completamente, por lo que debe quedar claro que es una metodología apta para detectar errores internos, nunca errores que puedan provenir de entradas del usuario (pues la consecuencia de no cumplimiento no es nada civilizada).

Específicamente, cuando las aserciones se sitúan al comienzo de un método están comprobando si la tarea se puede llevar a cabo, y se denominan precondiciones. Cuando se sitúan al final del método, se habla de postcondiciones, y están midiendo la calidad del método. Las invariantes de clase son condiciones que deben cumplirse siempre, por lo que se modelan como postcondiciones comunes a todos los métodos que cambian el estado del objeto.

Está claro que para poder comprobar en nuestro código si todos los patitos están en línea, vamos a necesitar una propiedad Dir (la dirección en la que mira, o los grados como número entero), tanto en la mamá pato como en el patito, por lo que crearemos una clase Pato común que proporcione ese dato a ambas. Ese dato, por cierto, será aleatorio.

public class Pato {
    public Pato()
    {
        this.Dir = new Random( DateTime.Now.Microsecond ).Next( 0, 360 );
    }

    public Dir { get; private set; }

    public override string ToString() => $"¡Cuac! {this.Dir}º";
}

public class Patito: Pato {
}

public class MamaPato: Pato {
}

Para obtener un dato realmente aleatorio, creamos un objeto Random, siendo la semilla el microsegundo actual. Esto es importante: si ponemos, por ejemplo, el segundo o el milisegundo actual (el código es ejecuta demasiado rápido), todos los patos se inicializarán con la misma semilla, y por tanto, todos los patos tendrán la misma dirección inicial (recordemos que se trata de números pseudoaleatorios).

Es importante poder identificar a cada patito: por eso, asignaremos un entero que incrementaremos con cada nuevo patito, como identificador.

public class Patito: Pato {
    public Patito()
    {
        this.Id = ++numPatitos;
    }

    public int Id { get; private set; }

    public override string ToString()
    {
        return $"{this.Id}/{base.ToString()}";
    }

    private static int numPatitos = 0;
}

MamáPato es muy especial. Ella lleva su camada de patitos, y es la que mantiene, a picot..., es la que motiva a sus patitos para estar en línea.

public class MamaPato {
    public MamaPato()
    {
        this._patitos = [];
    }

    public IList<Patito> Patitos
    {
        get => this._patitos;
        set {
            this._patitos.Clear();
            ( (List<Patito>) this._patitos ).AddRange( value );
        }
    }

    private IList<Patito> _patitos;
}

Esta clase funciona, pero tiene un error potencial: devolvemos directamente la lista de patitos. Esta es una mala idea. Al fin y al cabo, si una mamá pato permite injerencias externas dentro de su lista de patitos, podrían cambiarle unos patitos por otros, introducirle patitos que no son suyos, o peor aún, cambiarle totalmente su camada por otra. Así que podemos corregir el código anterior de dos formas: devolviendo una nueva lista: get => new List<Patito>( this._patitos ) o devolviendo la misma lista pero en formato de solo lectura: get => this._patitos.AsReadOnly(). Además, deberíamos mostrar a los patitos en su ToString().

public class MamaPato {
    // Más cosas...

    public IList<Patito> Patitos
    {
        get => this._patitos.AsReadOnly();
        set {
            this._patitos.Clear();
            ( (List<Patito>) this._patitos ).AddRange( value );
        }
    }

    public override string ToString()
    {
        return $"¡CUAC! {this.Dir}º -> "
                + string.Join( " - ", this._patitos );
    }
}

¿Cómo podemos saber si un patito está o no en línea? Se impone un método en Patito que nos devuelva esa información.

Un patito

public class Patito {
    private const int TOLERANCIA = 30;

    // Más cosas...

    public bool EstaEnLineaCon(Pato pato)
        => ( this.Dir >= ( pato.Dir - TOLERANCIA )
          && this.Dir <= ( pato.Dir + TOLERANCIA ) );
}

Es necesario soportar una cierta tolerancia. Solo nos queda que mamá pato corrija a picotaz..., quiero decir, motive a los patitos que se están saliendo de la línea. Así que, diremos que motiva con una fuerza de 0 a 10. El patito recibe la motivación y, según la fuerza recibida, corregirá su dirección:

public class Patito {
    private const int CAMBIO_DIR = 20;

    // Más cosas...

    /// <summary>Recibe motivación hacia una dirección.</summary>
    /// <param name="fuerza">Fuerza entre 0 y 10.</param>
    public void RecibeMotivacion(int fuerza)
    {
        if ( fuerza == 0 ) {
            fuerza = 1;
        }

        this.Dir += ( (int) ( ( fuerza / 10.0 ) * CAMBIO_DIR ) ) % 360;
    }
}

Y mamá pato debe picot... motivar a los patitos...

public class MamaPato {
    // Más cosas...

    public void Motiva()
    {
        foreach(Patito patito in this._patitos) {
            if ( !patito.EstaEnLineaCon( this ) ) {
                patito.RecibeMotivacion( new Random().Next( 0, 10 ) );
            }
        }
     }
}

La aplicación está creada con interfaz gráfica de Avalonia, si creamos una ventana con un TextBox y mostramos una mamá pato con sus patitos, como en el siguiente código, obtenemos:

public class MainWindowCore {
    private void Init()
    {
        var mp = new MamaPato();
        var p1 = new Patito();
        var p2 = new Patito();
        var p3 = new Patito();

        mp.Patitos = [ p1, p2, p3 ];
        this._edLog.Text += mp + "\n";
}

App Avalonia mostrando a la mamá pato con sus patitos

De acuerdo. Comencemos con la programación por contrato. Una precondición para el cambio de dirección es que esta no puede exceder los 360º...

public class Pato {
    // Más cosas...

    public int Dir {
        get => field;
        protected set {
            Debug.Assert( value >= 0 && value < 360,
                            $"0 <= {value} < 360!!" );
            field = value;
        }
    }
}

Lo que tenemos aquí es una de las características más nuevas de C#: no es necesario que nuestra propiedad deje de ser una propiedad automática por el hecho de que queramos modificar la asignación, o realizar una comprobación, como en este caso... Simplemente, hacemos referencia al atributo oculto/automático que ha creado C# con field.

En cualquier caso, lo importante es que usamos el método Debug.Assert( c: bool, m: string), para comprobar que se pasa un valor en el rango [0, 360). Cuando la condición se cumpla, no pasará nada, pero si fallase, entonces la aplicación se pararía completamente para mostrarnos por consola el mensaje y la pila de llamadas. Esta es también una invariante de clase. Como dijimos, se coloca como postcondición (normalmente, pero aquí estamos en una asignación directa), en aquellos métodos que cambian el estado.

Podemos encontrar la clase Debug dentro de System.Diagnostics.

La clase MamaPato debería comprobar si, después de motivar a sus pequeños, están ya en línea o no...

public class MamaPato {
    // Más cosas...

    public void Motiva()
    {
        foreach(Patito patito in this._patitos) {
            if ( !patito.EstaEnLineaCon( this ) )
            {
                patito.RecibeMotivacion( new Random().Next( 0, 10 ) );
            }
        }

        this.TodosPatitosEnLinea();
    }


    [Conditional("DEBUG")]
    private void TodosPatitosEnLinea()
    {
        Debug.Assert(
            this._patitos.All( p => p.EstaEnLineaCon( this ) ),
            "patitos fuera de línea" );
    }
}

Esta es una característica muy interesante de C#: podemos condicionar la compilación de ciertos métodos con el decorador Conditional(), que solo se activa, en este caso, cuando estamos en el perfil de depuración.

El método TodosPatitosEnLinea() es otra invariante de clase: los patitos deben estar siempre en línea.

Si ahora intentamos ejecutar la aplicación veremos que no aparece la ventana (ya que no llega a alcanzar el bucle interno de espera). En la consola, podremos ver el mensaje "patitos fuera de línea" y la pila de llamadas.

Es el momento de utilizar otra clase de Diagnostics: Trace. Nos permitirá mostrar mensajes de depuración, pero teniendo en cuenta que estos mensajes, al igual que las aserciones, desaparecerán cuando del perfil Debug (depuración) pasemos al perfil Release (distribución). Esto nos permite realizar tareas de depuración del calibre que sea necesario sin afectar al rendimiento, pues solo se llevarán a cabo en el citado perfil de depuración.

public class MamaPato {
    // Más cosas...

    public void Motiva()
    {
        Trace.WriteLine( "MamaPato.Motiva()" );
        Trace.Indent();
        Trace.WriteLine( this.ToString() );

        for(int _ = 0; _ < 36; ++_) {
            foreach(Patito patito in this._patitos) {
                if ( !patito.EstaEnLineaCon( this ) )
                {
                    patito.RecibeMotivacion( new Random().Next( 0, 10 ) );
                }
            }
        }

        Trace.Unindent();
        this.TodosPatitosEnLinea();
    }

    [Conditional("DEBUG")]
    private void TodosPatitosEnLinea()
    {
        Trace.WriteLine( this );
        Debug.Assert(
            this._patitos.All( p => p.EstaEnLineaCon( this ) ),
            "patitos fuera de línea" );
    }
}

La utilización de la clase Trace es bastante intuitiva. Trace.WriteLine(s: string) permite visualizar un mensaje por pantalla. Pero además, tenemos los métodos Trace.Indent() y Trace.Unindent(), que permiten estructurar la salida. Indent() hace que la salida se desplace, a partir de ese momento, unas columnas más a la derecha, mientras tanto Unindent() consigue lo contrario, devolviendo la columna de impresión más a la izquierda.

Para que Trace funcione, solo debemos asegurarnos de incluir un listener de la consola al comienzo del programa:

public class MainWindowCore {
    // Más cosas...
    public MainWindowCore()
    {
         Trace.Listeners.Add( new ConsoleTraceListener() );
         // Más cosas...
    }
}

Según la ejecución, al depender de números aleatorios, podemos obtener como error tanto "patitos fuera de linea" como "0 <= 403 < 360!!" (o similar). Efectivamente, tenemos varios problemas: por un lado estamos enviando una dirección errónea (mayor de 360º), y por otro las "motivaciones" de mamá pato... no son suficientes.

Es cierto, si examinamos el código, nos encontramos con lo siguiente:

public class Patito {
    // Más cosas...

    public void RecibeMotivacion(int fuerza)
    {
        if ( fuerza == 0 ) {
            fuerza = 1;
        }

        this.Dir += ( (int) ( ( fuerza / 10.0 ) * CAMBIO_DIR ) ) % 360;
    }
}

La línea en la que actualizamos la dirección es incorrecta: no queremos que Dir pase nunca de 360, pero en realidad es solo el valor que estamos sumando el que controlamos que no pase de 360º, no Dir en su totalidad. Es decir, el problema consiste en utilizar el operador +=, ya que el módulo de 360 debe aplicarse al resultado final. Es decir:

public class Patito {
    // Más cosas...

    public void RecibeMotivacion(int fuerza)
    {
        if ( fuerza == 0 ) {
            fuerza = 1;
        }

        this.Dir = ( this.Dir + ( (int) ( ( fuerza / 10.0 ) * CAMBIO_DIR ) ) ) % 360;
    }
}

Un ejemplo de precondición es comprobar que, efectivamente, la motivación recibida por mamá pato está entre una fuerza de 0 a 10.

public class Patito {
    // Más cosas...

    public void RecibeMotivacion(int fuerza)
    {
        Debug.Assert( fuerza >= 0 && fuerza < 360, "¡0 <= fuerza < 360!, y no: " + fuerza );

        if ( fuerza == 0 ) {
            fuerza = 1;
        }

        this.Dir = ( this.Dir + ( (int) ( ( fuerza / 10.0 ) * CAMBIO_DIR ) ) ) % 360;
    }
}

Esta precondición no saltará nunca en este ejemplo, pero recordemos que estamos intentando comprobar que la tarea se pueda llevar a cabo tanto ahora como en el futuro, por lo que nunca estará de más.

Fail fast, fail hard, no mercy

Recuerda que debemos intentar detectar los errores lo antes posible, de manera que podamos deducir qué es lo que los ha provocado. Por eso es importante que paremos la aplicación en cuanto el error sea detectado: el peor escenario posible es el de un error que, silenciosamente, se propaga por la aplicación hasta que es muy difícil razonar de dónde proviene.

Otro problema es que las motivaciones de mamá pato sean suficientes. Al fin y al cabo, y según los números aleatorios generados, cada motivación puede llevar a una máximo de giro de 20º. Esto quiere decir que una vuelta completa serían 360 / 20 = 18 motivaciones en el mejor de los casos, y 360 en el peor. Solo una, definitivamente, no será suficiente. Doblemos el número de motivaciones, solo para estar seguros...

public class MamaPato {
    public void Motiva()
    {
        Trace.WriteLine( "MamaPato.Motiva()" );
        Trace.Indent();
        Trace.WriteLine( this.ToString() );

        for(int _ = 0; _ < 36; ++_) {
            foreach(Patito patito in this._patitos) {
                if ( !patito.EstaEnLineaCon( this ) )
                {
                    patito.RecibeMotivacion( new Random().Next( 0, 10 ) );
                }
            }
        }

        Trace.Unindent();
        this.TodosPatitosEnLinea();
    }
}

Ahora sí, la aplicación funciona sin problemas. Tenemos todos los patitos en línea. ¡Qué alivio!

Ejercicio: En ocasiones, solo un par de motivaciones serán suficientes, o definitivamente, menos de 36. ¿Puedes cambiar el código para que mamá pato deje de motivar a los pequeños si ya están en línea?

Hay una pregunta que queda en suspenso, sin embargo: ¿Tenemos todos los patitos en línea si comprobamos que tenemos todos los patitos en línea?


This content originally appeared on DEV Community and was authored by Baltasar García Perez-Schofield


Print Share Comment Cite Upload Translate Updates
APA

Baltasar García Perez-Schofield | Sciencx (2025-11-21T11:49:37+00:00) ¿Tienes todos los patitos en línea?. Retrieved from https://www.scien.cx/2025/11/21/tienes-todos-los-patitos-en-linea/

MLA
" » ¿Tienes todos los patitos en línea?." Baltasar García Perez-Schofield | Sciencx - Friday November 21, 2025, https://www.scien.cx/2025/11/21/tienes-todos-los-patitos-en-linea/
HARVARD
Baltasar García Perez-Schofield | Sciencx Friday November 21, 2025 » ¿Tienes todos los patitos en línea?., viewed ,<https://www.scien.cx/2025/11/21/tienes-todos-los-patitos-en-linea/>
VANCOUVER
Baltasar García Perez-Schofield | Sciencx - » ¿Tienes todos los patitos en línea?. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/11/21/tienes-todos-los-patitos-en-linea/
CHICAGO
" » ¿Tienes todos los patitos en línea?." Baltasar García Perez-Schofield | Sciencx - Accessed . https://www.scien.cx/2025/11/21/tienes-todos-los-patitos-en-linea/
IEEE
" » ¿Tienes todos los patitos en línea?." Baltasar García Perez-Schofield | Sciencx [Online]. Available: https://www.scien.cx/2025/11/21/tienes-todos-los-patitos-en-linea/. [Accessed: ]
rf:citation
» ¿Tienes todos los patitos en línea? | Baltasar García Perez-Schofield | Sciencx | https://www.scien.cx/2025/11/21/tienes-todos-los-patitos-en-linea/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.