Kurz gesagt: Entwickeln Sie Ihre Software nicht für Wiederverwendbarkeit, da es keinen Endbenutzer interessiert, ob Ihre Funktionen wiederverwendet werden können. Stattdessen Ingenieur für Designverständlichkeit - ist mein Code leicht für andere oder mein zukünftiges vergessliches Selbst zu verstehen? - und Designflexibilität- Wenn ich unvermeidlich Fehler beheben, Features hinzufügen oder auf andere Weise die Funktionalität ändern muss, wie sehr wird mein Code den Änderungen widerstehen? Ihr Kunde kümmert sich nur darum, wie schnell Sie reagieren können, wenn er einen Fehler meldet oder nach einer Änderung fragt. Wenn Sie diese Fragen zu Ihrem Design stellen, ist der Code in der Regel wiederverwendbar. Bei diesem Ansatz konzentrieren Sie sich jedoch darauf, die tatsächlichen Probleme zu vermeiden, denen Sie im Laufe der Lebensdauer des Codes gegenüberstehen, damit Sie dem Endbenutzer einen besseren Service bieten können, als einen hohen, unpraktischen Wert zu verfolgen "engineering" -Ideen, um den Nackenbärten zu gefallen.
Für etwas so Einfaches wie das von Ihnen bereitgestellte Beispiel ist Ihre anfängliche Implementierung in Ordnung, da es so klein ist, aber dieses einfache Design wird schwer zu verstehen und spröde, wenn Sie versuchen, zu viel funktionale Flexibilität (im Gegensatz zu Designflexibilität) einzubeziehen ein Verfahren. Nachstehend finden Sie eine Erläuterung meines bevorzugten Ansatzes für die Gestaltung komplexer Systeme im Hinblick auf Verständlichkeit und Flexibilität. Ich hoffe, dass dies zeigen wird, was ich damit meine. Ich würde diese Strategie nicht für etwas anwenden, das in weniger als 20 Zeilen in einem einzigen Verfahren geschrieben werden könnte, da etwas so Kleines bereits meine Kriterien für Verständlichkeit und Flexibilität erfüllt.
Objekte, keine Prozeduren
Anstatt Klassen wie Old-School-Module mit einer Reihe von Routinen zu verwenden, die Sie aufrufen, um die Aufgaben Ihrer Software auszuführen, sollten Sie die Domäne als Objekte modellieren, die interagieren und zusammenarbeiten, um die jeweilige Aufgabe zu erfüllen. Methoden in einem objektorientierten Paradigma wurden ursprünglich erstellt, um Signale zwischen Objekten zu sein, Object1
damit sie sagen können Object2
, was auch immer das ist, und möglicherweise ein Rücksignal erhalten. Dies liegt daran, dass es beim objektorientierten Paradigma inhärent darum geht, Ihre Domänenobjekte und ihre Interaktionen zu modellieren, anstatt die alten Funktionen und Prozeduren des imperativen Paradigmas auf originelle Weise zu organisieren. Im Falle dervoid destroyBaghdad
Anstatt beispielsweise zu versuchen, eine kontextlose generische Methode zu schreiben, um mit der Zerstörung von Bagdad oder anderen Dingen umzugehen (die schnell komplex, schwer zu verstehen und spröde werden können), sollte alles, was zerstört werden kann, dafür verantwortlich sein, zu verstehen, wie sich selbst zerstören. Sie haben beispielsweise eine Schnittstelle, die das Verhalten von Dingen beschreibt, die zerstört werden können:
interface Destroyable {
void destroy();
}
Dann haben Sie eine Stadt, die diese Schnittstelle implementiert:
class City implements Destroyable {
@Override
public void destroy() {
...code that destroys the city
}
}
Nichts, das die Zerstörung einer Instanz von City
erfordert, wird sich jemals darum kümmern, wie dies geschieht. Es gibt also keinen Grund, warum dieser Code irgendwo außerhalb von sich existiert City::destroy
, und in der Tat wäre eine genaue Kenntnis der inneren Funktionsweise City
von sich selbst eine enge Kopplung, die sich verringert Flexibilität, da Sie diese externen Elemente berücksichtigen müssen, falls Sie jemals das Verhalten von ändern müssen City
. Dies ist der wahre Zweck der Kapselung. Stellen Sie sich das so vor, als hätte jedes Objekt eine eigene API, mit der Sie alles tun können, was Sie brauchen, damit es sich um die Erfüllung Ihrer Anforderungen kümmern kann.
Delegation, nicht "Kontrolle"
Nun, ob Ihre implementierende Klasse ist City
oder Baghdad
davon abhängt, wie allgemein sich der Prozess der Zerstörung der Stadt herausstellt. Es ist sehr wahrscheinlich, dass ein City
Wille aus kleineren Stücken besteht, die einzeln zerstört werden müssen, um die vollständige Zerstörung der Stadt zu erreichen. In diesem Fall würde also jeder dieser Stücke auch ausgeführt Destroyable
und jeder von ihnen angewiesen, City
zu zerstören sich in der gleichen Weise jemand von außen forderte die City
, sich selbst zu zerstören.
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);
}
}
Wenn Sie wirklich verrückt werden und die Idee eines Objekts implementieren möchten, das Bomb
an einem Ort abgelegt wird und alles in einem bestimmten Radius zerstört, sieht es möglicherweise so aus:
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
Stellt einen Satz von Objekten dar, der für die Bomb
aus den Eingaben Bomb
berechnet wird, da es egal ist, wie diese Berechnung durchgeführt wird, solange sie mit den Objekten arbeiten kann. Dies ist übrigens wiederverwendbar, aber das Hauptziel besteht darin, die Berechnung von den Prozessen des Ablegens Bomb
und Zerstörens der Objekte zu isolieren, damit Sie jedes Stück nachvollziehen können und wie sie zusammenpassen und das Verhalten eines einzelnen Stücks ändern können, ohne den gesamten Algorithmus umformen zu müssen .
Interaktionen, keine Algorithmen
Anstatt zu versuchen, die richtige Anzahl von Parametern für einen komplexen Algorithmus zu erraten, ist es sinnvoller, den Prozess als einen Satz interagierender Objekte mit jeweils extrem engen Rollen zu modellieren, da Sie die Komplexität Ihres Prozesses modellieren können durchlaufen die Wechselwirkungen zwischen diesen gut definierten, leicht verständlichen und nahezu unveränderlichen Objekten. Bei richtiger Ausführung sind selbst einige der komplexesten Änderungen so einfach wie die Implementierung einer oder zweier Schnittstellen und die Überarbeitung der in Ihrer main()
Methode instanziierten Objekte .
Ich würde Ihnen etwas zu Ihrem ursprünglichen Beispiel geben, aber ich kann ehrlich gesagt nicht herausfinden, was es bedeutet, "... Day Light Savings" zu drucken. Was ich zu dieser Kategorie von Problemen sagen kann, ist, dass jedes Mal, wenn Sie eine Berechnung durchführen, deren Ergebnis auf verschiedene Arten formatiert werden kann, meine bevorzugte Methode, dies aufzuschlüsseln, wie folgt lautet:
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
}
}
Da Ihr Beispiel Klassen aus der Java-Bibliothek verwendet, die dieses Design nicht unterstützen, können Sie einfach die API von ZonedDateTime
direkt verwenden. Die Idee dabei ist, dass jede Berechnung in einem eigenen Objekt gekapselt ist. Es werden keine Annahmen darüber getroffen, wie oft es ausgeführt werden soll oder wie das Ergebnis formatiert werden soll. Es geht ausschließlich um die einfachste Form der Berechnung. Dies macht es einfach zu verstehen und flexibel zu ändern. Ebenso befasst sich der Result
ausschließlich mit der Kapselung des Berechnungsergebnisses und der FormattedResult
ausschließlich mit der Interaktion mit dem, Result
um es gemäß den von uns definierten Regeln zu formatieren. Auf diese Weise,Wir können die perfekte Anzahl von Argumenten für jede unserer Methoden finden, da sie jeweils eine genau definierte Aufgabe haben . Es ist auch viel einfacher, Änderungen vorzunehmen, solange sich die Benutzeroberflächen nicht ändern (was weniger wahrscheinlich ist, wenn Sie die Verantwortlichkeiten Ihrer Objekte richtig minimiert haben). Unseremain()
Methode könnte so aussehen:
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()
);
});
}
}
Tatsächlich wurde die objektorientierte Programmierung speziell als Lösung für das Komplexitäts- / Flexibilitätsproblem des Imperativ-Paradigmas erfunden, da es keine gute Antwort gibt (auf die sich ohnehin jeder einigen oder unabhängig davon kommen kann), wie man optimal vorgeht Imperative Funktionen und Prozeduren innerhalb der Redewendung angeben.