Cómo mejorar la calidad del código Java al usar nuestro traductor

Vamos a hablar de enfoques y construcciones de lenguaje en C#: cuáles son buenos de usar y cuáles no. Por supuesto, bajo bueno o no bueno consideramos lo siguiente: qué tan legible y mantenible será el código Java resultante después de la traducción desde C#.

Compacto en C# – extenso en Java

C# tiene algunas construcciones de lenguaje compactas que hacen mucho trabajo oculto. Al traducir esas construcciones a otro idioma, tienes que reproducir implícitamente esa parte oculta y, en la mayoría de los casos, el código pierde su diseño original y difiere mucho.

Propiedades automáticas

Las propiedades automáticas tienen un uso muy extendido entre los programadores de C#. Usando ese tipo de propiedad, el programador puede interactuar con el campo oculto a través del método get y/o set. C# nos permite distraernos de la implementación real de las propiedades automáticas y usar construcciones muy compactas para declararlas. Pero, en Java, no tenemos tal construcción de lenguaje, y se hace necesario declarar propiedades explícitamente: como campo y métodos de control de acceso:

public int Value { get; set; }

En Java se convierte en:

private int auto_Value;
public int get_Value()
{
    return auto_Value;
}
public void set_Value(int value)
{
    auto_Value = value;
}

Ahora no es un miembro sólido, hay tres miembros separados. Imagina el mismo código repetido para cada propiedad automática, ¿cómo se vería? Evitemos tales experiencias desagradables. Pero, ¿cómo?
Intenta reemplazar la propiedad automática con un objeto, que proporcione control de acceso sobre el campo privado. Puede haber un mapa hash de tales objetos. Si seguimos un diseño de acceso limitado a algunos datos, sería una buena manera. Las propiedades automáticas pueden parecer agradables, pero no tenemos que usarlas sin necesidad.

Tipos de valor

En C# tenemos una lógica de memoria dedicada para estructuras (tipos de valor). Su vida útil está limitada por la vida útil del marco de pila o del objeto contenedor, y se copian con frecuencia: cuando los pasamos como argumentos de función, los devolvemos de una función o los asignamos a algún campo, operamos con valor, no con referencia. Cambiar la copia no cambia el original. Al traducir tipos de valor a Java, tenemos que recrear la misma lógica, aunque las clases de Java siempre son tipos de referencia. La copia frecuente se convierte en un problema ahora: para almacenar cada copia, asignamos memoria del montón, sobrecargando el recolector de basura. Si consideramos el rendimiento como uno de nuestros intereses, tenemos que abstraer nuestro código C# de los detalles de la gestión de memoria. Pero, ¿cómo?
La forma más fácil de hacerlo es hacer que tus tipos de valor sean inmutables. Cuando no tienes un estado mutable, no es necesario copiar ese estado para prevenir un comportamiento indeterminado.

Construcciones solo de sintaxis

Ahora es el momento de hablar de construcciones de lenguaje que cambian solo las propiedades visuales del código, pero no el comportamiento. Por ejemplo:

public class Item
{
    string name;
    string price;
    
    public Item(string name, int price) => (this.name, this.price) = (name, price);
    
    public string ToString() => $"Name = {name}, Price = {price}";
    public string Name => name;
    public int Price => price;
}

Aquí vemos la deconstrucción de tuplas (esa expresión realmente no crea una tupla), literal de cadena interpolada, métodos y propiedades con cuerpo de expresión.

Delegados

Los delegados son buenos de usar porque es una forma corta de declaración de método. Veamos el ejemplo:

using System;
using System.Linq;

class Program
{
    delegate int ChangeNumber(int arg);
	static void Main()
	{
	    Console.WriteLine("Input some numbers");
        int[] numbers = Console.ReadLine().Split(" ").Select(int.Parse).ToArray();
        Console.WriteLine("Input addition");
        int addition = int.Parse(Console.ReadLine());
        ChangeNumbers(n => n + addition, numbers);
        Console.WriteLine("Result :");
        Console.WriteLine(string.Join(" ", numbers.Select(n => n.ToString())));
	}
	static void ChangeNumbers(ChangeNumber change, int[] numbers)
	{
	    for(int i = 0; i < numbers.Length; i++)
	    {
	        numbers[i] = change(numbers[i]);
	    }
	}
}

Para esa expresión n => n + addition podríamos generar una expresión de clase anónima de Java:

// translated to Java code
interface ChangeNumber
{
    int invoke(int arg);
}
// ...static void main(String[] args)...

// anonymous class expression for Java 7 or older version
changeNumbers(new ChangeNumber()
{
    public int invoke(int n)
    {
        return n + addition;
    }
}, numbers);

// or lambda expression for higher Java 8 or newer version
changeNumbers(n -> n + addition, numbers);

Llegando a la conclusión

C# tiene muchas construcciones de lenguaje, que pueden hacer que nuestro código parezca simple, ocultando una gran implementación detrás del azúcar sintáctico. Algunas de esas construcciones son cuestionables y difícilmente soportables, otras son flexibles y fácilmente reproducibles. Cuando se trata de diseño, es mejor componer abstracciones por objetos, no por construcciones de lenguaje dedicadas de C#. Cuando se trata de rendimiento, deberíamos mantener la abstracción de la gestión de memoria, liberándonos de emular eso con doble costo.

Artículos relacionados: