Event sourcing, CQRS, a do tego message brokers

Event sourcing, CQRS, a do tego message brokers
LA
  • Rejestracja:ponad 5 lat
  • Ostatnio:9 miesięcy
  • Postów:112
0

Tak żeby potraktować ten temat jako skarbnicę pytań i odpowiedzi to mam pytanie:
Mam załóżmy agregat Order, pod niego mam jakiś OrderHandler - który odpowiada za publikowanie eventów, no i jakiś "recreate" agregatu na podstawie eventów z event store.
Teraz pytanie, bo mamy takowy OrderHandler, który wygląda tak:

Kopiuj
@Repository
public class OrderHandler {
    private final MongoTemplate mongoTemplate;

    public OrderHandler(final MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }

    public void publish(final DomainEvent event) {
        mongoTemplate.insert(event, "events");
    }
}

No i załóżmy, że teraz przy evencie OrderPlaced ma iść e-mail do usera z potwierdzeniem zamówienia. Oczywiście mój EmailSender jest to całkowicie wydzielony moduł, uniwersalny dla pozostałych. Mam w planach wysyłać po prostu za pomocą Kafka Streams DTO z danymi e-maila do tego drugiego modułu - reagowałby on automatycznie przy evencie OrderPlaced generując jeszcze jakiś "event" typu EmailSent, który leciałby Kafką do innego modułu.
No i tutaj jak to ograć? Zostawić OrderHandler jako abstrakcje, która będzie mi odpowiadała tylko za kontakt z Event Store, a wyżej dodać abstrakcję, która wykona mi całą pozostałą magię, czyli coś na zasadzie:

Kopiuj
class OrderPlacer {
   private final OrderHandler handler;
   private final ConfirmationEmailCreator creator;
  
