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 decir Object2
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:
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 City
nunca 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 City
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 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 City
o Baghdad
depende de cuán genérico resulte ser el proceso de destrucción de la ciudad. Con toda probabilidad, un City
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ía Destroyable
, y cada una de ellas recibiría instrucciones de City
destruir ellos mismos de la misma manera que alguien del exterior pidió City
que 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 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í:
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);
}
}
ObjectsByRadius
representa un conjunto de objetos que se calcula para las Bomb
entradas porque Bomb
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 soltar Bomb
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í:
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 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 y FormattedResult
se preocupa exclusivamente de interactuar con el Result
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í:
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.