¿Cómo sé qué tan reutilizables deberían ser mis métodos? [cerrado]

133

Me estoy ocupando de mi propio negocio en casa y mi esposa se acerca y me dice

Cariño ... ¿Puedes imprimir todos los Day Light Savings alrededor del mundo para 2018 en la consola? Necesito revisar algo.

Y estoy súper feliz porque eso era lo que había estado esperando toda mi vida con mi experiencia Java y se me ocurrió:

import java.time.*;
import java.util.Set;

class App {
    void dayLightSavings() {
        final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
        availableZoneIds.forEach(
            zoneId -> {
                LocalDateTime dateTime = LocalDateTime.of(
                    LocalDate.of(2018, 1, 1), 
                    LocalTime.of(0, 0, 0)
                );
                ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
                while (2018 == now.getYear()) {
                    int hour = now.getHour();
                    now = now.plusHours(1);
                    if (now.getHour() == hour) {
                        System.out.println(now);
                    }
                }
            }
        );
    }
}

Pero luego dice que solo me estaba probando si era un ingeniero de software con formación ética y me dice que parece que no lo estoy desde entonces (tomado de aquí ).

Cabe señalar que ningún ingeniero de software con formación ética permitiría escribir un procedimiento de DestroyBaghdad. En cambio, la ética profesional básica requeriría que él escribiera un procedimiento de DestroyCity, al cual Bagdad podría asignarse como parámetro.

Y yo estoy como, bien, está bien, me tienes .. Pasa cualquier año que quieras, aquí tienes:

import java.time.*;
import java.util.Set;

class App {
    void dayLightSavings(int year) {
        final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
        availableZoneIds.forEach(
            zoneId -> {
                LocalDateTime dateTime = LocalDateTime.of(
                    LocalDate.of(year, 1, 1), 
                    LocalTime.of(0, 0, 0)
                );
                ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
                while (year == now.getYear()) {
                    // rest is same..

Pero, ¿cómo sé cuánto (y qué) parametrizar? Después de todo, ella podría decir ...

  • quiere pasar un formateador de cadena personalizado, tal vez no le gusta el formato en el que ya estoy imprimiendo: 2018-10-28T02:00+01:00[Arctic/Longyearbyen]

void dayLightSavings(int year, DateTimeFormatter dtf)

  • solo le interesan ciertos períodos de mes

void dayLightSavings(int year, DateTimeFormatter dtf, int monthStart, int monthEnd)

  • ella está interesada en ciertos horarios

void dayLightSavings(int year, DateTimeFormatter dtf, int monthStart, int monthEnd, int hourStart, int hourend)

Si está buscando una pregunta concreta:

Si destroyCity(City city)es mejor que destroyBaghdad(), ¿es takeActionOnCity(Action action, City city)incluso mejor? ¿Por qué por qué no?

Después de todo, puedo llamar primero con Action.DESTROYentonces Action.REBUILD, ¿no es así?

Pero tomar medidas en las ciudades no es suficiente para mí, ¿qué tal takeActionOnGeographicArea(Action action, GeographicalArea GeographicalArea)? Después de todo, no quiero llamar:

takeActionOnCity(Action.DESTORY, City.BAGHDAD);

entonces

takeActionOnCity(Action.DESTORY, City.ERBIL);

y así sucesivamente cuando puedo hacer:

takeActionOnGeographicArea(Action.DESTORY, Country.IRAQ);

PD: Solo formulé mi pregunta en torno a la cita que mencioné, no tengo nada en contra de ningún país, religión, raza o cualquier otro en el mundo. Solo estoy tratando de hacer un punto.

Koray Tugay
fuente
71
El punto que está haciendo aquí es uno que he tratado de expresar muchas veces: la generalidad es costosa y, por lo tanto, debe justificarse por beneficios específicos y claros . Pero va más profundo que eso; sus diseñadores crean lenguajes de programación para hacer que algunos tipos de generalidad sean más fáciles que otros, y eso influye en nuestras elecciones como desarrolladores. Es fácil parametrizar un método por un valor, y cuando esa es la herramienta más fácil que tiene en su caja de herramientas, la tentación es usarlo independientemente de si tiene sentido para el usuario.
Eric Lippert
30
La reutilización no es algo que desee por sí mismo. Priorizamos la reutilización porque creemos que los artefactos de código son costosos de construir y, por lo tanto, deberían ser utilizables en tantos escenarios como sea posible, para amortizar ese costo en esos escenarios. Esta creencia con frecuencia no está justificada por observaciones, y por lo tanto, los consejos para diseñar la reutilización se aplican con frecuencia de manera incorrecta . Diseñe su código para reducir el costo total de la aplicación .
Eric Lippert
77
Su esposa es la poco ética por perder su tiempo mintiéndole. Ella pidió una respuesta y dio un medio sugerido; Según ese contrato, la forma en que obtiene ese resultado es solo entre usted y usted. Además, destroyCity(target)es mucho más poco ético que destroyBagdad()! ¿Qué tipo de monstruo escribe un programa para borrar una ciudad, y mucho menos cualquier ciudad del mundo? ¿Qué pasa si el sistema se vio comprometido? Además, ¿qué tiene que ver la gestión del tiempo / recursos (esfuerzo invertido) con la ética? Siempre y cuando el contrato verbal / escrito se haya completado según lo acordado.
Tezra
25
Creo que podrías estar leyendo demasiado este chiste. Es una broma acerca de cómo los programadores de computadoras toman malas decisiones éticas, porque priorizan las consideraciones técnicas sobre los efectos de su trabajo en los humanos. No pretende ser un buen consejo sobre el diseño del programa.
Eric Lippert

Respuestas:

114

Son tortugas hasta el fondo.

O abstracciones en este caso.

La codificación de buenas prácticas es algo que se puede aplicar infinitamente, y en algún momento estás abstrayendo en aras de la abstracción, lo que significa que lo has llevado demasiado lejos. Encontrar esa línea no es algo fácil de poner en una regla general, ya que depende mucho de su entorno.

Por ejemplo, hemos tenido clientes que eran conocidos por solicitar aplicaciones simples primero pero luego solicitar expansiones. También hemos tenido clientes que preguntan qué quieren y, en general, nunca vuelven a nosotros para una expansión.
Su enfoque variará según el cliente. Para el primer cliente, pagará abstraer el código de manera preventiva porque está razonablemente seguro de que tendrá que volver a visitar este código en el futuro. Para el segundo cliente, es posible que no desee invertir ese esfuerzo adicional si espera que no quiera expandir la aplicación en ningún momento (nota: esto no significa que no siga ninguna buena práctica, sino simplemente que evitas hacer más de lo que es necesario actualmente .

¿Cómo sé qué características implementar?

La razón por la que menciono lo anterior es porque ya has caído en esta trampa:

Pero, ¿cómo sé cuánto (y qué) parametrizar? Después de todo, ella podría decir .

"Ella podría decir" no es un requisito comercial actual. Es una conjetura sobre un requisito comercial futuro. Como regla general, no se base en conjeturas, solo desarrolle lo que se requiere actualmente.

Sin embargo, el contexto se aplica aquí. No conozco a tu esposa Tal vez calculó con precisión que, de hecho, ella querrá esto. Pero aún debe confirmar con el cliente que esto es realmente lo que quieren, porque de lo contrario pasará tiempo desarrollando una característica que nunca terminará usando.

¿Cómo sé qué arquitectura implementar?

Esto es más complicado. Al cliente no le importa el código interno, por lo que no puede preguntarle si lo necesita. Su opinión sobre el asunto es sobre todo irrelevante.

Sin embargo, aún puede confirmar la necesidad de hacerlo haciendo las preguntas correctas al cliente. En lugar de preguntar sobre la arquitectura, pregúnteles sobre sus expectativas de desarrollo futuro o expansiones a la base de código. También puede preguntar si el objetivo actual tiene una fecha límite, ya que es posible que no pueda implementar su arquitectura elegante en el plazo necesario.

¿Cómo sé cuándo resumir más mi código?

No sé dónde lo leí (si alguien sabe, hágamelo saber y le daré crédito), pero una buena regla general es que los desarrolladores deben contar como un hombre de las cavernas: uno, dos, muchos .

ingrese la descripción de la imagen aquí XKCD # 764

En otras palabras, cuando se utiliza un cierto algoritmo / patrón por tercera vez, debe abstraerse para que sea reutilizable (= utilizable muchas veces).

Para ser claros, no estoy insinuando que no debas escribir código reutilizable cuando solo se usan dos instancias del algoritmo. Por supuesto, también puedes abstraer eso, pero la regla debería ser que en tres casos debes abstraer.

Nuevamente, esto tiene en cuenta sus expectativas. Si ya sabe que necesita tres o más instancias, por supuesto, puede hacer un resumen de inmediato. Pero si solo adivina que es posible que desee implementarlo más veces, la corrección de la implementación de la abstracción depende totalmente de la corrección de su suposición.
Si adivinó correctamente, se ahorró algo de tiempo. Si adivinó erróneamente, desperdició parte de su tiempo y esfuerzo y posiblemente comprometió su arquitectura para implementar algo que finalmente no necesita.

Si destroyCity(City city)es mejor que destroyBaghdad(), ¿es takeActionOnCity(Action action, City city)incluso mejor? ¿Por qué por qué no?

Eso depende mucho de múltiples cosas:

  • ¿Existen múltiples acciones que se pueden tomar en cualquier ciudad?
  • ¿Se pueden usar estas acciones indistintamente? Porque si las acciones "destruir" y "reconstruir" tienen ejecuciones completamente diferentes, entonces no tiene sentido fusionar las dos en un solo takeActionOnCitymétodo.

También tenga en cuenta que si abstrae recursivamente esto, terminará con un método que es tan abstracto que no es más que un contenedor para ejecutar otro método, lo que significa que ha hecho que su método sea irrelevante y sin sentido.
Si todo el takeActionOnCity(Action action, City city)cuerpo de su método no es más que nada action.TakeOn(city);, debería preguntarse si el takeActionOnCitymétodo realmente tiene un propósito o no es solo una capa adicional que no agrega nada de valor.

Pero tomar medidas en las ciudades no es suficiente para mí, ¿qué tal takeActionOnGeographicArea(Action action, GeographicalArea GeographicalArea)?

La misma pregunta aparece aquí:

  • ¿Tiene un caso de uso para regiones geográficas?
  • ¿La ejecución de una acción en una ciudad y una región es la misma?
  • ¿Se puede tomar alguna medida en cualquier región / ciudad?

Si puede responder definitivamente "sí" a las tres preguntas, entonces se justifica una abstracción.

Flater
fuente
16
No puedo enfatizar lo suficiente la regla de "uno, dos, muchos". Hay infinitas posibilidades para abstraer / parametrizar algo, pero el subconjunto útil es pequeño, a menudo cero. Saber exactamente qué variante tiene valor solo se puede determinar en retrospectiva. Por lo tanto, cumpla con los requisitos inmediatos * y agregue complejidad según sea necesario por los nuevos requisitos o en retrospectiva. * A veces conoces bien el espacio del problema, entonces podría estar bien agregar algo porque sabes que lo necesitas mañana. Pero use este poder sabiamente, también puede conducir a la ruina.
Christian Sauer
2
> "No sé dónde lo leí [..]". Es posible que haya estado leyendo Coding Horror: The Rule of Three .
Runa
10
La "regla de uno, dos, muchos" está realmente allí para evitar que construyas la abstracción incorrecta aplicando DRY a ciegas. La cuestión es que dos piezas de código pueden comenzar a verse casi exactamente iguales, por lo que es tentador abstraer las diferencias; pero al principio, no sabes qué partes del código son estables y cuáles no; Además, podría resultar que realmente necesitan evolucionar independientemente (diferentes patrones de cambio, diferentes conjuntos de responsabilidades). En cualquier caso, una abstracción incorrecta funciona en tu contra y se interpone en el camino.
Filip Milovanović
44
Esperar más de dos ejemplos de "la misma lógica" le permite ser un mejor juez de lo que se debe abstraer y cómo (y realmente, se trata de administrar dependencias / acoplamiento entre código con diferentes patrones de cambio).
Filip Milovanović
1
@kukis: la línea realista debe dibujarse en 2 (según el comentario de Baldrickk): cero-uno-muchos (como es el caso de las relaciones de la base de datos). Sin embargo, esto abre la puerta al comportamiento innecesario de búsqueda de patrones. Dos cosas pueden parecerse vagamente, pero eso no significa que en realidad sean lo mismo. Sin embargo, cuando una tercera instancia entra en la refriega que se asemeja tanto a la primera como a la segunda instancia, puede hacer un juicio más preciso de que sus similitudes son de hecho un patrón reutilizable. Entonces, la línea de sentido común se dibuja en 3, lo que tiene en cuenta el error humano al detectar "patrones" entre solo dos instancias.
Flater
44

Práctica

Esto es Software Engineering SE, pero la creación de software es mucho más arte que ingeniería. No hay un algoritmo universal que seguir o una medición que tomar para determinar cuánta reutilización es suficiente. Como con cualquier cosa, mientras más práctica tengas para diseñar programas, mejor lo lograrás. Obtendrá una mejor idea de lo que es "suficiente" porque verá qué sale mal y cómo sale mal cuando parametriza demasiado o muy poco.

Sin embargo, eso no es muy útil ahora , entonces, ¿qué tal algunas pautas?

Mira de nuevo a tu pregunta. Hay un montón de "ella podría decir" y "yo podría". Muchas declaraciones teorizando sobre alguna necesidad futura. Los humanos son una mierda al predecir el futuro. Y usted (muy probablemente) es un humano. El problema abrumador del diseño de software es tratar de dar cuenta de un futuro que no conoce.

Pauta 1: No lo vas a necesitar

Seriamente. Solo para. La mayoría de las veces, ese problema futuro imaginado no aparece, y ciertamente no aparecerá tal como lo imaginó.

Directriz 2: Costo / beneficio

Genial, ¿ese pequeño programa te llevó unas horas escribir? Entonces, ¿qué pasa si su esposa regresa y pide esas cosas? En el peor de los casos, pasas unas horas más armando otro programa para hacerlo. Para este caso, no es demasiado tiempo para hacer que este programa sea más flexible. Y no va a agregar mucho a la velocidad de ejecución o al uso de memoria. Pero los programas no triviales tienen diferentes respuestas. Diferentes escenarios tienen diferentes respuestas. En algún momento, los costos claramente no valen la pena, incluso con habilidades imperfectas para contar en el futuro.

Directriz 3: Enfoque en constantes

Mira de nuevo a la pregunta. En su código original, hay muchas entradas constantes. 2018, 1. Entradas constantes, cadenas constantes ... Son las cosas más probables que necesitan ser no constantes . Mejor aún, toman solo un poco de tiempo para parametrizar (o al menos definir como constantes reales). Pero otra cosa a tener en cuenta es el comportamiento constante . losSystem.out.printlnpor ejemplo. Ese tipo de suposición sobre el uso tiende a ser algo que cambia en el futuro y tiende a ser muy costoso de arreglar. No solo eso, sino que IO así hace que la función sea impura (junto con la recuperación de la zona horaria). Parametrizar ese comportamiento puede hacer que la función sea más pura, lo que lleva a una mayor flexibilidad y capacidad de prueba. Grandes beneficios con un costo mínimo (especialmente si realiza una sobrecarga que usa System.outde forma predeterminada).

Telastyn
fuente
1
Es solo una guía, los 1 están bien, pero los miras y dices "¿esto cambiará alguna vez?" Y el println podría parametrizarse con una función de orden superior, aunque Java no es bueno para eso.
Telastyn
55
@KorayTugay: si el programa fuera realmente para que tu esposa viniera a casa, YAGNI te diría que tu versión inicial es perfecta y que no debes invertir más tiempo para introducir constantes o parámetros. YAGNI necesita contexto : ¿es su programa una solución desechable, o un programa de migración que se ejecuta solo durante unos meses, o es parte de un gran sistema ERP, destinado a ser utilizado y mantenido durante varias décadas?
Doc Brown
8
@KorayTugay: La separación de E / S de la computación es una técnica fundamental de estructuración de programas. Generación separada de datos del filtrado de datos de la transformación de datos del consumo de datos de la presentación de datos. Debería estudiar algunos programas funcionales, luego verá esto más claramente. En la programación funcional, es bastante común generar una cantidad infinita de datos, filtrar solo los datos que le interesan, transformar los datos en el formato que necesita, construir una cadena a partir de ella e imprimir esta cadena en 5 funciones diferentes, uno para cada paso.
Jörg W Mittag
3
Como nota al margen, seguir fuertemente a YAGNI lleva a la necesidad de refactorizar continuamente: "Usado sin refactorización continua, podría conducir a un código desorganizado y un trabajo masivo, conocido como deuda técnica". Entonces, aunque YAGNI es algo bueno en general, conlleva una gran responsabilidad de volver a evaluar y reevaluar el código, que no es algo que todos los desarrolladores / empresas estén dispuestos a hacer.
Flater
44
@Telastyn: sugiero ampliar la pregunta a "¿esto nunca cambiará y la intención del código es legible trivialmente sin nombrar la constante ?" Incluso para valores que nunca cambian, puede ser relevante nombrarlos solo para mantener las cosas legibles.
Flater
27

En primer lugar: ningún desarrollador de software con mentalidad de seguridad escribiría un método DestroyCity sin pasar un token de autorización por ningún motivo.

Yo también puedo escribir cualquier cosa como un imperativo que tiene una sabiduría evidente sin que sea aplicable en otro contexto. ¿Por qué es necesario autorizar una concatenación de cadenas?

En segundo lugar: todo el código cuando se ejecuta debe especificarse completamente .

No importa si la decisión se codificó en su lugar o si se aplazó a otra capa. En algún momento hay un código en algún lenguaje que sabe tanto lo que se debe destruir como la forma de instruirlo.

Eso podría estar en el mismo archivo de objeto destroyCity(xyz), y podría estar en un archivo de configuración: destroy {"city": "XYZ"}"o podría ser una serie de clics y pulsaciones de teclas en una interfaz de usuario.

En tercer lugar:

Cariño ... ¿Puedes imprimir todos los Day Light Savings alrededor del mundo para 2018 en la consola? Necesito revisar algo.

es un conjunto de requisitos muy diferente para:

quiere pasar un formateador de cadena personalizado, ... interesado solo en ciertos períodos de mes, ... [e] interesado en ciertos períodos de hora ...

Ahora, el segundo conjunto de requisitos obviamente hace una herramienta más flexible. Tiene un público objetivo más amplio y un ámbito de aplicación más amplio. El peligro aquí es que la aplicación más flexible del mundo es, de hecho, un compilador de código de máquina. Es literalmente un programa tan genérico que puede construir cualquier cosa para hacer que la computadora sea lo que sea que necesite (dentro de las limitaciones de su hardware).

En general, las personas que necesitan software no quieren algo genérico; Quieren algo específico. Al dar más opciones, de hecho estás haciendo sus vidas más complicadas. Si quisieran esa complejidad, en su lugar estarían usando un compilador, no preguntándote.

Su esposa estaba pidiendo funcionalidad y no le especificó sus requisitos. En este caso, aparentemente fue a propósito, y en general es porque no saben nada mejor. De lo contrario, habrían utilizado el compilador ellos mismos. Entonces, el primer problema es que no solicitó más detalles sobre exactamente lo que ella quería hacer. ¿Quería ejecutar esto durante varios años diferentes? ¿Lo quería en un archivo CSV? No descubriste qué decisiones quería tomar ella misma y qué te pedía que averiguaras y decidieras por ella. Una vez que haya descubierto qué decisiones deben diferirse, puede descubrir cómo comunicar esas decisiones a través de parámetros (y otros medios configurables).

Dicho esto, la mayoría de los clientes pierden la comunicación, presumen o ignoran ciertos detalles (también conocidos como decisiones) que realmente les gustaría tomar o que realmente no querían hacer (pero suena increíble). Es por eso que los métodos de trabajo como PDSA (plan-desarrollo-estudio-acto) son importantes. Ha planeado el trabajo de acuerdo con los requisitos, y luego desarrolló un conjunto de decisiones (código). Ahora es el momento de estudiarlo, ya sea solo o con su cliente y aprender cosas nuevas, y esto informará su pensamiento en el futuro. Finalmente, actúe sobre sus nuevos conocimientos: actualice los requisitos, refine el proceso, obtenga nuevas herramientas, etc. Luego comience a planificar nuevamente. Esto habría revelado cualquier requisito oculto a lo largo del tiempo, y demuestra el progreso para muchos clientes.

Finalmente. Tu tiempo es importante ; Es muy real y muy finito. Cada decisión que tome implica muchas otras decisiones ocultas, y de esto se trata el desarrollo de software. Retrasar una decisión como argumento puede hacer que la función actual sea más simple, pero hace que en otro lugar sea más complejo. ¿Es esa decisión relevante en esa otra ubicación? ¿Es más relevante aquí? ¿De quién es realmente la decisión? Estás decidiendo esto; Esto es codificación. Si repite conjuntos de decisiones con frecuencia, hay un beneficio muy real al codificarlos dentro de alguna abstracción. XKCD tiene una perspectiva útil aquí. Y esto es relevante a nivel de un sistema, ya sea una función, módulo, programa, etc.

El consejo al principio implica que las decisiones que su función no tiene derecho a tomar deben pasarse como un argumento. El problema es que una DestroyBaghdadfunción en realidad podría ser la función que tiene ese derecho.

Kain0_0
fuente
¡+1 ama la parte del compilador!
Lee
4

Aquí hay muchas respuestas largas, pero honestamente creo que es súper simple

Cualquier información codificada que tenga en su función que no sea parte del nombre de la función debe ser un parámetro.

así que en tu función

class App {
    void dayLightSavings() {
        final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
        availableZoneIds.forEach(zoneId -> {
            LocalDateTime dateTime = LocalDateTime.of(LocalDate.of(2018, 1, 1), LocalTime.of(0, 0, 0));
            ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
            while (2018 == now.getYear()) {
                int hour = now.getHour();
                now = now.plusHours(1);
                if (now.getHour() == hour) {
                    System.out.println(now);
                }
            }
        });
    }
}

Tienes:

The zoneIds
2018, 1, 1
System.out

Así que movería todo esto a los parámetros de una forma u otra. Podría argumentar que los zoneIds están implícitos en el nombre de la función, tal vez desee hacerlo aún más cambiándolo a "DaylightSavingsAroundTheWorld" o algo así

No tiene una cadena de formato, por lo que agregar una es una solicitud de función y debe referir a su esposa a la instancia de Jira de su familia. Se puede incluir en la cartera de pedidos y priorizar en la reunión del comité de gestión del proyecto correspondiente.

Ewan
fuente
1
Usted (dirigiéndome a OP) ciertamente no debería agregar una cadena de formato, ya que no debería imprimir nada. Lo único sobre este código que evita absolutamente su reutilización es que se imprime. Debería devolver las zonas, o un mapa de las zonas a cuando salen del horario de verano. (Aunque no entiendo por qué solo se identifica cuando se apaga el horario de verano y no en el horario de verano. No parece coincidir con la declaración del problema)
David Conrad
El requisito es imprimir en la consola. puede mitigar el acoplamiento apretado pasando el flujo de salida como un parámetro como sugiero
Ewan
1
Aun así, si desea que el código sea reutilizable, no debe imprimir en la consola. Escriba un método que devuelva los resultados, y luego escriba un llamador que los obtenga e imprima. Eso también lo hace comprobable. Si desea que produzca resultados, no pasaría en una secuencia de salida, pasaría en un consumidor.
David Conrad el
un flujo de transmisión es un consumidor
Ewan
No, un OutputStream no es un consumidor .
David Conrad el
4

En resumen, no modifique la capacidad de reutilización de su software porque a ningún usuario final le importa si sus funciones pueden reutilizarse. En cambio, ingeniero para la comprensión del diseño : ¿es mi código fácil de entender para otra persona o para mi futuro olvidadizo? - y flexibilidad de diseño- Cuando inevitablemente tengo que corregir errores, agregar funciones o modificar la funcionalidad, ¿cuánto resistirá mi código los cambios? Lo único que le importa a su cliente es la rapidez con que puede responder cuando informa un error o solicita un cambio. Incidentalmente, hacer estas preguntas sobre su diseño tiende a dar como resultado un código que es reutilizable, pero este enfoque lo mantiene enfocado en evitar los problemas reales que enfrentará durante la vida útil de ese código para que pueda servir mejor al usuario final en lugar de buscar un servicio elevado y poco práctico. ideales de "ingeniería" para complacer a los barbados.

Para algo tan simple como el ejemplo que proporcionó, su implementación inicial está bien debido a lo pequeño que es, pero este diseño directo será difícil de entender y quebradizo si intenta atascar demasiada flexibilidad funcional (en oposición a la flexibilidad de diseño) en Un procedimiento. A continuación se encuentra mi explicación de mi enfoque preferido para diseñar sistemas complejos de comprensión y flexibilidad que espero demuestren lo que quiero decir con ellos. No emplearía esta estrategia para algo que podría escribirse en menos de 20 líneas en un solo procedimiento porque algo tan pequeño ya cumple con mis criterios de comprensión y flexibilidad.


Objetos, no procedimientos

En lugar de usar clases como módulos de la vieja escuela con un montón de rutinas que llama para ejecutar las cosas que su software debería hacer, considere modelar el dominio como objetos que interactúan y cooperan para realizar la tarea en cuestión. Los métodos en un paradigma orientado a objetos se crearon originalmente para ser señales entre objetos, de modo que Object1pudieran decir Object2que hicieran lo suyo, sea lo que sea, y posiblemente recibir una señal de retorno. Esto se debe a que el paradigma orientado a objetos se basa inherentemente en modelar los objetos de su dominio y sus interacciones en lugar de una forma elegante de organizar las mismas funciones y procedimientos del paradigma imperativo. En el caso de lavoid destroyBaghdadPor ejemplo, en lugar de intentar escribir un método genérico sin contexto para manejar la destrucción de Bagdad o cualquier otra cosa (que podría volverse rápidamente compleja, difícil de entender y quebradiza), todo lo que pueda destruirse debería ser responsable de comprender cómo para destruirse a sí mismo. Por ejemplo, tiene una interfaz que describe el comportamiento de cosas que pueden destruirse:

interface Destroyable {
    void destroy();
}

Entonces tienes una ciudad que implementa esta interfaz:

class City implements Destroyable {
    @Override
    public void destroy() {
        ...code that destroys the city
    }
}

Nada que requiera la destrucción de una instancia Citynunca importará cómo sucede, por lo que no hay ninguna razón para que ese código exista en ningún lugar fuera City::destroy, y de hecho, el conocimiento íntimo del funcionamiento interno del Cityexterior de sí mismo sería un acoplamiento estrecho que reduce felxibility ya que debe considerar esos elementos externos si alguna vez necesita modificar el comportamiento de City. Este es el verdadero propósito detrás de la encapsulación. Piense en ello como si cada objeto tuviera su propia API que debería permitirle hacer todo lo que necesite con él para que pueda preocuparse por cumplir con sus solicitudes.

Delegación, no "Control"

Ahora, si su clase de implementación es Cityo Baghdaddepende de cuán genérico resulte ser el proceso de destrucción de la ciudad. Con toda probabilidad, un Cityestará compuesto por piezas más pequeñas que deberán destruirse individualmente para lograr la destrucción total de la ciudad, por lo que en ese caso, cada una de esas piezas también se implementaría Destroyable, y cada una de ellas recibiría instrucciones de Citydestruir ellos mismos de la misma manera que alguien del exterior pidió Cityque se destruyera a sí mismo.

interface Part extends Destroyable {
    ...part-specific methods
}

class Building implements Part {
    ...part-specific methods
    @Override
    public void destroy() {
       ...code to destroy a building
    }
}

class Street implements Part {
    ...part-specific methods
    @Override
    public void destroy() {
        ...code to destroy a building
    }
}

class City implements Destroyable {
    public List<Part> parts() {...}

    @Override
    public void destroy() {
        parts().forEach(Destroyable::destroy);            
    }
}

Si quieres volverte realmente loco e implementar la idea de Bombque se deja caer en una ubicación y destruye todo dentro de un cierto radio, podría verse más o menos así:

class Bomb {
    private final Integer radius;

    public Bomb(final Integer radius) {
        this.radius = radius;
    }

    public void drop(final Grid grid, final Coordinate target) {
        new ObjectsByRadius(
            grid,
            target,
            this.radius
        ).forEach(Destroyable::destroy);
    }
}

ObjectsByRadiusrepresenta un conjunto de objetos que se calcula para las Bombentradas porque Bombno le importa cómo se realiza ese cálculo siempre que pueda funcionar con los objetos. Por cierto, esto es reutilizable, pero el objetivo principal es aislar el cálculo de los procesos de soltar Bomby destruir los objetos para que pueda comprender cada pieza y cómo encajan entre sí y cambiar el comportamiento de una pieza individual sin tener que remodelar todo el algoritmo. .

Interacciones, no algoritmos

En lugar de tratar de adivinar el número correcto de parámetros para un algoritmo complejo, tiene más sentido modelar el proceso como un conjunto de objetos que interactúan, cada uno con roles extremadamente estrechos, ya que le dará la capacidad de modelar la complejidad de su procesar a través de las interacciones entre estos objetos bien definidos, fáciles de comprender y casi inmutables. Cuando se hace correctamente, esto hace que incluso algunas de las modificaciones más complejas sean tan triviales como implementar una interfaz o dos y reelaborar qué objetos se instancian en su main()método.

Le daría algo a su ejemplo original, pero honestamente no puedo entender lo que significa "imprimir ... Day Light Savings". Lo que puedo decir sobre esa categoría de problema es que cada vez que realiza un cálculo, cuyo resultado podría formatearse de varias maneras, mi forma preferida de desglosarlo es así:

interface Result {
    String print();
}

class Caclulation {
    private final Parameter paramater1;

    private final Parameter parameter2;

    public Calculation(final Parameter parameter1, final Parameter parameter2) {
        this.parameter1 = parameter1;
        this.parameter2 = parameter2;
    }

    public Result calculate() {
        ...calculate the result
    }
}

class FormattedResult {
    private final Result result;

    public FormattedResult(final Result result) {
        this.result = result;
    }

    @Override
    public String print() {
        ...interact with this.result to format it and return the formatted String
    }
}

Como su ejemplo usa clases de la biblioteca de Java que no admiten este diseño, puede usar la API ZonedDateTimedirectamente. La idea aquí es que cada cálculo está encapsulado dentro de su propio objeto. No hace suposiciones sobre cuántas veces debe ejecutarse o cómo debe formatear el resultado. Se ocupa exclusivamente de realizar la forma más simple del cálculo. Esto hace que sea fácil de entender y flexible para cambiar. Del mismo modo, Resultse ocupa exclusivamente de encapsular el resultado del cálculo y FormattedResultse preocupa exclusivamente de interactuar con el Resultpara formatearlo de acuerdo con las reglas que definimos. De este modo,Podemos encontrar el número perfecto de argumentos para cada uno de nuestros métodos, ya que cada uno tiene una tarea bien definida . También es mucho más simple modificar el avance siempre que las interfaces no cambien (lo cual no es probable que hagan si minimizas adecuadamente las responsabilidades de tus objetos). Nuestromain()método podría verse así:

class App {
    public static void main(String[] args) {
        final List<Set<Paramater>> parameters = ...instantiated from args
        parameters.forEach(set -> {
            System.out.println(
                new FormattedResult(
                    new Calculation(
                        set.get(0),
                        set.get(1)
                    ).calculate()
                ).print()
            );
        });
    }
}

De hecho, la Programación Orientada a Objetos se inventó específicamente como una solución al problema de complejidad / flexibilidad del paradigma Imperativo porque simplemente no hay una buena respuesta (de la que todos puedan estar de acuerdo o llegar de forma independiente) de cómo hacerlo de manera óptima especificar funciones y procedimientos imperativos dentro del idioma.

Stuporman
fuente
Esta es una respuesta muy detallada y pensada, pero desafortunadamente creo que pierde la marca de lo que realmente estaba pidiendo el OP. No estaba pidiendo una lección sobre buenas prácticas de OOP para resolver su ejemplo engañoso, estaba preguntando sobre los criterios para decidir la inversión de tiempo en una solución frente a la generalización.
maple_shaft
@maple_shaft Quizás perdí la marca, pero creo que tú también. El OP no pregunta sobre la inversión de tiempo frente a la generalización. Él pregunta "¿Cómo sé qué tan reutilizables deberían ser mis métodos?" Continúa preguntando en el cuerpo de su pregunta: "Si destroyCity (City city) es mejor que destroyBaghdad (), ¿takeActionOnCity (Action action, City city) es aún mejor? ¿Por qué / por qué no?" He defendido un enfoque alternativo a las soluciones de ingeniería que creo resuelve el problema de descubrir qué tan genérico es hacer métodos y proporcioné ejemplos para respaldar mi afirmación. Lamento que no te haya gustado.
Stuporman
@maple_shaft Francamente, solo el OP puede tomar la determinación si mi respuesta fue relevante para su pregunta, ya que el resto de nosotros podría luchar guerras defendiendo nuestras interpretaciones de sus intenciones, todo lo cual podría ser igualmente incorrecto.
Stuporman
@maple_shaft Agregué una introducción para tratar de aclarar cómo se relaciona con la pregunta y proporcioné una delineación clara entre la respuesta y la implementación del ejemplo. ¿Eso está mejor?
Stuporman
1
Honestamente, si aplica todos estos principios, la respuesta será natural, suave y legible de forma masiva. Además, cambiable sin mucho alboroto. ¡No sé quién eres, pero desearía que hubiera más de ti! Sigo extrayendo el código reactivo para OO decente, y SIEMPRE es la mitad del tamaño, más legible, más controlable y todavía tiene subprocesos / división / mapeo. Creo que React es para personas que no entienden los conceptos "básicos" que acaba de enumerar.
Stephen J
3

Experiencia , conocimiento de dominio y revisiones de código.

Y, independientemente de cuánta o poca experiencia , dominio de conocimiento o equipo que tenga, no puede evitar la necesidad de refactorizar según sea necesario.


Con Experience , comenzará a reconocer patrones en los métodos (y clases) no específicos de dominio que escriba. Y, si está interesado en el código DRY, sentirá malos sentimientos cuando se trata de escribir un método que instintivamente sabe que escribirá variaciones en el futuro. Por lo tanto, intuitivamente escribirá un mínimo común denominador parametrizado.

(Esta experiencia también puede transferirse instintivamente a algunos de sus objetos y métodos de dominio).

Con conocimiento de dominio , tendrá una idea de qué conceptos comerciales están estrechamente relacionados, qué conceptos tienen variables, cuáles son bastante estáticos, etc.

Con el código de comentarios, sub y sobre parametrización más probable será capturado antes de que sea el código de producción, debido a que sus compañeros (esperemos) tienen experiencias y perspectivas únicas, tanto en el dominio y en la codificación en general.


Dicho esto, los nuevos desarrolladores generalmente no tendrán estos Spidey Senses o un grupo experimentado de pares para apoyarse de inmediato. E incluso los desarrolladores experimentados se benefician de una disciplina básica para guiarlos a través de nuevos requisitos, o durante días de confusión mental. Entonces, esto es lo que sugeriría como comienzo :

  • Comience con la implementación ingenua, con una parametrización mínima.
    (Incluya cualquier parámetro que ya sepa que necesitará, obviamente ...)
  • Elimine números y cadenas mágicos, moviéndolos a configuraciones y / o parámetros
  • Factorice los métodos "grandes" en métodos más pequeños y bien nombrados
  • Refactorice métodos altamente redundantes (si es conveniente) en un denominador común, parametrizando las diferencias.

Estos pasos no ocurren necesariamente en el orden establecido. Si te sientas a escribir un método que ya sabes que es muy redundante con un método existente , salta directamente a la refactorización si es conveniente.(Si no va a tomar mucho más tiempo refactorizar que escribir, probar y mantener dos métodos).

Pero, además de tener mucha experiencia y demás, le recomiendo DRY-ing de código bastante minimalista. No es difícil refactorizar violaciones obvias. Y, si eres demasiado celoso , puedes terminar con un código "sobre SECO" que es aún más difícil de leer, comprender y mantener que el equivalente "MOJADO".

svidgen
fuente
2
¿Entonces no hay una respuesta correcta If destoryCity(City city) is better than destoryBaghdad(), is takeActionOnCity(Action action, City city) even better?? Es una pregunta de sí / no, pero no tiene una respuesta, ¿verdad? ¿O la suposición inicial es incorrecta, destroyCity(City)podría no ser necesariamente mejor y realmente depende ? Entonces, ¿no significa que no soy un ingeniero de software no capacitado éticamente porque lo implementé directamente sin ningún parámetro? Quiero decir, ¿cuál es la respuesta a la pregunta concreta que estoy haciendo?
Koray Tugay
Su pregunta hace algunas preguntas. La respuesta a la pregunta del título es: "experiencia, conocimiento de dominio, revisiones de código ... y ... no tengas miedo de refactorizar". La respuesta a cualquier pregunta concreta "¿son estos los parámetros correctos para el método ABC" es ... "No sé. ¿Por qué preguntas? ¿Hay algún problema con la cantidad de parámetros que tiene actualmente? it. Fix it ". ... Podría referirle a "el POAP" para obtener más orientación: ¡Necesita entender por qué está haciendo lo que está haciendo!
svidgen
Quiero decir ... incluso demos un paso atrás del destroyBaghdad()método. ¿Cuál es el contexto? ¿Es un videojuego donde el final del juego resulta en la destrucción de Bagdad? Si es así ... destroyBaghdad()podría ser un nombre / firma de método perfectamente razonable ...
svidgen
1
Entonces, no está de acuerdo con la cita citada en mi pregunta, ¿verdad? It should be noted that no ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure.Si estuvieras en la habitación con Nathaniel Borenstein, ¿dirías que realmente depende y que su afirmación no es correcta? Quiero decir que es hermoso que muchas personas estén respondiendo, gastando su tiempo y energía, pero no veo una sola respuesta concreta en ningún lado. Spidey-sensees, revisiones de código ... Pero, ¿cuál es la respuesta is takeActionOnCity(Action action, City city) better? null?
Koray Tugay
1
@svidgen Por supuesto, otra forma de abstraer esto sin ningún esfuerzo adicional es invertir la dependencia: haga que la función devuelva una lista de ciudades, en lugar de hacer cualquier acción sobre ellas (como "println" en el código original). Esto puede resumirse aún más si es necesario, pero solo este cambio por sí solo se ocupa de aproximadamente la mitad de los requisitos agregados en la pregunta original, y en lugar de una función impura que puede hacer todo tipo de cosas malas, solo tiene una función eso devuelve una lista, y la persona que llama hace las cosas malas.
Luaan
2

La misma respuesta que con calidad, usabilidad, deuda técnica, etc.

Tan reutilizable como usted, el usuario, 1 de ellos tiene que ser

Básicamente es una cuestión de juicio: si el costo de diseñar y mantener la abstracción será pagado por el costo (= tiempo y esfuerzo), le ahorrará más adelante.

  • Tenga en cuenta la frase "en la línea": aquí hay una mecánica de pago, por lo que dependerá de cuánto trabajará más con este código. P.ej:
    • ¿Es este un proyecto único o va a mejorar progresivamente durante mucho tiempo?
    • ¿Confía en su diseño, o es probable que tenga que desecharlo o cambiarlo drásticamente para el próximo proyecto / hito (por ejemplo, intente con otro marco)?
  • El beneficio proyectado también depende de su capacidad de predecir el futuro (cambios en la aplicación). A veces, puede ver razonablemente los lugares que tomará su aplicación. Más veces, crees que puedes pero en realidad no puedes. Las reglas generales aquí son el principio de YAGNI y la regla de tres : ambos enfatizan trabajar en lo que sabes ahora.

1 Esta es una construcción de código, por lo que usted es el "usuario" en este caso, el usuario del código fuente

ivan_pozdeev
fuente
1

Hay un proceso claro que puedes seguir:

  • Escriba una prueba fallida para una característica única que es en sí misma una "cosa" (es decir, no una división arbitraria de una característica donde ninguna de las dos tiene sentido).
  • Escriba el código mínimo absoluto para que pase verde, no una línea más.
  • Enjuague y repita.
  • (Refactorice sin descanso si es necesario, lo que debería ser fácil debido a la excelente cobertura de prueba).

Esto aparece con, al menos en la opinión de algunas personas, un código bastante óptimo, ya que es lo más pequeño posible, cada función terminada lleva el menor tiempo posible (lo que podría ser cierto o no si se mira el acabado) producto después de la refactorización), y tiene muy buena cobertura de prueba. También evita notablemente los métodos o clases demasiado genéricos excesivamente diseñados.

Esto también le da instrucciones muy claras sobre cuándo hacer las cosas genéricas y cuándo especializarse.

Me parece extraño el ejemplo de tu ciudad; Es muy probable que nunca codifique el nombre de una ciudad. Es tan obvio que más ciudades se incluirán más adelante, sea lo que sea que esté haciendo. Pero otro ejemplo serían los colores. En algunas circunstancias, la posibilidad de codificar "rojo" o "verde". Por ejemplo, los semáforos son de un color tan omnipresente que puede salirse con la suya (y siempre puede refactorizar). La diferencia es que "rojo" y "verde" tienen un significado universal, "codificado" en nuestro mundo, es increíblemente improbable que alguna vez cambie, y tampoco hay realmente una alternativa.

Su primer método de horario de verano simplemente no funciona. Si bien se ajusta a las especificaciones, el 2018 codificado es particularmente malo porque a) no se menciona en el "contrato" técnico (en el nombre del método, en este caso), yb) pronto estará desactualizado, por lo que la rotura está incluido desde el primer momento. Para cosas que están relacionadas con la hora / fecha, muy raramente tendría sentido codificar un valor específico ya que, bueno, el tiempo avanza. Pero aparte de eso, todo lo demás está en discusión. Si le das un año simple y luego siempre calculas el año completo, adelante. La mayoría de las cosas que enumeró (formateo, elección de un rango más pequeño, etc.) grita que su método está haciendo demasiado, y en su lugar probablemente debería devolver una lista / matriz de valores para que la persona que llama pueda formatear / filtrar por sí mismo.

Pero al final del día, la mayor parte de esto es opinión, gusto, experiencia y prejuicios personales, así que no te preocupes demasiado por eso.

AnoE
fuente
Con respecto al penúltimo párrafo, observe los "requisitos" inicialmente dados, es decir, aquellos en los que se basó el primer método. Especifica 2018, por lo que el código es técnicamente correcto (y probablemente coincida con su enfoque basado en funciones).
dwizum
@dwizum, es correcto con respecto a los requisitos, pero el nombre del método es engañoso. En 2019, cualquier programador que solo mire el nombre del método supondrá que está haciendo lo que sea (tal vez devolver los valores para el año actual), no 2018 ... Agregaré una oración a la respuesta para aclarar más lo que quise decir.
AnoE
1

He llegado a la opinión de que hay dos tipos de código reutilizable:

  • Código que es reutilizable porque es algo tan fundamental y básico.
  • Código que es reutilizable porque tiene parámetros, anulaciones y ganchos para todas partes.

El primer tipo de reutilización suele ser una buena idea. Se aplica a cosas como listas, hashmaps, almacenes de clave / valor, comparadores de cadenas (por ejemplo, expresiones regulares, glob, ...), tuplas, unificación, árboles de búsqueda (primero en profundidad, primero en amplitud, profundización iterativa, ...) , combinadores de analizadores, cachés / memorias, lectores / escritores de formato de datos (expresiones-s, XML, JSON, protobuf, ...), colas de tareas, etc.

Estas cosas son tan generales, de una manera muy abstracta, que se reutilizan en todo el lugar en la programación diaria. Si te encuentras escribiendo un código de propósito especial que sería más simple si se hiciera más abstracto / general (por ejemplo, si tenemos "una lista de pedidos de clientes", podríamos tirar las cosas de "pedidos de clientes" para obtener "una lista" ) entonces podría ser una buena idea sacar eso. Incluso si no se reutiliza, nos permite desacoplar funcionalidades no relacionadas.

El segundo tipo es donde tenemos un código concreto, que resuelve un problema real, pero lo hace al tomar un montón de decisiones. Podemos hacer que sea más general / reutilizable "codificando suavemente" esas decisiones, por ejemplo, convirtiéndolas en parámetros, complicando la implementación y elaborando detalles aún más concretos (es decir, el conocimiento de qué ganchos podríamos querer anular). Su ejemplo parece ser de este tipo. El problema con este tipo de reutilización es que podemos terminar tratando de adivinar los casos de uso de otras personas, o nuestro yo futuro. Eventualmente, podríamos terminar teniendo tantos parámetros que nuestro código no sea utilizable, y mucho menos reutilizable! En otras palabras, cuando se llama requiere más esfuerzo que simplemente escribir nuestra propia versión. Aquí es donde YAGNI (No lo vas a necesitar) es importante. Muchas veces, tales intentos de código "reutilizable" terminan no siendo reutilizados, ya que puede ser incompatible con esos casos de uso más fundamentalmente de lo que los parámetros pueden explicar, o esos usuarios potenciales preferirían rodar los suyos (diablos, mira todos los estándares y bibliotecas por ahí cuyos autores prefijaron con la palabra "Simple", para distinguirlos de los predecesores).

Esta segunda forma de "reutilización" debería hacerse básicamente según sea necesario. Claro, puede incluir algunos parámetros "obvios" allí, pero no comience a tratar de predecir el futuro. YAGNI

Warbo
fuente
¿Podemos decir que está de acuerdo en que mi primera toma estuvo bien, donde incluso el año fue codificado? O si inicialmente estuviera implementando ese requisito, ¿convertiría el año en un parámetro en su primera toma?
Koray Tugay
Su primera toma estuvo bien, ya que el requisito era un script único para 'verificar algo'. Falla su prueba 'ética', pero falla la prueba 'no dogma'. "Ella podría decir ..." es inventar requisitos que no vas a necesitar.
Warbo
No podemos decir qué 'destruir ciudad' es "mejor" sin más información: destroyBaghdades un script único (o al menos, es idempotente). Quizás destruir una ciudad sería una mejora, pero ¿y si destroyBaghdadfunciona inundando el Tigris? Eso puede ser reutilizable para Mosul y Basora, pero no para La Meca o Atlanta.
Warbo
Ya veo, así que no estás de acuerdo con el Nathaniel Borenstein, el dueño de la cita. Estoy tratando de entender lentamente, creo al leer todas estas respuestas y las discusiones.
Koray Tugay
1
Me gusta esta diferenciación. No siempre está claro, y siempre hay "casos fronterizos". Pero, en general, también soy un fanático de los "bloques de construcción" (a menudo en forma de staticmétodos) que son puramente funcionales y de bajo nivel, y en contraste con eso, decidir sobre los "parámetros y ganchos de configuración" suele ser donde tienes que construir algunas estructuras que piden ser justificadas.
Marco13
1

Ya hay muchas respuestas excelentes y elaboradas. Algunos de ellos profundizan en detalles específicos, exponen ciertos puntos de vista sobre las metodologías de desarrollo de software en general, y algunos ciertamente tienen elementos controvertidos u "opiniones" espolvoreadas.

La respuesta de Warbo ya señalaba diferentes tipos de reutilización. A saber, si algo es reutilizable porque es un elemento fundamental, o si algo es reutilizable porque es "genérico" de alguna manera. Refiriéndome a esto último, hay algo que consideraría como algún tipo de medida para la reutilización:

Si un método puede emular a otro.

Con respecto al ejemplo de la pregunta: Imagina que el método

void dayLightSavings()

fue la implementación de una funcionalidad solicitada por un cliente. Por lo tanto, será algo que otros programadores deben usar y, por lo tanto, será un método público , como en

publicvoid dayLightSavings()

Esto podría implementarse como mostró en su respuesta. Ahora, alguien quiere parametrizarlo con el año. Entonces puedes agregar un método

publicvoid dayLightSavings(int year)

y cambiar la implementación original para ser simplemente

public void dayLightSavings() {
    dayLightSavings(2018);
}

Las siguientes "solicitudes de características" y generalizaciones siguen el mismo patrón. Entonces, si y solo si hay demanda del formulario más genérico, puede implementarlo, sabiendo que este formulario más genérico permite implementaciones triviales de los más específicos:

public void dayLightSavings() {
    dayLightSavings(2018, 0, 12, 0, 12, new DateTimeFormatter(...));
}

Si hubiera previsto futuras extensiones y solicitudes de funciones, y tuviera un poco de tiempo a su disposición y quisiera pasar un fin de semana aburrido con generalizaciones (potencialmente inútiles), podría haber comenzado con la más genérica desde el principio. Pero solo como un método privado . Siempre y cuando solo exponga el método simple que el cliente solicitó como método público , estará seguro.

tl; dr :

La pregunta no es realmente "qué tan reutilizable debería ser un método". La pregunta es cuánto de esta reutilización está expuesta y cómo se ve la API. Crear una API confiable que pueda resistir la prueba del tiempo (incluso cuando surjan requisitos adicionales más adelante) es un arte y un oficio, y el tema es demasiado complejo para cubrirlo aquí. Eche un vistazo a esta presentación de Joshua Bloch o al wiki del libro de diseño API para comenzar.

Marco13
fuente
dayLightSavings()llamar dayLightSavings(2018)no me parece una buena idea.
Koray Tugay
@KorayTugay Cuando la solicitud inicial es que debe imprimir "el horario de verano de 2018", entonces está bien. De hecho, este es exactamente el método que implementó originalmente. Si imprimiera el "horario de verano del año en curso , entonces, por supuesto, llamaría dayLightSavings(computeCurrentYear());..."
Marco13
0

Una buena regla general es: su método debe ser tan reutilizable como ... reutilizable.

Si espera que llame a su método solo en un lugar, debe tener solo parámetros conocidos por el sitio de la llamada y que no estén disponibles para este método.

Si tiene más personas que llaman, puede introducir nuevos parámetros siempre que otras personas puedan pasar esos parámetros; de lo contrario, necesita un nuevo método.

Como la cantidad de personas que llaman puede aumentar a tiempo, debe estar preparado para refactorizar o sobrecargar. En muchos casos, significa que debe sentirse seguro al seleccionar la expresión y ejecutar la acción "extraer parámetro" de su IDE.

Karol
fuente
0

Respuesta ultracorta: cuanto menos acoplamiento o dependencia a otro código tenga su módulo genérico, más reutilizable puede ser.

Tu ejemplo solo depende de

import java.time.*;
import java.util.Set;

entonces, en teoría, puede ser altamente reutilizable.

En la práctica, no creo que alguna vez tenga un segundo caso de uso que necesite este código, por lo que siguiendo el principio de yagni no lo haría reutilizable si no hay más de 3 proyectos diferentes que necesiten este código.

Otros aspectos de la reutilización son la facilidad de uso y la documentación que se correlacionan con el desarrollo impulsado por pruebas : es útil si tiene una prueba unitaria simple que demuestra / documenta un uso fácil de su módulo genérico como un ejemplo de codificación para los usuarios de su biblioteca.

k3b
fuente
0

Esta es una buena oportunidad para establecer una regla que acuñé recientemente:

Ser un buen programador significa ser capaz de predecir el futuro.

¡Por supuesto, esto es estrictamente imposible! Después de todo, nunca sabe con certeza qué generalizaciones le resultarán útiles más adelante, qué tareas relacionadas querrá realizar, qué nuevas características desearán sus usuarios, etc. Pero la experiencia a veces te da una idea aproximada de lo que puede ser útil.

Los otros factores con los que tiene que equilibrar eso son cuánto tiempo adicional y esfuerzo involucrarían, y cuánto más complejo haría su código. A veces tienes suerte, ¡y resolver el problema más general es realmente más simple! (Al menos conceptualmente, si no en la cantidad de código). Pero con mayor frecuencia, hay un costo de complejidad, así como uno de tiempo y esfuerzo.

Entonces, si cree que es muy probable que sea necesaria una generalización, a menudo vale la pena hacerlo (a menos que agregue mucho trabajo o complejidad); pero si parece mucho menos probable, entonces probablemente no lo sea (a menos que sea muy fácil y / o simplifique el código).

(Para un ejemplo reciente: la semana pasada me dieron una especificación. Para las acciones que un sistema debería tomar exactamente 2 días después de que algo expiró. Así que, por supuesto, hice del período de 2 días un parámetro. Esta semana, la gente de negocios estaba encantada, ya que estaban a punto de pedir esa mejora. Tuve suerte: fue un cambio fácil y supuse que era muy probable que se quisiera. A menudo es más difícil de juzgar. Pero vale la pena intentar predecirlo, y la experiencia es a menudo una buena guía. .)

gidds
fuente
0

En primer lugar, la mejor respuesta a "¿Cómo sé qué tan reutilizables deberían ser mis métodos?" Es "experiencia". Haga esto miles de veces, y normalmente obtendrá la respuesta correcta. Pero como adelanto, puedo darle la última línea de esta respuesta: su cliente le dirá cuánta flexibilidad y cuántas capas de generalización debe buscar.

Muchas de estas respuestas tienen consejos específicos. Quería dar algo más genérico ... ¡porque la ironía es demasiado divertida para dejarla pasar!

Como han señalado algunas de las respuestas, la generalidad es costosa. Sin embargo, realmente no lo es. No siempre. Comprender el gasto es esencial para jugar el juego de reutilización.

Me concentro en poner las cosas en una escala de "irreversible" a "reversible". Es una escala suave. Lo único verdaderamente irreversible es el "tiempo dedicado al proyecto". Nunca recuperarás esos recursos. Un poco menos reversible podrían ser las situaciones de "esposas doradas" como la API de Windows. Las características obsoletas permanecen en esa API durante décadas porque el modelo comercial de Microsoft lo requiere. Si tiene clientes cuya relación se dañaría permanentemente al deshacer alguna función de la API, entonces eso debería tratarse como irreversible. Mirando el otro extremo de la escala, tienes cosas como el código prototipo. Si no le gusta a dónde va, simplemente puede tirarlo. Un poco menos reversible podría ser API de uso interno. Se pueden refactorizar sin molestar a un cliente,tiempo (¡el recurso más irreversible de todos!)

Así que ponlos en una escala. Ahora puede aplicar una heurística: cuanto más reversible es algo, más puede usarlo para actividades futuras. Si algo es irreversible, úselo solo para tareas concretas dirigidas por el cliente. Es por eso que ve principios como los de la programación extrema que sugieren hacer solo lo que el cliente pide y nada más. Estos principios son buenos para asegurarse de no hacer algo de lo que se arrepienta.

Cosas como el principio DRY sugieren una forma de mover ese equilibrio. Si te encuentras repitiéndote, es una oportunidad para crear lo que básicamente es una API interna. Ningún cliente lo ve, por lo que siempre puede cambiarlo. Una vez que tenga esta API interna, ahora puede comenzar a jugar con cosas con visión de futuro. ¿Cuántas tareas diferentes basadas en la zona horaria crees que tu esposa te va a dar? ¿Tiene otros clientes que deseen tareas basadas en la zona horaria? Su flexibilidad aquí es comprada por las demandas concretas de sus clientes actuales, y respalda las posibles demandas futuras de futuros clientes.

Este enfoque de pensamiento en capas, que proviene naturalmente de DRY, proporciona naturalmente la generalización que desea sin desperdicio. ¿Pero hay un límite? Por supuesto que lo hay. Pero para verlo, tienes que ver el bosque por los árboles.

Si tiene muchas capas de flexibilidad, a menudo conducen a una falta de control directo de las capas que enfrentan sus clientes. He tenido un software donde tuve la brutal tarea de explicarle a un cliente por qué no pueden tener lo que quieren debido a la flexibilidad incorporada en 10 capas que nunca se suponía que debían ver. Nos escribimos en una esquina. Nos hicimos un nudo con toda la flexibilidad que pensábamos que necesitábamos.

Entonces, cuando esté haciendo este truco de generalización / SECO, mantenga siempre el pulso a su cliente . ¿Qué crees que va a pedir tu esposa después? ¿Te estás poniendo en condiciones de satisfacer esas necesidades? Si tiene la habilidad, el cliente le dirá efectivamente sus necesidades futuras. Si no tienes la habilidad, bueno, ¡la mayoría de nosotros solo confiamos en las conjeturas! (¡especialmente con los cónyuges!) Algunos clientes querrán una gran flexibilidad y estarán dispuestos a aceptar el costo adicional de desarrollar con todas estas capas porque se benefician directamente de la flexibilidad de esas capas. Otros clientes tienen requisitos inquebrantables bastante fijos, y prefieren que el desarrollo sea más directo. Su cliente le dirá cuánta flexibilidad y cuántas capas de generalización debe buscar.

Cort Ammon
fuente
Bueno, debería haber otras personas que hicieron esto 10000 veces, entonces ¿por qué debería hacerlo 10000 veces y ganar experiencia cuando puedo aprender de los demás? ¿Porque la respuesta será diferente para cada individuo, de modo que las respuestas de los experimentados no me son aplicables? Además, Your customer will tell you how much flexibility and how many layers of generalization you should seek.¿qué mundo es este?
Koray Tugay
@KorayTugay Es un mundo de negocios. Si sus clientes no le dicen qué hacer, entonces no está escuchando lo suficiente. Por supuesto, no siempre te lo dicen con palabras, pero te lo dicen de otras maneras. La experiencia te ayuda a escuchar sus mensajes más sutiles. Si aún no tiene la habilidad, encuentre a alguien en su empresa que tenga la habilidad de escuchar esas sugerencias sutiles de los clientes, y hágalas llegar. Alguien tendrá esa habilidad, incluso si es el CEO o en marketing.
Cort Ammon
En su caso específico, si no pudo sacar la basura porque estaba demasiado ocupado codificando una versión generalizada de este problema de zona horaria en lugar de hackear la solución específica, ¿cómo se sentiría su cliente?
Cort Ammon
Entonces, ¿está de acuerdo en que mi primer enfoque fue el correcto, codificar 2018 en la primera toma en lugar de parametrizar el año? (por cierto, creo que no es realmente escuchar a mi cliente, el ejemplo de la basura. Eso es conocer a tu cliente ... Incluso si obtienes apoyo de The Oracle, no hay mensajes sutiles para escuchar cuando dice que necesito una lista de la luz del día cambios para 2018.) Gracias por su tiempo y responda por cierto.
Koray Tugay
@KorayTugay Sin conocer ningún detalle adicional, diría que la codificación rígida fue el enfoque correcto. No tenía forma de saber si iba a necesitar un código DLS futuro, ni tenía idea de qué tipo de solicitud podría hacer a continuación. Y si su cliente está tratando de ponerlo a prueba, obtiene lo que obtiene = D
Cort Ammon
0

Ningún ingeniero de software con formación ética consentiría escribir un procedimiento de DestroyBaghdad. En cambio, la ética profesional básica requeriría que él escribiera un procedimiento de DestroyCity, al cual Bagdad podría asignarse como parámetro.

Esto es algo conocido en los círculos avanzados de ingeniería de software como una "broma". Los chistes no tienen que ser lo que llamamos "verdaderos", aunque para ser graciosos generalmente deben insinuar algo verdadero.

En este caso particular, el "chiste" no es "verdadero". El trabajo involucrado en escribir un procedimiento general para destruir cualquier ciudad es, podemos asumir con seguridad, órdenes de magnitud más allá de lo requerido para destruir una específicaciudad. De lo contrario, cualquiera que haya destruido una o un puñado de ciudades (el bíblico Joshua, digamos, o el presidente Truman) podría generalizar trivialmente lo que hizo y ser capaz de destruir absolutamente cualquier ciudad a voluntad. De hecho, este no es el caso. Los métodos que esas dos personas usaron para destruir un pequeño número de ciudades específicas no necesariamente funcionarían en cualquier ciudad en cualquier momento. Otra ciudad cuyas paredes tenían una frecuencia de resonancia diferente o cuyas defensas aéreas a gran altitud eran bastante mejores, necesitaría cambios de enfoque menores o fundamentales (una trompeta de tono diferente o un cohete).

Esto también conduce al mantenimiento del código contra los cambios a lo largo del tiempo: ahora hay muchas ciudades que no caerían en ninguno de esos enfoques, gracias a los métodos de construcción modernos y al radar omnipresente.

Desarrollar y probar un medio completamente general que destruirá cualquier ciudad, antes de que aceptes destruir solo una ciudad, es un enfoque desesperadamente ineficiente. Ningún ingeniero de software con formación ética trataría de generalizar un problema en una medida que requiera órdenes de magnitud más trabajo del que su empleador / cliente realmente necesita pagar, sin un requisito demostrado.

Entonces, ¿qué es verdad? A veces agregar generalidad es trivial. ¿Deberíamos agregar siempre generalidad cuando es trivial hacerlo? Todavía argumentaría "no, no siempre", debido al problema de mantenimiento a largo plazo. Suponiendo que al momento de escribir, todas las ciudades son básicamente iguales, así que sigo con DestroyCity. Una vez que he escrito esto, junto con pruebas de integración que (debido al espacio de entradas finitamente enumerable) iteran sobre cada ciudad conocida y se aseguran de que la función funcione en cada una (no estoy seguro de cómo funciona. Probablemente llame a City.clone () y destruye el clon?

En la práctica, la función se usa únicamente para destruir Bagdad, supongamos que alguien construye una nueva ciudad que es resistente a mis técnicas (es subterránea o algo así). Ahora tengo un fallo de prueba de integración para un caso de uso que ni siquiera existe , y antes de que pueda continuar mi campaña de terror contra los civiles inocentes de Iraq, tengo que descubrir cómo destruir Subterrania. No importa si esto es ético o no, es tonto y una pérdida de mi maldito tiempo .

Entonces, ¿realmente desea una función que pueda generar el horario de verano de cualquier año, solo para generar datos para 2018? Tal vez, pero ciertamente va a requerir una pequeña cantidad de esfuerzo adicional para armar casos de prueba. Puede requerir una gran cantidad de esfuerzo obtener una mejor base de datos de zonas horarias que la que realmente tiene. Así, por ejemplo, en 1908, la ciudad de Port Arthur, Ontario, tuvo un período de horario de verano que comenzó el 1 de julio. ¿Está eso en la base de datos de zona horaria de su sistema operativo? Pensado que no, entonces su función generalizada está mal . No hay nada especialmente ético en escribir código que haga promesas que no pueda cumplir.

Bien, entonces, con advertencias apropiadas, es fácil escribir una función que haga zonas horarias por un rango de años, digamos 1970 hasta nuestros días. Pero es igual de fácil tomar la función que realmente escribió y generalizarla para parametrizar el año. Por lo tanto, no es más ético / sensato generalizar ahora, sino hacer lo que hizo y luego generalizar si lo necesita.

Sin embargo, si supiera por qué su esposa quería verificar esta lista de DST, entonces tendría una opinión informada sobre si es probable que haga la misma pregunta nuevamente en 2019 y, de ser así, si puede salir de ese círculo dando es una función que puede llamar, sin necesidad de volver a compilarla. Una vez que haya hecho ese análisis, la respuesta a la pregunta "si esto se generaliza a los últimos años" podría ser "sí". Pero crea otro problema para usted mismo, que es que los datos de la zona horaria en el futuro son solo provisionales y, por lo tanto, si lo ejecuta para 2019 hoy, ella puede o no darse cuenta de que eso le está dando una mejor conjetura. Por lo tanto, aún tiene que escribir un montón de documentación que no sería necesaria para la función menos general ("los datos provienen de la base de datos de zonas horarias bla, bla, aquí está el enlace para ver sus políticas sobre actualizaciones de bla, bla, bla"). Si rechaza el caso especial, todo el tiempo lo hace, ella no puede continuar con la tarea para la que necesitaba los datos de 2018, debido a algunas tonterías sobre 2019 que ni siquiera le importa.

No hagas cosas difíciles sin pensarlas bien, solo porque una broma te lo dijo. ¿Es útil? ¿Es lo suficientemente barato para ese grado de utilidad?

Steve Jessop
fuente
0

Esta es una línea fácil de dibujar porque la reutilización tal como la definen los astronautas de la arquitectura es una locura.

Casi todo el código creado por los desarrolladores de aplicaciones es extremadamente específico del dominio. Esto no es 1980. Casi todo lo que vale la pena ya es en un marco.

Las abstracciones y convenciones requieren documentación y esfuerzo de aprendizaje. Por favor, deje de crear otros nuevos por el simple hecho de hacerlo. (Estoy mirando a usted , la gente de JavaScript!)

Disfrutemos la fantasía inverosímil de que has encontrado algo que realmente debería estar en tu marco de elección. No puede simplemente agrupar el código de la forma en que lo hace habitualmente. Oh, no, necesita cobertura de prueba no solo para el uso previsto, sino también para las desviaciones del uso previsto, para todos casos límite conocidos, para todos los modos de falla imaginables, casos de prueba para diagnósticos, datos de prueba, documentación técnica, documentación del usuario, gestión de versiones, soporte guiones, pruebas de regresión, gestión de cambios ...

¿Está contento su empleador de pagar todo eso? Voy a decir que no

La abstracción es el precio que pagamos por la flexibilidad. Hace que nuestro código sea más complejo y más difícil de entender. A menos que la flexibilidad satisfaga una necesidad real y presente, simplemente no, porque YAGNI.

Echemos un vistazo a un ejemplo del mundo real con el que tuve que tratar: HTMLRenderer. No estaba escalando correctamente cuando traté de mostrar el contexto del dispositivo de una impresora. Me llevó todo el día descubrir que, por defecto, estaba usando GDI (que no escala) en lugar de GDI + (que sí, pero no antialias) porque tuve que atravesar seis niveles de indirección en dos ensamblajes antes de encontrar código que hizo algo .

En este caso perdonaré al autor. La abstracción es realmente necesaria porque esto es código marco que apunta a cinco objetivos de representación muy diferentes: WinForms, WPF, dotnet Core, Mono y PdfSharp.

Pero eso solo subraya mi punto: es casi seguro que no haciendo algo extremadamente complejo (representación HTML con hojas de estilo) dirigido a múltiples plataformas con un objetivo declarado de alto rendimiento en todas las plataformas.

Es casi seguro que su código es otra cuadrícula de base de datos con reglas comerciales que solo se aplican a su empleador y reglas fiscales que solo se aplican en su estado, en una aplicación que no está a la venta.

Toda esa indirección resuelve un problema que no tienes y hace que tu código sea mucho más difícil de leer, lo que aumenta enormemente el costo de mantenimiento y es un gran perjuicio para su empleador. Afortunadamente, las personas que deberían quejarse de esto no pueden comprender lo que les está haciendo.

Un contraargumento es que este tipo de abstracción es compatible con el desarrollo impulsado por pruebas, pero creo que TDD también es una locura, ya que presupone que el negocio tiene una comprensión clara, completa y correcta de sus requisitos. TDD es excelente para la NASA y el software de control para equipos médicos y autos sin conductor, pero demasiado caro para todos los demás.


Por cierto, no es posible predecir todos los ahorros de luz del día en el mundo. Israel en particular tiene alrededor de 40 transiciones cada año que saltan por todos lados porque no podemos tener personas orando en el momento equivocado y Dios no hace el horario de verano.

Peter Wone
fuente
Aunque estoy de acuerdo con lo que dijiste sobre las abstracciones y el esfuerzo de aprendizaje, y particularmente cuando está dirigido a la abominación que se llama "JavaScript", estoy totalmente en desacuerdo con la primera oración. La reutilización puede suceder en muchos niveles, puede ir muy lejos y los programadores pueden escribir código desechable. Pero no son las personas que están lo suficientemente idealistas a por lo menos objetivo en código reutilizable. Una pena para ti si no ves los beneficios que esto puede tener.
Marco13
@ Marco13 - Dado que su objeción es razonable, ampliaré el punto.
Peter Wone
-3

Si está utilizando al menos Java 8, escribiría la clase WorldTimeZones para proporcionar lo que en esencia parece ser una colección de zonas horarias.

Luego agregue un método de filtro (Filtro de predicado) a la clase WorldTimeZones. Esto proporciona la capacidad para que la persona que llama filtre en lo que quiera pasando una expresión lambda como parámetro.

En esencia, el método de filtro único admite el filtrado de cualquier cosa contenida en el valor pasado al predicado.

Alternativamente, agregue un método stream () a su clase WorldTimeZones que produce una secuencia de zonas horarias cuando se llama. Luego, la persona que llama puede filtrar, mapear y reducir según lo desee sin tener que escribir ninguna especialización.

Rodney P. Barbati
fuente
3
Estas son buenas ideas de generalización, sin embargo, esta respuesta pierde completamente la marca de lo que se pregunta en la pregunta. La pregunta no es sobre cómo generalizar mejor la solución, sino dónde trazamos la línea en generalizaciones y cómo sopesamos éticamente estas consideraciones.
maple_shaft
Entonces, estoy diciendo que debe sopesarlos por la cantidad de casos de uso que admiten modificados por la complejidad de la creación. Un método que admite un caso de uso no es tan valioso como un método que admite 20 casos de uso. Por otro lado, si todo lo que sabe es un caso de uso y le toma 5 minutos codificarlo, hágalo. A menudo, los métodos de codificación que admiten usos específicos le informan sobre la forma de generalizar.
Rodney P. Barbati