Ilmaisia resursseja, kirjoja, verkkokursseja ja koodausleirejä on nyt tarjolla niin paljon, että jokainen voi oppia koodaamaan. Koodauksen ja ohjelmistotekniikan välillä on kuitenkin edelleen laatuero. Pitääkö sen olla olemassa?
Kirjoitin ensimmäisen "Hello world" -kirjoitukseni yli kaksikymmentä vuotta sitten - sen vastauksen annan, jos joku kysyy minulta, kuinka kauan olen ollut koodaaja. Viimeisten kymmenen vuoden aikana olen nauttinut urasta, jolla olen kosketellut koodi melkein joka päivä - tämän vastauksen annan, jos minulta kysytään, kuinka kauan olen ollut ammattimainen koodaaja.
Kuinka kauan olen ollut ohjelmistosuunnittelija? Sanoisin noin viisi vuotta. Hetkinen, nämä luvut eivät tunnu täsmäävän! Mikä on siis muuttunut? Ketä pitäisin ohjelmistoinsinöörinä ja ketä "pelkkänä" koodarina?
Ohjelmistoinsinöörin määritelmä
Koodaus on suhteellisen helppoa. Se ei ole enää pelkkää assembly-memoniikkaa naurettavan rajoitetuissa järjestelmissä. Ja jos käytät jotain niinkin ilmaisuvoimaista ja tehokasta kuin Ruby, se on vielä helpompaa.
Otat vain lippusi, etsit, mihin sinun on lisättävä koodisi, mietit logiikan, joka sinun on lisättävä sinne, ja boom - valmis. Jos olet hieman edistyneempi, varmistat, että koodisi on nättiä. Se on loogisesti jaettu metodeihin. Siinä on kunnolliset speksit, jotka eivät testaa vain onnellista polkua. Sitä hyvä koodaaja tekee.
Ohjelmistoinsinööri ei enää ajattele metodeissa ja luokissa, ainakaan ensisijaisesti. Kokemukseni mukaan ohjelmistosuunnittelija ajattelee virroissa. He näkevät ennen kaikkea järjestelmän läpi riehuvan, raivoavan tiedon- ja vuorovaikutusvirran. Hän miettii, mitä hänen on tehtävä tämän virran ohjaamiseksi tai muuttamiseksi. Kaunis koodi, loogiset menetelmät ja loistavat tekniset tiedot tulevat lähes jälkikäteen.
Se on kilpikonnia koko matkan alaspäin.
Ihmiset ajattelevat yleensä tietyllä tavalla useimmista vuorovaikutustilanteista todellisuuden kanssa. Paremman termin puuttuessa kutsuttakoon sitä "ylhäältä alas" -näkökulmaksi. Jos aivojeni tehtävänä on hankkia itselleni kuppi teetä, ne selvittävät ensin yleiset vaiheet: menen keittiöön, laitan vedenkeittimen päälle, valmistan kupin, kaadan vettä, palaan työpöydän ääreen.
Se ei keksi ensin, mitä kuppia käyttää ensin, kun seison työpöydän ääressä vyöhykkeellä; vyöhykkeeltä poistuminen tapahtuu myöhemmin, kun seison kaapin edessä. Se ei ota huomioon, että tee saattaa olla loppu (tai ainakin teet ovat loppu hyvä tavaraa). Se on laaja, reaktiivinen ja virhealtis. Kaiken kaikkiaan - hyvin ihminen luonnossa.
Kun ohjelmistosuunnittelija harkitsee muutoksia hieman mielettömään tietovirtaan, hän tekee sen luonnollisesti samalla tavalla. Tarkastellaanpa tätä esimerkkikäyttäjätarinaa:
Asiakas tilaa widgetin. Tilauksen hinnoittelussa on otettava huomioon seuraavat seikat:
- Widgetin perushinta käyttäjän paikkakunnalla
- Widgetin muoto (hinnanmuokkaaja)
- Onko kyseessä kiireellinen tilaus (hinnanmuutos)?
- Toimitetaanko tilauksen toimitus juhlapäivänä käyttäjän paikkakunnalla (hintamuunnin).
Tämä kaikki saattaa vaikuttaa keksityltä (ja sitä se tietenkin onkin), mutta se ei ole kaukana todellisista käyttäjätarinoista, joita olen saanut viime aikoina murskata.
Käydään nyt läpi ajatusprosessi, jota ohjelmistosuunnittelija voisi käyttää tämän ongelman ratkaisemiseksi:
"No, meidän on saatava käyttäjä ja heidän tilauksensa. Sitten alamme laskea loppusummaa. Aloitamme nollasta. Sitten käytämme widgetin muodonmuokkaajaa. Sitten pikamaksu. Sitten katsomme, onko kyseessä pyhäpäivä, ja pam, valmista ennen lounasta!"
Ah, mikä kiire yksinkertaisesta käyttäjätarinasta voi seurata. Ohjelmistoinsinööri on kuitenkin vain ihminen, ei täydellinen monisäikeinen kone, ja edellä esitetty resepti on vain suuntaa-antava. Insinööri jatkaa sitten ajattelua syvemmälle:
"Widgetin muodon muokkaaja on... oi, se on hyvin riippuvainen widgetistä, eikö olekin. Ja ne voivat olla erilaisia alueittain, vaikka ei nyt, niin tulevaisuudessa." he luulevat, että muuttuvat liiketoimintatarpeet ovat aiemmin polttaneet heidät, "ja kiireellisyysmaksu saattaa myös olla. Ja juhlapyhät ovat myös erittäin paikkakuntakohtaisia, ja aikavyöhykkeet ovat mukana! Minulla oli täällä artikkeli eri aikavyöhykkeiden aikojen käsittelystä Railsissa täällä... ooh, tallennetaankohan tilauksen aika vyöhykkeineen tietokantaan! Parempi tarkistaa skeema."
Hyvä on, ohjelmistoinsinööri. Pysähdy. Sinun pitäisi keittää kuppi teetä, mutta istut kaapin ääressä miettimässä, soveltuuko kukkakuppi edes teepulmaasi.
Täydellisen kupin keittäminen widget
Mutta näin voi helposti käydä, kun yrität tehdä jotain ihmisaivoille niinkin luonnotonta kuin ajattelu useilla yksityiskohtien syvyyksillä. samanaikaisesti.
Lyhyen penkomisen jälkeen insinöörimme kerää itsensä yhteen ja ryhtyy purkamaan tätä varsinaiseen koodiin. Jos he yrittäisivät naiivia lähestymistapaa, se voisi näyttää jotakuinkin tältä:
def calculate_price(user, order)
order.price = 0
order.price = WidgetPrices.find_by(widget_type: order.widget.type).price
order.price = WidgetShapes.find_by(widget_shape: order.widget.shape).modifier
...
end
Ja he jatkoivat ja jatkoivat, tällä ihastuttavalla menettelytapamaisella tavalla, mutta heidät lopetettiin jo ensimmäisessä koodikatselmuksessa. Koska jos ajattelee asiaa, on täysin normaalia ajatella näin: ensin pääpiirteittäin ja vasta paljon myöhemmin yksityiskohtaisesti. Et tainnut edes luulla, että olit aluksi ulkona hyvästä teestä?
Insinöörimme on kuitenkin hyvin koulutettu eikä Service Object ole hänelle vieras, joten sen sijaan alkaa tapahtua seuraavaa:
luokka BaseOrderService
def self.call(käyttäjä, tilaus)
new(user, order).call
end
def initialize(user, order)
@user = käyttäjä
@order = tilaus
end
def call
puts "[WARN] Implement non-default call for #{self.class.name}!"
user, order
end
end
class WidgetPriceService < BaseOrderService; end
class ShapePriceModifier < BaseOrderService; end
class RushPriceModifier < BaseOrderService; end
class HolidayDeliveryPriceModifier < BaseOrderService; end
class OrderPriceCalculator < BaseOrderService
def call
user, order = WidgetPriceService.call(user, order)
user, order = ShapePriceModifier.call(user, order)
user, order = RushPriceModifier.call(user, order)
user, order = HolidayDeliveryPriceModifier.call(user, order)
user, order
end
end
```
Hyvä! Nyt voimme käyttää hyvää TDD:tä, kirjoittaa sille testitapauksen ja kehittää luokkia, kunnes kaikki palaset ovat kohdallaan. Ja siitä tulee myös kaunis.
Sekä täysin mahdoton järkeillä.
Vihollinen on valtio
Toki nämä kaikki ovat hyvin erillisiä objekteja, joilla on yksittäiset vastuualueet. Mutta tässä on ongelma: ne ovat silti objekteja. Palveluobjektien malli ja sen "väkisin teeskennellään, että tämä objekti on funktio" on oikeastaan kainalosauva. Mikään ei estä ketään kutsumasta HolidayDeliveryPriceModifier.new(user, order).something_else_entirely
. Mikään ei estä ihmisiä lisäämästä sisäistä tilaa näihin objekteihin.
Puhumattakaan käyttäjä
ja tilaus
ovat myös objekteja, ja niiden sekaantuminen on yhtä helppoa kuin jos joku hiippailee nopeasti order.save
jossain näissä muuten "puhtaissa" funktionaalisissa objekteissa, muuttaen totuuden, eli tietokannan, tilan taustalla olevaa lähdettä. Tässä keinotekoisessa esimerkissä se ei ole suuri asia, mutta se voi varmasti kostautua, jos järjestelmän monimutkaisuus kasvaa ja se laajenee uusiin, usein asynkronisiin osiin.
Insinöörillä oli oikea ajatus. Ja hän käytti hyvin luonnollista tapaa ilmaista tämä ajatus. OOP-paradigma oli kuitenkin melkeinpä estänyt sen, että hän tiesi, miten tämä ajatus voitaisiin ilmaista kauniilla ja helposti ymmärrettävällä tavalla. Ja jos joku, joka ei ole vielä tehnyt harppausta ajatustensa ilmaisemiseen tietovirran harhautuksina, yrittää vähemmän taitavasti muuttaa taustalla olevaa koodia, tapahtuu pahoja asioita.
Toiminnallisesti puhtaaksi tuleminen
Kunpa vain olisi olemassa paradigma, jossa ajatusten ilmaiseminen tietovirtojen muodossa ei olisi vain helppoa vaan myös välttämätöntä. Jos päättelystä voitaisiin tehdä yksinkertaista, eikä siinä olisi mitään mahdollisuutta aiheuttaa ei-toivottuja sivuvaikutuksia. Jos data voisi olla muuttumatonta, aivan kuten kukkakuppi, johon keität teesi.
Kyllä, vitsailen tietysti. Tämä paradigma on olemassa, ja sitä kutsutaan funktionaaliseksi ohjelmoinniksi.
Katsotaanpa, miltä yllä oleva esimerkki voisi näyttää henkilökohtaisessa suosikissani Elixirissä.
defmodule WidgetPrices do
def priceorder([user, order]) do
[user, order]
|> widgetprice
|> shapepricemodifier
|> rushpricemodifier
|> holidaypricemodifier
end
defp widgetprice([user, order]) do
%{widget: widget} = tilaus
hinta = WidgetRepo.getbase_price(widget)
[käyttäjä, %{tilaus | hinta: hinta }]]
end
defp shapepricemodifier([user, order]) do
%{widget: widget, price: currentprice} = tilaus.
modifier = WidgetRepo.getshapeprice(widget)
[user, %{tilaus | hinta: currentprice * modifier}] ]
end
defp rushpricemodifier([user, order]) do
%{rush: rush, price: currentprice} = tilaus
if rush do
[user, %{order | price: currentprice * 1.75} ] ]
else
[user, %{order | price: current_price}] ]
end
end
defp holidaypricemodifier([user, order]) do
%{date: date, price: currentprice} = tilaus
modifier = HolidayRepo.getholidaymodifier(user, date)
[user, %{order | price: currentprice * modifier}]]
end
end
```
Voit huomata, että se on täysin havainnollinen esimerkki siitä, miten käyttäjätarina voidaan todellisuudessa toteuttaa. Tämä johtuu siitä, että se on vähemmän suusanallinen kuin se olisi Rubyssä. Käytämme muutamia Elixirille ominaisia (mutta yleisesti funktionaalisissa kielissä käytettävissä olevia) ominaisuuksia:
Puhtaat toiminnot. Emme itse asiassa muuta saapuvaa tilaus
lainkaan, vaan luomme vain uusia kopioita - uusia iteraatioita alkutilasta. Emme myöskään hyppää sivuun muuttamaan mitään. Ja vaikka haluaisimmekin, tilaus
on vain "tyhmä" kartta, emme voi kutsua order.save
missään vaiheessa, koska se ei yksinkertaisesti tiedä, mitä se on.
Kuvion täsmäytys. Melko samanlainen kuin ES6:n rakenneuudistus, tämä antaa meille mahdollisuuden poimia hinta
ja widget
pois tilauksen ja välittää sen eteenpäin, sen sijaan, että pakottaa kaverimme WidgetRepo
ja HolidayRepo
tietää, miten käsitellä täyttä tilaus
.
Putken käyttäjä. Nähdään price_order
, sen avulla voimme siirtää dataa funktioiden läpi eräänlaisessa "putkessa" - käsite, joka on välittömästi tuttu kaikille, jotka ovat koskaan käyttäneet ps aux | grep postgres
tarkistaakseni, toimiiko se pirun vehje vielä.
Näin sinä ajattelet
Sivuvaikutukset eivät oikeastaan ole perustavanlaatuinen osa ajatusprosessiamme. Kun olet kaatanut vettä kuppiin, et yleensä ole huolissasi siitä, että vedenkeittimessä oleva virhe saattaisi aiheuttaa sen ylikuumenemisen ja räjähtämisen - et ainakaan niin paljon, että lähtisit tutkimaan sen sisäosia tarkistaaksesi, eikö joku ole vahingossa jättänyt explode_after_pouring
käännetty korkealle.
Tie koodaajasta ohjelmistosuunnittelijaksi - siirtyminen objekteista ja tiloista huolehtimisen sijaan tietovirroista huolehtimiseen - voi joissakin tapauksissa kestää vuosia. OOP-kasvatuksen saaneelle itselleni se vei ainakin kauan. Funktionaalisten kielten avulla pääsee ajattelemaan tietovirtoja. ensimmäisenä iltana.
Olemme tehneet ohjelmistotekniikka monimutkaista meille itsellemme ja jokaiselle uudelle alalle tulijalle. Ohjelmoinnin ei tarvitse olla vaikeaa ja aivoja raastavaa. Se voi olla helppoa ja luonnollista.
Ei tehdä tästä monimutkaista, vaan siirrytään jo toiminnallisiin asioihin. Sillä niin me ajattelemme.
Lue myös: