S množstvím bezplatných zdrojů, knih, online kurzů a táborů kódování, které jsou nyní k dispozici, se může naučit kódovat každý. Mezi kódováním a softwarovým inženýrstvím však stále existuje kvalitativní propast. Musí nějaký být?
Svůj první "Hello world" jsem napsal před více než dvaceti lety - to je odpověď, kterou dám, když se mě někdo zeptá, jak dlouho už programuju. Posledních deset let si užívám kariéru, která mě nutí dotýkat se kód téměř každý den - to je odpověď, kterou dám na otázku, jak dlouho jsem profesionálním programátorem.
Jak dlouho jsem byl softwarový inženýr? Řekl bych, že asi pět let. Počkejte, ta čísla mi nějak nesedí! Tak co se změnilo? Koho bych měl považovat za softwarového inženýra a koho "jen" za programátora?
Definice softwarového inženýra
Kódování je poměrně snadné. Už to není jen o mnemotechnice v assembleru na směšně omezených systémech. A pokud používáte něco tak expresivního a výkonného, jako je např. Ruby, je to ještě jednodušší.
Stačí si vzít lístek, najít místo, kam potřebujete vložit kód, zjistit, jakou logiku tam potřebujete vložit, a bum - hotovo. Pokud jste o něco pokročilejší, ujistíte se, že je váš kód hezký. Logicky rozdělený do metod. Má slušné specifikace, které netestují jen šťastnou cestu. To je to, co dělá dobrý kodér.
Softwarový inženýr už nepřemýšlí v metodách a třídách, alespoň ne primárně. Podle mých zkušeností softwarový inženýr přemýšlí v tocích. Vidí především bouřlivou, dravou řeku dat a interakcí, která se valí systémem. Přemýšlí o tom, co je třeba udělat, aby tento tok odklonili nebo změnili. Pěkný kód, logické metody a skvělé specifikace přicházejí na řadu téměř dodatečně.
Želvy až na dno
Lidé obecně uvažují určitým způsobem o většině interakcí s realitou. Z nedostatku lepšího termínu tomu říkejme "pohled shora dolů". Pokud to, na čem můj mozek pracuje, je připravit si šálek čaje, nejprve si vymyslí obecné kroky: jít do kuchyně, postavit konvici, připravit šálek, nalít vodu, vrátit se ke stolu.
Nebude nejdřív zjišťovat, který hrnek použít jako první, když budu stát jako v zóně u stolu; to přijde později, až budu stát před skříní. Nebude brát v úvahu, že by nám mohl dojít čaj (nebo přinejmenším, že došel). dobré věci). Je široký, reaktivní a náchylný k chybám. Celkově vzato - velmi člověk v přírodě.
Když softwarový inženýr zvažuje změny poněkud matoucího toku dat, bude to přirozeně dělat podobným způsobem. Uvažujme tento příklad uživatelského příběhu:
Zákazník si objedná widget. Při oceňování objednávky je třeba vzít v úvahu následující skutečnosti:
- Základní cena widgetu v místě uživatele
- Tvar widgetu (modifikátor ceny)
- Zda se jedná o spěšnou objednávku (modifikátor ceny).
- Zda se doručení objednávky uskuteční ve svátek v místě uživatele (modifikátor ceny)
Může se to zdát vymyšlené (a samozřejmě je to tak), ale není to daleko od některých skutečných uživatelských příběhů, které jsem měl v poslední době potěšení rozdrtit.
Nyní si projdeme myšlenkový proces, který by mohl softwarový inženýr použít k řešení tohoto problému:
"Musíme získat uživatele a jeho objednávku. Pak začneme počítat celkovou částku. Začneme od nuly. Pak použijeme modifikátor tvaru widgetu. Pak se provede poplatek za spěch. Pak se podíváme, jestli je svátek, a bum, před obědem je hotovo!".
Ach, ten spěch, který může přinést jednoduchý uživatelský příběh. Ale softwarový inženýr je jenom člověk, ne dokonalý vícevláknový stroj, a výše uvedený recept je jenom obecný. Inženýr pak pokračuje v hlubším přemýšlení:
"Modifikátor tvaru widgetu je... aha, to je velmi závislé na widgetu, že ano. A mohou se lišit podle lokality, i když ne teď, tak v budoucnu," si myslí, že se dříve spálili kvůli měnícím se obchodním požadavkům, "a může být i poplatek za spěch. A svátky jsou také super specifické pro danou lokalitu, augh, a časová pásma budou zapojena! O řešení časů v různých časových pásmech jsem tu měl článek v článku Rails tady... ooh, Zajímalo by mě, jestli je čas objednávky uložen se zónou v databázi! Raději zkontrolujte schéma."
Dobře, softwarový inženýre. Zastavte. Měl by sis uvařit čaj, ale ty jsi se usadil před skříňkou a přemýšlíš, jestli se ten květovaný šálek vůbec dá použít na tvůj problém s čajem.
Uvaření dokonalého šálku widget
Ale to se snadno může stát, když se snažíte dělat něco tak nepřirozeného pro lidský mozek, jako je přemýšlení v několika hloubkách detailů. současně.
Po krátkém prohledání jejich rozsáhlého arzenálu odkazů týkajících se práce s časovými pásmy se náš inženýr vzpamatuje a začne to rozebírat do skutečného kódu. Kdyby zkusili naivní přístup, mohlo by to vypadat asi takto:
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
A takhle rozkošně procedurálně pokračovali dál a dál, jen aby byli při první revizi kódu tvrdě odstaveni. Protože když se nad tím zamyslíte, je naprosto normální uvažovat tímto způsobem: nejprve široké tahy a detaily až mnohem později. Ani jste si nemysleli, že jste na začátku vypadli z dobrého čaje, že ne?
Náš inženýr je však dobře vyškolený a objekt Service Object mu není cizí, takže se místo toho začne dít toto:
třída BaseOrderService
def self.call(user, order)
new(user, order).call
konec
def initialize(user, order)
@user = user
@order = order
end
def call
puts "[WARN] Implementovat nestandardní volání pro #{self.class.name}!"
user, order
end
end
class WidgetPriceService < BaseOrderService; end
třída ShapePriceModifier < BaseOrderService; end
třída RushPriceModifier < BaseOrderService; end
třída HolidayDeliveryPriceModifier < BaseOrderService; end
třída 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
konec
konec
```
Dobře! Nyní můžeme použít dobrý TDD, napsat pro něj testovací případ a doplnit třídy, dokud všechny kousky nezapadnou na své místo. A bude to krásné.
Stejně jako je naprosto nemožné o tom uvažovat.
Nepřítelem je stát
Jistě, jsou to všechno dobře oddělené objekty s jedinou odpovědností. Ale tady je problém: stále jsou to objekty. Vzor servisních objektů se svým "násilně předstírat, že tento objekt je funkce" je ve skutečnosti berlička. Nikomu nic nebrání v tom, aby zavolal HolidayDeliveryPriceModifier.new(user, order).something_else_entirely. Nic nebrání tomu, aby lidé do těchto objektů přidávali vnitřní stav.
Nemluvě o tom, že uživatel a objednávka jsou také objekty, s nimiž si lze snadno pohrávat, jako když se někdo vplíží do rychlého order.save někde v těchto jinak "čistých" funkčních objektech, které mění základní zdroj pravdy, tj. stav databáze. V tomto vymyšleném příkladu o nic nejde, ale určitě se to může vymstít, pokud tento systém poroste na složitosti a rozšíří se o další, často asynchronní části.
Inženýr měl správný nápad. A použil velmi přirozený způsob vyjádření této myšlenky. Ale vědět, jak tuto myšlenku vyjádřit - krásným a snadno zdůvodnitelným způsobem - mu docela málem znemožnil základní OOP paradigma. A pokud se někdo, kdo ještě neudělal krok k vyjadřování svých myšlenek jako odklonů toku dat, pokusí méně obratně měnit základní kód, stane se něco špatného.
Stát se funkčně čistým
Kéž by existovalo paradigma, ve kterém by vyjadřování myšlenek pomocí datových toků bylo nejen snadné, ale i nezbytné. Kdyby bylo možné uvažovat jednoduše, bez možnosti zavádět nežádoucí vedlejší efekty. Kdyby data mohla být neměnná, stejně jako květovaný šálek, do kterého si vaříte čaj.
Ano, samozřejmě si dělám legraci. Toto paradigma existuje a nazývá se funkcionální programování.
Podívejme se, jak by výše uvedený příklad mohl vypadat v oblíbeném softwaru Elixir.
defmodule WidgetPrices do
def priceorder([user, order]) do
[user, order]
|> widgetprice
|> shapepricemodifier
|> rushpricemodifier
|> holidaypricemodifier
konec
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([user, order]) do
%{rush: rush, price: currentprice} = order
if rush do
[user, %{order | price: currentprice * 1.75} ]
jinak
[user, %{order | price: current_price} ] ]
end
end
defp holidaypricemodifier([user, order]) do
%{date: date, price: currentprice} = order
modifier = HolidayRepo.getholidaymodifier(user, date)
[uživatel, %{objednávka | cena: currentprice * modifier}]
konec
konec
```
Můžete si všimnout, že se jedná o plnohodnotný příklad toho, jak lze uživatelského příběhu skutečně dosáhnout. Je to proto, že je to menší sousto, než by bylo v jazyce Ruby. Používáme několik klíčových funkcí, které jsou jedinečné pro jazyk Elixir (ale jsou obecně dostupné ve funkcionálních jazycích):
Čisté funkce. Ve skutečnosti neměníme příchozí objednávka vůbec, pouze vytváříme nové kopie - nové iterace počátečního stavu. Neodskakujeme ani do strany, abychom něco změnili. A i kdybychom chtěli, objednávka je jen "hloupá" mapa, nemůžeme volat order.save v žádném okamžiku, protože jednoduše neví, co to je.
Porovnávání vzorů. Podobně jako destrukce v ES6 to umožňuje. nás vytrhnout cena a widget z objednávky a předat ji dál, místo abychom nutili naše kamarády. WidgetRepo a HolidayRepo vědět, jak se vypořádat s plnou objednávka.
Provozovatel potrubí. Viděno v price_order, umožňuje nám předávat data přes funkce v jakémsi "potrubí" - koncept, který je okamžitě známý každému, kdo někdy spustil program. ps aux | grep postgres aby zkontroloval, zda ta zatracená věc stále běží.
Takto přemýšlíte
Nežádoucí účinky ve skutečnosti nejsou základní součástí našeho myšlenkového procesu. Po nalití vody do šálku se zpravidla neobáváte, že by chyba v konvici mohla způsobit její přehřátí a výbuch - alespoň ne natolik, abyste se šli hrabat v jejích vnitřnostech a kontrolovali, zda tam někdo nedopatřením nenechal vodu. explode_after_pouring obrácený do výšky.
Cesta od programátora k softwarovému inženýrovi, který se přestane starat o objekty a stavy a začne se zabývat toky dat, může v některých případech trvat roky. Pro mě, který jsem se věnoval OOP, určitě ano. S funkcionálními jazyky se dostanete k přemýšlení o tocích. první noc.
Udělali jsme softwarové inženýrství komplikované pro nás i pro každého nováčka v oboru. Programování nemusí být těžké a náročné na mozek. Může být snadné a přirozené.
Nedělejme to složité a přejděme už na funkční řešení. Protože takhle přemýšlíme.
Přečtěte si také: