Tworzenie wielu rekordów jednocześnie

Tworzę kolejną inkarnację TODO, w której zamiast listy rzeczy do zrobienia mamy cele. Na każdy cel składa się wiele kroków. Dodałem możliwość tworzenia wielu kroków jednocześnie np. Jeżeli naszym celem jest przeczytanie książki, jako krok możemy wpisać “Przeczytać rozdział {1…10}”, co automatycznie utworzy 10 rekordów z odpowiednimi opisami:

https://github.com/enhavo/progresso/blob/master/app/models/step.rb

Zastanawiam się czy to rozwiązanie jest dobre. Na początku próbowałem utworzyć wszystkie rekordy w wierszu 15 i zwrócić false, żeby nie tworzyć dodatkowego rekordu, jednak ten sposób nie działa. Teraz tworzę n-1 rekordów w wierszu 15 i zmieniam część “{x…y}” łańcucha na y, ale mam mieszanie uczucia kiedy na to patrzę.

Czy istnieje bardziej eleganckie rozwiązanie, w którym będzie można łatwo dodać ograniczenie tworzonych rekordów?

Chodzi mi po głowie klasa StepBuilder, chociaż DHH zarzekał się kiedyś, że takie rozwiązania absolutnie nigdy nie są potrzebne i jeżeli taki problem występuje to coś nie jest zrobione “The Rails Way”.

http://codon.com/the-dhh-problem

Znam temat “software writera” :wink: Uważasz, że pomysł StepBuilder jest dobry?

<offtop>
to co koleś mówi jest bardzo z sensem, ale forma wizualna i slajdy odrzucają mnie na kilometr (zwłaszcza, że tego co pamiętam on czyta te slajdy, tylko ma na nich mało tekstu).
</offtop>

<ontop>
StepBuilder brzmi imho dobrze :slight_smile:
</ontop>

1 Like

Według mnie dużo jaśniej by było jeżeli miałbyś jakiś service object, który tworzy goal i wszystkie jego stepy. Wtedy możesz jasno powiedzieć coś w stylu

def create_step
  if @range = find_range
    create_multiple_steps
  else
    create_single_step
  end
end

Kod na pewno może być dużo lepszy i pewnie masz więcej warunków itd. W każdym wypadku od razu patrząc na kod wiesz co się dzieje. Jeżeli tego typu rzeczy wrzucasz do callbacków to jesteś na prostej drodze do momentu gdzie callbacków jest tyle, że już tak naprawdę nie wiesz co, gdzie, kiedy, i dlaczego się tworzy. I na przykład tworzysz sobie obiekt Goal z konsoli a potem się okazuje że wtedy tworzysz przy okazji 5 stepów, updateujesz usera, pytasz inny serwis o nowe tweety i na końcu zapisujesz coś do statystyk. Idąc po callbackach ciężko by Ci było to wszystko śledzić, dlatego jeżeli tworzenie jakiegoś modelu ciągnie za sobą inne akcje lubię do tego mieć oddzielny obiekt który jasno mówi jakie akcje są kolejno robione i pod jakimi warunkami.

A co do “The Rails Way” … no to tak naprawdę jest “The Basecamp Way” i nie do końca musi się sprawdzać w twojej aplikacji.

Jeszcze odnośnie callbacków to tak tematycznie:

Callbacks #1

Callbacks #2

1 Like

Dziękuję za pomoc. To było pouczające popołudnie :smile:

https://github.com/enhavo/progresso/commit/dfe8bfb33ac1a973cb192acdcb2a5bc17b5fbbdd

https://github.com/enhavo/progresso/blob/master/app/services/create_steps_for_goal.rb#L27 - brakuje Ci jeszcze transakcji wokół pętli. Jeśli jeden z creatów z jakiegoś powodu się nie powiedzie, to zostajesz z niespójną bazą danych.
Druga rzecz, to używanie funkcji statyczych i to jeszcze w połączeniu ze zmiennami instancji (które są tak naprawdę zmiennymi instancji klasy).

1 Like

Dzięki za wkład :slight_smile: Zmyliły mnie przykłady, w których jedyną wywoływaną metodą była call i chaniebnie wykorzystałem pierwszy pomysł, na jaki wpadłem.

https://github.com/enhavo/progresso/commit/85b2d8d08fcab7ed969c063d847c538ae6650914

https://github.com/enhavo/progresso/commit/f00e7c207ad39a819c620cec82e3e0515a105f06

Teraz jest elegancko :slight_smile: Można ewentualnie dopisać jescze metodę

def self.call goal, step_description
  new(goal, step_description).call
end

ale moim zdaniem niewiele to daje.

1 Like

Plusem tego jest późniejsza łatwość zastubowania/zamockowania tego w testach. (Disclaimer - przykłady w RSpecu)
Zamiast

instance = double
allow(CreateStepsForGoal).to receive(:new).
 with(expected_goal, expected_params).
 and_return(instance)
expect(instance).to receive(:call)

można zrobić

expect(CreateStepsForGoal).
  to receive(:call).with(expected_goal, expected_params)
2 Likes