Programowanie obiektowe - problem z klasami

Cześć

Jestem początkującym w RoR. Mam już za sobą aplikacje w stylu TODO, monitorowanie wagi, wysyłanie maili, eksport do CSV, skracanie linków i kilka stron www. Całkiem dobrze radzę sobie z pisaniem skryptów w Ruby, które mają coś wykonać. Chciałbym rozpocząć pierwszą pracę związaną z RoR.

Niestety nie mam za sobą żadnego większego projektu i nie wiem w jaki sposób “przeskoczyć” do programowania obiektowego. Aktualnie przerabiam książkę Sandi Metz: Practical Object Oriented Design in Ruby. Książkę rozumiem, ale nie do końca wiem jak przełożyć ją na projekt w RoR.

W książce jako przykłady podawane są klasy, które nie dziedziczą po ApplicationRecord a są klasami same w sobie, tj. zamiast modelu:

class Bicycle < ApplicationRecord

mamy klasę

class Bicycle

a po niej dziedziczą inne subklasy, np. CityBike, MountainBike.

Gdzie w aplikacji Rails umieszcza się takie klasy? (w modelach? /app/lib ? albo lib?)

Na wyczucie wygenerowałem sobie model bez pól i usunąłem z niego dziedziczenie po ApplicationRecord. Uzupełniłem kodem. Dopisałem subklasy. I niby jest OK (w konsoli Rails obiekt odpowiada na komendy), ale nie wiem za bardzo co z takim obiektem można zrobić? Fajnie, że mogę zdefiniować metodę która wykona skrypt Ruby, ale nie wiem jak go teraz zapisać do bazy bo nie dziedziczy po ApplicationRecord, albo jak pobrać jakieś dane z bazy lub wejść w interakcję z innym obiektem. Np. pobrać listę części zamiennych.

Czy ktoś mógłby mi to wyjaśnić?

Chętnie przyjmę pomysł na aplikację do wykonania, tak żebym mógł się tego nauczyć - sam nie wiem co by było najbardziej odpowiednie…

Dziedziczenie po ApplicationRecord sluzy zrobieniu z tej klasy, klase z zapisem/odczytem do bazy.
To co masz jako klasy *Bike to na pierwszy rzut oka modele. Jak to jest twoje pierwsze podejscie do ror-a to ja bym nie startowal od razu z dziedziczeniem modeli, tylko proponuje zrobic prosty model i do niego formularze realizujace operacje CRUD.
Jak juz poczujesz jak to dziala wtedy dodaj dziedziczenie. Latwiej sie uczyc jak dodajesz jedna rzecz na raz.

Dzięki wielkie za odpowiedź.

To nie pierwsze podejście do RoR, mam już za sobą pewien zestaw aplikacji, w tym formularze realizujące CRUD. Ale skorzystałem z Twojej rady i wygenerowałem proste modele Vehicle i Car. Skorzystałem z Single Table Inheritance.

Vehicle:

class Vehicle < ApplicationRecord
  attr_reader :model, :test
  
  def initialize(model)
    super
    @model = model || default_model
    @test = 'test'
  end

  def default_model
    'Audi'
  end

  def spare_parts
    { tires: 2 }
  end
end

Car:

class Car < Vehicle
  def spare_parts
    { tires: 4 }
  end
end

W konsoli rails tworzę nowy obiekt v = Vehicle.new.
wykonanie: v.test zwraca mi ‘test’
wykonanie: v.model zwraca mi ‘Audi’
jednak pola, które zostaną zapisane w bazie danych wypełnione są nilami.

  1. Dlaczego pole model nie jest wypełnione wartością ‘Audi’ ? v.model zwraca właśnie ‘Audi’

  2. Co daje mi zadeklarowanie metody spare_parts ? OK, zwraca ilość kół zapasowych, ale jak to wykorzystać w bardziej zaawansowanej aplikacji?

Attr_reader - to atrybut read only. Więc masz tylko odczyt. Jeśli chcesz zapisywać w bazie to usuń wiersz z attr_reader i dodaj atrybuty w migracji. Stworzyłeś migrację, i wykonałeś na bazie, prawda?

Metoda spare_parts zwraca ci stałe wartości zależnie od klasy modelu. Dzięki temu, w klasie Car metoda z Vehicle jest nadpisana, więc zwróci {tires: 4}, zamiast {tires: 2}

Pewnie, że wykonałem migrację :wink: mam w niej pola type (wymagane przez STI) i model. Pola test nie potrzebuję w bazie.
Jak usunę attr_reader to wykonanie c = Car.new a następnie c.model zwraca mi nil, zamiast initializowanego ‘Audi’.
Z kolei attr_accessor nic nie wnosi, tj. z nim c.model zwraca ‘Audi’ ale pole model nadal jest puste w bazie.

Ja to rozumiem. Taki był zamysł, żeby nadpisać metodę - po to aby modele czymś się różniły. Tylko moje pytanie jest takie - jak to wykorzystać w realnej aplikacji? Czy inne obiekty po prostu wykonują zapytanie w stylu:

c = Car.new
m = Mechanic.new
m.provide(c.spare_parts)

? Zakładając, że metoda provide zmniejszałaby ilość części w magazynie.

Nie stworzyłem jeszcze bardziej zaawansowanej aplikacji stąd moje pytania, nawet jeśli wydają się proste.

Kilka uwag, które powinny rozwiać Twoje wątpliwości:

  1. Atrybut obiektu generowany na podstawie kolumny w bazie danych nie jest tym samym, co zmienna instancji (@model). ActiveRecord implementuje to w sposób trochę bardziej złożony, więc przypisanie czegoś do zmiennej @model nie powoduje zmiany wartości atrybutu o tej samej nazwie. Najlepiej coś takiego realizować używając standardowego settera: self.model = ...
  2. Gdybyś nie dziedziczył po ApplicationRecord, to jak najbardziej miałoby to sens, bo attr_reader definiuje gettera, który odczytuje wartość zmiennej instancji o tej samej nazwie, więc Twój kod zadziałałby.
  3. Interfejs metody initialize nie jest zgodny z oryginałem, więc to raczej nie jest dobry pomysł nadpisywanie jej.
  4. Dziedziczenie klas ActiveRecord ma największy sens, jeśli używasz STI (coś do poczytania). Wtedy potrzebujesz jeszcze kolumny typu string o nazwie type
  5. Jeśli chodzi o klasy w czystym Rubym (tzw. PORO), to najlepiej je przechowywać w katalogu app w jakimś podkatalogu, który wskazuje, czym (jakim wzorcem) jest ta klasa, np. app/decorators, app/services, app/interactors, app/validators, itp. Takie katalogi będą już załadowanie do aplikacji, nie musisz nigdzie pisać require. Możesz ich używać w kontrolerach, modelach, czy też innych klasach.

Dzięki za odpowiedź @tiwi .

  1. A jak mogę sprawdzić oryginalny interfejs? Bez nadpisywania initialize cały mój kod chyba nie ma sensu.

  2. Np. zdefiniowana w kontrolerze zmienna instancji jest dostępna dla widoku - może przechowywać obiekt uzyskany po zapytaniu do bazy danych - to rozumiem. A jeżeli zmienna instancji to nie to samo co atrybut obiektu generowany na podstawie kolumny w bazie, to po co mi w modelu zmienna instancji? Jak ją wykorzystać?

Skąd Wy to wszystko wiecie? Chciałbym być w stanie napisać aplikację w stylu Redmine albo Spree ale ich złożoność jest dla mnie przytłaczająca. Nie chcę zatrzymywać się na formularzach realizujących CRUD… Jak Wy przeskoczyliście do większych projektów?

https://api.rubyonrails.org/classes/ActiveRecord/Core.html#method-c-new

To też nie jest tak do końca, ale tutaj to już jest bardziej zaawansowany temat. Mówiąc o zmiennej instancji, mam na myśli pole obiektu (bo takiej terminologii się też używa w innych językach/technologiach obiektowych)

Dokładnie tak samo jak to zrobiłeś, czyli używając attr_reader, attr_writer, attr_accessor lub zmiennej z @. Tylko z zastrzeżeniem, że ta nazwa nie jest kolumną w bazie danych. Może to być np. zmienna wyliczona na podstawie wartości wyciągniętych z bazy, do której chciałbyś mieć dostęp poza obiektem.

Zasadniczo doświadczenie, ale trochę teorii też się przydaje. Akurat Twój problem niekoniecznie musi mieć cokolwiek wspólnego z railsami. Powiedziałbym, że to jest ogólnie zagadnienie programowania obiektowego.

Dzięki za odpowiedzi. No to chyba pozostają mi książki i tutoriale.