Diseño de clase orientado a objetos

12

Me preguntaba sobre un buen diseño de clase orientado a objetos. En particular, me cuesta decidir entre estas opciones:

  1. método estático vs instancia
  2. método sin parámetros o valor de retorno vs método con parámetros y valor de retorno
  3. superposición vs funcionalidad método distinto
  4. método privado vs público

Ejemplo 1:

Esta implementación utiliza métodos de instancia, sin valor de retorno o parámetros, sin funcionalidad superpuesta, y todos los métodos públicos

XmlReader reader = new XmlReader(url);
reader.openUrl();
reader.readXml();
Document result = reader.getDocument();

Ejemplo 2

Esta implementación utiliza métodos estáticos, con valores y parámetros de retorno, con funcionalidad superpuesta y métodos privados.

Document result = XmlReader.readXml(url); 

En el ejemplo uno, todos los métodos son de instancia pública, lo que facilita su prueba unitaria. Aunque todos los métodos son distintos, readXml () depende de openUrl () en que openUrl () debe llamarse primero. Todos los datos se declaran en campos de instancia, por lo que no hay valores o parámetros de retorno en ningún método, excepto en el constructor y los accesores.

En el ejemplo dos, solo un método es público, el resto es estático privado, lo que dificulta la prueba de la unidad. Los métodos se superponen en que readXml () llama a openUrl (). No hay campos, todos los datos se pasan como parámetros en los métodos y el resultado se devuelve de inmediato.

¿Qué principios debo seguir para hacer una programación orientada a objetos adecuada?

siamii
fuente
3
Las cosas estáticas son malas cuando haces subprocesos múltiples. El otro día, tuve un XMLWriter estático, como XMLWriter.write (data, fileurl). Sin embargo, dado que tenía un FileStream estático privado, el uso de esta clase de varios subprocesos al mismo tiempo, provocó que el segundo subproceso sobrescribiera los primeros subprocesos FileStream, causando un error que sería muy difícil de encontrar. Las clases estáticas con miembros estáticos + subprocesos múltiples son una receta para el desastre.
Según Alexandersson el
1
@Paxinum. El problema que describe es un problema de estado, no un problema "estático". Si usara un singleton con miembros no estáticos, tendría el mismo problema con subprocesos múltiples.
mike30
2
@Per Alexandersson Los métodos estáticos no son malos en relación con la concurrencia. El estado estático es malo. Es por eso que la programación funcional, en la que todos los métodos son estáticos, funciona muy bien en situaciones concurrentes.
Yuli Bonner

Respuestas:

11

El ejemplo 2 es bastante malo para las pruebas ... y no quiero decir que no pueda probar las partes internas. Tampoco puede reemplazar su XmlReaderobjeto por un objeto simulado ya que no tiene ningún objeto.

El ejemplo 1 es innecesariamente difícil de usar. Qué pasa

XmlReader reader = new XmlReader(url);
Document result = reader.getDocument();

que no es más difícil de usar que tu método estático.

Cosas como abrir la URL, leer XML, convertir bytes a cadenas, analizar, cerrar sockets y lo que sea, no son interesantes. Crear un objeto y usarlo es importante.

Entonces, en mi humilde opinión, el diseño OO adecuado es hacer públicas solo las dos cosas (a menos que realmente necesite los pasos intermedios por alguna razón). La estática es malvada.

maaartinus
fuente
-1. En realidad PUEDES reemplazar XmlReader con un objeto simulado. No con un marco falso de código abierto, pero con un buen grado industrial que puede;) Cuesta unos cientos de dólares por desarrollador, pero hace maravillas para probar la funcionalidad sellada en las API que publica.
TomTom
2
+1 por no escuchar el argumento de venta de TomTom. Cuando quiero un trazador de líneas preferiría escribir algo como Document result = new XmlReader(url).getDocument();¿Por qué? Para que pueda actualizarlo Document result = whateverReader.getDocument();y me lo haya whateverReaderentregado por otra cosa.
candied_orange
6

Aquí está la cosa: no hay una respuesta correcta, y no hay una definición absoluta de "diseño orientado a objetos adecuado" (algunas personas le ofrecerán una, pero son ingenuos ... deles tiempo).

Todo se reduce a tus METAS.

Eres un artista y el papel está en blanco. Puede dibujar un retrato lateral en blanco y negro delicado y finamente dibujado a lápiz, o una pintura abstracta con enormes cortes de neones mixtos. O cualquier cosa en el medio.

Entonces, ¿qué SIENTE CORRECTO para el problema que está resolviendo? ¿Cuáles son las quejas de las personas que necesitan usar sus clases para trabajar con xml? ¿Qué tiene de difícil su trabajo? ¿Qué tipo de código están tratando de escribir que rodea las llamadas a su biblioteca, y cómo pueden ayudar a que fluya mejor para ellos?

¿Les gustaría más brevedad? ¿Les gustaría que fuera muy inteligente para determinar los valores predeterminados para los parámetros, de modo que no tengan que especificar mucho (o nada) y adivine correctamente? ¿Puedes automatizar las tareas de configuración y limpieza que requiere tu biblioteca para que les sea imposible olvidar esos pasos? ¿Qué más puedes hacer por ellos?

Demonios, lo que probablemente deba hacer es codificarlo de 4 a 5 formas diferentes, luego ponerse el sombrero de consumidor y escribir el código que usa los 5, y ver cuál se siente mejor. Si no puede hacer eso para toda su biblioteca, hágalo para un subconjunto. Y también necesita agregar algunas alternativas adicionales a su lista: ¿qué pasa con una interfaz fluida, o un enfoque más funcional, o parámetros con nombre, o algo basado en DynamicObject para que pueda inventar "pseudo-métodos" significativos que los ayuden? ¿fuera?

¿Por qué es jQuery el rey en este momento? Debido a que Resig y su equipo siguieron este proceso, hasta que encontraron un principio sintáctico que redujo increíblemente la cantidad de código JS que se necesita para trabajar con dom y eventos. Ese principio sintáctico no estaba claro para ellos ni para nadie más cuando comenzaron. Lo encontraron.

Como programador, esa es su mayor vocación. Andas a tientas en la oscuridad intentando cosas hasta que lo encuentras. Cuando lo hagas, lo sabrás. Y le dará a sus usuarios un gran salto de productividad. Y de eso se trata el diseño (en el ámbito del software).

Flores de charlie
fuente
1
Esto implica que no hay diferencias entre las mejores prácticas y otras prácticas además de cómo se "siente". Así es como terminas con bolas de lodo imposibles de mantener, porque para muchos desarrolladores, al llegar a través de los límites de la Clase, etc., "se siente" simplemente increíble.
Amy Blankenship
@Amy Blankenship, diría inequívocamente que no hay una "mejor manera" para tomar las decisiones sobre las que el OP está preguntando. Depende de un millón de cosas, y hay un millón de grados de libertad. Sin embargo, creo que hay un lugar para las "Mejores Prácticas", y eso es en un entorno de equipo , donde ya se han tomado ciertas decisiones , y necesitamos que el resto del equipo se mantenga consistente con esas elecciones anteriores. En otras palabras, en un contexto específico , puede haber razones para etiquetar ciertas cosas como "mejores prácticas". Pero el OP no ha dado ningún contexto. Él está construyendo algo y ...
Charlie Flowers
... se enfrenta a todas estas opciones posibles. No hay una "respuesta correcta" para esas elecciones. Está dirigido por los objetivos y los puntos débiles del sistema. Le garantizo que los programadores de Haskell no piensan que todos los métodos deberían ser métodos de instancia. Y los programadores de kernel de Linux no piensan que hacer que las cosas sean accesibles para TDD es muy importante. Y los programadores de juegos C ++ a menudo prefieren agrupar sus datos en una estructura de datos ajustada en la memoria que encapsular todo en objetos. Cada "Mejor práctica" es solo una "mejor práctica" en un contexto dado, y es un antipatrón en algún otro contexto.
Charlie Flowers
@AmyBlankenship Una cosa más: no estoy de acuerdo en que llegar a través de los límites de la Clase "se sienta como una maravilla". Conduce a bolas de barro imposibles de mantener, que se sienten horribles . Creo que está tratando de resolver el problema de que algunos trabajadores son descuidados / desmotivados / muy inexpertos. En ese caso, alguien que sea cuidadoso, motivado y experimentado debe tomar las decisiones clave y llamarlas "Mejores Prácticas". Pero, la persona que elige esas "Mejores Prácticas" todavía está tomando decisiones basadas en lo que "se siente bien", y no hay respuestas correctas establecidas. Solo estás controlando quién toma las decisiones.
Charlie Flowers
He trabajado con varios programadores que se consideraban a sí mismos como de nivel superior y que la gerencia pensaba de esa manera que creía firmemente que las estadísticas y los Singleton eran absolutamente la forma correcta de manejar los problemas de comunicación. La parte estática de esta pregunta ni siquiera se habría preguntado si llegar a través de los límites de la Clase de esa manera "se siente" mal para los desarrolladores, ni la respuesta que defiende la alternativa estática recibiría ningún voto positivo.
Amy Blankenship
3

La segunda opción es mejor, ya que es más simple de usar para las personas (incluso si solo eres tú), mucho más simple.

Para las pruebas unitarias, simplemente probaría la interfaz, no las internas, si realmente desea mover las internas de privadas a protegidas.


fuente
2
También puede hacer que su paquete de métodos sea privado (predeterminado), si sus casos de prueba están en el mismo paquete, para fines de pruebas unitarias.
Buen punto, eso es mejor.
3

1. Estático vs. instancia

Creo que hay pautas muy claras sobre qué es un buen diseño OO y qué no lo es. El problema es que la blogósfera hace que sea difícil separar lo bueno de lo malo y lo feo. Puede encontrar algún tipo de referencia que respalde incluso la peor práctica que se le ocurra.

Y la peor práctica que se me ocurre es el estado global, incluidas las estadísticas que mencionaste y el Singleton favorito de todos. Algunos extractos del clásico artículo de Misko Hevery sobre el tema .

Para comprender realmente las dependencias, los desarrolladores deben leer cada línea de código. Causa una acción espeluznante a distancia: cuando se ejecutan conjuntos de pruebas, el estado global mutado en una prueba puede hacer que una prueba posterior o paralela falle inesperadamente. Rompa la dependencia estática usando la inyección de dependencia manual o Guice.

La acción espeluznante a distancia es cuando ejecutamos una cosa que creemos que está aislada (ya que no pasamos ninguna referencia) pero se producen interacciones inesperadas y cambios de estado en ubicaciones distantes del sistema sobre las que no le informamos al objeto. Esto solo puede suceder a través del estado global.

Es posible que no lo haya pensado de esta manera antes, pero cada vez que usa un estado estático, está creando canales de comunicación secretos y no los deja claros en la API. Spooky Action at a Distance obliga a los desarrolladores a leer cada línea de código para comprender las posibles interacciones, reduce la productividad del desarrollador y confunde a los nuevos miembros del equipo.

Esto se reduce a que no debe proporcionar referencias estáticas a nada que tenga algún tipo de estado almacenado. El único lugar donde uso estadísticas estáticas es para constantes enumeradas, y tengo dudas sobre eso.

2. Métodos con parámetros de entrada y valores de retorno vs. métodos sin ninguno

Lo que debe tener en cuenta es que los métodos que no tienen parámetros de entrada ni parámetros de salida están garantizados para operar en algún tipo de estado almacenado internamente (de lo contrario, ¿qué están haciendo?). Hay idiomas enteros que se basan en la idea de evitar el estado almacenado.

Cada vez que haya almacenado el estado, tiene la posibilidad de efectos secundarios, así que asegúrese de usarlo siempre con atención. Esto implica que debe preferir funciones con entradas y / o salidas definidas.

Y, de hecho, las funciones que tienen entradas y salidas definidas son mucho más fáciles de probar: no tiene que ejecutar una función aquí e ir a mirar allí para ver qué sucedió, y no tiene que establecer una propiedad en alguna parte más antes de ejecutar la función bajo prueba.

También puede usar este tipo de función de forma segura como estática. Sin embargo, no lo haría, porque si luego quisiera usar una implementación ligeramente diferente de esa función en algún lugar, en lugar de proporcionar una instancia diferente con la nueva implementación, no tengo forma de reemplazar la funcionalidad.

3. Superposición vs. Distinción

No entiendo la pregunta. ¿Cuál sería la ventaja en 2 métodos superpuestos?

4. Privado vs. Público

No exponga nada que no necesite exponer. Sin embargo, tampoco soy un gran fanático de lo privado. No soy un desarrollador de C #, sino un desarrollador de ActionScript. Pasé mucho tiempo en el código de Adobe Flex Framework, que fue escrito alrededor del año 2007. Y tomaron algunas decisiones realmente malas sobre qué hacer privado, lo que hace que sea una pesadilla tratar de extender sus clases.

Entonces, a menos que piense que es un mejor arquitecto que los desarrolladores de Adobe alrededor del año 2007 (según su pregunta, diría que tiene unos años más antes de tener la oportunidad de hacer esa afirmación), es probable que solo desee usar la protección predeterminada .


Hay algunos problemas con sus ejemplos de código que significan que no están bien diseñados, por lo que no es posible elegir A o B.

Por un lado, probablemente debería separar la creación de su objeto de su uso . Por lo tanto, generalmente no tendría su new XMLReader()derecho al lado de donde se usa.

Además, como dice @djna, debe encapsular los métodos utilizados en los usos de su lector XML, de modo que su API (ejemplo de instancia) podría simplificarse para:

_document Document = reader.read(info);

No sé cómo funciona C #, pero dado que he trabajado con una serie de tecnologías web, sospecho que no siempre podrás devolver un documento XML de inmediato (excepto tal vez como una promesa o un tipo futuro objeto), pero no puedo darle consejos sobre cómo manejar una carga asincrónica en C #.

Tenga en cuenta que con este enfoque, puede crear varias implementaciones que pueden tomar un parámetro que les dice dónde / qué leer y devolver un objeto XML, y cambiarlos según las necesidades de su proyecto. Por ejemplo, puede estar leyendo directamente desde una base de datos, desde una tienda local o, como en su ejemplo original, desde una URL. No puede hacer eso si usa un método estático.

Amy Blankenship
fuente
2

Centrarse en la perspectiva del cliente.

IReader reader = new XmlReader.readXml(url);  // or injection, or factory or ...
Document document = reader.read();

Los métodos estáticos tienden a limitar la evolución futura, nuestro cliente está trabajando en términos de una interfaz proporcionada posiblemente por muchas implementaciones diferentes.

El principal problema con su idioma abierto / de lectura es que el cliente necesita saber el orden en el que llamar a los métodos, cuando solo quiere hacer un trabajo simple. Obvio aquí, pero en una clase más grande está lejos de ser obvio.

El método principal para probar es read (). Los métodos internos pueden hacerse visibles para los programas de prueba al no hacerlos públicos o privados y al poner las pruebas en el mismo paquete; las pruebas aún pueden mantenerse separadas del código publicado.

djna
fuente
¿Los métodos con visibilidad predeterminada siguen siendo visibles si el conjunto de pruebas está en un proyecto diferente?
siamii
Java no sabe sobre proyectos. Los proyectos son una construcción IDE. El compilador y JVM observan los paquetes en los que se encuentran las clases probadas y de prueba: mismo paquete, se permite la visibilidad predeterminada. En Eclipse utilizo un solo proyecto, con dos directorios de origen diferentes. Acabo de probar con dos proyectos, funciona.
2

método estático vs instancia

En la práctica, encontrará que los métodos estáticos generalmente se limitan a las clases de utilidad y no deberían saturar sus objetos de dominio, administradores, controladores o DAO. Los métodos estáticos son más útiles cuando todas las referencias necesarias pueden pasarse razonablemente como parámetros y ofrecer cierta funcionalidad que puede reutilizarse en muchas clases. Si se encuentra utilizando un método estático como solución alternativa para tener una referencia a un objeto de instancia, pregúntese por qué no solo tiene esa referencia.

método sin parámetros o valor de retorno vs método con parámetros y valor de retorno

Si no necesita parámetros en el método, no los agregue. Lo mismo ocurre con el valor de retorno. Tener esto en cuenta simplificará su código y garantizará que no esté codificando para una multitud de escenarios que nunca terminan sucediendo.

superposición de funcionalidad de método distinto

Es una buena idea tratar de evitar la superposición de funciones. A veces puede ser difícil, pero cuando es necesario un cambio en la lógica, es mucho más fácil cambiar un método que se reutiliza que cambiar un montón de métodos con una funcionalidad similar.

método privado vs público

Comúnmente los captadores, colocadores y constructores deben ser públicos. Todo lo demás querrás intentar mantenerlo en privado a menos que haya un caso en el que otra clase necesite ejecutarlo. Mantener los métodos predeterminados en privado ayudará a mantener la Encapsulación . Lo mismo ocurre con los campos, acostumbrarse a privado como predeterminado

Erich
fuente
1

No responderé a su pregunta, pero creo que los términos que ha utilizado crean un problema. P.ej

XmlReader.read => twice "read"

Creo que necesita un XML, así que crearé un XML de objeto que se puede crear a partir de un tipo de texto (no sé C # ... en Java se llama String). P.ej

class XML {
    XML(String text) { [...] }
}

Puedes probarlo y está claro. Luego, si necesita una fábrica, puede agregar un método de fábrica (y puede ser estático como su segundo ejemplo). P.ej

class XML {
    XML(String text) { [...] }

    static XML fromUrl(url) { [...] }

}
sixro
fuente
0

Puedes seguir algunas reglas simples. Ayuda si comprende las razones de las reglas.

método estático vs instancia

Para los métodos no tiene que tomar esta decisión conscientemente. Si parece que su método no está utilizando ningún miembro de campo (su analizador favorito debería decírselo), debe agregar la palabra clave estática.

método sin parámetros o valor de retorno vs método con parámetros y valor de retorno

La segunda opción es mejor debido al alcance. Siempre debe mantener su alcance ajustado. Buscar cosas que necesita es malo, debe tener aportes, trabajar en ellos localmente y devolver un resultado. Su primera opción es mala por la misma razón por la que las variables globales generalmente son malas: son significativas solo para una parte de su código, pero son visibles (y, por lo tanto, ruidosas) en cualquier otro lugar y pueden manipularse desde cualquier lugar. Esto dificulta obtener una imagen completa de su lógica.

superposición de funcionalidad de método distinto

No creo que esto sea un problema. Los métodos que llaman a otros métodos están bien si esto divide las tareas en fragmentos más pequeños y claramente funcionales.

método privado vs público

Haga que todo sea privado a menos que lo necesite para ser público. El usuario de su clase puede prescindir del ruido, solo quiere ver lo que le importa.

Martin Maat
fuente