Con la quantità di risorse gratuite, libri, corsi online e bootcamp di codifica disponibili in questo momento, tutti possono imparare a codificare. Tuttavia, c'è ancora un divario di qualità tra il coding e l'ingegneria del software. Deve esistere?
Ho scritto il mio primo "Hello world" più di vent'anni fa: questa è la risposta che do se qualcuno mi chiede da quanto tempo sono un coder. Negli ultimi dieci anni, mi sono goduto una carriera che mi ha permesso di toccare codice quasi ogni giorno - è la risposta che do quando mi chiedono da quanto tempo sono un codificatore professionista.
Da quanto tempo sono un ingegnere del software? Direi circa cinque anni. Aspettate, questi numeri non sembrano coincidere! Allora cosa è cambiato? Chi dovrei considerare un ingegnere del software e chi "semplicemente" un codificatore?
La definizione di ingegnere del software
La codifica è relativamente facile. Non si tratta più di mnemotecniche di assemblaggio su sistemi ridicolmente vincolati. E se si usa qualcosa di espressivo e potente come Ruby, è ancora più facile.
Basta prendere un biglietto, trovare il punto in cui inserire il codice, capire la logica da inserire e bum, il gioco è fatto. Se siete un po' più avanzati, vi assicurate che il vostro codice sia bello. Sia logicamente suddiviso in metodi. abbia specifiche decenti che non testino solo il percorso felice. Questo è ciò che fa un buon codificatore.
Un ingegnere del software non pensa più a metodi e classi, almeno non principalmente. Secondo la mia esperienza, un ingegnere del software pensa per flussi. Vede innanzitutto il fiume impetuoso e fragoroso di dati e interazioni che scorre nel sistema. Pensano a ciò che devono fare in termini di deviazione o alterazione di questo flusso. Il bel codice, i metodi logici e le ottime specifiche sono quasi un ripensamento.
Le tartarughe sono tutte in basso
In genere le persone pensano in un certo modo alla maggior parte delle interazioni con la realtà. In mancanza di un termine migliore, chiamiamola prospettiva "dall'alto verso il basso". Se il mio cervello sta lavorando per prepararmi una tazza di tè, capirà innanzitutto i passaggi generali: andare in cucina, accendere il bollitore, preparare la tazza, versare l'acqua, tornare alla scrivania.
Non si accorgerà di quale tazza usare per prima, mentre sono assopito alla scrivania; l'assopimento avverrà in un secondo momento, quando mi troverò di fronte alla credenza. Non prende in considerazione il fatto che potrebbe essere finito il tè (o perlomeno, il tè è finito). buono cose). È ampio, reattivo e soggetto a errori. Tutto sommato - molto umano in natura.
Quando l'ingegnere del software prende in considerazione le modifiche al flusso di dati un po' strabiliante, lo farà naturalmente in modo simile. Consideriamo questo esempio di storia utente:
Un cliente ordina un widget. Nella determinazione del prezzo dell'ordine, si deve tenere conto di quanto segue:
- Prezzo base del widget nella località dell'utente
- Forma del widget (modificatore di prezzo)
- Se si tratta di un ordine urgente (modificatore di prezzo)
- Se la consegna dell'ordine avviene in un giorno festivo nella località dell'utente (modificatore di prezzo)
Tutto questo può sembrare artificioso (e ovviamente lo è), ma non si discosta molto da alcune storie di utenti reali che ho avuto il piacere di analizzare di recente.
Esaminiamo ora il processo di pensiero che un ingegnere del software potrebbe impiegare per affrontare questo problema:
"Dobbiamo ottenere l'utente e il suo ordine. Poi iniziamo a calcolare il totale. Inizieremo da zero. Poi applicheremo il modificatore di forma del widget. Poi il costo della fretta. Poi vediamo se è un giorno festivo, e boom, fatto prima di pranzo!".
Ah, l'emozione che può dare una semplice storia utente. Ma l'ingegnere informatico è solo un essere umano, non una perfetta macchina multi-thread, e la ricetta di cui sopra è un'idea di massima. L'ingegnere continua a pensare in modo più approfondito:
"Il modificatore di forma del widget è... oh, dipende molto dal widget, vero? E potrebbero essere diversi per ogni locale, anche se non ora, ma in futuro". pensano, bruciati in precedenza dall'evoluzione dei requisiti aziendali, "e anche il costo della fretta potrebbe esserlo. E anche le festività sono super specifiche del luogo, e i fusi orari saranno coinvolti! Ho pubblicato qui un articolo su come gestire gli orari in fusi orari diversi in Rails... oh, mi chiedo se l'orario dell'ordine sia memorizzato con il fuso orario nel database! Meglio controllare lo schema".
Va bene, ingegnere del software. Fermatevi. Dovresti preparare una tazza di tè, ma sei assopito davanti alla credenza a pensare se la tazza fiorita sia applicabile al tuo problema del tè.
Widget per la preparazione della tazza perfetta
Ma questo è facilmente ciò che può accadere quando si cerca di fare qualcosa di innaturale per il cervello umano come pensare a diversi livelli di dettaglio. contemporaneamente.
Dopo una breve ricerca nell'ampio arsenale di link sulla gestione dei fusi orari, il nostro ingegnere si ricompone e inizia a scomporre il tutto in codice. Se avessero tentato un approccio ingenuo, il codice sarebbe stato più o meno così:
def calculate_price(user, order)
ordine.prezzo = 0
order.price = WidgetPrices.find_by(widget_type: order.widget.type).price
order.price = WidgetShapes.find_by(widget_shape: order.widget.shape).modifier
...
fine
E continuavano ad andare avanti, in questo modo deliziosamente procedurale, per poi essere pesantemente bloccati alla prima revisione del codice. Perché se ci pensate, è perfettamente normale pensare in questo modo: prima le grandi linee, poi i dettagli. All'inizio non pensavate nemmeno di essere fuori dal tè buono, vero?
Il nostro ingegnere, tuttavia, è ben addestrato e non è estraneo all'oggetto Service, quindi ecco cosa succede:
classe BaseOrderService
def self.call(utente, ordine)
nuovo(utente, ordine).call
fine
def initialize(utente, ordine)
@utente = utente
@ordine = ordine
fine
def call
mette "[WARN] Implementare una chiamata non predefinita per #{self.class.name}!".
utente, ordine
fine
fine
classe WidgetPriceService < BaseOrderService; fine
classe ShapePriceModifier < BaseOrderService; fine
classe RushPriceModifier < BaseOrderService; fine
classe HolidayDeliveryPriceModifier < BaseOrderService; fine
classe OrderPriceCalculator < BaseOrderService
def chiamata
utente, ordine = WidgetPriceService.call(utente, ordine)
utente, ordine = ShapePriceModifier.call(utente, ordine)
utente, ordine = RushPriceModifier.call(utente, ordine)
utente, ordine = HolidayDeliveryPriceModifier.call(utente, ordine)
utente, ordine
fine
fine
```
Bene! Ora possiamo impiegare un buon TDD, scrivere un caso di test e completare le classi finché tutti i pezzi non vanno al loro posto. E sarà anche bello.
Oltre che completamente impossibile da ragionare.
Il nemico è lo Stato
Certo, si tratta di oggetti ben separati con singole responsabilità. Ma il problema è che si tratta pur sempre di oggetti. Lo schema degli oggetti di servizio, con il suo "fingere forzatamente che questo oggetto sia una funzione", è davvero una stampella. Non c'è nulla che impedisca a qualcuno di chiamare HolidayDeliveryPriceModifier.new(utente, ordine).something_else_entirely
. Nulla impedisce di aggiungere uno stato interno a questi oggetti.
Per non parlare utente
e ordine
sono anch'essi oggetti, e manipolarli è facile come se qualcuno si intrufolasse in un rapido ordine.salvare
da qualche parte in questi oggetti funzionali altrimenti "puri", modificando la fonte sottostante della verità, ovvero un database, lo stato. In questo esempio artificioso non è un grosso problema, ma di sicuro può ritorcersi contro di voi se questo sistema cresce in complessità e si espande in parti aggiuntive, spesso asincrone.
L'ingegnere ha avuto l'idea giusta. E ha usato un modo molto naturale di esprimere questa idea. Ma sapere come esprimere questa idea - in un modo bello e facile da ragionare - era quasi impedito dal paradigma OOP sottostante. E se qualcuno che non ha ancora fatto il salto di qualità nell'esprimere i propri pensieri come deviazioni del flusso di dati cerca di alterare meno abilmente il codice sottostante, accadranno brutte cose.
Diventare funzionalmente puri
Se solo esistesse un paradigma in cui esprimere le proprie idee in termini di flussi di dati fosse non solo facile, ma necessario. Se il ragionamento potesse essere semplice, senza la possibilità di introdurre effetti collaterali indesiderati. Se i dati potessero essere immutabili, proprio come la tazza fiorita in cui si prepara il tè.
Sì, sto scherzando, naturalmente. Quel paradigma esiste e si chiama programmazione funzionale.
Consideriamo come l'esempio precedente potrebbe apparire in uno dei nostri preferiti, Elixir.
defmodulo WidgetPrices do
def priceorder([utente, ordine]) do
[utente, ordine]
|> widgetprice
|> modificatore del prezzo di forma
|> modificatore di prezzi urgenti
|> modificatore prezzo vacanze
fine
defp widgetprice([utente, ordine]) do
%{widget: widget} = ordine
prezzo = WidgetRepo.getbase_price(widget)
[utente, %{ordine | prezzo: prezzo }]
fine
defp shapepricemodifier([utente, ordine]) do
%{widget: widget, prezzo: prezzo corrente} = ordine
modificatore = WidgetRepo.getshapeprice(widget)
[utente, %{ordine | prezzo: prezzo corrente * modificatore} ]
fine
defp rushpricemodifier([user, order]) do
%{sfogo: sfogo, prezzo: prezzo corrente} = ordine
se fretta, fare
[utente, %{ordine | prezzo: prezzo corrente * 1,75} ]
altrimenti
[utente, %{ordine | prezzo: prezzo_corrente} ]
fine
fine
defp holidaypricemodifier([utente, ordine]) do
%{data: data, prezzo: prezzo corrente} = ordine
modificatore = HolidayRepo.getholidaymodifier(utente, data)
[utente, %{ordine | prezzo: prezzo corrente * modificatore}]
fine
fine
```
Si può notare che si tratta di un esempio completo di come la storia utente può essere effettivamente realizzata. Questo perché è meno complicato di quanto sarebbe in Ruby. Utilizziamo alcune caratteristiche chiave esclusive di Elixir (ma generalmente disponibili nei linguaggi funzionali):
Funzioni pure. In realtà non cambiamo l'ingresso ordine
Stiamo solo creando nuove copie, nuove iterazioni dello stato iniziale. Non saltiamo a lato per cambiare qualcosa. E anche se volessimo, ordine
è solo una mappa "stupida", non si può chiamare ordine.salvare
in qualsiasi momento, perché semplicemente non sa cosa sia.
Corrispondenza dei modelli. In modo piuttosto simile alla destrutturazione di ES6, questo ci permette di prelevare prezzo
e widget
dall'ordine e passarlo, invece di costringere i nostri compagni WidgetRepo
e VacanzeRepo
di sapere come gestire un'intera ordine
.
Operatore del tubo. Visto in ordine_prezzo
permette di passare i dati attraverso le funzioni in una sorta di "pipeline" - un concetto immediatamente familiare a chiunque abbia mai eseguito ps aux | grep postgres
per verificare se l'aggeggio fosse ancora in funzione.
Questo è il modo in cui si pensa
Gli effetti collaterali non sono una parte fondamentale del nostro processo di pensiero. Dopo aver versato l'acqua nella tazza, in genere non ci si preoccupa che un errore nel bollitore possa causarne il surriscaldamento e l'esplosione, almeno non tanto da andare a frugare nei suoi interni per verificare se qualcuno non abbia inavvertitamente lasciato esplodere_dopo_la_versione
ribaltato in alto.
Il percorso da coder a ingegnere del software - che va oltre la preoccupazione per gli oggetti e gli stati, per arrivare a preoccuparsi dei flussi di dati - in alcuni casi può richiedere anni. Per il sottoscritto, cresciuto con l'OOP, è stato sicuramente così. Con i linguaggi funzionali, si inizia a pensare ai flussi di dati. la prima notte.
Abbiamo fatto ingegneria del software complicato per noi stessi e per ogni nuovo arrivato nel settore. La programmazione non deve essere per forza difficile e cervellotica. Può essere facile e naturale.
Non complichiamo le cose e diventiamo già funzionali. Perché è così che pensiamo.
Leggi anche: