Más sobre getters y setters

Es un principio de diseño orientado a objetos (OO) de hace 25 años que no debe exponer la implementación de un objeto a ninguna otra clase del programa. El programa es innecesariamente difícil de mantener cuando se expone la implementación, principalmente porque cambiar un objeto que expone su implementación exige cambios en todas las clases que usan el objeto.

Desafortunadamente, el lenguaje getter / setter que muchos programadores consideran como orientado a objetos viola este principio fundamental de OO con creces. Considere el ejemplo de una Moneyclase que tiene un getValue()método que devuelve el "valor" en dólares. Tendrá un código como el siguiente en todo su programa:

double orderTotal; Cantidad de dinero = ...; // ... orderTotal + = amount.getValue (); // orderTotal debe estar en dólares

El problema con este enfoque es que el código anterior hace una gran suposición sobre cómo Moneyse implementa la clase (que el "valor" se almacena en a double). Código que hace que los supuestos de implementación se rompan cuando la implementación cambia. Si, por ejemplo, necesita internacionalizar su aplicación para que admita monedas distintas del dólar, getValue()no devuelve nada significativo. Podría agregar un getCurrency(), pero eso haría que todo el código que rodea la getValue()llamada sea mucho más complicado, especialmente si persiste en usar la estrategia getter / setter para obtener la información que necesita para hacer el trabajo. Una implementación típica (defectuosa) podría verse así:

Cantidad de dinero = ...; // ... valor = cantidad.getValue (); moneda = monto.getCurrency (); conversion = CurrencyTable.getConversionFactor (moneda, USDOLLARS); total + = valor * conversión; // ...

Este cambio es demasiado complicado para ser manejado por refactorización automatizada. Además, tendría que realizar este tipo de cambios en todas partes de su código.

La solución a nivel de lógica empresarial para este problema es realizar el trabajo en el objeto que tiene la información necesaria para realizar el trabajo. En lugar de extraer el "valor" para realizar alguna operación externa en él, debe hacer que la Moneyclase realice todas las operaciones relacionadas con el dinero, incluida la conversión de moneda. Un objeto correctamente estructurado manejaría el total de esta manera:

Total de dinero = ...; Cantidad de dinero = ...; total.increaseBy (monto);

El add()método calcularía la moneda del operando, haría cualquier conversión de moneda necesaria (que es, correctamente, una operación con dinero ) y actualizaría el total. Si usó esta estrategia de objeto-que-tiene-la-información-hace-el-trabajo para empezar, la noción de moneda podría agregarse a la Moneyclase sin ningún cambio requerido en el código que usa Moneyobjetos. Es decir, el trabajo de refactorizar un dólar -sólo a una implementación internacional- estaría concentrado en un solo lugar: la Moneyclase.

El problema

La mayoría de los programadores no tienen dificultad para comprender este concepto en el nivel de la lógica empresarial (aunque puede requerir un poco de esfuerzo pensar de esa manera de manera consistente). Sin embargo, comienzan a surgir problemas cuando la interfaz de usuario (UI) entra en escena. El problema no es que no pueda aplicar técnicas como la que acabo de describir para construir una interfaz de usuario, sino que muchos programadores están atrapados en una mentalidad de captador / definidor cuando se trata de interfaces de usuario. Culpo de este problema a las herramientas de construcción de código fundamentalmente procedimentales como Visual Basic y sus clones (incluidos los constructores de UI de Java) que te obligan a adoptar esta forma de pensar procedimental, getter / setter.

(Digresión: algunos de ustedes se resistirán a la declaración anterior y gritarán que VB se basa en la arquitectura sagrada Modelo-Vista-Controlador (MVC), por lo que es sacrosanto. Tenga en cuenta que MVC se desarrolló hace casi 30 años. En la década de 1970, la supercomputadora más grande estaba a la par con las computadoras de escritorio actuales. La mayoría de las máquinas (como la DEC PDP-11) eran computadoras de 16 bits, con 64 KB de memoria y velocidades de reloj medidas en decenas de megahercios. Su interfaz de usuario probablemente pila de tarjetas perforadas. Si tuvo la suerte de tener un terminal de video, es posible que haya estado utilizando un sistema de entrada / salida (E / S) de consola basado en ASCII. Hemos aprendido mucho en los últimos 30 años. Incluso Java Swing tuvo que reemplazar MVC con una arquitectura similar de "modelo separable", principalmente porque MVC puro no aísla suficientemente la interfaz de usuario y las capas del modelo de dominio).

Entonces, definamos el problema en pocas palabras:

Si un objeto no puede exponer información de implementación (a través de métodos get / set o por cualquier otro medio), entonces es lógico pensar que un objeto debe crear de alguna manera su propia interfaz de usuario. Es decir, si la forma en que se representan los atributos de un objeto está oculta al resto del programa, entonces no puede extraer esos atributos para construir una interfaz de usuario.

Tenga en cuenta, por cierto, que no está ocultando el hecho de que existe un atributo. (Estoy definiendo atributo , aquí, como una característica esencial del objeto.) Usted sabe que un Employeedebe tener un salario o un atributo de salario, de lo contrario no sería un Employee. (Sería a Person, a Volunteer, a Vagrant, o cualquier otra cosa que no tenga un salario). Lo que no sabe, o quiere saber, es cómo se representa ese salario dentro del objeto. Podría ser un double, una String, una escala longdecimal, o codificadas binario. Puede ser un atributo "sintético" o "derivado", que se calcula en tiempo de ejecución (a partir de un grado de pago o título de trabajo, por ejemplo, o al obtener el valor de una base de datos). Aunque un método de obtención puede ocultar algunos de estos detalles de implementación,como vimos con elMoney ejemplo, no se puede ocultar lo suficiente.

Entonces, ¿cómo un objeto produce su propia interfaz de usuario y se mantiene mantenible? Solo los objetos más simplistas pueden admitir algo como un displayYourself()método. Los objetos realistas deben:

  • Se muestran en diferentes formatos (XML, SQL, valores separados por comas, etc.).
  • Mostrar diferentes vistas de sí mismos (una vista puede mostrar todos los atributos; otra puede mostrar solo un subconjunto de los atributos; y una tercera puede presentar los atributos de una manera diferente).
  • Se muestran en diferentes entornos (del lado del cliente ( JComponent) y servidos al cliente (HTML), por ejemplo) y manejan tanto la entrada como la salida en ambos entornos.

Algunos de los lectores de mi artículo anterior sobre getter / setter llegaron a la conclusión de que yo estaba defendiendo que se agregaran métodos al objeto para cubrir todas estas posibilidades, pero esa "solución" obviamente no tiene sentido. El objeto de peso pesado resultante no solo es demasiado complicado, sino que tendrá que modificarlo constantemente para manejar los nuevos requisitos de la interfaz de usuario. Prácticamente, un objeto simplemente no puede construir todas las interfaces de usuario posibles por sí mismo, si no fuera por otra razón que muchas de esas interfaces de usuario ni siquiera fueron concebidas cuando se creó la clase.

Construye una solución

La solución de este problema es separar el código de la interfaz de usuario del objeto comercial principal colocándolo en una clase separada de objetos. Es decir, debe dividir algunas funciones que podrían estar en el objeto en un objeto completamente separado.

Esta bifurcación de los métodos de un objeto aparece en varios patrones de diseño. Lo más probable es que esté familiarizado con la estrategia, que se usa con las diversas java.awt.Containerclases para hacer el diseño. Se podría resolver el problema de diseño con una solución derivación: FlowLayoutPanel, GridLayoutPanel, BorderLayoutPanel, etc, sino que los mandatos demasiadas clases y una gran cantidad de código duplicado en esas clases. Una única solución de clase pesada (agregar métodos a Me Containergusta layOutAsGrid(), layOutAsFlow()etc.) tampoco es práctica porque no puede modificar el código fuente Containersimplemente porque necesita un diseño no compatible. En el patrón de estrategia, se crea una Strategyinterfaz ( LayoutManager) implementado por varias Concrete Strategyclases ( FlowLayout, GridLayout, etc.). Luego le dice a un Contextobjeto (unContainer) cómo hacer algo pasándole un Strategyobjeto. (Se pasa una Containera LayoutManagerque define una estrategia de diseño).

El patrón de constructor es similar a la estrategia. La principal diferencia es que la Builderclase implementa una estrategia para construir algo (como un JComponentflujo XML o que representa el estado de un objeto). Builderlos objetos también suelen fabricar sus productos mediante un proceso de varias etapas. Es decir, se requieren llamadas a varios métodos Builderpara completar el proceso de construcción, y Buildernormalmente no sabe el orden en el que se realizarán las llamadas o el número de veces que se llamará a uno de sus métodos. La característica más importante del constructor es que el objeto comercial (llamado Context) no sabe exactamente qué Builderestá construyendo. El patrón aísla el objeto comercial de su representación.

La mejor manera de ver cómo funciona un constructor simple es mirar uno. Primero veamos el Context, el objeto comercial que necesita exponer una interfaz de usuario. El Listado 1 muestra una Employeeclase simplista . Los atributos Employeetiene name, idy salary. (Los códigos auxiliares de estas clases se encuentran al final de la lista, pero estos códigos auxiliares son solo marcadores de posición para lo real. Espero que puedas imaginar fácilmente cómo funcionarían estas clases).

Este particular Contextusa lo que yo considero un constructor bidireccional. El clásico Gang of Four Builder va en una dirección (salida), pero también agregué Builderun Employeeobjeto que puede usar para inicializarse. Se Builderrequieren dos interfaces. La Employee.Exporterinterfaz (Listado 1, línea 8) maneja la dirección de salida. Define una interfaz para un Builderobjeto que construye la representación del objeto actual. El Employeedelega la construcción de la interfaz de usuario real Builderen el export()método (en la línea 31). No Builderse le pasan los campos reales, sino que usa Strings para pasar una representación de esos campos.

Listado 1. Empleado: el contexto del constructor

1 importar java.util.Locale; 2 3 clase pública Empleado 4 {nombre privado Nombre; 5 ID de empleado privado; 6 salario privado en dinero; 7 8 Exportador de interfaz pública 9 {void addName (nombre de cadena); 10 void addID (ID de cadena); 11 void addSalary (salario de cadena); 12} 13 14 Importador de interfaz pública 15 {String provideName (); 16 String provideID (); 17 String provideSalary (); 18 vacío abierto (); 19 cierre vacío (); 20} 21 22 Empleado público (constructor importador) 23 {constructor.open (); 24 this.name = nuevo Nombre (constructor.nombreProveedor ()); 25 this.id = new EmployeeId (constructor.provideID ()); 26 this.salary = new Money (builder.provideSalary (), 27 new Locale ("en", "US")); 28 constructor.close (); 29} 30 31 exportación vacía pública (constructor exportador) 32 {constructor.addName (nombre.toString ()); 33 constructor.addID (id.toString ()); 34 builder.addSalary (salario.Encadenar() ); 35} 36 37// ... 38} 39 // ---------------------------------------- ------------------------------ 40 // Material de prueba unitaria 41 // 42 Nombre de clase 43 {valor de cadena privada; 44 Nombre público (valor de cadena) 45 {this.value = value; 46} 47 public String toString () {valor de retorno; }; 48} 49 50 class EmployeeId 51 {valor de cadena privada; 52 public EmployeeId (valor de cadena) 53 {this.value = value; 54} 55 public String toString () {valor de retorno; } 56} 57 58 clase Money 59 {valor de cadena privada; 60 public Money (valor de cadena, ubicación de la configuración regional) 61 {this.value = value; 62} 63 public String toString () {valor de retorno; } 64}

Veamos un ejemplo. El siguiente código crea la interfaz de usuario de la Figura 1:

Empleado wilma = ...; JComponentExporter uiBuilder = nuevo JComponentExporter (); // Crea el constructor wilma.export (uiBuilder); // Construye la interfaz de usuario JComponent userInterface = uiBuilder.getJComponent (); // ... someContainer.add (userInterface);

El Listado 2 muestra la fuente de JComponentExporter. Como puede ver, todo el código relacionado con la interfaz de usuario se concentra en el Concrete Builder(el JComponentExporter), y el Context(el Employee) impulsa el proceso de compilación sin saber exactamente qué está compilando.

Listado 2. Exportación a una interfaz de usuario del lado del cliente

1 importación javax.swing. *; 2 importar java.awt. *; 3 importar java.awt.event. *; 4 5 clase JComponentExporter implementa Employee.Exporter 6 {nombre de cadena privada, id, salario; 7 8 public void addName (nombre de la cadena) {this.name = nombre; } 9 public void addID (String id) {this.id = id; } 10 public void addSalary (String salario) {this.salary = salario; } 11 12 JComponent getJComponent () 13 {Panel JComponent = new JPanel (); 14 panel.setLayout (nuevo GridLayout (3,2)); 15 panel.add (new JLabel ("Nombre:")); 16 panel.add (nuevo JLabel (nombre)); 17 panel.add (new JLabel ("ID de empleado:")); 18 panel.add (nuevo JLabel (id)); 19 panel.add (nuevo JLabel ("Salario:")); 20 panel.add (nuevo JLabel (salario)); 21 panel de retorno; 22} 23}