Med de mange gratis ressourcer, bøger, onlinekurser og bootcamps i kodning, der er tilgængelige lige nu, kan alle lære at kode. Men der er stadig en kvalitetskløft mellem kodning og softwareudvikling. Skal der være en sådan?
Jeg skrev mit første "Hello world" for over tyve år siden - det er det svar, jeg giver, hvis nogen spørger mig, hvor længe jeg har været koder. I de sidste ti år har jeg haft en karriere, der har givet mig mulighed for at røre ved Kode næsten hver dag - det er det svar, jeg giver, hvis jeg bliver spurgt, hvor længe jeg har været professionel koder.
Hvor længe har jeg været softwareingeniør? Jeg vil sige omkring fem år. Vent lidt, disse tal ser ikke ud til at passe sammen! Så hvad har ændret sig? Hvem ville jeg betragte som en softwareingeniør, og hvem er "bare" en koder?
Definitionen af en softwareingeniør
Det er relativt nemt at kode. Det er ikke længere kun assemblagemønstre på latterligt begrænsede systemer. Og hvis du bruger noget så udtryksfuldt og kraftfuldt som Ruby, er det endnu nemmere.
Du henter bare en billet, finder ud af, hvor du skal indsætte din kode, du finder ud af, hvilken logik du skal bruge der, og bum - færdig. Hvis du er lidt mere avanceret, sørger du for, at din kode er pæn. Logisk opdelt i metoder. Har ordentlige specifikationer, der ikke kun tester den lykkelige vej. Det er, hvad en god koder gør.
En softwareingeniør tænker ikke længere i metoder og klasser, i hvert fald ikke primært. Min erfaring er, at en softwareingeniør tænker i flows. De ser først og fremmest den tordnende, rasende flod af data og interaktion, der bruser gennem systemet. De tænker på, hvad de skal gøre for at aflede eller ændre denne strøm. Den smukke kode, de logiske metoder og de gode specifikationer kommer næsten som en eftertanke.
Det er skildpadder hele vejen ned
Folk tænker generelt på en bestemt måde om de fleste interaktioner med virkeligheden. Lad os i mangel af bedre kalde det "top-down"-perspektivet. Hvis min hjerne arbejder på at lave en kop te til mig selv, vil den først finde ud af de generelle trin: gå ud i køkkenet, sætte kedlen over, lave en kop te, hælde vand op, gå tilbage til skrivebordet.
Den finder ikke først ud af, hvilken kop jeg skal bruge, når jeg står og zoner ud ved mit skrivebord; den zoneudgang kommer senere, når jeg står foran skabet. Den overvejer ikke, at vi måske er løbet tør for te (eller i det mindste for godt ting). Det er bredt, reaktivt og fejlbehæftet. Alt i alt - meget menneske i naturen.
Når softwareingeniøren overvejer ændringer i det noget uoverskuelige dataflow, vil han naturligvis gøre det på en lignende måde. Lad os se på dette eksempel på en brugerhistorie:
En kunde bestiller en widget. Ved prissætning af ordren skal følgende tages i betragtning:
- Widgets basispris i brugerens lokalitet
- Widget-form (prismodifikator)
- Om det er en hasteordre (prismodifikator)
- Om levering af ordren finder sted på en helligdag i brugerens land (prismodifikator)
Alt dette kan virke konstrueret (og det er det selvfølgelig også), men det er ikke langt fra nogle faktiske brugerhistorier, som jeg har haft fornøjelsen af at knuse for nylig.
Lad os nu gennemgå den tankeproces, som en softwareingeniør kan bruge til at tackle dette:
"Vi skal have fat i brugeren og deres ordre. Så begynder vi at beregne det samlede beløb. Vi starter på nul. Så anvender vi widgetformmodifikatoren. Derefter hastegebyret. Så ser vi, om det er en helligdag, og så er vi færdige inden frokost!"
Ah, det sus, som en simpel brugerhistorie kan give. Men softwareingeniøren er kun et menneske, ikke en perfekt maskine med flere tråde, og ovenstående opskrift er en grovkornet beskrivelse. Så fortsætter ingeniøren med at tænke dybere:
"Widgetformmodifikatoren er ... åh, det er super afhængigt af widgetten, er det ikke. Og de kan være forskellige fra land til land, selv om det ikke er nu, så i fremtiden." tror de, tidligere brændt af på grund af ændrede forretningskrav, "og det kan hastegebyret også være. Og helligdage er også super lokalspecifikke, og tidszoner vil være involveret! Jeg havde en artikel her om håndtering af tider i forskellige tidszoner i Rails her ... åh, gad vide, om bestillingstiden er gemt med zone i databasen! Jeg må hellere tjekke skemaet."
Okay, softwareingeniør. Stop op. Det er meningen, at du skal lave en kop te, men du sidder foran skabet og tænker på, om den blomstrede kop overhovedet er relevant for dit teproblem.
Brygning af den perfekte kop widget
Men det er, hvad der nemt kan ske, når man forsøger at gøre noget så unaturligt for den menneskelige hjerne som at tænke i flere detaljer. på samme tid.
Efter en kort gennemgang af deres store arsenal af links om tidszonehåndtering tager vores ingeniør sig sammen og begynder at nedbryde det til egentlig kode. Hvis de prøvede den naive tilgang, ville det måske se nogenlunde sådan ud:
def beregn_pris(bruger, 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 de blev ved og ved, på denne dejlige proceduremæssige måde, kun for at blive lukket ned ved den første kodegennemgang. For hvis man tænker over det, er det helt normalt at tænke på denne måde: de store linjer først og detaljerne meget senere. Du troede ikke engang, at du var ude af den gode te i starten, gjorde du?
Vores ingeniør er imidlertid veluddannet og ikke fremmed for serviceobjektet, så her er, hvad der begynder at ske i stedet:
klasse BaseOrderService
def self.call(bruger, ordre)
new(bruger, ordre).call
slut
def initialize(bruger, ordre)
@user = bruger
@order = ordre
end
def opkald
puts "[WARN] Implementer ikke-standardkald for #{self.class.name}!"
bruger, ordre
end
end
class WidgetPriceService < BaseOrderService; end
class ShapePriceModifier < BaseOrderService; end
class RushPriceModifier < BaseOrderService; end
class HolidayDeliveryPriceModifier < BaseOrderService; end
class OrderPriceCalculator < BaseOrderService
def opkald
bruger, ordre = WidgetPriceService.call(bruger, ordre)
bruger, ordre = ShapePriceModifier.call(bruger, ordre)
bruger, ordre = RushPriceModifier.call(bruger, ordre)
bruger, ordre = HolidayDeliveryPriceModifier.call(bruger, ordre)
bruger, ordre
slut
end
```
Det er godt! Nu kan vi bruge god TDD, skrive en testcase til den og udbygge klasserne, indtil alle brikkerne falder på plads. Og det bliver også smukt.
Og det er helt umuligt at ræsonnere sig frem til.
Fjenden er staten
Ja, det er alle velafgrænsede objekter med et enkelt ansvarsområde. Men her er problemet: De er stadig objekter. Serviceobjektmønsteret med dets "lad med magt som om dette objekt er en funktion" er i virkeligheden en krykke. Der er ikke noget, der forhindrer nogen i at kalde HolidayDeliveryPriceModifier.new(user, order).something_else_entirely
. Der er ikke noget, der forhindrer folk i at tilføje intern tilstand til disse objekter.
For ikke at nævne bruger
og Bestil
er også objekter, og det er lige så nemt at pille ved dem som at snige sig ind i en hurtig ordre.gem
et eller andet sted i disse ellers "rene" funktionelle objekter og ændrer den underliggende kilde til sandhedens, dvs. en databases, tilstand. I dette konstruerede eksempel er det ikke så slemt, men det kan komme bag på dig, hvis systemet bliver mere komplekst og udvides med flere, ofte asynkrone, dele.
Ingeniøren havde den rigtige idé. Og brugte en meget naturlig måde at udtrykke denne idé på. Men at vide, hvordan man udtrykker denne idé - på en smuk og letforståelig måde - blev næsten forhindret af det underliggende OOP-paradigme. Og hvis nogen, der endnu ikke har taget springet til at udtrykke deres tanker som afledninger af datastrømmen, forsøger at ændre den underliggende kode på en mindre dygtig måde, vil der ske dårlige ting.
At blive funktionelt ren
Hvis bare der fandtes et paradigme, hvor det ikke kun var nemt, men også nødvendigt at udtrykke sine ideer i form af datastrømme. Hvis ræsonnementer kunne gøres enkle uden mulighed for at indføre uønskede bivirkninger. Hvis data kunne være uforanderlige, ligesom den blomstrede kop, du laver din te i.
Ja, jeg laver selvfølgelig sjov. Det paradigme findes, og det kaldes funktionel programmering.
Lad os se på, hvordan ovenstående eksempel kan se ud i en personlig favorit, Elixir.
defmodule WidgetPrices do
def priceorder([user, order]) do
[bruger, ordre]
|> widgetprice
|> shapepricemodifier
|> rushpricemodifier
|> ferieprismodifikator
slut
defp widgetprice([user, order]) do
%{widget: widget} = ordre
price = WidgetRepo.getbase_price(widget)
[bruger, %{ordre | pris: pris }]
slut
defp shapepricemodifier([user, order]) do
%{widget: widget, price: currentprice} = ordre
modifier = WidgetRepo.getshapeprice(widget)
[bruger, %{ordre | pris: aktuel pris * modifikator} ]
slut
defp rushpricemodifier([user, order]) do
%{rush: rush, price: currentprice} = ordre
hvis rush do
[user, %{order | price: currentprice * 1.75} ]
else
[user, %{order | price: current_price} ]
end
slut
defp holidaypricemodifier([user, order]) do
%{date: date, price: currentprice} = ordre
modifier = HolidayRepo.getholidaymodifier(user, date)
[bruger, %{ordre | pris: aktuel pris * modifikator}]
slut
end
```
Du vil måske bemærke, at det er et fuldt udbygget eksempel på, hvordan brugerhistorien faktisk kan opnås. Det er fordi, det er en mindre mundfuld, end det ville være i Ruby. Vi bruger nogle få nøglefunktioner, som er unikke for Elixir (men som generelt er tilgængelige i funktionelle sprog):
Rene funktioner. Vi ændrer faktisk ikke de indkommende Bestil
overhovedet, vi skaber bare nye kopier - nye iterationer af den oprindelige tilstand. Vi hopper heller ikke ud til siden for at ændre noget. Og selv hvis vi ville, Bestil
bare er et "dumt" kort, kan vi ikke kalde ordre.gem
på noget tidspunkt her, fordi den simpelthen ikke ved, hvad det er.
Mønstermatchning. I lighed med ES6's destrukturering giver dette os mulighed for at plukke Pris
og widget
af ordren og give den videre, i stedet for at tvinge vores venner WidgetRepo
og FerieRepo
for at vide, hvordan man håndterer en fuld Bestil
.
Røroperatør. Set i pris_ordre
lader den os sende data gennem funktioner i en slags "pipeline" - et koncept, der er velkendt for alle, der nogensinde har kørt ps aux | grep postgres
for at tjekke, om den stadig kørte.
Det er sådan, du tænker
Bivirkninger er egentlig ikke en grundlæggende del af vores tankegang. Når du har hældt vand i din kop, bekymrer du dig normalt ikke om, at en fejl i kedlen kan få den til at overophede og eksplodere - i hvert fald ikke nok til at gå og rode i dens indre for at tjekke, om nogen ikke utilsigtet har efterladt eksplodere_efter_hældning
vendt højt.
Vejen fra koder til softwareingeniør - at gå fra at bekymre sig om objekter og tilstande til at bekymre sig om datastrømme - kan i nogle tilfælde tage flere år. Det gjorde den i hvert fald for undertegnede, der er opvokset med OOP. Med funktionelle sprog kommer du til at tænke på flows på din første aften.
Vi har lavet softwareudvikling kompliceret for os selv og hver eneste nybegynder på området. Programmering behøver ikke at være svært og hjernevridende. Det kan være nemt og naturligt.
Lad os ikke gøre det kompliceret, og lad os blive funktionelle. For det er sådan, vi tænker.
Læs også her: