Por qué los métodos getter y setter son malos

No tenía la intención de comenzar una serie de "es malvado", pero varios lectores me pidieron que explicara por qué mencioné que debería evitar los métodos get / set en la columna del mes pasado, "Por qué se extiende es malo".

Aunque los métodos getter / setter son comunes en Java, no están particularmente orientados a objetos (OO). De hecho, pueden dañar la capacidad de mantenimiento de su código. Además, la presencia de numerosos métodos getter y setter es una señal de alerta de que el programa no está necesariamente bien diseñado desde una perspectiva orientada a objetos.

Este artículo explica por qué no debería usar getters y setters (y cuándo puede usarlos) y sugiere una metodología de diseño que lo ayudará a salir de la mentalidad de getter / setter.

Sobre la naturaleza del diseño

Antes de lanzarme a otra columna relacionada con el diseño (con un título provocativo, nada menos), quiero aclarar algunas cosas.

Me sorprendieron algunos comentarios de lectores que resultaron de la columna del mes pasado, "¿Por qué se extiende es malvado?" (Ver Talkback en la última página del artículo). Algunas personas creyeron que yo sostenía que la orientación a objetos es mala simplemente porque extendstiene problemas, como si los dos conceptos fueran equivalentes. Ciertamente eso no es lo que pensé que había dicho, así que déjame aclarar algunos meta-problemas.

Esta columna y el artículo del mes pasado tratan sobre diseño. El diseño, por naturaleza, es una serie de compensaciones. Cada elección tiene un lado bueno y uno malo, y usted hace su elección en el contexto de criterios generales definidos por la necesidad. Sin embargo, lo bueno y lo malo no son absolutos. Una buena decisión en un contexto puede ser mala en otro.

Si no comprende ambos lados de un problema, no puede tomar una decisión inteligente; de hecho, si no comprende todas las ramificaciones de sus acciones, no está diseñando en absoluto. Estás tropezando en la oscuridad. No es casualidad que todos los capítulos del libro Patrones de diseño de Gang of Four incluyan una sección de "Consecuencias" que describe cuándo y por qué el uso de un patrón es inapropiado.

Afirmar que alguna característica del lenguaje o lenguaje de programación común (como los accesos) tiene problemas no es lo mismo que decir que nunca debe usarlos bajo ninguna circunstancia. Y el hecho de que una característica o un idioma se use comúnmente no significa que usted deba usarlo tampoco. Los programadores desinformados escriben muchos programas y el simple hecho de ser empleados por Sun Microsystems o Microsoft no mejora mágicamente las habilidades de programación o diseño de alguien. Los paquetes de Java contienen mucho código excelente. Pero también hay partes de ese código que estoy seguro de que los autores se avergüenzan de admitir que lo escribieron.

Del mismo modo, los incentivos políticos o de marketing suelen impulsar los modismos del diseño. A veces, los programadores toman malas decisiones, pero las empresas quieren promover lo que la tecnología puede hacer, por lo que restan importancia a que la forma en que lo haces es menos que ideal. Sacan lo mejor de una mala situación. En consecuencia, actúa de manera irresponsable cuando adopta cualquier práctica de programación simplemente porque "esa es la forma en que se supone que debe hacer las cosas". Muchos proyectos fallidos de Enterprise JavaBeans (EJB) prueban este principio. La tecnología basada en EJB es una gran tecnología cuando se usa apropiadamente, pero literalmente puede derribar a una empresa si se usa de manera inapropiada.

Mi punto es que no debes programar a ciegas. Debe comprender el caos que puede causar una característica o un idioma. Al hacerlo, estará en una posición mucho mejor para decidir si debe usar esa función o ese idioma. Sus elecciones deben ser informadas y pragmáticas. El propósito de estos artículos es ayudarlo a abordar su programación con los ojos abiertos.

Abstracción de datos

Un precepto fundamental de los sistemas OO es que un objeto no debe exponer ninguno de sus detalles de implementación. De esta forma, puede cambiar la implementación sin cambiar el código que usa el objeto. Se deduce entonces que en los sistemas OO debe evitar las funciones getter y setter, ya que en su mayoría brindan acceso a los detalles de implementación.

Para ver por qué, considere que puede haber 1,000 llamadas a un getX()método en su programa, y ​​cada llamada asume que el valor de retorno es de un tipo particular. Puede almacenar getX()el valor de retorno en una variable local, por ejemplo, y ese tipo de variable debe coincidir con el tipo de valor de retorno. Si necesita cambiar la forma en que se implementa el objeto de tal manera que cambie el tipo de X, está en serios problemas.

