Dank der vielen kostenlosen Ressourcen, Bücher und Online-Kurse kann heute jeder das Programmieren lernen. Dennoch gibt es immer noch eine Qualitätslücke zwischen Programmierung und Softwaretechnik. Muss es eine geben?
Mein erstes "Hello World" habe ich vor über zwanzig Jahren geschrieben - das ist die Antwort, die ich gebe, wenn mich jemand fragt, wie lange ich schon Programmierer bin. In den letzten zehn Jahren habe ich eine Karriere genossen, bei der ich mit Code fast jeden Tag - das ist die Antwort, die ich gebe, wenn ich gefragt werde, wie lange ich schon ein professioneller Programmierer bin.
Wie lange bin ich schon ein Software-Ingenieur? Ich würde sagen, etwa fünf Jahre. Moment mal, diese Zahlen scheinen nicht zusammenzupassen! Was hat sich also geändert? Wen würde ich als Software-Ingenieur bezeichnen, und wen "nur" als Programmierer?
Die Definition eines Software-Ingenieurs
Kodierung ist relativ einfach. Es geht nicht mehr nur um Assembler-Mnemonics auf lächerlich eingeschränkten Systemen. Und wenn Sie etwas so ausdrucksstarkes und leistungsfähiges wie Ruby verwenden, ist es sogar noch einfacher.
Sie nehmen einfach ein Ticket in die Hand, suchen die Stelle, an der Sie Ihren Code einfügen müssen, überlegen sich die Logik, die Sie dort einfügen müssen, und bumm - fertig. Wenn Sie etwas fortgeschrittener sind, sorgen Sie dafür, dass Ihr Code schön ist. Logisch in Methoden aufgeteilt ist. Anständige Spezifikationen hat, die nicht nur den glücklichen Weg testen. Das ist es, was ein guter Programmierer tut.
Ein Softwareentwickler denkt nicht mehr in Methoden und Klassen, zumindest nicht in erster Linie. Nach meiner Erfahrung denkt ein Software-Ingenieur in Flüssen. Sie sehen in erster Linie den tosenden, wütenden Fluss von Daten und Interaktionen, der durch das System braust. Sie denken darüber nach, was sie tun müssen, um diesen Fluss umzuleiten oder zu verändern. Der schöne Code, die logischen Methoden und die großartigen Spezifikationen kommen fast wie ein nachträglicher Einfall.
Schildkröten auf dem Weg nach unten
Im Allgemeinen denken die Menschen bei den meisten Interaktionen mit der Realität auf eine bestimmte Weise. Nennen wir es in Ermangelung eines besseren Begriffs die "Top-down"-Perspektive. Wenn mein Gehirn damit beschäftigt ist, mir eine Tasse Tee zu machen, wird es zuerst die allgemeinen Schritte herausfinden: in die Küche gehen, den Wasserkocher aufsetzen, die Tasse zubereiten, Wasser einschenken, an den Schreibtisch zurückkehren.
Er wird nicht zuerst herausfinden, welche Tasse er zuerst benutzen soll, wenn ich entspannt am Schreibtisch sitze; das wird später geschehen, wenn ich vor dem Schrank stehe. Er wird nicht daran denken, dass wir vielleicht keinen Tee mehr haben (oder zumindest nicht mehr den gut Zeug). Es ist breit angelegt, reaktiv und fehleranfällig. Alles in allem - sehr menschlich in der Natur.
Wenn der Software-Ingenieur über Änderungen an dem etwas verwirrenden Datenfluss nachdenkt, wird er dies natürlich auf ähnliche Weise tun. Betrachten wir dieses Beispiel einer User Story:
Ein Kunde bestellt ein Widget. Bei der Preisgestaltung für den Auftrag muss Folgendes berücksichtigt werden:
- Grundpreis des Widgets in der Region des Nutzers
- Widgetform (Preismodifikator)
- ob es sich um einen Eilauftrag handelt (Preismodifikator)
- ob die Lieferung der Bestellung an einem Feiertag im Land des Nutzers erfolgt (Preismodifikator)
Das alles mag konstruiert erscheinen (und ist es natürlich auch), aber es ist nicht weit entfernt von einigen tatsächlichen Anwenderberichten, die ich in letzter Zeit erleben durfte.
Lassen Sie uns nun den Gedankengang durchgehen, den ein Software-Ingenieur anwenden könnte, um dies zu bewältigen:
"Nun, wir müssen den Benutzer und seine Bestellung erfassen. Dann fangen wir an, die Summe zu berechnen. Wir beginnen bei Null. Dann wenden wir den Modifikator für die Widgetform an. Dann die Eilgebühr. Dann schauen wir, ob es ein Feiertag ist, und schon ist die Bestellung vor dem Mittagessen fertig!"
Ah, der Rausch, den eine einfache Benutzergeschichte auslösen kann. Aber der Software-Ingenieur ist nur ein Mensch und keine perfekte Multi-Thread-Maschine, und das obige Rezept ist nur ein grober Entwurf. Der Ingenieur denkt dann tiefer:
"Der Modifikator für die Form des Widgets ist... oh, das ist sehr abhängig vom Widget, nicht wahr. Und sie können je nach Gebietsschema unterschiedlich sein, wenn auch nicht jetzt, so doch in Zukunft. Sie denken, dass sie zuvor durch veränderte Geschäftsanforderungen verbrannt wurden, "und die Eilzuschläge könnten auch sein. Und Feiertage sind auch super ortsspezifisch, augh, und Zeitzonen werden beteiligt sein! Ich hatte hier einen Artikel über den Umgang mit Zeiten in verschiedenen Zeitzonen in Rails hier... ooh, ich frage mich, ob die Bestellzeit mit Zone in der Datenbank gespeichert ist! Ich überprüfe besser das Schema."
In Ordnung, Software-Ingenieur. Stopp. Du sollst dir eigentlich eine Tasse Tee machen, aber du sitzt vor dem Schrank und überlegst, ob die geblümte Tasse überhaupt für dein Teeproblem geeignet ist.
Das Widget für die perfekte Tasse
Aber das kann leicht passieren, wenn man versucht, etwas zu tun, das für das menschliche Gehirn so unnatürlich ist wie das Denken in mehreren Detailtiefen gleichzeitig.
Nach einem kurzen Stöbern in ihrem geräumigen Arsenal an Links zur Handhabung von Zeitzonen reißt sich unser Ingenieur zusammen und fängt an, dies in konkreten Code umzusetzen. Wenn sie den naiven Ansatz versuchen würden, könnte es ungefähr so aussehen:
def preisberechnung(benutzer, auftrag)
bestellung.preis = 0
bestellung.preis = WidgetPreise.find_by(widget_type: bestellung.widget.typ).preis
auftrag.preis = WidgetFormen.find_by(widget_form: auftrag.widget.form).modifier
...
end
Und so ging es weiter und weiter, auf diese herrlich prozedurale Art und Weise, nur um dann bei der ersten Codeüberprüfung schwer zu scheitern. Denn wenn man darüber nachdenkt, ist es völlig normal, so zu denken: erst die groben Züge, und die Details viel später. Sie haben anfangs gar nicht gedacht, dass Sie aus dem guten Tee raus sind, oder?
Unser Techniker ist jedoch gut geschult und ihm ist das Serviceobjekt nicht fremd, so dass er stattdessen Folgendes tut:
class BaseOrderService
def self.call(benutzer, auftrag)
new(benutzer, auftrag).call
end
def initialize(benutzer, auftrag)
@Benutzer = Benutzer
@Auftrag = Auftrag
end
def aufruf
puts "[WARN] Implementiere Nicht-Standardaufruf für #{self.class.name}!"
benutzer, auftrag
end
end
class WidgetPriceService < BaseOrderService; end
Klasse ShapePriceModifier < BaseOrderService; end
Klasse RushPriceModifier < BaseOrderService; end
class HolidayDeliveryPriceModifier < BaseOrderService; end
class OrderPriceCalculator < BaseOrderService
def Aufruf
Benutzer, Bestellung = WidgetPriceService.call(Benutzer, Bestellung)
benutzer, auftrag = ShapePriceModifier.call(benutzer, auftrag)
benutzer, auftrag = RushPriceModifier.call(benutzer, auftrag)
benutzer, bestellung = HolidayDeliveryPriceModifier.call(benutzer, bestellung)
Benutzer, Bestellung
end
end
```
Gut! Jetzt können wir etwas gutes TDD anwenden, einen Testfall dafür schreiben und die Klassen ausbauen, bis alle Teile an ihrem Platz sind. Und schön wird es auch noch sein.
Und es ist auch völlig unmöglich, darüber nachzudenken.
Der Feind ist der Staat
Sicher, das sind alles gut getrennte Objekte mit einzelnen Verantwortlichkeiten. Aber hier ist das Problem: Es sind immer noch Objekte. Das Dienstobjektmuster mit seinem "zwangsweise vorgeben, dass dieses Objekt eine Funktion ist" ist wirklich eine Krücke. Es gibt nichts, was jemanden daran hindert, die Funktion HolidayDeliveryPriceModifier.new(user, order).something_else_entirely
. Nichts hindert die Menschen daran, diesen Objekten einen internen Status hinzuzufügen.
Nicht zu vergessen Benutzer
und Bestellung
sind ebenfalls Objekte, und sie zu manipulieren ist so einfach, wie wenn jemand heimlich eine schnelle bestellen.speichern
irgendwo in diesen ansonsten "reinen" funktionalen Objekten die zugrundeliegende Quelle des Zustands der Wahrheit, auch bekannt als Datenbank, verändern. In diesem konstruierten Beispiel ist das keine große Sache, aber es kann sich als nachteilig erweisen, wenn das System an Komplexität zunimmt und sich in zusätzliche, oft asynchrone Teile ausdehnt.
Der Ingenieur hatte die richtige Idee. Und er benutzte eine sehr natürliche Art, diese Idee auszudrücken. Aber zu wissen, wie diese Idee auszudrücken ist - auf eine schöne und leicht zu begründende Weise - wurde durch das zugrundeliegende OOP-Paradigma fast verhindert. Und wenn jemand, der noch nicht den Sprung geschafft hat, seine Gedanken als Ablenkungen des Datenflusses auszudrücken, versucht, den zugrundeliegenden Code weniger geschickt zu verändern, werden schlechte Dinge passieren.
Funktional rein werden
Wenn es doch nur ein Paradigma gäbe, in dem die Formulierung von Ideen in Form von Datenflüssen nicht nur einfach, sondern notwendig wäre. Wenn die Argumentation einfach wäre, ohne die Möglichkeit, unerwünschte Nebeneffekte einzuführen. Wenn Daten unveränderlich wären, so wie die Blumentasse, in der Sie Ihren Tee zubereiten.
Ja, ich scherze natürlich nur herum. Dieses Paradigma existiert, und es heißt funktionale Programmierung.
Schauen wir uns an, wie das obige Beispiel in einem persönlichen Favoriten, Elixir, aussehen könnte.
defmodule WidgetPreise do
def preisbestellung([benutzer, bestellung]) do
[Benutzer, Bestellung]
|> widgetprice
|> formPreisModifizierer
|> rushpricemodifier
|> Feiertagspreismodifikator
Ende
defp widgetprice([Benutzer, Bestellung]) do
%{widget: widget} = Bestellung
Preis = WidgetRepo.getbase_price(widget)
[benutzer, %{bestellung | preis: preis }]
end
defp shapepricemodifier([user, order]) do
%{widget: widget, Preis: currentprice} = Bestellung
modifier = WidgetRepo.getshapeprice(widget)
[Benutzer, %{Bestellung | Preis: aktuellerPreis * Modifikator} ]
end
defp rushpricemodifier([user, order]) do
%{Eile: Eile, Preis: aktuellerPreis} = Bestellung
if rush do
[Benutzer, %{Bestellung | Preis: aktuellerPreis * 1.75} ]
sonst
[Benutzer, %{Bestellung | Preis: aktueller_Preis} ]
end
end
defp urlaubspreismodifizierer([benutzer, bestellung]) do
%{Datum: Datum, Preis: aktuellerPreis} = Bestellung
modifier = HolidayRepo.getholidaymodifier(user, date)
[Benutzer, %{Bestellung | Preis: aktuellerPreis * Modifikator}]
end
end
```
Sie werden feststellen, dass es sich um ein vollständiges Beispiel dafür handelt, wie die User Story tatsächlich umgesetzt werden kann. Das liegt daran, dass es weniger langatmig ist als es in Ruby der Fall wäre. Wir verwenden einige Schlüsselfunktionen, die es nur in Elixir gibt (die aber allgemein in funktionalen Sprachen verfügbar sind):
Reine Funktionen. Wir ändern nicht wirklich die eingehenden Bestellung
Wir erstellen lediglich neue Kopien - neue Iterationen des Ausgangszustands. Wir springen auch nicht zur Seite, um etwas zu ändern. Und selbst wenn wir das wollten, Bestellung
nur eine "dumme" Karte ist, können wir nicht bestellen.speichern
an irgendeiner Stelle, weil es einfach nicht weiß, was das ist.
Mustervergleich. Ähnlich wie bei der Destrukturierung in ES6 können wir damit die Preis
und Widget
und weiterzugeben, anstatt unsere Kumpels zu zwingen, die Bestellung WidgetRepo
und UrlaubsRepo
zu wissen, wie man mit einer vollen Bestellung
.
Rohrleitungsbetreiber. Gesehen in preis_bestellung
können wir Daten in einer Art "Pipeline" durch Funktionen leiten - ein Konzept, das jedem, der schon einmal ps aux | grep postgres
um zu überprüfen, ob das verdammte Ding noch läuft.
So denken Sie
Nebenwirkungen sind nicht wirklich Teil unseres Denkprozesses. Nachdem Sie Wasser in Ihre Tasse gegossen haben, machen Sie sich in der Regel keine Sorgen, dass ein Fehler im Wasserkocher dazu führen könnte, dass er überhitzt und explodiert - zumindest nicht so sehr, dass Sie in seinen Innereien herumstochern, um zu prüfen, ob nicht jemand versehentlich etwas vergessen hat explodieren_nach_dem_Ausgießen
hochgeklappt.
Der Weg vom Programmierer zum Software-Ingenieur - der über die Beschäftigung mit Objekten und Zuständen hinausgeht und sich mit Datenflüssen befasst - kann in manchen Fällen Jahre dauern. Bei mir, der ich mit OOP aufgewachsen bin, hat es jedenfalls gedauert. Mit funktionalen Sprachen beginnt man, über Datenflüsse nachzudenken in Ihrer ersten Nacht.
Wir haben Softwaretechnik für uns selbst und für jeden einzelnen Neuling auf diesem Gebiet kompliziert. Programmieren muss nicht schwer und hirnzermürbend sein. Es kann einfach und natürlich sein.
Machen wir es nicht kompliziert und gehen wir gleich zur Sache. Denn so denken wir nun einmal.
Lesen Sie auch: