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.DESTROY
entonces 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.
fuente
destroyCity(target)
es mucho más poco ético quedestroyBagdad()
! ¿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.Respuestas:
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:
"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 .
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.
Eso depende mucho de múltiples cosas:
takeActionOnCity
mé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 nadaaction.TakeOn(city);
, debería preguntarse si eltakeActionOnCity
método realmente tiene un propósito o no es solo una capa adicional que no agrega nada de valor.La misma pregunta aparece aquí:
Si puede responder definitivamente "sí" a las tres preguntas, entonces se justifica una abstracción.
fuente
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.println
por 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 usaSystem.out
de forma predeterminada).fuente
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:
es un conjunto de requisitos muy diferente para:
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
DestroyBaghdad
función en realidad podría ser la función que tiene ese derecho.fuente
Aquí hay muchas respuestas largas, pero honestamente creo que es súper simple
así que en tu función
Tienes:
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.
fuente
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
Object1
pudieran decirObject2
que 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 destroyBaghdad
Por 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:Entonces tienes una ciudad que implementa esta interfaz:
Nada que requiera la destrucción de una instancia
City
nunca importará cómo sucede, por lo que no hay ninguna razón para que ese código exista en ningún lugar fueraCity::destroy
, y de hecho, el conocimiento íntimo del funcionamiento interno delCity
exterior 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 deCity
. 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
City
oBaghdad
depende de cuán genérico resulte ser el proceso de destrucción de la ciudad. Con toda probabilidad, unCity
estará 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íaDestroyable
, y cada una de ellas recibiría instrucciones deCity
destruir ellos mismos de la misma manera que alguien del exterior pidióCity
que se destruyera a sí mismo.Si quieres volverte realmente loco e implementar la idea de
Bomb
que se deja caer en una ubicación y destruye todo dentro de un cierto radio, podría verse más o menos así:ObjectsByRadius
representa un conjunto de objetos que se calcula para lasBomb
entradas porqueBomb
no 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 soltarBomb
y 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í:
Como su ejemplo usa clases de la biblioteca de Java que no admiten este diseño, puede usar la API
ZonedDateTime
directamente. 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,Result
se ocupa exclusivamente de encapsular el resultado del cálculo yFormattedResult
se preocupa exclusivamente de interactuar con elResult
para 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í: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.
fuente
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 :
(Incluya cualquier parámetro que ya sepa que necesitará, obviamente ...)
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".
fuente
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?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 ...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 respuestais takeActionOnCity(Action action, City city) better?
null
?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.
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
fuente
Hay un proceso claro que puedes seguir:
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.
fuente
He llegado a la opinión de que hay dos tipos de código reutilizable:
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
fuente
destroyBaghdad
es un script único (o al menos, es idempotente). Quizás destruir una ciudad sería una mejora, pero ¿y sidestroyBaghdad
funciona inundando el Tigris? Eso puede ser reutilizable para Mosul y Basora, pero no para La Meca o Atlanta.static
mé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.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
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
public
void dayLightSavings()
Esto podría implementarse como mostró en su respuesta. Ahora, alguien quiere parametrizarlo con el año. Entonces puedes agregar un método
public
void dayLightSavings(int year)
y cambiar la implementación original para ser simplemente
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:
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.
fuente
dayLightSavings()
llamardayLightSavings(2018)
no me parece una buena idea.dayLightSavings(computeCurrentYear());
..."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.
fuente
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
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.
fuente
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. .)
fuente
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.
fuente
Your customer will tell you how much flexibility and how many layers of generalization you should seek.
¿qué mundo es este?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?
fuente
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.
fuente
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.
fuente