Si X era un int, pero ahora debe ser un long, obtendrá 1000 errores de compilación. Si soluciona incorrectamente el problema al convertir el valor de retorno en int, el código se compilará limpiamente, pero no funcionará. (El valor de retorno puede estar truncado). Debe modificar el código que rodea a cada una de esas 1,000 llamadas para compensar el cambio. Ciertamente no quiero hacer tanto trabajo.

Un principio básico de los sistemas OO es la abstracción de datos . Debe ocultar completamente la forma en que un objeto implementa un controlador de mensajes del resto del programa. Esa es una de las razones por las que todas sus variables de instancia (los campos no constantes de una clase) deberían serlo private.

Si crea una variable de instancia public, entonces no puede cambiar el campo a medida que la clase evoluciona con el tiempo porque rompería el código externo que usa el campo. No desea buscar 1,000 usos de una clase simplemente porque cambia esa clase.

Este principio de ocultación de la implementación conduce a una buena prueba ácida de la calidad de un sistema OO: ¿Puede realizar cambios masivos en la definición de una clase, incluso desechar todo y reemplazarlo con una implementación completamente diferente, sin afectar el código que lo usa? objetos de la clase? Este tipo de modularización es la premisa central de la orientación a objetos y facilita mucho el mantenimiento. Sin la ocultación de la implementación, no tiene mucho sentido usar otras características de OO.

Los métodos getter y setter (también conocidos como accesores) son peligrosos por la misma razón que los publiccampos son peligrosos: brindan acceso externo a los detalles de implementación. ¿Qué sucede si necesita cambiar el tipo de campo al que se accede? También debe cambiar el tipo de retorno del descriptor de acceso. Utiliza este valor de retorno en numerosos lugares, por lo que también debe cambiar todo ese código. Quiero limitar los efectos de un cambio a una sola definición de clase. No quiero que se extiendan a todo el programa.

Dado que los descriptores de acceso violan el principio de encapsulación, puede argumentar razonablemente que un sistema que utiliza en gran medida o de manera inapropiada los descriptores de acceso simplemente no está orientado a objetos. Si pasa por un proceso de diseño, en lugar de simplemente codificar, encontrará casi ningún acceso en su programa. El proceso es importante. Tengo más que decir sobre este tema al final del artículo.

La falta de métodos getter / setter no significa que algunos datos no fluyan a través del sistema. No obstante, es mejor minimizar el movimiento de datos tanto como sea posible. Mi experiencia es que la capacidad de mantenimiento es inversamente proporcional a la cantidad de datos que se mueven entre los objetos. Aunque es posible que aún no vea cómo, en realidad puede eliminar la mayor parte de este movimiento de datos.

Al diseñar cuidadosamente y enfocarse en lo que debe hacer en lugar de en cómo lo hará, elimina la gran mayoría de métodos getter / setter en su programa. No pida la información que necesita para realizar el trabajo; Pídale al objeto que tiene la información que haga el trabajo por usted.La mayoría de los accesadores encuentran su camino hacia el código porque los diseñadores no estaban pensando en el modelo dinámico: los objetos en tiempo de ejecución y los mensajes que se envían entre sí para hacer el trabajo. Empiezan (incorrectamente) por diseñar una jerarquía de clases y luego intentan calzar esas clases en el modelo dinámico. Este enfoque nunca funciona. Para construir un modelo estático, necesita descubrir las relaciones entre las clases y estas relaciones corresponden exactamente al flujo de mensajes. Existe una asociación entre dos clases solo cuando los objetos de una clase envían mensajes a los objetos de la otra. El objetivo principal del modelo estático es capturar esta información de asociación a medida que modela dinámicamente.

Sin un modelo dinámico claramente definido, solo está adivinando cómo usará los objetos de una clase. En consecuencia, los métodos de acceso a menudo terminan en el modelo porque debe proporcionar tanto acceso como sea posible, ya que no puede predecir si lo necesitará o no. Este tipo de estrategia de diseño por adivinación es, en el mejor de los casos, ineficaz. Pierde tiempo escribiendo métodos inútiles (o agregando capacidades innecesarias a las clases).

Los accesorios también terminan en diseños por la fuerza de la costumbre. Cuando los programadores procedimentales adoptan Java, tienden a empezar por crear un código familiar. Los lenguajes procedimentales no tienen clases, pero tienen la C struct(piense: clase sin métodos). Parece natural, entonces, imitar un structconstruyendo definiciones de clases prácticamente sin métodos y nada más que publiccampos. privateSin embargo, estos programadores de procedimientos leen en algún lugar que los campos deberían estar , por lo que crean los campos privatey proporcionan publicmétodos de acceso. Pero solo han complicado el acceso público. Ciertamente no han hecho que el sistema esté orientado a objetos.

Dibujate a ti mismo

Una ramificación de la encapsulación de campo completo es la construcción de la interfaz de usuario (UI). Si no puede usar descriptores de acceso, no puede hacer que una clase de constructor de IU llame a un getAttribute()método. En cambio, las clases tienen elementos como drawYourself(...)métodos.

Un getIdentity()método también puede funcionar, por supuesto, siempre que devuelva un objeto que implemente la Identityinterfaz. Esta interfaz debe incluir un método drawYourself()(o dame-un- JComponentque-represente-tu-identidad). Aunque getIdentitycomienza con "get", no es un descriptor de acceso porque no solo devuelve un campo. Devuelve un objeto complejo que tiene un comportamiento razonable. Incluso cuando tengo un Identityobjeto, todavía no tengo idea de cómo se representa internamente una identidad.

Por supuesto, una drawYourself()estrategia significa que yo (¡jadeo!) Pongo el código de la interfaz de usuario en la lógica empresarial. Considere lo que sucede cuando cambian los requisitos de la interfaz de usuario. Digamos que quiero representar el atributo de una manera completamente diferente. Hoy una "identidad" es un nombre; mañana es un nombre y un número de identificación; el día siguiente es un nombre, un número de identificación y una foto. Limito el alcance de estos cambios a un lugar en el código. Si tengo una JComponentclase de dame-una- que-representa-tu-identidad, entonces he aislado la forma en que se representan las identidades del resto del sistema.

Tenga en cuenta que en realidad no he incluido ningún código de interfaz de usuario en la lógica empresarial. Escribí la capa de interfaz de usuario en términos de AWT (Abstract Window Toolkit) o ​​Swing, que son capas de abstracción. El código de IU real está en la implementación de AWT / Swing. Ese es el objetivo de una capa de abstracción: aislar su lógica empresarial de la mecánica de un subsistema. Puedo migrar fácilmente a otro entorno gráfico sin cambiar el código, por lo que el único problema es un poco de desorden. Puede eliminar fácilmente este desorden moviendo todo el código de la interfaz de usuario a una clase interna (o utilizando el patrón de diseño Façade).

JavaBeans

Puede objetar diciendo: "¿Pero qué pasa con los JavaBeans?" ¿Que hay de ellos? Ciertamente puede construir JavaBeans sin getters ni setters. El BeanCustomizer, BeanInfoy BeanDescriptorclases todos existen precisamente para este propósito. Los diseñadores de especificaciones de JavaBean introdujeron el lenguaje getter / setter en la imagen porque pensaron que sería una manera fácil de hacer un bean rápidamente, algo que puede hacer mientras aprende a hacerlo bien. Desafortunadamente, nadie hizo eso.

Los descriptores de acceso se crearon únicamente como una forma de etiquetar determinadas propiedades para que un programa de creación de interfaz de usuario o equivalente pudiera identificarlas. Se supone que no debes llamar a estos métodos tú mismo. Existen para usar una herramienta automatizada. Esta herramienta utiliza las API de introspección en la Classclase para encontrar los métodos y extrapolar la existencia de ciertas propiedades a partir de los nombres de los métodos. En la práctica, este modismo basado en la introspección no ha funcionado. Ha hecho que el código sea demasiado complicado y procesal. Los programadores que no comprenden la abstracción de datos en realidad llaman a los accesores y, como consecuencia, el código es menos fácil de mantener. Por este motivo, se incorporará una función de metadatos en Java 1.5 (prevista para mediados de 2004). Entonces en lugar de:

propiedad privada int; public int getProperty () {propiedad de retorno; } public void setProperty (int valor} {propiedad = valor;}

Podrás usar algo como:

propiedad privada @property int; 

La herramienta de construcción de IU o equivalente utilizará las API de introspección para encontrar las propiedades, en lugar de examinar los nombres de los métodos e inferir la existencia de una propiedad a partir de un nombre. Por lo tanto, ningún descriptor de acceso en tiempo de ejecución daña su código.

¿Cuándo está bien un acceso?

Primero, como mencioné anteriormente, está bien que un método devuelva un objeto en términos de una interfaz que implementa el objeto porque esa interfaz lo aísla de los cambios en la clase de implementación. Este tipo de método (que devuelve una referencia de interfaz) no es realmente un "captador" en el sentido de un método que solo proporciona acceso a un campo. Si cambia la implementación interna del proveedor, simplemente cambie la definición del objeto devuelto para adaptarse a los cambios. Aún protege el código externo que usa el objeto a través de su interfaz.