Gran cantidad de tiempo, no puedo pensar en una razón para tener un objeto en lugar de una clase estática. ¿Los objetos tienen más beneficios de lo que creo? [cerrado]

9

Entiendo el concepto de un objeto, y como programador de Java siento que el paradigma OO me resulta bastante natural en la práctica.

Sin embargo, recientemente me encontré pensando:

Espere un segundo, ¿cuáles son realmente los beneficios prácticos de usar un objeto sobre el uso de una clase estática (con las prácticas adecuadas de encapsulación y OO)?

Podría pensar en dos beneficios de usar un objeto (ambos son significativos y poderosos):

  1. Polimorfismo: le permite intercambiar funcionalidades de forma dinámica y flexible durante el tiempo de ejecución. También permite agregar nuevas funciones 'partes' y alternativas al sistema fácilmente. Por ejemplo, si hay una Carclase diseñada para trabajar con Engineobjetos, y desea agregar un nuevo motor al sistema que el automóvil puede usar, puede crear una nueva Enginesubclase y simplemente pasar un objeto de esta clase al Carobjeto, sin tener que cambiar nada acerca Car. Y puede decidir hacerlo durante el tiempo de ejecución.

  2. Ser capaz de 'pasar funcionalidad': puede pasar un objeto alrededor del sistema de forma dinámica.

¿Pero hay más ventajas para los objetos sobre las clases estáticas?

A menudo, cuando agrego nuevas 'partes' a un sistema, lo hago creando una nueva clase e instanciando objetos a partir de él.

Pero recientemente, cuando me detuve y lo pensé, me di cuenta de que una clase estática haría lo mismo que un objeto, en muchos de los lugares donde normalmente uso un objeto.

Por ejemplo, estoy trabajando para agregar un mecanismo de guardar / cargar archivo a mi aplicación.

Con un objeto, la línea de código de llamada se verá así: Thing thing = fileLoader.load(file);

Con una clase estática, se vería así: Thing thing = FileLoader.load(file);

¿Cual es la diferencia?

Con bastante frecuencia, no puedo pensar en una razón para crear una instancia de un objeto cuando una clase estática simple y antigua actuaría de la misma manera. Pero en los sistemas OO, las clases estáticas son bastante raras. Entonces debo estar perdiendo algo.

¿Hay más ventajas para los objetos que no sean los dos que enumeré? Por favor explique.

EDITAR: Para aclarar. Sí encuentro objetos muy útiles al intercambiar funcionalidad o al pasar datos. Por ejemplo, escribí una aplicación que inventa melodías. MelodyGeneratortenían varias subclases que crean melodías de manera diferente, y los objetos de estas clases eran intercambiables (patrón de estrategia).

Las melodías también eran objetos, ya que es útil pasarlas. También los acordes y escalas.

Pero, ¿qué pasa con las partes 'estáticas' del sistema, que no se van a pasar? Por ejemplo, un mecanismo de 'guardar archivo'. ¿Por qué debería implementarlo en un objeto y no en una clase estática?

Aviv Cohn
fuente
Entonces, en lugar de un objeto con campos y quizás métodos, sin objetos, sin registros, ¿solo una gran cantidad de valores escalares pasados ​​y devueltos de métodos estáticos? Editar: Su nuevo ejemplo sugiere lo contrario: ¿Objetos, simplemente sin métodos de instancia? De lo contrario, ¿qué es Thing?
¿Qué sucede cuando necesitas cambiar tu FileLoaderpor uno que lee desde un socket? ¿O un simulacro para probar? ¿O uno que abra un archivo zip?
Benjamin Hodgson
1
@delnan Bien, resolví una pregunta que la respuesta me ayudará a entender: ¿por qué implementarías una parte 'estática' del sistema como un objeto? Como el ejemplo en la pregunta: un mecanismo para guardar archivos. ¿Qué gano al implementarlo en un objeto?
Aviv Cohn
1
El valor predeterminado debería ser utilizar un objeto no estático. Solo use clases estáticas si siente que realmente expresa mejor su intención. System.Mathen .NET es un ejemplo de algo que tiene mucho más sentido como clase estática: nunca necesitará cambiarlo o burlarse de él y ninguna de las operaciones podría formar parte de una instancia. Realmente no creo que su ejemplo de "ahorro" se ajuste a esa factura.
Benjamin Hodgson

Respuestas:

14

jajaja suenas como un equipo en el que solía trabajar;)

Java (y probablemente C #) ciertamente admite ese estilo de programación. Y trabajo con personas cuyo primer instinto es: "¡Puedo hacer que sea un método estático!" Pero hay algunos costos sutiles que lo alcanzan con el tiempo.

1) Java es un lenguaje orientado a objetos. Y enloquece a los chicos funcionales, pero realmente se sostiene bastante bien. La idea detrás de OO es agrupar la funcionalidad con el estado para tener pequeñas unidades de datos y funcionalidades que preserven su semántica al ocultar el estado y exponer solo las funciones que tienen sentido en ese contexto.

Al pasar a una clase con solo métodos estáticos, está rompiendo la parte de "estado" de la ecuación. Pero el estado todavía tiene que vivir en alguna parte. Entonces, lo que he visto con el tiempo es que una clase con todos los métodos estáticos comienza a tener listas de parámetros cada vez más complicadas, porque el estado se está moviendo desde la clase hacia las llamadas a funciones.

Después de crear una clase con todos los métodos estáticos, revise y solo examine cuántos de esos métodos tienen un único parámetro común. Es una pista de que ese parámetro debería ser la clase que contiene esas funciones, o ese parámetro debería ser un atributo de una instancia.

2) Las reglas para OO se entienden bastante bien. Después de un tiempo, puede mirar un diseño de clase y ver si cumple con criterios como SOLID. Y después de muchas pruebas de unidad de práctica, desarrolla un buen sentido de lo que hace que una clase sea "adecuada" y "coherente". Pero no hay buenas reglas para una clase con todos los métodos estáticos, y no hay una razón real por la que no debas agrupar todo allí. La clase está abierta en su editor, entonces, ¿qué diablos? Simplemente agregue su nuevo método allí. Después de un tiempo, su aplicación se convierte en una serie de "Objetos de Dios" en competencia, cada uno tratando de dominar el mundo. Nuevamente, refactorizarlos en unidades más pequeñas es muy subjetivo y difícil de determinar si lo hizo bien.

3) Las interfaces son una de las características más poderosas de Java. La herencia de clases ha demostrado ser problemática, pero la programación con interfaces sigue siendo uno de los trucos más poderosos del lenguaje. (ídem C #) Las clases totalmente estáticas no pueden colocarse en ese modelo.

4) Cierra la puerta a importantes técnicas de OO que no puedes aprovechar. Por lo tanto, puede trabajar durante años con solo un martillo en su caja de herramientas, sin darse cuenta de lo mucho más fácil que hubiera sido si hubiera tenido también un destornillador.

4.5) Crea las dependencias de tiempo de compilación más difíciles e irrompibles posibles. Entonces, por ejemplo, si tiene, FileSystem.saveFile()entonces no hay forma de cambiar eso, salvo falsificar su JVM en tiempo de ejecución. Lo que significa que cada clase que hace referencia a su clase de función estática tiene una dependencia dura en tiempo de compilación de esa implementación específica, lo que hace que la extensión sea casi imposible y complica enormemente las pruebas. Puede probar la clase estática de forma aislada, pero se vuelve muy difícil probar las clases que se refieren a esa clase de forma aislada.

5) Enloquecerás a tus compañeros de trabajo. La mayoría de los profesionales con los que trabajo toman en serio su código y prestan atención al menos a algún nivel de principios de diseño. Dejando a un lado la intención central de un lenguaje, hará que se quiten el pelo porque refactorizarán constantemente el código.

Cuando estoy en un idioma, siempre trato de usar bien un idioma. Entonces, por ejemplo, cuando estoy en Java, utilizo un buen diseño de OO, porque realmente estoy aprovechando el lenguaje para lo que hace. Cuando estoy en Python, mezclo funciones de nivel de módulo con clases ocasionales: solo podía escribir clases en Python, pero luego creo que no estaría usando el lenguaje para lo que es bueno.

Otra táctica es usar mal un lenguaje, y se quejan de todos los problemas que está causando. Pero eso se aplica a casi cualquier tecnología.

La característica clave de Java es administrar la complejidad en unidades pequeñas y comprobables que se unen para que sean fáciles de entender. Java enfatiza definiciones claras de interfaz independientes de la implementación, lo cual es un gran beneficio. Es por eso que (y otros lenguajes OO similares) siguen siendo tan ampliamente utilizados. A pesar de toda la verbosidad y el ritualismo, cuando termino con una gran aplicación Java, siempre siento que las ideas están más separadas en código que mis proyectos en un lenguaje más dinámico.

Sin embargo, es difícil. He visto a personas obtener el error "todo estático", y es un poco difícil disuadirlos. Pero los he visto sentir un gran alivio cuando lo superan.

Robar
fuente
Habla principalmente de mantener las cosas pequeñas y modulares, y mantener el estado y la funcionalidad juntos. No hay nada que me impida hacer esto con clases estáticas. Pueden tener estado. Y puedes encapsularlos con getters-setters. Y puede dividir el sistema en clases pequeñas que encapsulan la funcionalidad y el estado. Todo esto se aplica tanto a las clases como a los objetos. Y este es exactamente mi dilema: digamos que estoy agregando nuevas funcionalidades a mi aplicación. Obviamente, irá en una clase separada para obedecer SRP. Pero, ¿por qué exactamente debería crear una instancia de esta clase? Esto es lo que no entiendo.
Aviv Cohn
Quiero decir: a veces está claro por qué quiero objetos. Usaré el ejemplo de la edición para mi pregunta: tenía una aplicación que crea melodías. Había diferentes escalas que podía conectar MelodyGenerators para generar diferentes melodías. Y las Melodías eran objetos, así que podría ponerlas en una pila y volver a tocar las viejas, etc. En casos como estos, tengo claro por qué debería usar objetos. Lo que no entiendo es por qué debería usar objetos para representar partes 'estáticas' del sistema: por ejemplo, un mecanismo para guardar archivos. ¿Por qué no debería ser esto simplemente una clase estática?
Aviv Cohn
Una clase con métodos estáticos tiene que externalizar su estado. Lo cual, a veces, es una técnica de factorización muy importante. Pero la mayoría de las veces quieres encapsular el estado. Un ejemplo concreto: hace años, un compañero de trabajo escribió un "selector de fecha" en Javascript. Y fue una pesadilla. Los clientes se quejaban constantemente. Así que lo refactorice en objetos de "calendario", y de repente estaba creando instancias de varios de ellos, colocándolos uno al lado del otro, saltando entre meses y años, etc. Fue tan rico que en realidad tuvimos que desactivar las funciones. La instanciación te da esa escala.
Rob
3
"La idea detrás de OO es agrupar la funcionalidad con el estado para tener pequeñas unidades de datos y funcionalidades que preserven su semántica al ocultar el estado y exponer solo las funciones que tienen sentido en ese contexto". Esto es fundamentalmente incorrecto. OO no implica un estado mutable y la abstracción no es exclusiva de OO. Hay dos formas de lograr la abstracción y los objetos son solo uno de ellos (el otro son los tipos de datos abstractos).
Doval
1
@Doval No entiendo por qué cada discusión sobre OO debe necesariamente convertirse en una discusión sobre programación funcional.
Rob
6

Tu preguntaste:

¿Pero hay más ventajas para los objetos sobre las clases estáticas?

Antes de esta pregunta, enumeró el polimorfismo y se lo pasó como dos beneficios del uso de objetos. Quiero decir que esas son las características del paradigma OO. La encapsulación es otra característica del paradigma OO.

Sin embargo, esos no son los beneficios. Los beneficios son:

  1. Mejores abstracciones
  2. Mejor mantenibilidad
  3. Mejor comprobabilidad

Tu dijiste:

Pero recientemente, cuando me detuve y lo pensé, me di cuenta de que una clase estática haría lo mismo que un objeto, en muchos de los lugares donde normalmente uso un objeto.

Creo que tienes un punto válido allí. En esencia, la programación no es más que la transformación de datos y la creación de efectos secundarios basados ​​en datos. Algunas veces la transformación de datos requiere datos auxiliares. Otras veces, no lo hace.

Cuando se trata de la primera categoría de transformaciones, los datos auxiliares deben pasarse como entrada o almacenarse en algún lugar. Un objeto es el mejor enfoque que las clases estáticas para tales transformaciones. Un objeto puede almacenar los datos auxiliares y usarlos en el momento adecuado.

Para la segunda categoría de transformaciones, una clase estática es tan buena como un Objeto, si no mejor. Las funciones matemáticas son ejemplos clásicos de esta categoría. La mayoría de las funciones estándar de la biblioteca C también se incluyen en esta categoría.

Tu preguntaste:

Con un objeto, la línea de código de llamada se verá así: Thing thing = fileLoader.load(file);

Con una clase estática, se vería así: Thing thing = FileLoader.load(file);

¿Cual es la diferencia?

Si FileLoaderno lo hace, cada vez , la necesidad de almacenar todos los datos, me gustaría ir con el segundo enfoque. Si existe una pequeña posibilidad de que necesite datos auxiliares para realizar la operación, el primer enfoque es una apuesta más segura.

Tu preguntaste:

¿Hay más ventajas para los objetos que no sean los dos que enumeré? Por favor explique.

Enumeré los beneficios (ventajas) de usar el paradigma OO. Espero que se expliquen por sí mismos. Si no, estaré encantado de dar más detalles.

Tu preguntaste:

Pero, ¿qué pasa con las partes 'estáticas' del sistema, que no se van a pasar? Por ejemplo, un mecanismo de 'guardar archivo'. ¿Por qué debería implementarlo en un objeto y no en una clase estática?

Este es un ejemplo donde una clase estática simplemente no funcionará. Hay muchas formas de guardar los datos de su aplicación en un archivo:

  1. Guárdelo en un archivo CSV.
  2. Guárdelo en un archivo XML.
  3. Guárdelo en un archivo json.
  4. Guárdelo como un archivo binario, con volcado directo de los datos.
  5. Guárdelo directamente en alguna tabla de una base de datos.

La única forma de proporcionar tales opciones es crear una interfaz y crear objetos que implementen la interfaz.

En conclusión

Use el enfoque correcto para el problema en cuestión. Es mejor no ser religioso acerca de un enfoque frente al otro.

R Sahu
fuente
Además, si es necesario guardar muchos tipos de objetos diferentes (clases) (p Melody. Ej Chord. Improvisations, SoundSampleY otros), será económico simplificar la implementación mediante abstracciones.
rwong
5

Depende del idioma y el contexto, a veces. Por ejemplo, los PHPscripts utilizados para atender solicitudes siempre tienen una solicitud de servicio y una respuesta para generar durante toda la vida útil del script, por lo que los métodos estáticos para actuar sobre la solicitud y generar una respuesta pueden ser apropiados. Pero en un servidor escrito Node.js, puede haber muchas solicitudes y respuestas diferentes, todo al mismo tiempo. Entonces, la primera pregunta es: ¿está seguro de que la clase estática realmente corresponde a un objeto singleton?

En segundo lugar, incluso si tiene singletons, el uso de objetos le permite aprovechar el polimorfismo utilizando técnicas como Dependency_injection y Factory_method_pattern . Esto a menudo se usa en varios patrones de Inversion_of_control , y es útil para crear objetos simulados para pruebas, registros, etc.

Mencionó las ventajas anteriores, así que aquí hay una que no hizo: herencia. Muchos idiomas no tienen la capacidad de anular los métodos estáticos de la misma manera que los métodos de instancia pueden anularse. En general, es mucho más difícil heredar y anular los métodos estáticos.

Gregory Magarshak
fuente
Gracias por responder. En su opinión: al crear una parte 'estática' del sistema, algo que no se va a 'pasar', por ejemplo, la parte del sistema que reproduce sonidos o la parte del sistema que guarda datos en un archivo: ¿Debo implementarlo en un objeto o en una clase estática?
Aviv Cohn
3

Además de la publicación de Rob Y


Siempre que la funcionalidad de tu load(File file)pueda separarse claramente de todas las demás funciones que utilices, está bien usar un método / clase estático. Usted externaliza el estado (lo que no es malo) y tampoco obtiene redundancia, ya que puede, por ejemplo, aplicar parcialmente o modificar su función para que no tenga que repetirlo. (eso es lo mismo o similar a usar un factorypatrón)

Sin embargo, tan pronto como dos de estas funciones comiencen a tener un uso común, querrá poder agruparlas de alguna manera. Imagine que no solo tiene una loadfunción sino también una hasValidSyntaxfunción. ¿Qué harás?

     if (FileLoader.hasValidSyntax(myfile))
          Thing thing = FileLoader.load(myfile);
     else
          println "oh noes!"

¿Ves las dos referencias myfileaquí? Comienza a repetirse porque su estado externalizado debe pasarse para cada llamada. Rob Y ha descrito cómo internalizas el estado (aquí el file) para que puedas hacerlo así:

     FileLoader myfileLoader = new FileLoader(myfile)
     if (myfileLoader.hasValidSyntax())
          Thing thing = myfileLoader.load();
     else
          println "oh noes!"
valenterry
fuente
Tener que pasar el mismo objeto dos veces es un síntoma superficial; el problema real que podría indicar es que algunos trabajos se realizan de forma redundante - el archivo podría tener que abrir dos veces, analizada dos veces, y cerrado dos veces, si hasValidSyntax()y load()no se les permite la reutilización del estado (el resultado de un archivo parcialmente analizada).
rwong
2

Un problema importante al usar clases estáticas es que te obligan a ocultar tus dependencias y te obligan a depender de las implementaciones. En la siguiente firma del constructor, cuáles son sus dependencias:

public Person(String name)

Bueno, a juzgar por la firma, una persona solo necesita un nombre, ¿verdad? Bueno, no si la implementación es:

public Person(String name) {
    ResultSet rs = DBConnection.getPersonFilePathByName(name);
    File f = FileLoader.load(rs.getPath());
    PersonData.setDataFile(f);
    this.name = name;
    this.age = PersonData.getAge();
}

Entonces una persona no es solo instanciada. De hecho, extraemos datos de una base de datos, lo que nos da una ruta a un archivo, que luego debe analizarse, y luego los datos reales que queremos deben ser extraídos. Este ejemplo está claramente por encima, pero prueba el punto. Pero espera, ¡puede haber mucho más! Escribo la siguiente prueba:

public void testPersonConstructor() {
    Person p = new Person("Milhouse van Houten");
    assertEqual(10, p.age);
}

Sin embargo, esto debería pasar, ¿verdad? Quiero decir, encapsulamos todas esas otras cosas, ¿verdad? Bueno, en realidad explota con una excepción. ¿Por qué? Ah, sí, las dependencias ocultas que no conocíamos tienen un estado global. Las DBConnectionnecesidades inicializadas y conectadas. Las FileLoadernecesidades inicializadas con un FileFormatobjeto (como XMLFileFormato CSVFileFormat). Nunca he oído hablar de esos? Bueno, ese es el punto. Su código (y su compilador) no pueden decirle que necesita estas cosas porque las llamadas estáticas ocultan esas dependencias. ¿Dije prueba? Quise decir que el nuevo desarrollador junior acaba de enviar algo como esto en su versión más reciente. Después de todo, compilado = funciona, ¿verdad?

Además, supongamos que está en un sistema que no tiene una instancia de MySQL en ejecución. O, digamos que está en un sistema Windows pero su DBConnectionclase solo va a su servidor MySQL en una caja Linux (con rutas de Linux). O bien, supongamos que está en un sistema en el que la ruta devuelta por el DBConnectionno es de lectura / escritura para usted. Eso significa que intentar ejecutar o probar este sistema en cualquiera de estas condiciones fallará, no debido a un error de código, sino debido a un error de diseño que limita la flexibilidad del código y lo vincula a una implementación.

Ahora, digamos, queremos registrar cada llamada a la base de datos para una Personinstancia específica , una que atraviesa una ruta determinada y problemática. Podríamos iniciar sesión DBConnection, pero esto registrará todo , poniendo mucho desorden y dificultando distinguir la ruta de código particular que queremos rastrear. Sin embargo, si estamos utilizando inyección de dependencia con una DBConnectioninstancia, podríamos simplemente implementar la interfaz en un decorador (o extender la clase, ya que tenemos ambas opciones disponibles con un objeto). Con una clase estática, no podemos inyectar la dependencia, no podemos implementar una interfaz, no podemos envolverla en un decorador, y no podemos extender la clase. Solo podemos llamarlo directamente, en algún lugar oculto en nuestro código. Por lo tanto, estamos obligados tener una dependencia oculta de una implementación.

¿Esto es siempre malo? No necesariamente, pero podría ser mejor invertir su punto de vista y decir "¿Hay alguna buena razón para que esto no sea ​​una instancia?" en lugar de "¿Hay una buena razón de que esto debería ser un ejemplo?" Si realmente puede decir que su código será tan inquebrantable (y sin estado) como Math.abs(), tanto en su implementación como en la forma en que se utilizará, entonces puede considerar hacerlo estático. Sin embargo, tener instancias sobre clases estáticas le brinda un mundo de flexibilidad que no siempre es fácil de recuperar después del hecho. También puede darle más claridad sobre la verdadera naturaleza de las dependencias de su código.

cbojar
fuente
Las dependencias ocultas son un punto extremadamente bueno. Casi me olvido de mi modismo de que "la calidad del código debe juzgarse por cómo se usa, no por cómo se implementa".
Eufórico
Pero las dependencias ocultas también pueden existir en una clase no estática, ¿verdad? Así que no veo por qué esto sería un inconveniente de las clases estáticas, pero no de las clases no estáticas.
valenterry
@valenterry Sí, también podemos ocultar dependencias no estáticas, pero el punto clave es que ocultar dependencias no estáticas es una opción . Podría newlevantar un montón de objetos en el constructor (lo que generalmente no es aconsejable), pero también puedo inyectarlos. Con las clases estáticas, no hay forma de inyectarlas (no es fácil en Java de todos modos, y esa sería una discusión completamente diferente para los lenguajes que lo permiten), por lo que no hay otra opción. Es por eso que destaco la idea de ser forzado a ocultar dependencias tan fuertemente en mi respuesta.
cbojar
Pero puede inyectarlos cada vez que llame al método proporcionándolos como argumentos. Por lo tanto, no está obligado a ocultarlos, sino a hacerlos explícitos para que todos vean "ajá, ese método depende de estos argumentos" en lugar de "ajá, ese método depende de estos (parte de) argumentos y algún estado interno oculto que no saber".
valenterry
En Java, que yo sepa, no puede proporcionar una clase estática como parámetro sin pasar por una rigmarola de reflexión completa. (Lo intenté de todas las formas posibles. Si tiene una forma, me gustaría verla). Y si elige hacerlo, esencialmente está subvirtiendo el sistema de tipos incorporado (el parámetro es un Class, luego nos aseguramos de que sea la clase correcta) o si está escribiendo pato (nos aseguramos de que responda al método correcto). Si desea hacer lo último, debe volver a evaluar su elección de idioma. Si desea hacer lo primero, las instancias funcionan bien y están integradas.
cbojar
1

Solo mis dos centavos.

Para su pregunta:

Pero, ¿qué pasa con las partes 'estáticas' del sistema, que no se van a pasar? Por ejemplo, un mecanismo de 'guardar archivo'. ¿Por qué debería implementarlo en un objeto y no en una clase estática?

Puede preguntarse si el mecanismo de la parte del sistema que reproduce sonidos o la parte del sistema que guarda los datos en un archivo CAMBIARÁ . Si su respuesta es sí, eso indica que debe abstraerlos usando abstract class/ interface. Usted puede preguntar, ¿cómo puedo saber las cosas futuras? Definitivamente, no podemos. Entonces, si la cosa no tiene estado, puede usar 'clase estática' como java.lang.Math, de lo contrario, usar un enfoque orientado a objetos.

usuario3663635
fuente
Hay un consejo irónico: tres golpes y refactorización , lo que sugiere que uno puede esperar hasta que el cambio sea realmente necesario. Lo que quiero decir con "cambio" es: es necesario guardar más de un tipo de objeto; o necesita guardar en más de un formato de archivo (tipo de almacenamiento); o un aumento drástico en la complejidad de los datos guardados. Por supuesto, si uno está bastante seguro de que el cambio ocurrirá pronto, puede incorporar un diseño más flexible por adelantado.
rwong