Met de hoeveelheid gratis hulpmiddelen, boeken, online lessen en bootcamps voor codering die nu beschikbaar zijn, kan iedereen leren coderen. Er is echter nog steeds een kwaliteitskloof tussen coderen en software engineering. Moet die er zijn?
Ik schreef mijn eerste "Hello world" meer dan twintig jaar geleden - dat is het antwoord dat ik geef als iemand me vraagt hoe lang ik al coder. De afgelopen tien jaar heb ik genoten van een carrière waarin ik in aanraking kwam met code bijna elke dag - dat is het antwoord dat ik geef als me wordt gevraagd hoe lang ik al professioneel codeur ben.
Hoe lang ben ik al een softwareontwikkelaar? Ik zou zeggen zo'n vijf jaar. Wacht even, deze getallen lijken niet te kloppen! Dus wat is er veranderd? Wie zou ik als software engineer beschouwen en wie "slechts" als programmeur?
De definitie van een software engineer
Coderen is relatief eenvoudig. Het is niet meer allemaal assembly mnemonics op belachelijk beperkte systemen. En als je zoiets expressiefs en krachtigs als Ruby gebruikt, is het nog eenvoudiger.
Je pakt gewoon een kaartje, zoekt waar je je code moet invoegen, zoekt uit welke logica je daar moet plaatsen en boem - klaar. Als je iets geavanceerder bent, zorg je ervoor dat je code mooi is. Logisch is opgedeeld in methodes. Goede specificaties heeft die niet alleen het gelukkige pad testen. Dat is wat een goede codeur doet.
Een software engineer denkt niet meer in methods en classes, althans niet primair. In mijn ervaring denkt een software engineer in flows. Ze zien in de eerste plaats de denderende, razende rivier van data en interactie die door het systeem raast. Ze denken na over wat ze moeten doen om deze stroom om te leiden of te veranderen. Mooie code, logische methodes en goede specificaties komen bijna als bijzaak.
Het zijn helemaal schildpadden
Mensen denken over het algemeen op een bepaalde manier over de meeste interacties met de werkelijkheid. Laten we het bij gebrek aan een betere term het "top-down" perspectief noemen. Als mijn hersenen bezig zijn met het zetten van een kopje thee, zullen ze eerst de algemene stappen bedenken: naar de keuken gaan, de waterkoker opzetten, het kopje klaarmaken, water inschenken, teruggaan naar mijn bureau.
Het zal niet eerst uitzoeken welk kopje ik als eerste moet gebruiken, terwijl ik in slaap val achter mijn bureau; dat in slaap vallen komt later wel, als ik voor de kast sta. Het zal er niet bij stilstaan dat we misschien geen thee meer hebben (of in ieder geval geen goed dingen). Het is breed, reactief en foutgevoelig. Alles bij elkaar - zeer menselijk in de natuur.
Als de software engineer overweegt om wijzigingen aan te brengen in de enigszins verbijsterende gegevensstroom, zal hij dat natuurlijk op een vergelijkbare manier doen. Laten we eens kijken naar dit voorbeeld van een user story:
Een klant bestelt een widget. Bij de prijsbepaling van de bestelling moet rekening worden gehouden met het volgende:
- Widget basisprijs in de locatie van de gebruiker
- Widgetvorm (prijsmodificator)
- Of het een spoedbestelling is (prijswijziger)
- Of de levering van de bestelling plaatsvindt op een feestdag in de locatie van de gebruiker (prijswijziger)
Dit lijkt misschien allemaal gekunsteld (en dat is het natuurlijk ook), maar het is niet ver verwijderd van enkele echte gebruikersverhalen die ik onlangs heb mogen verpletteren.
Laten we nu eens het denkproces doorlopen dat een software engineer zou kunnen gebruiken om dit aan te pakken:
"We moeten de gebruiker en zijn bestelling krijgen. Dan beginnen we met het berekenen van het totaal. We beginnen bij nul. Dan passen we de widgetvormmodificator toe. Dan de spoedkosten. Dan kijken we of het op een feestdag is, boem, klaar voor de lunch!"
Ah, de kick die een eenvoudig gebruikersverhaal teweeg kan brengen. Maar de software engineer is ook maar een mens, geen perfecte multi-threaded machine, en het recept hierboven is in grote lijnen. De ingenieur moet dan dieper nadenken:
"De widget shape modifier is... oh, dat is super afhankelijk van de widget, nietwaar. En ze kunnen verschillen per locale, zelfs als het niet nu is dan in de toekomst," denken ze, eerder verbrand door veranderende bedrijfsvereisten, "en de spoedkosten kunnen dat ook zijn. En feestdagen zijn ook super plaatsgebonden, augh, en tijdzones zullen een rol spelen! Ik had hier een artikel over het omgaan met tijden in verschillende tijdzones in Rails... ooh, ik vraag me af of de besteltijd wordt opgeslagen met zone in de database! Beter het schema controleren."
Oké, software engineer. Stop. Je wordt verondersteld een kopje thee te zetten, maar je zit in gedachten verzonken voor de kast te bedenken of het bloemige kopje wel van toepassing is op je theeprobleem.
De perfecte kop brouwen widget
Maar dat is gemakkelijk wat er kan gebeuren als je iets probeert te doen dat zo onnatuurlijk is voor het menselijk brein als denken in verschillende dieptes van detail tegelijkertijd.
Na een korte blik in hun ruime arsenaal aan links over het omgaan met tijdzones, vermand onze ingenieur zich en begint hij dit om te zetten in echte code. Als ze de naïeve benadering zouden proberen, zou het er ongeveer zo uitzien:
def bereken_prijs(gebruiker, order)
order.prijs = 0
order.price = WidgetPrices.find_by(widget_type: order.widget.type).price
order.price = WidgetShapes.find_by(widget_shape: order.widget.shape).modifier
...
einde
En zo gingen ze maar door, op deze heerlijke procedurele manier, om dan bij de eerste code review zwaar te worden afgeslagen. Want als je erover nadenkt, is het heel normaal om op deze manier te denken: eerst grote lijnen en pas veel later details. In het begin dacht je niet eens dat je uit de goede thee was, toch?
Onze technicus is echter goed getraind en niet onbekend met het Service Object, dus dit is wat er in plaats daarvan gaat gebeuren:
klasse BasisOrderService
def zelf.oproep(gebruiker, bestelling)
nieuw(gebruiker, order).aanroepen
einde
def initialiseer(gebruiker, order)
@user = gebruiker
@order = order
einde
def oproep
puts "[WARN] Implementeer niet standaard aanroep voor #{self.class.name}!"
gebruiker, volgorde
einde
einde
Klasse WidgetPriceService < BaseOrderService; einde
Klasse ShapePriceModifier < BaseOrderService; einde
klasse RushPriceModifier < BaseOrderService; einde
klasse HolidayDeliveryPriceModifier < BaseOrderService; einde
klasse OrderPriceCalculator < BaseOrderService
def oproep
gebruiker, order = WidgetPriceService.call(gebruiker, order)
gebruiker, order = ShapePriceModifier.call(gebruiker, order)
gebruiker, bestelling = RushPriceModifier.aanroep(gebruiker, bestelling)
gebruiker, order = HolidayDeliveryPriceModifier.call(gebruiker, order)
gebruiker, bestelling
einde
einde
```
Goed! Nu kunnen we wat goede TDD toepassen, er een testcase voor schrijven en de klassen verder uitwerken totdat alle stukjes op hun plaats vallen. En het wordt nog mooi ook.
En ook volstrekt onmogelijk om over te redeneren.
Vijand is de staat
Natuurlijk, dit zijn allemaal goed gescheiden objecten met afzonderlijke verantwoordelijkheden. Maar hier is het probleem: het zijn nog steeds objecten. Het service object patroon met zijn "doe alsof dit object een functie is" is echt een hulpmiddel. Er is niets dat iemand ervan weerhoudt om HolidayDeliveryPriceModifier.new(user, order).something_else_entirely
. Niets weerhoudt mensen ervan om interne status toe te voegen aan deze objecten.
En niet te vergeten gebruiker
en bestel
zijn ook objecten en ermee knoeien is net zo makkelijk als iemand die stiekem even snel bestellen.opslaan
ergens in deze anders "zuivere" functionele objecten, waardoor de onderliggende bron van de staat van de waarheid, a.k.a. een database, verandert. In dit gekunstelde voorbeeld is het geen groot probleem, maar het kan je zeker bijten als dit systeem groeit in complexiteit en zich uitbreidt naar aanvullende, vaak asynchrone, onderdelen.
De ingenieur had het juiste idee. En gebruikte een heel natuurlijke manier om dit idee uit te drukken. Maar weten hoe dit idee uit te drukken - op een mooie en makkelijk te beredeneren manier - werd bijna verhinderd door het onderliggende OOP-paradigma. En als iemand die de sprong nog niet heeft gemaakt om zijn gedachten uit te drukken als omleidingen van de gegevensstroom, probeert om minder vaardig de onderliggende code te wijzigen, zullen er slechte dingen gebeuren.
Functioneel zuiver worden
Was er maar een paradigma waarin het uitdrukken van je ideeën in termen van gegevensstromen niet alleen eenvoudig, maar ook noodzakelijk was. Als redeneren eenvoudig kon worden gemaakt, zonder de mogelijkheid om ongewenste neveneffecten te introduceren. Als gegevens onveranderlijk konden zijn, net als het bloemrijke kopje waar je je thee in zet.
Ja, ik maak natuurlijk een grapje. Dat paradigma bestaat, en het heet functioneel programmeren.
Laten we eens kijken hoe het bovenstaande voorbeeld eruit zou kunnen zien in een persoonlijke favoriet, Elixir.
defmodule WidgetPrijzen doen
def prijsorder([gebruiker, order]) doen
[gebruiker, order]
|> widgetprijs
|> shapepricemodifier
|> spoedprijs-aanpasser
|> vakantieprijsmodificator
einde
defp widgetprijs([gebruiker, order]) doen
%{widget: widget} = order
prijs = WidgetRepo.getbase_price(widget)
[gebruiker, %{order | prijs: prijs }]
einde
defp shapepricemodifier([gebruiker, order]) do
%{widget: widget, prijs: huidige prijs} = order
modifier = WidgetRepo.getshapeprice(widget)
[gebruiker, %{order | prijs: huidigeprijs * modifier} ]
einde
defp rushpricemodifier([gebruiker, order]) doen
%{rush: rush, prijs: huidige prijs} = order
Als rush doen
[gebruiker, %{order | prijs: huidigeprijs * 1.75} ]
anders
[gebruiker, %{bestelling | prijs: huidige_prijs} ]
einde
einde
defp holidaypricemodifier([gebruiker, order]) do
%{datum: datum, prijs: huidige prijs} = order
modifier = HolidayRepo.getholidaymodifier(gebruiker, datum)
[gebruiker, %{order | prijs: huidigeprijs * modifier}]
einde
einde
```
Het zal je misschien opvallen dat het een volledig uitgewerkt voorbeeld is van hoe de user story eigenlijk kan worden bereikt. Dat komt omdat het minder een mond vol is dan het in Ruby zou zijn. We gebruiken een paar belangrijke functies die uniek zijn voor Elixir (maar algemeen beschikbaar in functionele talen):
Zuivere functies. We veranderen de inkomende bestel
helemaal niet, we maken gewoon nieuwe kopieën - nieuwe iteraties op de begintoestand. We springen ook niet opzij om iets te veranderen. En zelfs als we dat zouden willen, bestel
gewoon een "domme" kaart is, kunnen we niet bestellen.opslaan
op geen enkel punt hier, omdat het gewoon niet weet wat dat is.
Patroonmatching. Vergelijkbaar met ES6's destructureren, stelt dit ons in staat om prijs
en widget
van de bestelling en het doorgeven, in plaats van onze vrienden te dwingen WidgetRepo
en VakantieRepo
om te weten hoe om te gaan met een volledige bestel
.
Pijpexploitant. Gezien in prijsorder
Hiermee kunnen we gegevens door functies laten lopen in een soort "pijplijn" - een concept dat iedereen meteen kent die ooit ps aux | grep postgres
om te controleren of het verdomde ding nog draaide.
Dit is hoe je denkt
Bijwerkingen maken niet echt deel uit van ons denkproces. Nadat je water in je kopje hebt gegoten, maak je je over het algemeen geen zorgen dat een fout in de waterkoker ertoe kan leiden dat deze oververhit raakt en ontploft - in ieder geval niet genoeg om in de binnenkant van de waterkoker te gaan neuzen om te controleren of iemand hem niet per ongeluk heeft achtergelaten. exploderen_na_gieten
hoog omgedraaid.
De weg van codeur naar software engineer - voorbij het zorgen maken over objecten en toestanden naar het zorgen maken over datastromen - kan in sommige gevallen jaren duren. Dat deed het zeker voor de OOP-geoefende ondergetekende. Met functionele talen ga je nadenken over flows op je eerste nacht.
We hebben softwareontwikkeling ingewikkeld voor onszelf en elke nieuwkomer op dit gebied. Programmeren hoeft niet moeilijk en hersenkrakend te zijn. Het kan gemakkelijk en natuurlijk zijn.
Laten we dit niet ingewikkeld maken en al functioneel gaan. Want zo denken we.
Lees ook: