Kuna praegu on saadaval nii palju tasuta vahendeid, raamatuid, veebikursusi ja kodeerimislaagreid, siis võib igaüks õppida kodeerima. Siiski on kodeerimise ja tarkvaratehnika vahel endiselt kvaliteedilõhe. Kas see peabki olema?
Kirjutasin oma esimese "Hello worldi" üle kahekümne aasta tagasi - see on vastus, mille ma annan, kui keegi küsib, kui kaua ma olen programmeerinud. Viimased kümme aastat olen nautinud karjääri, mis on mind puudutanud kood peaaegu iga päev - see on vastus, mille ma annan, kui minult küsitakse, kui kaua ma olen olnud professionaalne kooder.
Kui kaua ma olen olnud tarkvarainsener? Ma ütleksin, et umbes viis aastat. Oodake, need numbrid ei tundu kokku sobivat! Mis siis on muutunud? Keda ma pean tarkvarainseneriks ja keda "lihtsalt" kodeerijaks?
Tarkvarainseneri määratlus
Kodeerimine on suhteliselt lihtne. See ei ole enam kõik assembleri mnemoonika naeruväärselt piiratud süsteemides. Ja kui te kasutate midagi nii väljendusrikast ja võimsat kui Ruby, on see veelgi lihtsam.
Sa lihtsalt võtad pileti, leiad, kuhu sa pead oma koodi sisestama, mõtled välja loogika, mille sa pead sinna panema, ja boom - valmis. Kui olete veidi edasijõudnud, siis veendute, et teie kood on ilus. Loogiliselt jagatakse meetoditeks. On korralikud spetsifikatsioonid, mis ei testi ainult õnnelikku teed. Seda teeb hea kooder.
Tarkvarainsener ei mõtle enam meetodites ja klassides, vähemalt mitte peamiselt. Minu kogemuse kohaselt mõtleb tarkvarainsener voogudes. Nad näevad ennekõike läbi süsteemi möllavat, tormavat andmete ja interaktsiooni jõge. Nad mõtlevad, mida nad peavad tegema, et seda voolu ümber suunata või muuta. Ilus kood, loogilised meetodid ja suurepärased spetsifikatsioonid tulevad peaaegu tagantjärele.
See on kilpkonnad kogu tee alla
Inimesed mõtlevad üldiselt teatud viisil enamiku suhtlemise kohta tegelikkusega. Parema termini puudumisel nimetame seda "ülalt-alla" perspektiiviks. Kui minu aju töötab sellega, et saada endale tass teed, siis mõtleb ta kõigepealt välja üldised sammud: lähen kööki, panen veekeetja üles, valmistan tassi, valan vett, naasen töölaua juurde.
See ei leia kõigepealt välja, millist tassi esimesena kasutada, kui ma seisan oma laua taga, sest see tuleb hiljem, kui ma seisan kapi ees. See ei mõtle sellele, et meil võib olla tee otsas (või vähemalt ei ole enam teed hea asjad). See on laiaulatuslik, reaktiivne ja vigadele kalduv. Kokkuvõttes - väga inimene looduses.
Kui tarkvarainsener kaalub muudatusi mõnevõrra meeletus andmevoos, teeb ta seda loomulikult samamoodi. Vaatleme seda näite kasutaja lugu:
Klient tellib vidina. Tellimuse hinnakujundamisel tuleb arvesse võtta järgmist:
- Vidina baashind kasutaja asukohas
- Vidina kuju (hinnamuutja)
- kas tegemist on kiiretellimusega (hinnamuutja)
- Kas tellimuse kättetoimetamine toimub kasutaja asukohas puhkepäeval (hinnamuutja).
See kõik võib tunduda väljamõeldud (ja ilmselt ongi), kuid see ei erine kaugeltki mõnest tegelikust kasutaja loost, mida mul on olnud rõõm viimasel ajal purustada.
Nüüd vaatame läbi mõtteprotsessi, mida tarkvarainsener võiks kasutada selle probleemi lahendamiseks:
"Noh, me peame saama kasutaja ja tema tellimuse. Siis hakkame arvutama kogusummat. Alustame nullist. Siis rakendame vidina kuju modifikaatorit. Siis kiirustasu. Siis vaatame, kas see on puhkepäeval, boom, valmis enne lõunat!"
Ah, milline kiirustamine, mida lihtne kasutuslugu võib tekitada. Kuid tarkvarainsener on ainult inimene, mitte täiuslik mitmehäälne masin, ja ülaltoodud retsept on laialivalguv. Insener mõtleb siis edasi sügavamalt:
"Vidina kuju modifikaator on... oh, see on ju super sõltuv vidinast, eks ole. Ja need võivad olla erinevad iga lokaali kohta, isegi kui mitte praegu, siis tulevikus," nad arvavad, et nad on varem muutunud ärinõuete tõttu põletatud, "ja ka kiirustasu võib olla. Ja pühad on ka üliolulised, augh, ja ajavööndid on ka kaasatud! Mul oli siin artikkel erinevate ajavööndite aegade käsitlemisest Railsis siin... ooh, huvitav, kas tellimuse aeg salvestatakse andmebaasis koos tsooniga! Parem kontrollida skeemi."
Hea küll, tarkvarainsener. Lõpeta. Te peaksite tegema tassi teed, aga te istute tassikausi ees ja mõtlete, kas lilleline tass on üldse kohaldatav teie teeprobleemile.
Täiusliku tassi vidina valmistamine
Aga see võib kergesti juhtuda, kui üritad teha midagi inimaju jaoks nii ebaloomulikku kui mitmes detailsusastmes mõtlemine. samaaegselt.
Pärast lühikest rummu läbi nende avarate linkide arsenali seoses ajavööndi käitlemisega võtab meie insener end kokku ja hakkab seda tegelikuks koodiks lahtimurdma. Kui nad prooviksid naiivset lähenemist, võiks see välja näha umbes nii:
def calcul_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.kuju).modifier
...
end
Ja nad jätkasid ja jätkasid seda meeldivalt protseduurilist tegevust, et saada juba esimesel koodikontrollimisel tugevalt kinni pandud. Sest kui te mõtlete selle peale, siis on täiesti normaalne niimoodi mõelda: kõigepealt laiad jooned ja palju hiljem üksikasjad. Sa ei arvanudki, et oled alguses heast teest väljas, eks ole?
Meie insener on aga hästi koolitatud ja teenindusobjekt ei ole talle võõras, nii et selle asemel hakkab toimuma järgmine asi:
klass BaseOrderService
def self.call(kasutaja, tellimus)
new(user, order).call
end
def initialize(user, order)
@user = kasutaja
@order = order
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
```
Hea! Nüüd saame kasutada head TDD-d, kirjutada selle jaoks testjuhtumid ja täiustada klassid, kuni kõik tükid on paigas. Ja see saab ka ilus olema.
Nagu ka täiesti võimatu arutleda.
Vaenlane on riik
Muidugi, need on kõik hästi eraldatud objektid, millel on üksikud kohustused. Aga siin on probleem: need on ikkagi objektid. Teenuseobjektide muster oma "teeskleme, et see objekt on funktsioon" on tegelikult vaid abinõu. Mitte miski ei takista kedagi kutsumast HolidayDeliveryPriceModifier.new(user, order).something_else_entirely
. Mitte miski ei takista inimesi lisamast nendele objektidele sisemist olekut.
Rääkimata sellest, et kasutaja
ja tellimus
on samuti objektid ja nendega segamine on sama lihtne, kui keegi hiilib kiiresti sisse order.save
kuskil nendes muidu "puhtates" funktsionaalsetes objektides, muutes tõe allika, st andmebaasi, seisundit. Selles väljamõeldud näites ei ole see suur asi, kuid see võib kindlasti tagasi tulla, kui see süsteem muutub keerulisemaks ja laieneb täiendavateks, sageli asünkroonseteks osadeks.
Inseneril oli õige mõte. Ja kasutas selle idee väljendamiseks väga loomulikku viisi. Aga seda, kuidas seda ideed väljendada - ilusti ja kergesti mõistetavalt - teadis peaaegu et takistada aluseks olev OOP-paradigma. Ja kui keegi, kes ei ole veel teinud seda hüpet, et väljendada oma mõtteid andmevoo diversioonidena, püüab vähem oskuslikult muuta aluseks olevat koodi, juhtub halba.
Funktsionaalselt puhtaks muutumine
Oleks vaid olemas paradigma, kus oma ideede väljendamine andmevoogude kaudu ei oleks mitte ainult lihtne, vaid ka vajalik. Kui arutlemine oleks võimalik teha lihtsaks, ilma et oleks võimalik soovimatuid kõrvalmõjusid sisse tuua. Kui andmed võiksid olla muutumatud, nagu lilleline tass, millega te oma teed keedate.
Jah, ma muidugi teen nalja. See paradigma on olemas ja seda nimetatakse funktsionaalseks programmeerimiseks.
Vaatleme, kuidas ülaltoodud näide võiks välja näha isiklikus lemmikprogrammis Elixir.
defmodule WidgetPrices do
def priceorder([user, order]) do
[user, order]
|> widgetprice
|> shapepricemodifier
|> rushpricemodifier
|> holidaypricemodifier
end
defp widgetprice([user, order]) do
%{widget: widget} = order
price = WidgetRepo.getbase_price(widget)
[user, %{order | price: price }]
end
defp shapepricemodifier([user, order]) do
%{widget: widget, price: currentprice} = order
modifier = WidgetRepo.getshapeprice(widget)
[user, %{order | price: currentprice * modifier}] ]
end
defp rushpricemodifier([kasutaja, tellimus]) do
%{rush: rush, price: currentprice} = order
if rush do
[user, %{order | price: currentprice * 1.75}] ]
else
[user, %{order | price: current_price} ]
end
end
defp holidaypricemodifier([kasutaja, tellimus]) do
%{kuupäev: kuupäev, hind: jooksevhind} = order
modifier = HolidayRepo.getholidaymodifier(user, date)
[user, %{order | price: currentprice * modifier}]
end
end
```
Võiksite tähele panna, et see on täielikult lihastunud näide sellest, kuidas kasutaja lugu tegelikult saavutada. Seda seetõttu, et see on vähem suupärane kui see oleks Ruby's. Me kasutame mõningaid Elixirile ainuomaseid (kuid funktsionaalsetes keeltes üldiselt kättesaadavaid) põhifunktsioone:
Puhtad funktsioonid. Me ei muuda tegelikult sissetulevat tellimus
üldse, me lihtsalt loome uusi koopiaid - uusi iteratsioone algseisundist. Me ei hüppa ka kõrvale, et midagi muuta. Ja isegi kui me tahaksime, tellimus
on lihtsalt "rumal" kaart, me ei saa kutsuda order.save
mis tahes hetkel siin, sest ta lihtsalt ei tea, mis see on.
Mustri sobitamine. Pigem sarnaneb see ES6 destruktureerimisele, mis võimaldab meil pukseerida hind
ja vidin
ära tellimus ja anda see edasi, selle asemel, et sundida meie sõpru WidgetRepo
ja HolidayRepo
teada, kuidas tulla toime täis tellimus
.
Toruoperaator. Nähtud price_order
võimaldab see meil edastada andmeid läbi funktsioonide mingisuguse "torujuhtme" kaudu - mõiste, mis on kohe tuttav kõigile, kes on kunagi jooksutanud ps aux | grep postgres
et kontrollida, kas see neetud asi ikka veel töötab.
Nii mõtled sa
Kõrvaltoimed ei ole tegelikult meie mõtteprotsessi põhiline osa. Pärast vee tassi valamist ei muretse sa üldiselt selle pärast, et veekeetja võib vea tõttu üle kuumeneda ja plahvatada - vähemalt mitte nii palju, et minna selle sisemusse torkima, et kontrollida, kas keegi ei jätnud kogemata explode_after_pouring
keeratud kõrgele.
Tee programmeerijast tarkvarainseneriks - minnes objektide ja olekute muretsemisest andmevoogude muretsemisse - võib mõnel juhul võtta aastaid. OOP-õppe läbinud tõelisele inimesele kulus see kindlasti. Funktsionaalsete keelte puhul hakkate mõtlema andmevoogude üle. esimesel õhtul.
Me oleme teinud tarkvaratehnika keeruline meie endi ja iga uue tulija jaoks. Programmeerimine ei pea olema raske ja ajupingutust nõudev. See võib olla lihtne ja loomulik.
Ärme tee seda keeruliseks ja läheme juba funktsionaalseks. Sest nii me mõtleme.
Loe ka: