Хората обичат да категоризират езиците за програмиране в парадигми. Съществуват обектно-ориентирани (ОО) езици, императивни езици, функционални езици и др. Това може да бъде полезно при определянето на това кои езици решават подобни проблеми и какви типове проблеми даден език е решен.
Във всеки случай парадигмата обикновено има един „основен” фокус и техника, които са движещата сила за това семейство езици:
В езиците OO това е клас или обект като начин за капсулиране на състояние (данни) с манипулиране на това състояние (методи).
Във функционалните езици това може да бъде манипулиране на функции себе си или неизменни данни предавани от функция на функция.
Докато Еликсир (и Ерланг преди него) често се категоризират като функционални езици, тъй като показват неизменните данни, общи за функционалните езици, бих казал, че представляват отделна парадигма от много функционални езици . Те съществуват и са приети поради съществуването на OTP и затова бих ги категоризирал като ориентирани към процесите езици .
В тази публикация ще разберем значението на това, което е ориентирано към процесите програмиране, когато използваме тези езици, ще изследваме разликите и приликите с други парадигми, ще видим последиците както за обучение, така и за приемане и ще завършим с кратък пример за програмиране, ориентирано към процеса.
Нека започнем с определение: Процесно ориентирано програмиране е парадигма, базирана на Комуникация на последователни процеси , първоначално от хартия на Тони Хоаре през 1977 г. Това също е популярно наричано актьор модел на съвпадение. Други езици, които имат някаква връзка с тази оригинална творба, включват Occam, Limbo и Go. Официалната статия се занимава само със синхронна комуникация; повечето актьорски модели (включително OTP ) използвайте и асинхронна комуникация. Винаги е възможно да се изгради синхронна комуникация върху асинхронната комуникация и OTP поддържа и двете форми.
В тази история OTP създаде система за издръжливост на изчисления чрез комуникация на последователни процеси. Съоръженията за устойчивост на неизправности идват от подхода „нека се провали“ със солидно възстановяване на грешки под формата на надзорни органи и използването на разпределена обработка, разрешена от модела на играча. „Нека се провали“ може да се противопостави на „предотвратяването му да се провали“, тъй като първият е много по-лесен за приспособяване и е доказано в OTP като много по-надежден от втория. Причината е, че усилията за програмиране, необходими за предотвратяване на грешки (както е показано в модела за изключения, проверени от Java), са много по-ангажирани и взискателни.
Така че, програмно-ориентираното програмиране може да се определи като a парадигма, в която структурата на процеса и комуникацията между процесите на системата са основните грижи .
При обектно-ориентираното програмиране основната грижа е статичната структура на данните и функциите. Какви методи са необходими за манипулиране на затворените данни и какви трябва да бъдат връзките между обекти или класове. По този начин диаграмата на класовете на UML е отличен пример за този фокус, както се вижда на Фигура 1.
Може да се отбележи, че често срещана критика към обектно-ориентираното програмиране е, че няма видим контролен поток. Тъй като системите са съставени от голям брой класове / обекти, дефинирани поотделно, за по-малко опитен човек може да бъде трудно да визуализира контролния поток на системата. Това важи особено за системи с много наследяване, които използват абстрактни интерфейси или нямат силно въвеждане. В повечето случаи става важно за разработчик да запомня голямо количество от системната структура, за да бъде ефективно (какви класове имат какви методи и кои се използват по какви начини).
Силата на обектно-ориентирания подход за развитие е, че системата може да бъде разширена, за да поддържа нови типове обекти с ограничено въздействие върху съществуващия код, стига новите типове обекти да отговарят на очакванията на съществуващия код.
Много езици за функционално програмиране адресират едновременността по различни начини, но основният им фокус е неизменните данни, предаващи се между функции, или създаването на функции от други функции (функции от по-висок ред, които генерират функции). В по-голямата си част фокусът на езика все още е едно адресно пространство или изпълним файл и комуникациите между такива изпълними файлове се обработват по специфичен за операционната система начин.
Например, Scala е функционален език изграден върху виртуалната машина Java. Въпреки че има достъп до Java съоръжения за комуникация, това не е неразделна част от езика. Въпреки че е общ език, използван в програмирането на Spark, той отново е библиотека, използвана заедно с езика.
Силна страна на функционалната парадигма е способността да се визуализира контролния поток на системата, като се дава функция от най-високо ниво. Потокът на управление е изричен, тъй като всяка функция извиква други функции и предава всички данни от една на друга. Във функционалната парадигма няма странични ефекти, което улеснява определянето на проблема. Предизвикателството с чистите функционални системи е, че 'страничните ефекти' трябва да имат постоянно състояние. В добре проектирани системи, запазването на състоянието се обработва на най-високото ниво на контролния поток, което позволява по-голямата част от системата да няма странични ефекти.
В Elixir / Erlang и OTP комуникационните примитиви са част от виртуалната машина, която изпълнява езика. Способността за комуникация между процесите и между машините е вградена и е в центъра на езиковата система. Това подчертава значението на комуникацията в тази парадигма и в тези езикови системи.
Докато еликсирният език е предимно функционален по отношение на логиката, изразена в езика, използването му е ориентирани към процеса .
Да бъдеш ориентиран към процеса, както е дефинирано в тази публикация, означава първо да проектираш система под формата на това какви процеси съществуват и как те комуникират. Един от основните въпроси е кои процеси са статични и кои динамични, които се раждат при поискване към заявки, които обслужват дългосрочна цел, които държат споделено състояние или част от споделеното състояние на системата и кои характеристики на системата по своята същност са едновременни. Точно както OO има типове обекти, а функционалният има типове функции, процесно ориентираното програмиране има типове процеси.
Като такъв, процес-ориентиран дизайн е идентифициране на набора от типове процеси, необходими за решаване на проблем или адресиране на нужда .
Аспектът на времето навлиза бързо в усилията за проектиране и изисквания. Какъв е жизненият цикъл на системата? Какви потребителски нужди са случайни и кои са постоянни? Къде е натоварването в системата и каква е очакваната скорост и обем? Едва след като се разберат тези видове съображения, процес-ориентираният дизайн започва да дефинира функцията на всеки процес или логиката, която трябва да бъде изпълнена.
Значението на тази категоризация за обучението е, че обучението трябва да започне не с езиков синтаксис или примери „Hello World“, а с системно инженерно мислене и дизайн фокус върху разпределението на процесите .
Проблемите с кодирането са второстепенни за процеса на проектиране и разпределение, които се решават най-добре на по-високо ниво и включват междуфункционално мислене за жизнения цикъл, QA, DevOps и бизнес изискванията на клиентите. Всеки курс за обучение по Elixir или Erlang трябва (и обикновено включва) да включва OTP и трябва да има ориентация на процеса от самото начало, а не като подхода „Сега можете да кодирате в Elixir, така че нека направим едновременно“.
Значението за приемане е, че езикът и системата се прилагат по-добре при проблеми, които изискват комуникация и / или разпространение на компютри. Проблемите, които представляват единично натоварване на един компютър, са по-малко интересни в това пространство и може да се решат по-добре с друг език. Дълговечните системи за непрекъсната обработка са основна цел за този език, тъй като той има толерантност към повреди, вградена от нулата.
За документация и проектиране може да бъде много полезно да се използва графична нотация (като фигура 1 за езиците OO). Предложението за Elixir и процесно-ориентираното програмиране от UML би било диаграмата на последователността (пример на фигура 2), за да покаже времеви връзки между процесите и да идентифицира кои процеси участват в обслужването на заявка. Не съществува тип UML диаграма за улавяне на жизнения цикъл и структурата на процеса, но би могъл да бъде представен с проста кутия и диаграма на стрелките за типовете процеси и техните взаимоотношения. Например, Фигура 3:
Накрая ще разгледаме кратък пример за прилагане на ориентация на процеса към даден проблем. Да предположим, че имаме за задача да осигурим система, която поддържа глобални избори. Този проблем е избран, тъй като много отделни дейности се извършват на интервали, но обобщаването или обобщаването на резултатите е желателно в реално време и може да види значително натоварване.
Първоначално можем да видим, че подаването на гласове от всеки отделен човек е излишен трафик към системата от много дискретни входове, не е подреден по време и може да има голямо натоварване. За да подкрепим тази дейност, бихме искали голям брой процеси, които всички събират тези данни и ги препращат към по-централен процес за таблициране. Тези процеси могат да бъдат разположени в близост до популациите във всяка държава, които биха генерирали гласове, и по този начин да осигурят ниска латентност. Те щяха да запазят местни резултати, незабавно да регистрират входящите си данни и да ги изпратят за групиране на групи, за да намалят честотната лента и режийните разходи.
Първоначално можем да видим, че ще трябва да има процеси, които да проследяват гласовете във всяка юрисдикция, в които трябва да се представят резултатите. Нека приемем за този пример, че трябва да проследяваме резултатите за всяка държава и във всяка държава по провинция / щат. За да подкрепим тази дейност, бихме искали поне един процес на държава, който извършва изчисленията и запазва текущите общи суми, и друг набор за всяка държава / провинция във всяка държава. Това предполага, че трябва да можем да отговорим на сумите за държава и държава / провинция в реално време или с ниска латентност. Ако резултатите могат да бъдат получени от система от бази данни, бихме могли да изберем различно разпределение на процесите, където сумите се актуализират чрез преходни процеси. Предимството на използването на специални процеси за тези изчисления е, че резултатите се получават със скоростта на паметта и могат да бъдат получени с ниска латентност.
И накрая, можем да видим, че много и много хора ще гледат резултатите. Тези процеси могат да бъдат разделени по много начини. Може да искаме да разпределим товара, като поставим процеси във всяка държава, отговорна за резултатите от тази държава. Процесите могат да кешират резултатите от изчислителните процеси, за да намалят натоварването на заявките върху изчислителните процеси, и / или изчислителните процеси могат да изтласкват резултатите си към правилните процеси на резултатите периодично, когато резултатите се променят със значително количество или след изчислителният процес става празен, което показва забавен темп на промяна.
И при трите типа процеси можем да мащабираме процесите независимо един от друг, да ги разпределяме географски и да гарантираме, че резултатите никога не се губят чрез активно потвърждаване на трансферите на данни между процесите.
Както беше обсъдено, ние започнахме примера с дизайн на процеса, независим от бизнес логиката във всеки процес. В случаите, когато бизнес логиката има специфични изисквания за агрегиране на данни или география, които могат да повлияят итеративно на разпределението на процеса. Нашият дизайн на процеса досега е показан на фигура 4.
Използването на отделни процеси за получаване на гласове позволява всеки глас да бъде получен независимо от всеки друг глас, регистриран при получаване и групиран към следващия набор от процеси, намалявайки значително натоварването на тези системи. За система, която консумира голямо количество данни, намаляването на обема данни чрез използване на слоеве от процеси е често срещан и полезен модел.
Извършвайки изчисленията в изолиран набор от процеси, ние можем да управляваме натоварването на тези процеси и да гарантираме тяхната стабилност и изисквания за ресурси.
Поставяйки представянето на резултата в изолиран набор от процеси, и двамата контролираме натоварването към останалата част от системата и позволяваме набора от процеси да се мащабира динамично за натоварване.
Сега, нека добавим някои усложняващи изисквания. Да предположим, че във всяка юрисдикция (държава или щат) преброяването на гласовете може да доведе до пропорционален резултат, резултат от победителите - всички или никакъв резултат, ако са подадени недостатъчно гласове спрямо населението на тази юрисдикция. Всяка юрисдикция има контрол върху тези аспекти. С тази промяна резултатите на страните не са просто обобщаване на суровите резултати от гласуване, а са обобщение на резултатите от държавата / провинцията. Това променя разпределението на процеса от оригинала, за да се изисква резултатите от процесите на държавата / провинцията да се подават в процесите в страната. Ако протоколът, използван между събирането на гласове и процесите на държава / провинция и провинция към държава, е един и същ, тогава логиката на агрегиране може да бъде използвана повторно, но са необходими отделни процеси, съдържащи резултатите и техните комуникационни пътища са различни, както е показано на фигура 5.
За да завършим примера, ще разгледаме изпълнението на примера в Elixir OTP. За да се опростят нещата, този пример предполага, че уеб сървър като Phoenix се използва за обработка на действителни уеб заявки и тези уеб услуги отправят заявки към процеса, идентифициран по-горе. Това има предимството да опрости примера и да запази фокуса върху Elixir / OTP. В производствената система това да бъдат отделни процеси има някои предимства, както и отделя проблеми, позволява гъвкаво внедряване, разпределя натоварването и намалява латентността. Пълният изходен код с тестове може да бъде намерен на https://github.com/technomage/voting . Източникът е съкратен в тази публикация за четливост. Всеки процес по-долу се вписва в дърво за наблюдение на OTP, за да се гарантира, че процесите се рестартират при отказ. Вижте източника за повече информация относно този аспект на примера.
Този процес получава гласове, записва ги в постоянен магазин и групира резултатите до агрегаторите. Модулът VoteRecoder използва Task.Supervisor за управление на краткотрайни задачи за запис на всеки глас.
defmodule Voting.VoteRecorder do @moduledoc ''' This module receives votes and sends them to the proper aggregator. This module uses supervised tasks to ensure that any failure is recovered from and the vote is not lost. ''' @doc ''' Start a task to track the submittal of a vote to an aggregator. This is a supervised task to ensure completion. ''' def cast_vote where, who do Task.Supervisor.async_nolink(Voting.VoteTaskSupervisor, fn -> Voting.Aggregator.submit_vote where, who end) |> Task.await end end
Този процес обединява гласовете в рамките на дадена юрисдикция, изчислява резултата за тази юрисдикция и препраща обобщенията на гласовете към следващия по-висок процес (юрисдикция от по-високо ниво или представящ резултат).
defmodule Voting.Aggregator do use GenStage ... @doc ''' Submit a single vote to an aggregator ''' def submit_vote id, candidate do pid = __MODULE__.via_tuple(id) :ok = GenStage.call pid, {:submit_vote, candidate} end @doc ''' Respond to requests ''' def handle_call {:submit_vote, candidate}, _from, state do n = state.votes[candidate] || 0 state = % votes: Map.put(state.votes, candidate, n+1) {:reply, :ok, [%{state.id => state.votes}], state} end @doc ''' Handle events from subordinate aggregators ''' def handle_events events, _from, state do votes = Enum.reduce events, state.votes, fn e, votes -> Enum.reduce e, votes, fn {k,v}, votes -> Map.put(votes, k, v) # replace any entries for subordinates end end # Any jurisdiction specific policy would go here # Sum the votes by candidate for the published event merged = Enum.reduce votes, %{}, fn {j, jv}, votes -> # Each jourisdiction is summed for each candidate Enum.reduce jv, votes, fn {candidate, tot}, votes -> Logger.debug '@@@@ Votes in #{inspect j} for #{inspect candidate}: #{inspect tot}' n = votes[candidate] || 0 Map.put(votes, candidate, n + tot) end end # Return the published event and the state which retains # Votes by jourisdiction {:noreply, [%{state.id => merged}], % votes: votes} end end
Този процес получава гласове от агрегатор и кешира тези резултати в сервизни заявки за представяне на резултати.
defmodule Voting.ResultPresenter do use GenStage … @doc ''' Handle requests for results ''' def handle_call :get_votes, _from, state do {:reply, {:ok, state.votes}, [], state} end @doc ''' Obtain the results from this presenter ''' def get_votes id do pid = Voting.ResultPresenter.via_tuple(id) {:ok, votes} = GenStage.call pid, :get_votes votes end @doc ''' Receive votes from aggregator ''' def handle_events events, _from, state do Logger.debug '@@@@ Presenter received: #{inspect events}' votes = Enum.reduce events, state.votes, fn v, votes -> Enum.reduce v, votes, fn {k,v}, votes -> Map.put(votes, k, v) end end {:noreply, [], %state } end end
Тази публикация изследва Elixir / OTP от неговия потенциал като ориентиран към процесите език, сравнява го с обектно-ориентирани и функционални парадигми и разглежда последиците от това за обучението и приемането.
Публикацията включва и кратък пример за прилагане на тази ориентация към примерен проблем. В случай, че искате да прегледате целия код, ето линк към нашия пример на GitHub отново, само за да не се налага да се превъртате назад в търсене.
Ключовият извод е да се разглеждат системите като съвкупност от комуникационни процеси. Първо планирайте системата от гледна точка на дизайна на процеса и второ от гледна точка на логическото кодиране.
Elixir е функционален език за програмиране, изграден върху виртуалната машина Erlang. OTP е програмно ориентирана рамка за програмиране, неразделна част от Erlang и Elixir.
Процесно-ориентираното развитие се фокусира първо върху структурата на процесите на системата, а втората - върху функционалната логика на системата.
Започнете с обучение или проучване, което се фокусира върху OTP и управлението на процесите и след това върху синтаксиса и функционалните аспекти на Elixir. Избягвайте обучение, което започва с примери за кодиране в здрав свят и стига до OTP само по средата.
Аспектите за надеждност и паралелност на Elixir / OTP са главите и раменете над конкуриращите се стекове, изисква по-малко умения на програмист, за да бъде опитен, и има по-добра производителност от кутията, отколкото Ruby on Rails или Node.
Elixir / OTP са за продължителни процеси или такива, които изискват многоядрена производителност. Те се фокусират повече върху ниска латентност, отколкото висока производителност. Те не биха били силни за взискателни едноядрени приложения за пропускателна способност или за приложения, които се изпълняват рядко в пакетна среда или среда на командния ред.