  void placeOrder(PlaceOrder placeOrder) {
         orderHandler.handler(OrderPlaced.builder().///dane///
                                                     .build());
         confirmationEmailCreator.create(//tu przekaże dane, które będą potrzebne (adresat, treść itp.)); // i w środku wyślę jakimś Sourcem ten zbudowany wewnątrz DTO do modułu odp. za wysyłkę emaili?
  }
}
edytowany 1x, ostatnio: lavoholic
Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
2

To by mijało się trochę z celem używania eventów (poza używaniem eventów dla samego event sourcingu oczywiście). Robiąc coś takiego co zaproponowałeś wiążesz logikę biznesową z działaniami które należy wykonać po tym jak zamówienie już zostanie złożone. Zamiast tego, po zapisaniu eventów do event store możesz je publikować- czy to za pomocą jakiegoś mediatora w pamięci czy też przy użyciu brokera- i wtedy mieć handlery nasłuchujące konkretnego eventu. Na przykład właśnie handler obsługujący wysłanie maila do klienta. Ale co ważne to fakt że będzie się to działo już bez "wiedzy" logiki biznesowej odpowiedzialnej za obsługę składania zamówienia. Na tej samej zasadzie możesz uruchamiać inne procesy biznesowe. Np. moduł odpowiedzialny za pobranie płatności reagujący na event że zamówienie zostało złożone, i pobierający płatność od klienta. Innymi słowy eventy pochodzącego z jednego agregatu mogą być odpowiedzialne za "uruchamianie" innych agregatów- jakiś inny handler przechwyci event i utworzy command wysłany do nowego agregatu.

Ciekawi mnie ten fragment handlera

Kopiuj
    public void publish(final DomainEvent event) {
        mongoTemplate.insert(event, "events");
    }

Czy dobrze zgaduję że metoda publish jest wywoływana przez agregat, który przekazuje utworzony obiekt eventu do tej metody?


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
LA
  • Rejestracja:ponad 5 lat
  • Ostatnio:9 miesięcy
  • Postów:112
0

No dobrze, ale w takim razie muszę wyodrębnić jakoś, że tylko ten wybrany event będzie publikowany przez broker (chyba, że publikować wszystkie, a to nie będzie miało jakiegoś złego znaczenia z punktu widzenia wydajności/sensu).

Co do handlera to jest właśnie kolejne pytanie jak to w zasadzie powinno wyglądać (jakby cykl życia od requesta do response).
Dostaję strzał z API -> odbudowuję obecny stan(OrderHandler) -> wywołuję metodę reagującą na podaną komendę w tym agregacie (Order) -> wysyłam event do Store (jednocześnie publikując go jakimś b rokerem) -> co w zasadzie powinienem dostać przy wyjściu? - po prostu jakiś status bez body?

Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
2
lavoholic napisał(a):

No dobrze, ale w takim razie muszę wyodrębnić jakoś, że tylko ten wybrany event będzie publikowany przez broker (chyba, że publikować wszystkie, a to nie będzie miało jakiegoś złego znaczenia z punktu widzenia wydajności/sensu).

Pracowałem przy systemach gdzie publikowane były wszystkie eventy. Brokery takie jak np. RabbitMQ są "mądre" i świetnie sobie radzą z obsługą wielu eventów. Tym bardziej że eventy nadaje się do wielu odbiorców, i subskrypcje można skonfigurować tak że odbierają tylko eventy którymi są zainteresowane.

Co nie znaczy że nie można publikować eventów selektywnie. Tyle tylko że to wymaga więcej pracy administracyjnej, ponieważ za każdym razem kiedy pojawia się jakiś moduł który potrzebuje danego eventu to trzeba skonfigurować ten event żeby był publikowany, jeśli nie był on już skonfigurowany wcześniej. Reasumując- wydajnością martw się wtedy kiedy pojawiają się problemy wydajnościowe.

Co do handlera to jest właśnie kolejne pytanie jak to w zasadzie powinno wyglądać (jakby cykl życia od requesta do response).
Dostaję strzał z API -> odbudowuję obecny stan(OrderHandler) -> wywołuję metodę reagującą na podaną komendę w tym agregacie (Order) -> wysyłam event do Store (jednocześnie publikując go jakimś brokerem) -> co w zasadzie powinienem dostać przy wyjściu? - po prostu jakiś status bez body?

To jest dobre pytanie. Zazwyczaj właśnie agregat nie powinien nic zwracać, a to z kolei wnosi ze sobą szereg innych zmian- np. asynchroniczność po stronie klienta. Kiedy klient wysyła request do API i dostaje odpowiedź OK, to ta odpowiedź oznacza tylko tyle że request został przyjęty i wysłany do przetwarzania. Wtedy np. w UI widać jakąś ikonkę ładowania, a logika frontendu oczekuje na otrzymanie wyniku asynchronicznie. W .Net można to bardzo łatwo osiągnąć używając SignalR które dostarcza dwukierunkowej komunikacji klient-serwer. Nie wiem jakie są odpowiedniki tego w innych technologiach, np. w ekosystemie Javy.

Ale zakładając że nie możesz albo nie chcesz użyć takiego rozwiązania, wtedy możesz po prostu zwrócić wynik z agregatu (chociaż co niektórzy puryści złapali by się za głowy). Oczywiście zakładając że logika agregatu wykonuje się synchronicznie, w tym samym kontekście co request HTTP. To rozwiązanie oczywiście nie będzie działać jeśli agregat wykonuje się w tym samym kontekście tylko jako pierwszy element procesu, a następnie gdzieś w innym serwisie inny agregat musi coś zrobić zanim cokolwiek zostanie wyświetlone dla użytkownika. Wtedy znów wracamy do tego co napisałem wyżej, a więc zwracanie wyników do klienta asynchronicznie.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
LA
  • Rejestracja:ponad 5 lat
  • Ostatnio:9 miesięcy
  • Postów:112
0

Rozumiem, jednak kolejność jest poprawna? Czy zazwyczaj między to wchodzi coś jeszcze? Ciekawi mnie też fakt wykonywania logiki i walidacji w naszym agregacie (który ma nie być anemicznym modelem danych) - nasz agregat nie powinien mieć żadnych zależności do jakichś fasad itp., prawda? Co jeśli musimy zwalidowac coś co jest poza naszym agregatem? To znaczy, że coś źle zamodelowalem czy raczej takie sytuacje mają miejsce? Czy np. jeśli ktoś na moim Order dokonał już Payment to mój agregat Order powinien zareagować na ten event z zewnątrz i dokonać zmiany na agregacie zmieniając stan na OrderStare.PAID. Potem Shipment odpowiednio też zmienia OrderState na IN_DELIVERY, DELIVERED?

Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
3

Rozumiem, jednak kolejność jest poprawna?

Tak, moim zdaniem wygląda ok.

Czy zazwyczaj między to wchodzi coś jeszcze?

O tym za chwilę.

Ciekawi mnie też fakt wykonywania logiki i walidacji w naszym agregacie (który ma nie być anemicznym modelem danych) - nasz agregat nie powinien mieć żadnych zależności do jakichś fasad itp., prawda? Co jeśli musimy zwalidowac coś co jest poza naszym agregatem? To znaczy, że coś źle zamodelowalem czy raczej takie sytuacje mają miejsce?

Oczywiście że takie sytuacje mają miejsce, i tak- nie powinno to należeć do odpowiedzialności agregatu. Użyję klasycznego przykładu bo ten temat nieustannie przewija się w pytaniach osób wchodzących w świat agregatów/DDD. Otóż mamy następujący problem:

Każdy nowy user musi mieć unikalny adres email. Zakładając istnienie agregatu User, jak zagwarantować unikalność adresu email rejestrując nowego użytkownika?

Często pierwsze co przychodzi na myśli to wstrzyknięcie jakiegoś serwisu/repozytorium do agregatu. Jest to podejście błędne, ponieważ pogwałca podstawową zasadę granicy transakcyjności pojedynczego agregatu. No bo czym jest instancja agregatu User? Jest użytkownikiem. A użytkownik nie wie, ani wiedzieć nie może o wszystkich adresach email zarezerwowanych w systemie. I tu przechodzimy do meritum- do tego służą serwisy domenowe. Posiadają one logikę biznesową która wykracza poza granice pojedynczego agregatu. Taki serwis możesz np. wstrzyknąć do handlera, i wywołać go zanim wywołasz metodę agregatu.

Czy np. jeśli ktoś na moim Order dokonał już Payment to mój agregat Order powinien zareagować na ten event z zewnątrz i dokonać zmiany na agregacie zmieniając stan na OrderStare.PAID. Potem Shipment odpowiednio też zmienia OrderState na IN_DELIVERY, DELIVERED?

To trochę inna sprawa. W tym przypadku faktycznie coś reaguje na event z zewnątrz, ale nie jest to agregat (agregaty reagują na commany a nie eventy) a np. event handler. Ten event handler utworzy odpowiednią command którą wyśle do agregatu, np. CompleteOrder który zakończy się wyemitowaniem eventu OrderCompleted. Tutaj ważna uwaga: w opisanym przypadku będziesz miał tzw. choreografię (ang. choreography), ponieważ poszczególne elementy systemu (event handlers) reagują na eventy w różnych miejscach. Jest to zdecentralizowane zarządzanie procesem. Alternatywą dla tego jest tzw. orkiestracja (ang. orchestration) gdzie masz centralne moduły zarządzania procesami, twz. process managers lub sagas. Wtedy taki centralny zarządca- np. OrderingProcessManager- reaguje na event i na ich podstawie tworzy commandy wysyłane do agregatów, aż do zakończenia procesu.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
edytowany 2x, ostatnio: Aventus
LA
  • Rejestracja:ponad 5 lat
  • Ostatnio:9 miesięcy
  • Postów:112
0

Poczytałem o sagach i muszę przyznać, że trochę rozwiązuje to kłopotów, podobnie serwisy domenowe - za bardzo nie wiedziałem gdzie w hierarchii mogę je umieścić.
Teraz przyszły mi kolejne zapewne banalne pytania :D
Jak wygląda w takim podejściu ogólnie pojęte Security? Zakładając, że wszystko jest oddzielnym modułem (czy też mikroserwisem) to zapewne Security będzie całkowicie wydzielonym mikroserwisem tak aby ukryć naszą "czarną stronę zabezpieczeń" gdzieś w oddzielnym module. Tylko, że! Zakładając, że używam Springa czy da się takie coś w ogóle uzyskać? Bo jednak jeśli chodzi o to to mam wrażenie, że część Security w Springu jest trudna do ujarzmienia w ten sposób - wymusza często sprawdzenie per endpoint, a żeby skutecznie wyciągnąć dane o naszym Subject, który się dobija pod dany endpoint - dajemy w Controllerach jakiegoś Principala żeby dostać gotowe dane ewentualnego usera - tak więc jednak wpychamy część "brudu security" w nasze moduły, które są stricte biznesowe. (tutaj myślę, że wiele mógłby wnieść również @Shalom ze względu na to, że Springa Security bardzo dobrze zna). Kolejna rzecz mamy też często wspólne rzeczy z punktu widzenia biznesu, czyli np. Customer, User, Receiver, Payer - to jest praktycznie ta sama osoba, jednak ze względu na nasze bounded contexty jest to całkowicie co innego. Jak takie coś połączyć? Skąd mamy wiedzieć, że ten Customer to jest ten User i ten Receiver?

Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
1

To już podchodzi pod inne zagadnienie niż CQRS czy event sourcing więc nie ciągnął bym tematu w tym wątku. Ogólnie jeśli mowa o mikroserwisach to raczej ciężko się obejść bez użycia JWT. Ostatnio na ten temat było trochę w tych wątkach:

JWT, a bezpieczeństwo i trzymanie sesji
Zasada zabezpieczeń mikroserwisów

@lavoholic: wybacz, nie odpisałem wcześniej na Twoje drugie pytanie:

Kolejna rzecz mamy też często wspólne rzeczy z punktu widzenia biznesu, czyli np. Customer, User, Receiver, Payer - to jest praktycznie ta sama osoba, jednak ze względu na nasze bounded contexty jest to całkowicie co innego. Jak takie coś połączyć? Skąd mamy wiedzieć, że ten Customer to jest ten User i ten Receiver?

Dla tego w takich architekturach używa się globalnie unikalnych identyfikatorów (UUID) a nie np. intów. Tutaj małe sprostowanie- to nie jest tak że to całkowicie coś innego, tylko różne klasy różnie się do tego odwołują. To nadal ta sama osoba, a więc ID pozostaje to samo.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
edytowany 1x, ostatnio: Aventus
LA
  • Rejestracja:ponad 5 lat
  • Ostatnio:9 miesięcy
  • Postów:112
0

Cześć, pojawił się czas do powrotu do tematu, więc pojawiają się i pytania.
Z perspektywy takiego dodawania jakiegoś Item do Order przydałoby się walidować czy mamy na stanie Item o takim ID, w takiej ilości i czy jest w odpowiedniej cenie, prawda? Od usera powinniśmy dostawać coś takiego:

Kopiuj
class AddItem {
  UUID orderId;
  UUID itemId;
  Quantity quantity;
}

No i musimy sprawdzić czy mamy taki agregat (po OrderId) - to się odbędzie po prostu przy odbudowywaniu stanu z eventów. No, ale dalej mamy do sprawdzenia czy Item, który user chce dodać jest na stanie w odpowiedniej ilości, czy w ogóle taki istnieje - no i pobrać skądś jego cenę. Jak takie dane powinniśmy chować? Bez sensu chyba przechowywać całą historię zmian dla każdego Item (czy wtedy nie musielibyśmy go też traktować jako agregat?). Nie lepiej trzymać tylko aktualne stany tych Item? Jeśli tak to też musielibyśmy mieć jakąś bazę z aktualnym stanem (np. SQL) i jednocześnie event store (jakiś NoSQL) - wtedy mamy dwa źródła danych w zasadzie przy jednym wejściu do (dodaniu produktu do zamówienia). Nie prosimy się wtedy o jakieś problemy? @Aventus

Podobna sytuacja jest np. przy dodawaniu kodu rabatowego do zamówienia. Czy jest sens trzymać jakiegoś Customer jako historię eventów? Szczególnie, że module order nasz customer będzie miał raczej ubogie dane, więcej informacji będzie o nim raczej jako user lub recipient.
Z góry dzięki.

edytowany 1x, ostatnio: lavoholic
Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
1
lavoholic napisał(a)

Z perspektywy takiego dodawania jakiegoś Item do Order przydałoby się walidować czy mamy na stanie Item o takim ID, w takiej ilości i czy jest w odpowiedniej cenie, prawda?

To zależy od tego jak to chcesz zaimplementować. Owszem, możesz to sprawdzać w momencie dodawania Item do Order, ale możesz też sprawdzić czy wszystko się zgadza na sam koniec zamówienia. Jeśli chcesz to sprawdzać "w locie" to możesz zastosować serwis domenowy.

No i musimy sprawdzić czy mamy taki agregat (po OrderId) - to się odbędzie po prostu przy odbudowywaniu stanu z eventów. No, ale dalej mamy do sprawdzenia czy Item, który user chce dodać jest na stanie w odpowiedniej ilości, czy w ogóle taki istnieje - no i pobrać skądś jego cenę. Jak takie dane powinniśmy chować? Bez sensu chyba przechowywać całą historię zmian dla każdego Item (czy wtedy nie musielibyśmy go też traktować jako agregat?).

Musiałbyś, i całkiem możliwe że właśnie powinieneś.

Nie lepiej trzymać tylko aktualne stany tych Item? Jeśli tak to też musielibyśmy mieć jakąś bazę z aktualnym stanem (np. SQL) i jednocześnie event store (jakiś NoSQL) - wtedy mamy dwa źródła danych w zasadzie przy jednym wejściu do (dodaniu produktu do zamówienia). Nie prosimy się wtedy o jakieś problemy?

Tutaj znów wracamy do tego co- o ile dobrze pamiętam- już dyskutowaliśmy. Twój aktualny stan to tylko i wyłącznie agregaty które są ładowane za pomocą eventów. To co będziesz miał zapisane w bazie SQL (czy jakiejkolwiek innej) to tylko widok aktualnego stanu. Widok który jak już było wcześniej wspomniane może nie zawsze wskazywać najnowszy stan (eventual consistency),

Podobna sytuacja jest np. przy dodawaniu kodu rabatowego do zamówienia. Czy jest sens trzymać jakiegoś Customer jako historię eventów? Szczególnie, że module order nasz customer będzie miał raczej ubogie dane, więcej informacji będzie o nim raczej jako user lub recipient.

Customer może nie być najlepszym miejscem na to (chociaż niekoniecznie). Nic nie stoi jednak na przeszkodzie aby mieć lekki agregat od obsługiwania kodów rabatowych dla danego użytkownika, np. UserDisocuntsDetail. Ogólnie nie jest złą praktyką modelowanie "lekkich" agregatów obsługujących dany proces. Często jest to wręcz polecane. Preferuje się więcej mniejszych, wyspecjalizowanych agregatów, niż mało dużych które wiedzą wszystko.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)