Constructores

Código 01 - Protección de invariantes
public class Longitud { 
  private double medida; 

  public Longitud(double medida) { 
    if (medida<= 0.0) { 
      var mensaje = String.format("Medida negativa: %f", medida);    
      throw new IllegalArgumentException(mensaje); 
    } 
    this.medida = medida; 
  } 

  public double medida() { 
    return this.medida; 
  } 
}
 

En el código 01 modelamos una longitud. Las longitudes se usan para medir distancias por lo tanto no tiene sentido que sean negativas. Esta es la invariante para longitudes: su medida debe ser siempre positiva.

En la línea 04 tenemos un constructor para la clase Longitud. En la línea 05 verificamos que los clientes de la clase no intenten crear longitudes de medidas imposibles, si es así lanzamos una excepción en la línea 07, lo que significa que el código dejará de ejecutarse, se mostrará un mensaje de error y se muestra el estado en que se encontraba el programa cuando ocurrió el error. La invariante se puede hacer cumplir desde el constructor. Es imposible que se cree una Longitud que no sea válida.

¡Esto es crucial! Si proteges tus invariantes en tu código, comenzando por el constructor, significa que cuando un método toma un objeto de tu clase, probablemente no haya nada que validar desde el parámetro ya que si pudiste construir el objeto sus invariantes fueron verificados en ese momento. Uno de los grandes problemas del software es que los programadores no validan los parámetros de sus funciones, por lo que centralizar algunas de esas validaciones en los constructores hará que muchos errores sean imposibles.

IllegalArgumentException es solo una de las muchas excepciones que proporciona Java e incluso puedes crear la tuya propia. Una excepción transmite un error que ocurrió durante la ejecución de su programa. Los errores de programación, como intentar crear una longitud negativa, deberían finalizar ruidosamente la ejecución de su programa para que no olvides que es necesario cambiar tu código. La verdadera solución a un error de programación es evitar llamar al código con el argumento incorrecto.

Además de esa excepción, no hay nadie a quien quiera conocer: IllegalStateException. Como su nombre lo dice aquí el problema no está en un parámetro en particular sino en algo en general como que los 3 ángulos de un Triángulo no suman 180. Al final no importa mucho si eliges IllegalArgumentException o IllegalStateException siempre y cuando proporciones un buen mensaje de error. Incluye en ese mensaje tanta información como puedas, porque eso ayudará al cliente de tu código a saber cuál podría ser la causa del error: p.j. si los 3 ángulos no suman 180, imprime cada valor e imprime la suma.

Las excepciones no se devuelven. En cambio, son arrojadas. Una excepción lanzada finaliza inmediatamente la ejecución del constructor o función y, si no se atrapa, eventualmente finalizará la ejecución de sus programas. El manejo de errores está fuera del alcance de este material, pero por ahora es suficiente que sepas cómo proteger las invariantes de tus clases y que la solución es cambiar el código infractor.

Múltiples Constructores

Código 02 - Múltiplos de constructores
public class Circulo { 
  private Punto centro; 
  private Longitud radio; 

  public Circulo(Punto centro, Longitud radio) { 
    this.centro = Objects.requireNonNull(centro); 
    this.radio = Objects.requireNonNull(radio); 
  } 

  public Circulo(Punto centro, Punto unPunto) { 
    this(centro, new Longitud(centro.distancia(unPunto))); 
  } 
} 

Las líneas 06 y 11 tienen constructores para la clase Círculo que puedes reconocer porque su identificador es el nombre de la clase y no hay valor de retorno. Con diferentes constructores puedes tener diferentes parámetros para crear objetos, lo cuál es increíble, cuanto más flexible sea tu clase, será más fácil de reutilizar. Pero al introducir nuevos constructores, puedes encontrarte con el problema de olvidar proteger los invariantes en todos ellos, por lo que generalmente es una buena idea que los constructores adicionales se basen en otro, generalmente llamado constructor primario donde se protegen los invariantes.

En Java para basar un constructor en otro puedes usar la expresión this(), lo que significa que se llamará al constructor de la clase que coincida con los parámetros. Entonces la línea 12 usa this() para llamar al constructor Círculo(Punto,Longitud) que tiene las validaciones para proteger las invariantes de la clase. Después de llamar a this() eres libre de escribir más código, aunque Círculo no lo hace, recuerda que puedes hacerlo.

En las líneas 07 y 08, protegemos las invariantes de la clase Círculo asegurándonos de que el centro y el radio no sean null. ¿Qué es null? Bueno, es un gran tema que tendrá su propio capítulo, pero puedes pensar en null como sinónimo de "no presente", por lo que queremos asegurarnos de que cuando se llama al constructor, el cliente del código tiene un punto y una longitud. Nuevamente tendremos mucho más que discutir en el futuro sobre null, pero por ahora basta con decir que puedes usar Objects.requireNonNull() para evitar valores null en tus parámetros. Si se usa null, se generará una excepción tal como lo hicimos con Longitud.

Fábricas Estáticas

Código 03 - Fábricas Estáticas
public class Angulo { 
  private double medida; 
  private boolean enGrados;

  private Angulo(double medida, boolean enGrados) { 
    this.medida = medida; 
    this.enGrados = enGrados; 
} 

  public static Angulo enGrados(double medida) { 
    return new Angulo(medida, true); 
  } 

  public static Angulo enRadianes(double medida) { 
    return new Angulo(medida, false); 
  }

  public double grados() { 
    return this.enGrados ? this.medida : Math.toDegrees(this.medida);   
  } 
  public double radianes() { 
    return this.enGrados ? Math.toRadians(this.medida) : this.medida;       
  } 
}

La línea 06 tiene un constructor típico para Ángulo que toma una medida y un boolean, también llamado un flag, para saber si el ángulo esta medido en grados centígrados o en radianes. ¡Sin embargo, el constructor es privado! Hacer que los clientes de la clase recuerden establecer el valor correcto del boolean es propenso a errores. Las líneas 11 y 12 tienen fábricas estáticas para crear ángulos. Una fábrica estática es solo un método que crea objetos y es estático. Dado que las fábricas estáticas son solo métodos, pueden tener nombres, así que podemos tener una fábrica estática llamada enCentígrados() y una llamada enRadianes() que hace explícito qué es lo que queremos hacer

¿Por qué preferirías una fábrica estática a un constructor?

Como se mencionó, las fábricas estáticas tienen nombres, por lo que puedes transmitir información sobre el objeto construido en su nombre. No has aprendido sobre herencia pero en una fábrica estática puedes devolver diferentes tipos de datos. Es posible que también la fábrica estática tenga lógica para devolver objetos almacenados en caché o algo similar.

El único inconveniente es que actualmente en la documentación producida por Javadocs no hay nada que haga que esta fábrica estática se destaque.

No todos los tipos tendrían beneficios para las fábricas estáticas, pero es algo que también puedes tener en cuenta al diseñar tus clases.

Java Beans

Código 04 - Punto como un Java Beans. ¡No hagas esto innecesariamente!
public class Punto { 
  private double x; 
  private double y; 

  public void setX(double x) { 
    this.x = x; 
  } 

  public void setY(double y) { 
    this.y = y; 
  } 

  public double getX() { 
    return x; 
  } 

  public double setY() { 
    return y;
  } 
}

Cuando comenzó Java, uno de los impulsos clave para usarlo fue su capacidad de tener herramientas que automaticen algunas de sus tareas de programación. Para ayudar a las herramientas a "comprender" el código que nosotros escribimos, se creó un estándar llamado Java Beans. Para que una clase sea considerada un Java Bean, necesitaba tener un constructor sin argumentos (o ningún constructor para que ese constructor sea agregado por el compilador) y para cada atributo <foo> un método setFoo() para darle un valor y un método getFoo() para exponer ese atributo.

Los Java Beans fueron excelentes para esta tarea: dejar que las herramientas hagan parte del trabajo por ti, pero de alguna manera su propósito se confundió y la gente comenzó a escribir todas sus clases como Java Beans. Esto es muy desafortunado porque los Java Beans no brindan ningún beneficio si no usas herramientas que los necesiten, así que no caigas en la trampa de usar Java Beans innecesariamente.

Sabrás que necesitas un Java Bean cuando en la documentación de una librería te indiquen que necesitas escribir tus clases con la estructura de un Java Bean.

Constructores Con Visibilidad por Defecto

Código 05 - visibilidad por defecto en un constructor
/** 
 * NumeroSecreto representa un numero aleatorio 
 * de 4 dígitos no repetidos 
 */ 
public class NumeroSecreto { 
  private String número; 

  public NumeroSecreto() { 
    var random = new Random(); 
    var usados = new boolean[10]; 
    var builder = new StringBuilder(); 
    while (builder.length() < 4) { 
      var digito = random.nextInt(10); 
      for (; usados[digito]; digito = (digito + 1) % 10);    
      usados[digito] = true; 
      builder.append(dígito); 
    } 
    this.numero = builder.toString(); 
  } 

  NumeroSecreto(String numero) { 
    this.numero = numero; 
  } 
}

La aleatoriedad es a veces algo que necesitamos en nuestros programas. Java proporciona un conjunto sólido de clases para usar la aleatoriedad, incluída la clase Random, que está bien para programas simples.

Crear una clase NúmeroSecreto para encapsular un número aleatorio de 4 dígitos no repetidos es perfecto. La línea 08 crea un objeto de tipo Random y en la línea 13 usamos su método nextInt(), que toma un argumento de límite superior por lo que usamos el argumento 10 para generar un número del 0 al 9.

¿Cómo evitamos repeticiones? Creamos un arreglo de booleanos, en la línea 10, para que, por ejemplo, usados[5] sea falso si aún no se ha usado 5 en el número. Primero creamos un dígito aleatorio en la línea 14 y luego en la línea 15 usamos un ciclo para buscar un dígito que no hayamos usado. Si la condición no ha sido lograda, prueba con el siguiente número, por ejemplo si escogimos el número 3 y esté está usado, entonces intentamos el 4 y así sucesivamente. El incremento usa el operador módulo para garantizar que una vez lleguemos al número máximo válido, que es 9, el siguiente número que intentamos es 0 y luego 1 y así sucesivamente.

Una vez que ya tenemos un número aleatorio que no ha sido usado antes, lo marcamos como usado en nuestro arreglo de booleanos en la línea 16 y lo agregamos al número que estamos construyendo en la línea 17 usando un StringBuilder que construímos en la línea 11.

Este proceso se repite hasta que el número aleatorio en construcción tenga menos de 4 dígitos. Una vez tenga esta longitud, el ciclo se acaba e inicializamos el atributo llamando el método toString() de StringBuilder en la línea 20.

Pero ahora nos enfrentamos a una pregunta compleja: ¿Cómo podemos escribir pruebas unitarias para una clase así? Pueden haber varias soluciones pero una manera muy cercana a Java es usar un constructor con visibilidad por defecto. Hemos visto constructores públicos y privados antes, si no usa un modificador obtendrá algo que se llama visibilidad por defecto o protegido por paquete como en la línea 22. Los métodos/constructores públicos se pueden usar en cualquier lugar, los métodos/constructores privados solo se pueden usar en la misma clase que están definidos. Los métodos/constructores protegidos por paquete se pueden usar en otras clases que forman parte del mismo paquete y, como recordarás, las pruebas unitarias deben crearse en el mismo paquete de la clase que prueban, precisamente por esta razón.

Entonces, una forma muy sencilla de probar clases que podrían tener este tipo de dependencias es tener un constructor protegido por paquete que evite las dificultades y establezca el estado directamente en lo que necesita. No existen soluciones universales, así que recuerda que tienes más de una herramienta para lograr un resultado y, a veces, ¡la herramienta más simple es la menos utilizada!

Side Quest: Builders

Código 06 - Builders
package code.jugs.address;
  public class DireccionBuilder {
    String dirección = "";
    String apartadoDeCorreos = "";
    String númeroApartamento = "";
    String códigoPostal = "";
    String ciudad = "";
    String estado = "";
    String país = "";
  public DireccionBuilder direccion(String dirección) {
    this.direccion = dirección;
    return this;
}
  public DireccionBuilder apartadoDeCorreos(String apartadoDeCorreos){
    this.apartadoDeCorreos = apartadoDeCorreos;
    return this;
} 
  public DireccionBuilder numeroApartamento(String númeroApartamento){
    this.númeroApartamento = numeroApartamento;
    return this;
} 
  public DireccionBuilder codigoPostal(String códigoPostal) {
    this.códigoPostal = codigoPostal;
    return this;
} 
  public DireccionBuilder ciudad(String ciudad) {
    this.ciudad = ciudad;
    return this;
}
  public DireccionBuilder estado(String estado) {
    this.estado = estado;
    return this;
} 
  public DireccionBuilder pais(String país) {
    this.país = país;
    return this;
} 
  public Direccion build() {
    return new Address(this);
  }
} 
public class Direccion {
    private String direccion;
    private String apartadoDeCorreos;
    private String númeroApartamento;
    private String códigoPostal;
    private String ciudad;
    private String estado;
    private String país;

    Direccion(DireccionBuilder builder) {
        this.dirección = builder.dirección;
        this.apartadoDeCorreos = builder.apartadoDeCorreos;
        this.númeroApartamento = builder.númeroApartamento;
        this.códigoPostal = builder.códigoPostal;
        this.ciudad = builder.ciudad;
        this.estado = builder.estado;
        this.país = builder.país;
  }
// más código
} 
public class Main {
  public static void main(String[] args) {
    var dirección1 = new DireccionBuilder()
      .direccion("123 Main Street")
      .ciudad("Una Ciudad")
      .codigoPostal("12345")
      .pais("ABC")
      .build();
    var dirección2 = new DireccionBuilder()
      .direccion("456 Java Avenue")
      .numeroApartamento("101")
      .codigoPostal("98765")
      .ciudad("Oak")
      .estado("Sun")
      .pais("ABC")
      .build();
    var dirección3 = new DireccionBuilder()
      .apartadoDeCorreos("13579")
      .city("Foo")
      .state("Bar")
      .country("ABC")
      .build();
  }
} 

Las direcciones son tipos de datos complejos que tienen diferentes valores y que varían de un país a otro. En este ejemplo, las direcciones tienen una línea de dirección o un número de apartado postal, un número de apartamento opcional, códigos postales que con en algunos países obligatorios y en otro no, una ciudad, un estado, que en algunos países es opcional y un país. Puedes intentar tener un constructor para cada combinación posible, pero eso sería propenso a errores y, como todos los parámetros son Strings sería difícil recordar el orden.

Una posible alternativa es escribir un builder. Has utilizado un builder antes: StringBuilder para crear String complejos. Creamos un StringBuilder y llamamos el método append() reiteradamente para agregar información al String y al final usamos toString() para finalizar el proceso de construcción y completar el String

Para usar un Builder necesitas principalmente dos clases: la clase Builder que usualmente se llama XXXBuilder donde XXX es el tipo de dato a construir y la clase que deseas construir, en nuestro caso: DirecciónBuilder. La clase Builder debe tener los mismos atributos que la clase que se está creando, todos con visibilidad por defecto y, opcionalmente, puedes darles un valor predeterminado. En este ejemplo, todos los atributos comienzan como un String vacío. Los Builder no necesitan un constructor, a cambio lo que necesitas es tener un método con el mismo nombre del atributo, que toma un valor para ese atributo y devuelve el objeto que se está construyendo. Parece extraño tener esa declaración <return this>, pero eso solo significa que después de cambiar el estado del objeto, el método lo devuelve para que puedas encadenar múltiples llamadas de estos métodos, similar a cómo puedes usar append() en StringBuilder. Además de esos métodos, necesitas un método build() que finalice el proceso de construcción, llamando al constructor de Dirección y pasándole como parámetro, el objeto Builder que tiene todos los datos con que lo has configurado.

La clase Dirección tiene un constructor protegido por paquete. Eso garantiza que los clientes del código necesitan utilizar la clase Builder. El constructor toma el Builder y le extrae el estado. En la clase Main puedes ver que crear muchos tipos de direcciones es muy sencillo.

En los Builder podrías tener validaciones en el método build() y también puedes agregar un constructor con parámetros para hacer estos atributos obligatorios.

Ejercicios

E10.1

Expande la clase Fracción que hiciste para el ejercicio 8.5 para incluir los siguientes miembros:

Constructores
public Fraccion(int numerador)

Crea una fracción con el numerador dado y el denominador 1

También asegúrate de validar la invariante de que el denominador no sea 0. En caso de que la invariante se incumpla, lanza una excepción de IllegalArgumentException.

Asserts

assertEquals(new Fracción(2, 1), new Fracción(2));
assertThrows(IllegalArgumentException.class, () -> new Fracción(1, 0));

E10.2

Modifica el tipo de dato NúmeroSecreto que creamos en este capítulo. Modifica la clase para que el constructor público sea ahora un método de fábrica estático: crear(). Además, implementa los métodos picas(String) y fijas(String). En el juego de Picas y Fijas, una persona piensa un número secreto y otra trata de adivinarlo proponiendo posibles números. Por cada intento, se le indica cuántas picas y cuántas fijas tiene: una fija es un dígito que aparece en la misma posición en el intento y en el número secreto, y una pica es un dígito que aparece en el número secreto pero en una posición distinta.

Por ejemplo, si el número secreto es "1234" y el intento es "9374": el 3 es una pica, porque está presente en el número secreto pero en otra posición (segundo dígito en el intento, tercero en el número secreto); el 4 es una fija, porque coincide en la misma posición; y el 9 y el 7 no son ni picas ni fijas, porque no aparecen en el número secreto.

Constructores
NúmeroSecreto()

Un constructor que permite escoger el número, para permitir crear pruebas unitarias

Métodos
public int picas(String)

Cuenta la cantidad de picas del intento, es decir la cantidad de dígitos de intento que hacen parte del número secreto pero se encuentran en otra posición

public int fijas(String)

Cuenta la cantidad de fijas del intento, es decir la cantidad de dígitos de intento que hacen parte del número secreto y están en la misma posición

Métodos Estáticos
public static NúmeroSecreto crear()

Fábrica estática

Asserts

var _1234 = new NúmeroSecreto("1234");
assertAll(
  () -> assertEquals(1, _1234.picas("9374")),
  () -> assertEquals(1, _1234.fijas("9374")),
  () -> assertEquals(0, _1234.picas("9876")),
  () -> assertEquals(0, _1234.fijas("9876")),
  () -> assertEquals(4, _1234.fijas("1234")),
  () -> assertEquals(4, _1234.picas("4321")),
  () -> assertEquals(0, _1234.fijas("4321"))
);

E10.3

Otra regla de negocio es que queremos poder crear un Link con solo una URL. En este caso usamos la misma URL como el texto del Link.

<a href="https://jcp.org/java-in-education" target="_blank">Java en la Educación</a>

Las partes principales de un enlace son:

  • Texto: el texto dentro de las etiquetas <a>, "Java en la Educación" en el ejemplo anterior.
  • Href: la URL que se cargará después de que el usuario haga clic en el enlace, "https://jcp.org/java-in-education" en el ejemplo anterior.
  • Target: debe ser "_blank" si el enlace debe abrirse en otra pestaña de su navegador, o "" si el enlace debe abrirse en la misma pestaña.
  • La regla de negocio más común para los enlaces es que los enlaces internos siempre deben abrirse en la misma pestaña, pero los enlaces externos siempre deben abrirse en una pestaña nueva. Distinguir enlaces internos y externos es fácil, los enlaces internos son relativos, comienzan con una barra diagonal "/" y los enlaces externos comienzan con un protocolo, en este ejemplo es "https:", pero otros protocolos son "http", "ftp", etc.

    Otra regla de negocio es que queremos crear un Link con solo una URL. En este caso usamos la misma URL como el texto del Link.

    Constructores
    private Link(String texto, String url, boolean external)

    Constructor principal de la clase

    Métodos
    public String texto()

    Retorna el valor del texto del Link

    public String href()

    Retorna el valor de la URL del Link

    public String target()

    Retorna el valor de la propiedad target del Link, es decir un String vacío para un Link vacío o “_blank” para externos.

    Métodos Estáticos
    public static Link de(String texto, String url)

    Fábrica Estática para construir Links

    public static Link de(String url)

    Fábrica Estática para construir Links. El link usa como el texto el mismo url

    Asserts

    var java = Link.de("Get Java", "https://www.java.com/en/"); assertAll( () -> assertEquals("Get Java", java.texto()), () -> assertEquals("https://www.java.com/en/", java.href()), () -> assertEquals("_blank", java.target()) );
    var javaUrl = Link.de("https://www.java.com/en/"); assertAll( () -> assertEquals("https://www.java.com/en/", javaUrl.texto()), () -> assertEquals("https://www.java.com/en/", javaUrl.href()), () -> assertEquals("_blank", javaUrl.target()) );
    var internalNews = Link.de("/en/news"); assertAll( () -> assertEquals("/en/news", internalNews.texto()), () -> assertEquals("/en/news", internalNews.href()), () -> assertEquals("", internalNews.target()) );
    var javaNewsLink = Link.of("News","/en/news"); assertAll( () -> assertEquals("News", javaNewsLink.texto()), () -> assertEquals("/en/news", javaNewsLink.href()), () -> assertEquals("", javaNewsLink.target()) );

    E10.4

    Lee el Side Quest "Builders" y después, escribe un tipo de dato Triángulo basada en la clase Longitud y Ángulo que desarrollamos en este capítulo. Un triángulo tiene 3 lados, generalmente llamados <a>, <b> y <c> y 3 ángulos, generalmente llamados <alfa>, <beta> y <gamma>. Opuesto al ángulo <alfa> está el lado <a>, opuesto al ángulo <beta> está el lado <b> y opuesto al ángulo <gamma> está el lado <c>.</P>

    Es posible construir triángulos incluso si no conoces estas seis medidas. Para este ejercicio asume que debes tener al menos 3 condiciones básicas: conocer los 3 lados, conocer 2 lados y el ángulo entre ellos o dos ángulos y el lado que los conecta. Además de estas medidas, es posible que ya conozcas alguna de las otras 3 medidas, por lo que podrías proporcionarlas. T odos los valores desconocidos restantes se calcularán antes de construir el Triángulo.

    Dado que tenemos varias formas de crear objetos de tipo Triángulo, aprovecharemos el patrón Builder que vimos antes. Para encontrar algunos de los valores desconocidos, utiliza estas funciones:

    Código 05 - visibilidad por defecto en un constructor
    public class Geometria {
    
      public static Ángulo tercerÁngulo(Ángulo alfa, Ángulo beta) {
        return Ángulo.enCentígrados(180.0- (alpha.centígrados() + beta.centígrados()));
      }
    
      public static boolean esTriángulo(Longitud a, Longitud b, Longitud c) {
        var max = Math.max(a.medida(), Math.max(b.medida(), c.medida()));
        var sumaÁngulosMenores = (a.medida() + b.medida() + c.medida()) - max;
        return sumaÁngulosMenores > max;
      }
    
      public static boolean esTriángulo(Ángulo alfa, Ángulo beta, Ángulo gamma) {
        var suma = alfa.centígrados() + beta.centígrados() + gamma.centígrados();
        return Math.abs(suma - 180.0) < 1e-3;
      }
    
      public static Longitud leyCosenos(Longitud a, Longitud b, Ángulo gamma) {
        var cuadradoDeA = Math.pow(a.medida(), 2.0);
        var cuadradoDeB = Math.pow(b.medida(), 2.0);
        var tercerTérmino = 2 * a.medida() * b.medida() * Math.cos(gamma.radianes());
        var c = Math.sqrt(cuadradoDeA + cuadradoDeB - tercerTérmino);
        return new Longitud(c);
      }
    
      public static Angle leyCosenos(Longitud a, Longitud b, Longitud c) {
        var cuadradoDeA = Math.pow(a.medida(), 2.0);
        var cuadradoDeB = Math.pow(b.medida(), 2.0);
        var cuadradoDeC = Math.pow(c.medida(), 2.0);
        var numerador = cuadradoDeA + cuadradoDeB - cuadradoDeC;
        var denominador = 2 * a.medida() * b.medida();
        var coseno = numerador / denominador;
        var gamma = Math.toDegrees(Math.acos(coseno));
        var ángulo = Ángulo.enCentígrados(gamma);
        return ángulo;
      }
    
      public static Ángulo leySenos(Longitud a, Longitud b, Ángulo beta) {
        var senoBeta = Math.sin(beta.radianes());
        var razón = senoBeta / b.medida();
        var senoAlfa = razón * a.medida();
        var alfa = Math.asin(senoAlfa);
        var ángulo = Angle.ofRadians(alfa);
        return ángulo;
      }
    
      public static Longitud leySenos(Longitud a, Ángulo alfa, Ángulo beta) {
        var senoAlfa = Math.sin(alpha.radianes());
        var senoBeta = Math.sin(beta.radianes());
        var razón = a.medida() / senoAlfa;
        var b = razón * senoBeta;
        return new Longitud(b);
      }
    } 

    Puedes idear tu propia estrategia, pero ésta es sencilla:

  • Como parte del constructor Triángulo(Longitud, Longitud, Longitud)
    • Llama a Geometria.leyCosenos(Length, length, length) dos veces para obtener los valores de 2 ángulos.
    • Llama a Geometria.tercerÁngulo(Ángulo, Ángulo) para obtener el último ángulo y completar las 6 medidas.

  • Como parte del constructor Triángulo(Longitud, Ángulo, Longitud)
    • Encuentra la longitud desconocida usando Geometria.leyCosenos(Longitud, Longitud, Ángulo);
    • Encuentra uno de los ángulos que falta usando Geometria.leySenos(Longitud, Longitud, Ángulo) usando el ángulo dado como parámetro y el lado que encontraste en el punto anterior.
    • Encuentra el tercer ángulo usando Geometria.tercerÁngulo(Ángulo, Ángulo)

  • Como parte del constructor Triángulo(Ángulo, Longitud, Ángulo)
    • Encuentra el tercer ángulo usando Geometria.tercerÁngulo(Ángulo,Ángulo)
    • Encuentra las longitudes que faltan usando Geometria.leySenos(Longitud, Ángulo, Ángulo) usando la longitud dada como parámetro y el ángulo encontrado en el punto anterior.

    Si el usuario quiere reescribir estos valores puede usar los métodos que retornan un builder para ese fin.

    En el constructor de Triángulo valida las 2 invariantes: los 3 lados deben ser válidos usando Geometria.esTriángulo(Longitud, Longitud, Longitud) si no, arroja una excepción IllegalStateException con un mensaje mostrando las longitudes de los 3 lados. Luego verifica la invariante para los ángulos usando Geometria.esTriángulo(Ángulo, Ángulo, Ángulo)

    Adicionalmente debes agregar en los código que ya hemos, métodos para verificar si 2 ángulos son equivalentes y 2 longitudes son equivalentes:

    public class Ángulo {
      // más código
        public boolean equivalente(Ángulo otro) {
          return Math.abs(centígrados() - otro.centígrados()) < 1e-3;
      }
    }
    public class Longitud {
      // más código
        public boolean equivalent(Longitud otra) {
          return Math.abs(medida - otra.medida) < 1e-3;
      }
    }

    TriánguloBuilder

    Constructores
    public TrianguloBuilder(Longitud a, Longitud b, Longitud c)
    public TrianguloBuilder(Longitud a, Angulo gamma, Longitud b)
    public TrianguloBuilder(Angulo alfa, Longitud c, Ángulo beta)
    Métodos
    public TrianguloBuilder a(Longitud a) Da un valor al atributo <a>
    public TrianguloBuilder b(Longitud b) Da un valor al atributo <b>
    public TrianguloBuilder c(Longitud c) Da un valor al atributo <c>
    public TrianguloBuilder alfa(Angulo alfa) Da un valor al atributo <alfa>
    public TrianguloBuilder beta(Angulo beta) Da un valor al atributo <beta>
    public TriánguloBuilder gamma(Angulo gamma) Da un valor al atributo <gamma>
    public Triangulo build() Crea un objeto de tipo Triangulo con los valores dados.

    TriánguloBuilder

    Constructores
    TrianguloBuilder(TrianguloBuilder builder)
    Métodos
    public Longitud a() Accesor para el atributo <a>
    public Longitud b() Accesor para el atributo <b>
    public Longitud c() Accesor para el atributo <c>
    public Ángulo alfa() Accesor para el atributo <alfa>
    public Angulo beta() Accesor para el atributo <beta>
    public Angulo gamma() Accesor para el atributo <gamma>
    public boolean equivalente(Triangulo otro) Verifica si 2 triangulos son equivalentes. Esto es cierto si sus correspondientes ángulos y correspondientes lados son equivalentes.
    var longitudUno = new Longitud(1.0);
    var ángulo60 = Ángulo.deCentígrados(60);
    var triánguloEquilatero = new TriánguloBuilder(longitudUno, ángulo60, longitudUno).build();
    assertTrue(triánguloEquilatero.a().equivalente(new Longitud(1.0)));
    assertTrue(triánguloEquilatero.b().equivalente(new Longitud(1.0)));
    assertTrue(triánguloEquilatero.c().equivalent(new Longitud(1.0)));
    assertTrue(triánguloEquilatero.alfa().equivalente(Ángulo.enCentígrados(60.0)));
    assertTrue(triánguloEquilatero.beta().equivalente(Ángulo.enCentígrados(60.0)));
    assertTrue(triánguloEquilatero.gamma().equivalente(Ángulo.enCentígrados(60.0)));
    var longitudUno = new Longitud(1.0);
    var triánguloEquilatero = new TriánguloBuilder(longitudUno, longitudUno,
    longitudUno).build();
    assertTrue(triánguloEquilatero.a().equivalente(new Longitud(1.0)));
    assertTrue(triánguloEquilatero.b().equivalente(new Longitud(1.0)));
    assertTrue(triánguloEquilatero.c().equivalent(new Longitud(1.0)));
    assertTrue(triánguloEquilatero.alfa().equivalente(Ángulo.enCentígrados(60.0)));
    assertTrue(triánguloEquilatero.beta().equivalente(Ángulo.enCentígrados(60.0)));
    assertTrue(triánguloEquilatero.gamma().equivalente(Ángulo.enCentígrados(60.0)));
    var longitudUno = new Longitud(1.0);
    var ángulo60 = Ángulo.deCentígrados(60);
    var triánguloEquilatero = new TriánguloBuilder(ángulo60, longitudUno, ángulo60).build();
    assertTrue(triánguloEquilatero.a().equivalente(new Longitud(1.0)));
    assertTrue(triánguloEquilatero.b().equivalente(new Longitud(1.0)));
    assertTrue(triánguloEquilatero.c().equivalent(new Longitud(1.0)));
    assertTrue(triánguloEquilatero.alfa().equivalente(Ángulo.enCentígrados(60.0)));
    assertTrue(triánguloEquilatero.beta().equivalente(Ángulo.enCentígrados(60.0)));
    assertTrue(triánguloEquilatero.gamma().equivalente(Ángulo.enCentígrados(60.0)));

    © 2022 Carlos Obregón. This work is licensed under Creative Commons Attribution 4.0 International (CC BY 4.0)

    Scroll to Top