Hallo Habr! Ich präsentiere Ihnen den Artikel "Vier bessere Regeln für das Software-Design" von David Bryant Copeland. David Bryant Copeland ist Software-Architekt und CTO für Stitch Fix. Er unterhält einen Blog und ist Autor mehrerer Bücher .
Martin Fowler hat kürzlich mit einem Link zu seinem Blog-Beitrag über vier einfache Designregeln von Kent Beck getwittert, die meiner Meinung nach weiter verbessert werden können (und die den Programmierer manchmal in die falsche Richtung schicken können):
Kents Regeln aus der extremen Programmierung erklärt :
- Kent sagt: "Führen Sie alle Tests durch."
- Duplizieren Sie die Logik nicht. Vermeiden Sie versteckte Duplikate wie parallele Klassenhierarchien.
- Alle für den Programmierer wichtigen Absichten sollten deutlich sichtbar sein.
- Der Code sollte die kleinstmögliche Anzahl von Klassen und Methoden haben.
Nach meiner Erfahrung entsprechen diese Regeln nicht ganz den Anforderungen des Software-Designs. Meine vier Regeln für ein gut gestaltetes System könnten sein:
- es ist gut durch Tests abgedeckt und besteht sie erfolgreich.
- Es gibt keine Abstraktionen, die das Programm nicht direkt benötigt.
- Sie hat ein eindeutiges Verhalten.
- es erfordert die geringste Anzahl von Konzepten.
Für mich ergeben sich diese Regeln aus dem, was wir mit unserer Software machen.
Was machen wir also mit unserer Software?
Wir können nicht über Software-Design sprechen, ohne vorher darüber zu sprechen, was wir damit machen wollen.
Die Software wurde geschrieben, um das Problem zu lösen. Das Programm läuft und hat ein Verhalten. Dieses Verhalten wird untersucht, um einen korrekten Betrieb sicherzustellen oder Fehler zu erkennen. Software ändert sich auch häufig, um ihr neues oder geändertes Verhalten zu verleihen.
Daher sollte sich jeder Ansatz für das Software-Design darauf konzentrieren, sein Verhalten vorherzusagen, zu untersuchen und zu verstehen, um die Änderung dieses Verhaltens so einfach wie möglich zu gestalten.
Wir überprüfen das korrekte Verhalten durch Tests. Daher stimme ich Kent zu, dass das Erste und Wichtigste darin besteht, dass gut gestaltete Software die Tests bestehen muss. Ich werde sogar noch weiter gehen und darauf bestehen, dass die Software Tests enthält (d. H. Durch Tests gut abgedeckt ist).
Nachdem das Verhalten überprüft wurde, beziehen sich die folgenden drei Punkte in beiden Listen auf das Verständnis unserer Software (und damit ihres Verhaltens). Seine Liste beginnt mit der Codeduplizierung, die wirklich vorhanden ist. Nach meiner persönlichen Erfahrung ist es jedoch teuer, sich zu sehr auf die Reduzierung der Codeduplizierung zu konzentrieren. Um es zu beseitigen, müssen Abstraktionen erstellt werden, die es verbergen, und es sind diese Abstraktionen, die es schwierig machen, die Software zu verstehen und zu ändern.
Das Eliminieren von Codeduplizierungen erfordert Abstraktionen, und Abstraktionen führen zu Komplexität
Nicht wiederholen oder DRY wird verwendet, um kontroverse Designentscheidungen zu rechtfertigen. Haben Sie jemals einen ähnlichen Code gesehen?
ZERO = BigDecimal.new(0)
Außerdem haben Sie wahrscheinlich so etwas gesehen:
public void call(Map payload, boolean async, int errorStrategy) {
Wenn Sie Methoden oder Funktionen mit Flags, Booleschen Werten usw. sehen, bedeutet dies normalerweise, dass beim Refactoring jemand das DRY-Prinzip verwendet hat, der Code jedoch an beiden Stellen nicht exakt gleich war, sodass der resultierende Code vorhanden sein sollte flexibel genug sein, um beide Verhaltensweisen zu berücksichtigen.
Solche verallgemeinerten Abstraktionen sind schwer zu testen und zu verstehen, da sie viel mehr Fälle behandeln sollten als der ursprüngliche (möglicherweise duplizierte) Code. Mit anderen Worten, Abstraktionen unterstützen viel mehr Verhaltensweisen, als für das normale Funktionieren des Systems erforderlich sind. Das Eliminieren von Codeduplizierungen kann daher zu einem neuen Verhalten führen, das das System nicht benötigt.
Daher ist es wirklich wichtig , einige Arten von Verhalten zu kombinieren, aber es kann schwierig sein zu verstehen, welche Art von Verhalten wirklich dupliziert wird. Oft sehen Codeteile ähnlich aus, aber dies geschieht nur zufällig.
Überlegen Sie, wie viel einfacher es ist, Codeduplikationen zu beseitigen, als sie erneut zurückzugeben (z. B. nachdem Sie eine schlecht durchdachte Abstraktion erstellt haben). Daher müssen wir darüber nachdenken, doppelten Code zu hinterlassen, es sei denn, wir sind absolut sicher, dass wir einen besseren Weg haben, ihn loszuwerden.
Das Erstellen von Abstraktionen sollte uns zum Nachdenken anregen. Wenn Sie beim Entfernen von doppeltem Code eine sehr flexible verallgemeinerte Abstraktion erstellen, sind Sie möglicherweise in die falsche Richtung gegangen.
Dies führt uns zum nächsten Punkt - Absicht versus Verhalten.
Die Absicht des Programmierers ist bedeutungslos - Verhalten bedeutet alles
Wir loben oft Programmiersprachen, Konstrukte oder Codefragmente dafür, dass sie "die Absichten des Programmierers enthüllen". Aber was bringt es, Absichten zu kennen, wenn Sie kein Verhalten vorhersagen können? Und wenn Sie Verhalten kennen, wie viel bedeutet Absicht? Es stellt sich heraus, dass Sie wissen müssen , wie sich die Software verhalten soll, aber dies ist nicht dasselbe wie die "Absichten des Programmierers".
Schauen wir uns dieses Beispiel an, das die Absichten des Programmierers sehr gut widerspiegelt, sich aber nicht wie beabsichtigt verhält:
function LastModified(props) { return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); }
Offensichtlich plante der Programmierer, dass diese React-Komponente ein Datum mit der Meldung "Zuletzt geändert am" anzeigt. Funktioniert das wie vorgesehen? Nicht wirklich. Was ist, wenn this.prop.date keine Rolle spielt? Alles bricht einfach zusammen. Wir wissen nicht, ob es so konzipiert wurde oder ob jemand es einfach vergessen hat, und es spielt keine Rolle. Was zählt, ist das Verhalten.
Und genau das sollten wir wissen, wenn wir diesen Teil des Codes ändern wollen. Stellen Sie sich vor, wir müssen die Zeile in "Letzte Änderung" ändern. Obwohl wir dies tun können, ist nicht klar, was passieren soll, wenn das Datum fehlt. Es wäre besser, wenn wir die Komponente stattdessen so schreiben, dass ihr Verhalten verständlicher wird.
function LastModified(props) { if (!props.date) { throw "LastModified requires a date to be passed"; } return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); }
Oder sogar so:
function LastModified(props) { if (props.date) { return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); } else { return <div>Never modified</div>; } }
In beiden Fällen ist das Verhalten verständlicher und die Absichten des Programmierers spielen keine Rolle. Angenommen, wir wählen die zweite Alternative (die den fehlenden Datumswert behandelt). Wenn wir aufgefordert werden, die Nachricht zu ändern, können wir das Verhalten sehen und prüfen, ob die Nachricht "Nie geändert" korrekt ist oder ob sie auch geändert werden muss.
Je eindeutiger das Verhalten ist , desto größer sind die Chancen, dass wir es erfolgreich ändern können. Und dies bedeutet, dass wir möglicherweise mehr Code schreiben oder genauer machen oder manchmal sogar doppelten Code schreiben müssen.
Dies bedeutet auch, dass wir mehr Klassen, Funktionen, Methoden usw. benötigen. Natürlich möchten wir ihre Anzahl minimal halten, aber wir sollten diese Zahl nicht als unsere Metrik verwenden. Das Erstellen einer großen Anzahl von Klassen oder Methoden verursacht konzeptionellen Aufwand , und in der Software werden mehr Konzepte angezeigt als Einheiten der Modularität. Daher müssen wir die Anzahl der Konzepte reduzieren, was wiederum zu einer Verringerung der Anzahl der Klassen führen kann.
Konzeptionelle Kosten tragen zu Verwirrung und Komplexität bei
Um zu verstehen, was der Code tatsächlich tut, müssen Sie nicht nur den Themenbereich kennen, sondern auch alle in diesem Code verwendeten Konzepte (wenn Sie beispielsweise nach der Standardabweichung suchen, müssen Sie die Zuordnung, Addition, Multiplikation, Schleifen und Arraylängen kennen). Dies erklärt, warum mit zunehmender Anzahl von Konzepten in einem Entwurf die Komplexität für das Verständnis zunimmt.
Früher habe ich über konzeptionelle Ausgaben geschrieben , und ein guter Nebeneffekt bei der Reduzierung der Anzahl von Konzepten in einem System ist, dass mehr Menschen dieses System verstehen können. Dies wiederum erhöht die Anzahl der Personen, die Änderungen an diesem System vornehmen können. Auf jeden Fall ist ein Software-Design, das von vielen Menschen sicher geändert werden kann, besser als eines, das nur von einer kleinen Handvoll geändert werden kann. (Daher glaube ich, dass Hardcore-Funktionsprogrammierung niemals populär werden wird, da es ein tiefes Verständnis vieler sehr abstrakter Konzepte erfordert.)
Durch die Reduzierung der konzeptionellen Kosten wird natürlich die Anzahl der Abstraktionen reduziert und das Verhalten verständlicher. Ich sage nicht „niemals ein neues Konzept einführen“, ich sage, dass es einen eigenen Preis hat, und wenn dieser Preis den Nutzen überwiegt, sollte die Einführung eines neuen Konzepts sorgfältig abgewogen werden.
Wenn wir Code oder Design-Software schreiben, sollten wir aufhören, über die Eleganz , Schönheit oder andere subjektive Maße unseres Codes nachzudenken. Stattdessen sollten wir uns immer daran erinnern, was wir mit der Software machen werden.
Sie hängen den Code nicht an die Wand - Sie ändern ihn
Ein Code ist kein Kunstwerk, das Sie drucken und in einem Museum aufhängen können. Der Code wird ausgeführt. Es wird untersucht und getestet. Und vor allem ändert es sich . Und oft. Jedes Design, mit dem schwer zu arbeiten ist, sollte in Frage gestellt und überprüft werden. Jedes Design, das die Anzahl der Personen reduziert, die damit arbeiten können, sollte ebenfalls in Frage gestellt werden.
Der Code sollte funktionieren, also sollte er getestet werden. Der Code weist Fehler auf und erfordert das Hinzufügen neuer Funktionen. Daher müssen wir sein Verhalten verstehen. Der Code lebt länger als die Fähigkeit eines bestimmten Programmierers, ihn zu unterstützen. Daher sollten wir uns um Code bemühen, der für eine breite Palette von Menschen verständlich ist.
Vereinfachen Sie beim Schreiben Ihres Codes oder beim Entwerfen Ihres Systems die Erklärung des Systemverhaltens? Wird es einfacher zu verstehen, wie sie sich verhalten wird? Konzentrieren Sie sich darauf, das Problem direkt vor Ihnen oder auf ein abstrakteres zu lösen?
Versuchen Sie immer, das Verhalten für Demonstration, Vorhersage und Verständnis einfach zu halten und die Anzahl der Konzepte auf ein absolutes Minimum zu beschränken.