Resource Representations

Przenoszę dyskusję tutaj z offtopic w wątku “Staż/praktytka Ruby on Rails”

Plik README opisuje tylko mały fragmencik pomysłu, dość by można zacząć pracę.

Nie lubię proceduralnego podejścia helperów. Jeden płaski namespace powoduje, że albo helper staje się bardziej skompliowany niż trzeba albo muszę tworzyć wiele helperów o coraz bardziej skomplikowanych nazwach:

[code=ruby]description(user)
description(profile) # description musi rozróżnić z jakim modelem ma do czynienia, słaba czytelność kodu

alternatywa

user_description(user)
profile_description(profile) #redundancja przy użyciu[/code]
Ja chcę czegoś takiego:

user.description profile.description
Co ważne: user i profile w tym wypadku to nie modele ActiveRecord ale ich reprezentacje.

Sevos słusznie zwrócił uwagę, że pisanie:

- user = r(@user)

nie wygląda najlepiej. Dlatego kolejnym krokiem będzie rozszerzenie resource_controller tak, by automatycznie opakowywało objekty ActiveRecord w odpowiednie reprezentacje i tylko reprezentacje przekazywało do widoku. Dodatkowym bonusem takiego rozwiązania może być niejako przy okazji wyłączenie destrukcyjnych metod z ActiveRecord tak, by nie dało się we view zrobić user.delete.

Moim zdaniem z każdym rodzajem danych (model ActiveRecord, jego pole typu String, Time czy Bool) należy powiązać metody prezentacyjne. String domyślnie powinien się escapować - ale też mieć reprezentację w postaci input. Natomiast nie uważam, żeby dobrym pomysłem było dodawanie takich metod do klas bazowych. Stąd pomysł:

Opakowujemy ActiveRecord (np. user) w ActiveRecordRepresentation (user_representation), prosty obiekt proxy. Gdy wywołamy na nim dowolną metodę jest ona przekazywana do objektu oryginalnego. To co zwrócił user jest następnie opakowywane w kolejną reprezentację, zależną od typu zwracanej danej.

Gdy wywołamy user_representation.login to otrzymamy objekt StringRepresentation. StringRepresentation oferuje metody: to_s (która zwraca oryginalny login potraktowany html_escape), text_field (tworzy input z odpowiednim name) i pozostałe. W ten sposób w widoku wybieramy jak chcemy przedstawić dane pole:
Kod: ruby

user.login # => ERB/Haml uruchamia to_s, otrzymujemy login potraktowany html_escape user.login.text_field # => tworzy pole input typu text user.login.label # => tworzy label z odpowiednim id oraz domyślnym tekstem 'Login' user.login.label("Nazwa użytkownika", :class => 'login') # => tworzy label z własnym tekstem i klasą CSS
Do tego w katalogu /app/representations można dodawać lub nadpisywać domyślne metody, np. DateRepresentation.to_s może wyświetlać aktualny czas w formacie wygodnym dla docelowych użytkowników (np. mm/dd/yy lub dd/mm/yy).

Nie chcę mieć kolejnego sprytnego/semantycznego form buildera, takie rozwiązania już istnieją. Chcę móc wywoływać metody w sposób naturalny dla każdego innego obszaru Ruby, czyli pisać user.form zamiast form_for(user). Nie interesuje mnie też na razie super szybkość i zużycie pamięci a jedynie wygoda korzystania.

Rozwijając myśl: każda reprezentacja wywołana z blokiem zwraca siebie. To pozwala na jeszcze większe DRY:

- user.login do |login| %p= login # => login potraktowany html_escape %dl %dt= login.label # => tworzy label z odpowiednim id oraz domyślnym tekstem 'Login' %dd= login.text_field # => tworzy pole input typu text end
Jest to szczególnie wygodne przy tworzeniu pól dla zagnieżdżonych obiektów:

- user.profile do |profile| %fieldset %legend= "Your profile" %dl %dt= profile.first_name.label %dt= profile.first_name.text_field #=> zwraca input z name user[profile][first_name]

[quote=Bragi]Nie lubię proceduralnego podejścia helperów. Jeden płaski namespace powoduje, że albo helper staje się bardziej skompliowany niż trzeba albo muszę tworzyć wiele helperów o coraz bardziej skomplikowanych nazwach:

[code=ruby]description(user)
description(profile) # description musi rozróżnić z jakim modelem ma do czynienia, słaba czytelność kodu

alternatywa

user_description(user)
profile_description(profile) #redundancja przy użyciu[/code]
[/quote]
Zastanawiałeś się, claczego musisz rozróżniać pomiędzy description a description? Skoro ma taką samą nazwę, to powinny zachowywać się tak samo

f.text_area :description

I w takim poleceniu powinno się zawierać tworzenie pola tekstowego wraz z odpowiednią etykietą oczywiście.

[quote=Bragi]Ja chcę czegoś takiego:

user.description profile.description
Co ważne: user i profile w tym wypadku to nie modele ActiveRecord ale ich reprezentacje.[/quote]
To jest dopiero pomieszanie widoku z modelem. W jednym obiekcie chcesz zamknąć model wraz z jego reprezentacją.
Trend (tak wiem, wynalazcy szli pod prąd :wink: ) jest taki, by rozdzielać nawet logikę widoku od niego samego:
http://www.rubyinside.com/mustache-for-logicfree-views-in-your-ruby-web-apps-2599.html

Proponujesz wprowadzić dość sztuczny moim zdaniem obiekt reprezentacji. Dokładanie kolejnej warstwy abstrakcji niepotrzebnie skomplikuje stos tworzenia aplikacji. Generalnie jestem zwolennikiem nazywania rzeczy po imieniu:

  1. operuję na użytkowniku i pobieram dane od użytkownika (model)
  2. na stronie (widoku) widzę formularz dla użytkownika, który posiada pole tekstowe powiazane z polem name w modelu, którego formularz dotyczy, nie zaś użytkownika.

Nie znoszę rozwiązań mądrzejszych od programisty; jeżeli masz kogoś, kto używa takich metod w widoku to natychmiast go wymień!

To jest moim zdaniem absolutnie niedopuszczalne. Zostawmy modele w spokoju.

[quote=Bragi]Stąd pomysł: Opakowujemy ActiveRecord (np. user) w ActiveRecordRepresentation (user_representation), prosty obiekt proxy. Gdy wywołamy na nim dowolną metodę jest ona przekazywana do objektu oryginalnego. To co zwrócił user jest następnie opakowywane w kolejną reprezentację, zależną od typu zwracanej danej.

Gdy wywołamy user_representation.login to otrzymamy objekt StringRepresentation. StringRepresentation oferuje metody: to_s (która zwraca oryginalny login potraktowany html_escape), text_field (tworzy input z odpowiednim name) i pozostałe. W ten sposób w widoku wybieramy jak chcemy przedstawić dane pole:[/quote]
Podstawą jest wciąż nazywanie rzeczy po imieniu. Drażni cię obecnie istnienei helperów per controller, chciałbyś natomiast grupować helpery per model. Pomysł fajny - kupuję. Zostawiłbym natomiast nieco swobody użytkownikowi, co do wyboru sposobów reprezentacji i popracował nad sposobem dostarczenia tych obiektów do widoku.

[quote=Bragi]user.login # => ERB/Haml uruchamia to_s, otrzymujemy login potraktowany html_escape user.login.input # => tworzy pole input typu text user.login.label # => tworzy label z odpowiednim id oraz domyślnym tekstem 'Login' user.login.label("Nazwa użytkownika", :class => 'login') # => tworzy label z własnym tekstem i klasą CSS
Do tego w katalogu /app/representations można dodawać lub nadpisywać domyślne metody, np. DateRepresentation.to_s może wyświetlać aktualny czas w formacie wygodnym dla docelowych użytkowników (np. mm/dd/yy lub dd/mm/yy).[/quote]
Takie coś wymusza chyba stosowanie takiego generowania złożonych łańcuchów znaków:

zmienna = "#{user.login} - #{user.email}"

Musiałbym pomyśleć nad pozostałymi konsekwencjami.

Po to jest MVC, by w tych obszarach operować pojęciami specyficznymi dla każdego z nich.
Od warstwy bazy danych: tabel, kolumn, rekordów, uciekamy poprzez modele, które nam dostarczają REPREZENTACJI obiektów rzeczywistych w postaci abstrakcji utworzonych dla potrzeb projektu (z konkretnego punktu widzenia).
Kontrolery operują na tych obiektach, liczą oraz przygotowują dane dla widoków.
Widoki (także partiale) służą do REPREZENTACJI modeli lub wyników pracy kontrolerów w formie przyswajalnej przez użytkownika.

No rzeczywiście ma to kilka ciekawych pomysłów i nieszablonowe podejście. Ale może często lepiej użyć np. Liquid i udostępnić tylko niektóre metody widokom przy pomocy “to_liquid” jeśli chcemy schować metody destrukcyjne. Ruby to ruby i niezależnie czy z widoku czy z kontrolera czy z modelu, zawsze będzie można namieszać i wykonać coś niedozwolonego, ale jak rozumiem ta funkcjonalność jest tylko “przy okazji”.

Czy tylko mi się to kojarzy ze słynnymi djangowymi formularzami “generowanymi przez model”? Bragi, skojarzenie poprawne?

Skojarzenie BARDZO nie poprawne. Więcej: dokładnie tego chcę uniknąć. Model to model, jest w nim miejsce na logikę biznesową i już.

Metody prezentacyjne, nawet proste i popularne:

def full_name [name, surname].compact.join(" ") end
nie powinny znajdować się w modelu. Ich miejsce jest właśnie w reprezentacji.

Reprezentacje oddzielają wszystko co związane z widokiem od modelu i przenoszą to do osobnej klasy. Obiekt reprezentacji zastępuje helpery a proszony o poszczególne pola deleguje zapytania do modelu, zwracane wartości opakowując w odpowiednie reprezentacje. Natomiast sam z siebie nie robi więcej. To nie jest automagiczny builder, który zrobi za Ciebie formularz. Tu nadal we view musisz dzielnie samemu napisać gdzie ma się pojawić input.

AFAIR w Django generowanie formularza i innych tekstowo-htmlowych reprezentacji siedzi w osobnej klasie (czy osobnym module), dostępnej “przezroczyście” wyłącznie w warstwie widoku. Stąd moje skojarzenie.

http://www.rubyinside.com/mustache-for-logicfree-views-in-your-ruby-web-apps-2599.html

Podobny pomysł.

Można już ściągnąć wczesną wersje gema Representations. Co gem potrafi:

[quote]Representations change syntax to object oriented and model specific.
Example usage

Rails helpers:

  • form_for(user) do |f|
    login:
    = h(user.login)
    = f.label(:email, “Email”)
    = f.text_field(:email)
    • if user.profile
      • fields_for(user.profile) do |p|
        Full name:
        = h(full_name§)
        = f.label(:first_name, “First name”)
        = f.text_field(:first_name)
        = f.label(:last_name, “Last name”)
        = f.text_field(:last_name)
        = f.radio_button(:eye_color, ‘blue’)
        = f.label(:eye_color_blue, “Blue”)
        = f.submit(“Submit”)

Representations

  • r(user).form do
    login:
    = user.login
    = user.email.label
    = user.email.text_field
    • user.profile do |p|
      = full_name§
      = p.first_name.label
      = p.first_name.text_field
      = p.last_name.label
      = p.last_name.text_field
      = p.eye_color.radio_button(‘blue’)
      = p.eye_color.radio_button_label(‘blue’, ‘Blue’)
      Extensions

Representations can be altered. For example to add new method DefaultRepresentation create file app/representations/default_representation.rb with the content:
module DefaultRepresentation
def new_method
some code
end
end
Nested attributes

  • user.children.each do |child|
    = child.name.label
    = child.name.text_field
    = child.delete_checkbox
    = child.delete_checkbox_label

Or even:

  • user.children.build do |child|
    = child.name.label
    = child.name.text_field[/quote]
    Instalacja:
gem install representations --source http://gemcutter.org

Strona domowa:
http://github.com/bragi/representations
Gemcutter:
http://gemcutter.org/gems/representations
Czekam na konstruktywną krytykę :wink: w tym wątku lub mejlem
malpa = “@”
“skimos00#{malpa}gmail.com

Obecnie wygląda to faktycznie ciekawiej, niż można było wywnioskować z tych wcześniejszych postów. Podejście jest faktycznie odmienne i sensowne.