Med den mängd gratis resurser, böcker, onlinekurser och bootcamps för kodning som finns tillgängliga just nu kan alla lära sig att koda. Det finns dock fortfarande ett kvalitetsgap mellan kodning och programvaruteknik. Måste det finnas en?
Jag skrev mitt första "Hello world" för över tjugo år sedan - det är det svar jag ger om någon frågar mig hur länge jag har varit kodare. Under de senaste tio åren har jag haft en karriär som har gett mig möjlighet att röra vid kod nästan varje dag - det är det svar jag ger om jag får frågan hur länge jag har varit professionell kodare.
Hur länge har jag varit Programvaruingenjör? Jag skulle säga ungefär fem år. Vänta lite, de här siffrorna verkar inte stämma! Så vad är det som har förändrats? Vem skulle jag betrakta som en mjukvaruingenjör och vem "bara" som en kodare?
Definitionen av en mjukvaruingenjör
Kodning är relativt enkelt. Det handlar inte längre om mnemotekniker på löjligt begränsade system. Och om du använder något så uttrycksfullt och kraftfullt som Ruby är det ännu enklare.
Du plockar bara upp en biljett, hittar var du behöver infoga din kod, du räknar ut logiken du behöver lägga där, och boom - klart. Om du är lite mer avancerad ser du till att din kod är vacker. Logiskt uppdelad i metoder. Har anständiga specifikationer som inte bara testar den lyckliga vägen. Det är vad en bra kodare gör.
En mjukvaruutvecklare tänker inte längre i metoder och klasser, åtminstone inte i första hand. Enligt min erfarenhet tänker en mjukvaruingenjör i flöden. De ser först och främst den dånande, rasande floden av data och interaktion som brusar genom systemet. De funderar på vad de behöver göra för att avleda eller förändra detta flöde. Den snygga koden, de logiska metoderna och de bra specifikationerna kommer nästan som en eftertanke.
Det är sköldpaddor hela vägen ner
Människor tänker i allmänhet på ett visst sätt om de flesta interaktioner med verkligheten. I brist på en bättre term kan vi kalla det för "uppifrån och ner"-perspektivet. Om min hjärna arbetar med att göra mig en kopp te kommer den först att räkna ut de allmänna stegen: gå till köket, sätta på vattenkokaren, förbereda koppen, hälla upp vatten, återvända till skrivbordet.
Den kommer inte att räkna ut vilken kopp jag ska använda först, när jag står och zonerar vid mitt skrivbord; den zoneringen kommer senare, när jag står framför skåpet. Den kommer inte att tänka på att teet kan vara slut (eller i alla fall bra grejer). Den är bred, reaktiv och felbenägen. Sammantaget - mycket människa i naturen.
När programvaruingenjören överväger ändringar i det något häpnadsväckande dataflödet kommer de naturligtvis att göra det på ett liknande sätt. Låt oss titta på det här exemplet på en användarberättelse:
En kund beställer en widget. Vid prissättningen av ordern måste följande beaktas:
- Widgets baspris på användarens ort
- Widget-form (prismodifierare)
- Om det är en brådskande order (prismodifierare)
- Om orderleveransen sker på en helgdag i användarens land (prismodifierare)
Allt det här kan verka konstruerat (och det är det naturligtvis), men det är inte långt ifrån några faktiska användarberättelser som jag har haft nöjet att ta del av nyligen.
Låt oss nu gå igenom den tankeprocess som en programvaruutvecklare kan använda för att ta itu med detta:
"Vi måste få fram användaren och deras beställning. Sedan börjar vi beräkna totalsumman. Vi börjar på noll. Sedan applicerar vi widgetformsmodifieraren. Sedan rusningsavgiften. Sen ser vi om det är en helgdag, boom, klart före lunch!"
Åh, den kick som en enkel användarberättelse kan ge. Men programvaruingenjören är bara en människa, inte en perfekt flertrådad maskin, och receptet ovan är ett brett grepp. Ingenjören fortsätter att tänka djupare då:
"Widgetens formmodifierare är... det är väldigt beroende av widgeten, eller hur. Och de kan vara olika per lokal, även om det inte är nu så i framtiden," tror de, tidigare brända av förändrade affärskrav, "och rusningsavgiften kan också vara det. Och helgdagar är också superlokalspecifika, augh, och tidszoner kommer att vara inblandade! Jag hade en artikel här om att hantera tider i olika tidszoner i Rails här ... ooh, jag undrar om ordertiden lagras med zon i databasen! Bäst att kolla schemat."
Okej, mjukvaruingenjör. Sluta, sluta. Det är meningen att du ska göra en kopp te, men du sitter framför skåpet och funderar på om den blommiga koppen ens är tillämplig på ditt teproblem.
Bryggning av den perfekta koppen widget
Men det är vad som lätt kan hända när man försöker göra något så onaturligt för den mänskliga hjärnan som att tänka på flera detaljnivåer samtidigt.
Efter en kort genomgång av deras rymliga arsenal av länkar om tidszonshantering tar vår ingenjör sig samman och börjar bryta ner detta till faktisk kod. Om de försökte med den naiva metoden skulle det kunna se ut ungefär så här:
def beräkna_pris(användare, order)
order.pris = 0
order.price = WidgetPrices.find_by(widget_type: order.widget.type).price
order.price = WidgetShapes.find_by(widget_shape: order.widget.shape).modifier
...
slut
Och så fortsatte de på det här härliga procedurmässiga sättet, bara för att bli rejält nedslagna vid den första kodgranskningen. För om man tänker efter är det helt normalt att tänka på det här sättet: stora drag först och detaljer mycket senare. Du trodde inte ens att du var ute ur det goda teet först, eller hur?
Vår ingenjör är dock välutbildad och inte främmande för Service Object, så här är vad som börjar hända istället:
klass BaseOrderService
def self.call(användare, order)
new(användare, order).call
slut
def initialize(användare, order)
@användare = användare
@order = order
slut
def anrop
puts "[WARN] Implementera icke-standardanrop för #{self.class.name}!"
användare, order
slut
slut
class WidgetPriceService < BaseOrderService; end
class ShapePriceModifier < BaseOrderService; end
klass RushPriceModifier < Basbeställningstjänst; end
klass HolidayDeliveryPriceModifier < Basbeställningstjänst; end
klass OrderPriceCalculator < Basbeställningstjänst
def anrop
användare, order = WidgetPriceService.call(användare, order)
användare, order = ShapePriceModifier.call(användare, order)
användare, order = RushPriceModifier.call(användare, order)
användare, order = HolidayDeliveryPriceModifier.call(användare, order)
användare, beställning
slut
slut
```
Bra! Nu kan vi använda lite bra TDD, skriva ett testfall för det och fylla på med klasser tills alla bitar faller på plats. Och det kommer att bli vackert också.
Samt helt omöjligt att resonera kring.
Fienden är staten
Visst, det här är alla väl separerade objekt med enskilda ansvarsområden. Men här är problemet: de är fortfarande objekt. Serviceobjektmönstret med dess "tvinga låtsas att det här objektet är en funktion" är verkligen en krycka. Det finns inget som hindrar någon från att ringa HolidayDeliveryPriceModifier.new(användare, order).something_else_entirely
. Inget hindrar människor från att lägga till interna tillstånd till dessa objekt.
För att inte tala om användare
och ordning
är också objekt, och att mixtra med dem är lika enkelt som att smyga in en snabb order.spara
någonstans i dessa annars "rena" funktionella objekt, ändra den underliggande källan till sanningens, alias en databas, tillstånd. I det här konstruerade exemplet är det ingen stor sak, men det kan verkligen komma tillbaka och bita dig om det här systemet växer i komplexitet och expanderar till ytterligare, ofta asynkrona, delar.
Ingenjören hade rätt idé. Och använde ett mycket naturligt sätt att uttrycka denna idé. Men att veta hur man skulle uttrycka denna idé - på ett vackert och lättbegripligt sätt - hindrades nästan helt av det underliggande OOP-paradigmet. Och om någon som ännu inte har tagit steget till att uttrycka sina tankar som avledningar av dataflödet försöker att mindre skickligt ändra den underliggande koden, kommer dåliga saker att hända.
Att bli funktionellt ren
Om det ändå fanns ett paradigm där det inte bara var enkelt, utan nödvändigt, att uttrycka sina idéer i termer av dataflöden. Om resonemang kunde göras enkla, utan möjlighet att införa oönskade bieffekter. Om data kunde vara oföränderliga, precis som den blommiga koppen du gör ditt te i.
Ja, jag skämtar naturligtvis. Det paradigmet finns, och det kallas funktionell programmering.
Låt oss titta på hur ovanstående exempel kan se ut i en personlig favorit, Elixir.
defmodul WidgetPriser do
def priceorder([användare, order]) do
[användare, order]
|> widgetpris
|> shapepricemodifier
|> rushprismodifierare
|> semesterprismodifierare
slut
defp widgetpris([användare, order]) do
%{widget: widget} = order
pris = WidgetRepo.getbase_price(widget)
[användare, %{order | pris: pris }]
slut
defp shapepricemodifier([användare, order]) do
%{widget: widget, pris: aktuellt pris} = order
modifier = WidgetRepo.getshapeprice(widget)
[användare, %{beställning | pris: aktuellt pris * modifierare} ]
slut
defp rushpricemodifier([användare, order]) do
%{rusning: rusning, pris: aktuellt pris} = order
if rush do
[användare, %{order | pris: aktuellt pris * 1,75} ]
annat
[användare, %{order | pris: aktuellt_pris} ]
slut
slut
defp holidaypricemodifier([user, order]) do
%{datum: datum, pris: aktuellt pris} = order
modifier = HolidayRepo.getholidaymodifier(användare, datum)
[användare, %{order | pris: aktuellt pris * modifier}]
slut
slut
```
Du kanske noterar att det är ett fullfjädrat exempel på hur användarberättelsen faktiskt kan uppnås. Det beror på att det är mindre av en munfull än det skulle vara i Ruby. Vi använder några nyckelfunktioner som är unika för Elixir (men som är allmänt tillgängliga i funktionella språk):
Rena funktioner. Vi ändrar faktiskt inte den inkommande ordning
Vi skapar bara nya kopior - nya iterationer av det ursprungliga tillståndet. Vi hoppar inte heller åt sidan för att ändra något. Och även om vi skulle vilja det, ordning
är bara en "dum" karta, vi kan inte kalla order.spara
på någon punkt här eftersom den helt enkelt inte vet vad det är.
Mönstermatchning. På samma sätt som ES6:s destrukturering kan vi här plocka ut pris
och widget
av ordern och skicka den vidare, istället för att tvinga våra kompisar att WidgetRepo
och SemesterRepo
för att veta hur man hanterar en full ordning
.
Röroperatör. Sett i pris_order
låter den oss skicka data genom funktioner i en slags "pipeline" - ett koncept som är välbekant för alla som någonsin kört ps aux | grep postgres
för att kontrollera om den fortfarande var igång.
Det är så här du tänker
Biverkningar är egentligen inte en grundläggande del av vår tankeprocess. När du har hällt upp vatten i din kopp oroar du dig i allmänhet inte för att ett fel i vattenkokaren kan leda till att den överhettas och exploderar - åtminstone inte tillräckligt för att du ska gå och peta i dess interna delar för att kontrollera om någon inte oavsiktligt har lämnat explodera_efter_hällning
vänds högt.
Vägen från kodare till mjukvaruutvecklare - att gå från att oroa sig för objekt och tillstånd till att oroa sig för dataflöden - kan i vissa fall ta flera år. Det gjorde den verkligen för undertecknad som var OOP-skolad. Med funktionella språk börjar du tänka på flöden på din första natt.
Vi har gjort programvaruutveckling komplicerat för oss själva och för varenda nykomling på området. Programmering behöver inte vara svårt och hjärntvättande. Det kan vara enkelt och naturligt.
Låt oss inte göra det här komplicerat och gå funktionellt redan nu. För det är så vi tänker.
Läs också: