Med alle de gratis ressursene, bøkene og nettkursene i koding som er tilgjengelige akkurat nå, kan alle lære å kode. Men det er fortsatt et kvalitetsgap mellom koding og programvareteknikk. Må det være et slikt gap?
Jeg skrev mitt første "Hello world" for over tjue år siden - det er svaret jeg gir hvis noen spør meg hvor lenge jeg har vært koder. De siste ti årene har jeg hatt en karriere som har gitt meg mulighet til å berøre kode nesten hver dag - det er svaret jeg gir hvis jeg blir spurt om hvor lenge jeg har vært profesjonell koder.
Hvor lenge har jeg vært programvareingeniør? Jeg vil si rundt fem år. Vent litt, disse tallene ser ikke ut til å stemme! Så hva er det som har endret seg? Hvem vil jeg betrakte som programvareingeniør, og hvem er "bare" en koder?
Definisjonen av en programvareingeniør
Koding er relativt enkelt. Det er ikke lenger bare assemblermnemonikk på latterlig begrensede systemer. Og hvis du bruker noe så uttrykksfullt og kraftfullt som Ruby, er det enda enklere.
Du bare plukker opp en billett, finner ut hvor du skal sette inn koden, finner ut hvilken logikk du skal legge inn der, og pang - ferdig. Hvis du er litt mer avansert, sørger du for at koden din er pen. Logisk delt inn i metoder. Har skikkelige spesifikasjoner som ikke bare tester den lykkelige stien. Det er det en god koder gjør.
En programvareingeniør tenker ikke i metoder og klasser lenger, i hvert fall ikke først og fremst. Min erfaring er at en programvareingeniør tenker i flyter. De ser først og fremst den tordnende, rasende elven av data og interaksjon som bruser gjennom systemet. De tenker på hva de må gjøre for å avlede eller endre denne strømmen. Den pene koden, de logiske metodene og de gode spesifikasjonene kommer nesten som en ettertanke.
Det er skilpadder hele veien ned
Folk tenker generelt på en bestemt måte om de fleste interaksjoner med virkeligheten. I mangel av et bedre begrep kan vi kalle det "ovenfra-og-ned"-perspektivet. Hvis hjernen min jobber med å lage meg en kopp te, vil den først finne ut av de generelle trinnene: gå til kjøkkenet, sette på kjelen, tilberede koppen, helle vann, gå tilbake til skrivebordet.
Den vil ikke først finne ut hvilken kopp jeg skal bruke mens jeg står avsondret ved skrivebordet; den avsondringen kommer senere, når jeg står foran skapet. Den vil ikke tenke på at vi kanskje er tomme for te (eller i det minste tomme for bra ting). Det er bredt, reaktivt og feilutsatt. Alt i alt - veldig menneske i naturen.
Når programvareingeniøren vurderer endringer i den noe forvirrende dataflyten, vil han eller hun naturligvis gjøre det på en lignende måte. La oss se på dette eksempelet på en brukerhistorie:
En kunde bestiller en widget. Ved prising av bestillingen må følgende tas i betraktning:
- Widgets grunnpris i brukerens lokalitet
- Widgetform (prismodifikator)
- Om det er en hastebestilling (prismodifikator)
- Om bestillingen skal leveres på en helligdag i brukerens hjemland (prismodifikator)
Alt dette kan virke konstruert (og det er det selvsagt også), men det er ikke langt unna noen faktiske brukerhistorier jeg har hatt gleden av å knuse i det siste.
La oss nå gå gjennom tankeprosessen som en programvareingeniør kan bruke for å takle dette:
"Vi må få tak i brukeren og bestillingen deres. Så begynner vi å beregne totalsummen. Vi begynner på null. Så bruker vi widgetformmodifikatoren. Så hasteavgiften. Så ser vi om det er en helligdag, og vips, så er vi ferdige før lunsj!"
Å, rusen som en enkel brukerhistorie kan gi. Men programvareingeniøren er bare et menneske, ikke en perfekt flertrådet maskin, og oppskriften ovenfor er bare grove trekk. Ingeniøren fortsetter å tenke dypere da:
"Modifikatoren for widgetform er ... å, det er veldig avhengig av widgeten, ikke sant. Og de kan være forskjellige fra land til land, om ikke nå, så i fremtiden." de tror, tidligere brent av endrede forretningskrav, "og rushtidsavgiften kan også være det. Og helligdager er også superlokalspesifikke, og tidssoner vil være involvert! Jeg hadde en artikkel her om hvordan man håndterer tider i ulike tidssoner i Rails her ... åh, jeg lurer på om bestillingstiden er lagret med sone i databasen! Best å sjekke skjemaet."
Greit, programvareingeniør. Stopp! Du skal egentlig lage en kopp te, men du sitter foran skapet og tenker på om den blomstrete koppen i det hele tatt er relevant for teproblemet ditt.
Brygger den perfekte koppwidgeten
Men det er lett det som kan skje når du prøver å gjøre noe så unaturlig for menneskehjernen som å tenke på flere detaljnivåer samtidig.
Etter en kort gjennomgang av det store arsenalet av lenker om håndtering av tidssoner, tar ingeniøren vår seg sammen og begynner å bryte dette ned til faktisk kode. Hvis de prøvde den naive tilnærmingen, kunne det se omtrent slik ut:
def calculate_price(bruker, ordre)
ordre.pris = 0
order.price = WidgetPrices.find_by(widget_type: order.widget.type).price
order.price = WidgetShapes.find_by(widget_shape: order.widget.shape).modifier
...
end
Og så fortsatte de på denne herlige prosedyreaktige måten, bare for å bli kraftig nedstemt ved første kodegjennomgang. For hvis du tenker deg om, er det helt normalt å tenke på denne måten: de store linjene først, og detaljene mye senere. Du trodde vel ikke engang at du var ute av den gode teen til å begynne med?
Ingeniøren vår er imidlertid godt trent og ikke fremmed for Service Object, så her er hva som begynner å skje i stedet:
klasse BaseOrderService
def self.call(bruker, ordre)
new(bruker, ordre).call
end
def initialize(bruker, ordre)
@user = bruker
@ordre = ordre
end
def call
puts "[WARN] Implementere ikke-standardkall for #{self.class.name}!"
bruker, rekkefølge
end
end
class WidgetPriceService < BaseOrderService; end
class ShapePriceModifier < BaseOrderService; end
class RushPriceModifier < BaseOrderService; end
class HolidayDeliveryPriceModifier < BaseOrderService; end
class OrderPriceCalculator < BaseOrderService
def anrop
user, order = WidgetPriceService.call(user, order)
user, order = ShapePriceModifier.call(user, order)
user, order = RushPriceModifier.call(user, order)
bruker, bestilling = HolidayDeliveryPriceModifier.call(bruker, bestilling)
bruker, bestilling
end
end
```
Så bra! Nå kan vi bruke god TDD, skrive et testtilfelle for det, og fylle ut klassene til alle bitene faller på plass. Og det blir vakkert også.
I tillegg til at det er helt umulig å resonnere seg frem til.
Fienden er staten
Joda, dette er alle godt adskilte objekter med enkeltansvar. Men her er problemet: De er fortsatt objekter. Tjenesteobjektmønsteret med sitt "lat som om dette objektet er en funksjon" er egentlig en krykke. Det er ingenting som hindrer noen i å kalle HolidayDeliveryPriceModifier.new(user, order).something_else_entirely
. Ingenting hindrer folk i å legge til intern tilstand i disse objektene.
For ikke å nevne bruker
og rekkefølge
er også objekter, og det er like enkelt å tukle med dem som å snike seg inn en rask ordre.lagre
et eller annet sted i disse ellers "rene" funksjonelle objektene, og endrer den underliggende kilden til sannhetens, dvs. databasens, tilstand. I dette konstruerte eksemplet er det ikke så farlig, men det kan bli et problem hvis systemet blir mer komplekst og utvides med flere, ofte asynkrone, deler.
Ingeniøren hadde den rette ideen. Og brukte en veldig naturlig måte å uttrykke denne ideen på. Men å vite hvordan man skulle uttrykke denne ideen - på en vakker og lettfattelig måte - ble nærmest forhindret av det underliggende OOP-paradigmet. Og hvis noen som ennå ikke har tatt spranget til å uttrykke tankene sine som avledninger av dataflyten, prøver å endre den underliggende koden på en mindre dyktig måte, vil det skje dårlige ting.
Å bli funksjonelt ren
Hvis det bare fantes et paradigme der det ikke bare var enkelt, men også nødvendig, å uttrykke ideene dine i form av dataflyt. Hvis resonnementer kunne gjøres enkle, uten mulighet for å introdusere uønskede bieffekter. Hvis data kunne være uforanderlige, akkurat som den blomstrende koppen du koker teen din i.
Ja, jeg tuller selvfølgelig. Det paradigmet finnes, og det kalles funksjonell programmering.
La oss se på hvordan eksempelet ovenfor kan se ut i en personlig favoritt, Elixir.
defmodule WidgetPrices do
def priceorder([bruker, ordre]) do
[bruker, ordre]
|> widgetprice
|> shapepricemodifier
|> rushpricemodifier
|> ferieprismodifikator
slutt
defp widgetprice([bruker, ordre]) do
%{widget: widget} = ordre
price = WidgetRepo.getbase_price(widget)
[bruker, %{ordre | pris: pris }]
end
defp shapepricemodifier([user, order]) do
%{widget: widget, price: currentprice} = ordre
modifier = WidgetRepo.getshapeprice(widget)
[bruker, %{bestilling | pris: gjeldende pris * modifikator} ]
end
defp rushpricemodifier([user, order]) do
%{rush: rush, price: currentprice} = ordre
if rush do
[user, %{ordre | pris: gjeldende pris * 1,75} ]
else
[bruker, %{ordre | pris: nåværende_pris} ]
end
end
defp holidaypricemodifier([user, order]) do
%{date: date, price: currentprice} = ordre
modifier = HolidayRepo.getholidaymodifier(user, date)
[bruker, %{ordre | pris: gjeldende pris * modifikator}]
end
end
```
Du vil kanskje legge merke til at det er et fullverdig eksempel på hvordan brukerhistorien faktisk kan realiseres. Det er fordi det er mindre av en munnfull enn det ville vært i Ruby. Vi bruker noen nøkkelfunksjoner som er unike for Elixir (men som generelt er tilgjengelige i funksjonelle språk):
Rene funksjoner. Vi endrer faktisk ikke de innkommende rekkefølge
Vi lager bare nye kopier - nye iterasjoner av den opprinnelige tilstanden. Vi hopper heller ikke til siden for å endre noe. Og selv om vi ville det, rekkefølge
bare er et "dumt" kart, kan vi ikke kalle ordre.lagre
på noe tidspunkt her, fordi den rett og slett ikke vet hva det er.
Mønstermatching. På samme måte som ES6s destrukturering, gjør dette at vi kan plukke pris
og widget
av ordren og sende den videre, i stedet for å tvinge kompisene våre til å WidgetRepo
og HolidayRepo
å vite hvordan man skal håndtere en full rekkefølge
.
Røroperatør. Sett i price_order
kan vi sende data gjennom funksjoner i en slags "pipeline" - et konsept som er velkjent for alle som noen gang har kjørt ps aux | grep postgres
for å sjekke om den pokkers greia fortsatt var i gang.
Det er slik du tenker
Bivirkninger er egentlig ikke en grunnleggende del av tankeprosessen vår. Etter at du har skjenket vann i koppen din, bekymrer du deg vanligvis ikke for at en feil i vannkokeren kan føre til at den overopphetes og eksploderer - i hvert fall ikke nok til at du går og roter i innmaten for å sjekke om noen utilsiktet har glemt eksplodere_etter_påfylling
vendt høyt.
Veien fra koder til programvareingeniør - fra å bekymre seg for objekter og tilstander til å bekymre seg for dataflyt - kan i noen tilfeller ta flere år. Det gjorde den i hvert fall for undertegnede, som er OOP-oppvokst. Med funksjonelle språk begynner du å tenke på dataflyt på din første natt.
Vi har laget programvareutvikling komplisert for oss selv og alle nykommere på feltet. Programmering trenger ikke å være vanskelig og hjernevridende. Det kan være enkelt og naturlig.
La oss ikke gjøre dette komplisert, og la oss bli funksjonelle. For det er slik vi tenker.
Les også: