Pull to refresh

Comments 9

Здравствуйте
Подобную проблему также можно решить и с помощью модуля ActiveModel::Model
class DailyActiveUsersData
  include ActiveModel::Model

  attr_accessor :app_id, :ad_type, :first_request_date

  validates :app_id, :ad_type, :first_request_date, presence: true
  validates :app_id, numericality: { greater_than: 0 }
  validates :ad_type, inclusion: { in: [:android, :ios] }
  validates :first_request_date, date: true
end

Вы можете сбилдить объект, передав все аргументы в new
DailyActiveUsersData.new(app_id: 1, ad_type: :ios, first_request_date: Time.now)

Либо же наполнять объект в процессе
data = DailyActiveUsersData.new
data.app_id = 1
data.ad_type = :ios

В тот момент, когда вы решаете что новые данные уже не поступят, вы можете вызвать на объекте #validate!.. Теперь вы можете быть уверенным, что объект содержит корректные данные
data.validate!

Чтобы разработчик мог понять, из чего состоит объект, он может просмотреть правила валидации определенные в классе
Если жы мы хотим быть уверенными в том, что невалидного объекта существовать не должно, мы можем сделать следующее
class DailyActiveUsersData
  include ActiveModel::Model

  def self.new!(attrs)
    object = new(attrs)
    object.validate!
    object
  end
end

Ну, или же на крайний случай вот так (правда, я не любитель переопределять такие штуки)
class DailyActiveUsersData
  include ActiveModel::Model

  def initialize(attrs)
    super(attrs)
    validate!
  end
end

P.S. для ad_type я использовал простой inclusion. Естественно это не полноценная замена, например, enum-а
Надеюсь идея кому-то пригодится, хотя она и не нова. В инете много статей на тему использования form object-ов или чего-то подобного.
# если передать не корректное значение, получим ошибку
Types::PlatformId['windows']
# => Dry::Types::ConstraintError


а fetch зачем придумали? нафигачат костылей нафиг не нужных никому и потом пытайся сообразить как с этим говном работать.
Я бы разделил две проблемы: проблему валидации и проблему типов.

Начну с валидации
Зачем мы разрешаем создавать объекты с невалидным состоянием?

1. Потому что это нам требуется для решения наших задач. Например при создании объекта и для возвращения ошибок валидации. Создается объект, проходит его валидация, если объект не валиден, то мы возвращаем невалидный объект с ошибками в контроллер и рендер формы.
Из данных объекта мы проставляем значения в полях, из объекта ошибок мы выводим ошибки.
Т.е. например если бы наш объект Order не мог бы находиться в невалидном состоянии, нам бы пришлось создать еще какой-то класс NonValidOrder и периодически мы рендерили бы форму/json из Order, а периодически из NonValidOrder, и пришлось бы еще делать какие-то механизмы превращения одного в другое. Зачем, если мы всегда можно вызывать метод valid? везде где оно требуется.
2. Часто мы создаем объект и потом до-обогащаем его данными. Применяем купоны, добавляем скидки, привязываем заказ к клиенту и т.д. До обогащения данными наш Order не валидный. Валидность мы проверяем уже перед сохранением в базу. Если бы мы не могли создать невалидный объект, то нам бы пришлось создавать объекты вроде MayBeValidOrder, со сходным функционалом нашего Order
3. Наш Order может быть валидным с заполненными client_id, manager_id, а может быть валидным и без них, поэтому проверять их наличие нам придется все равно.
4. Ну и последнее самое интересное: тот факт, что у нас в
order.client_id
записано
Types::Strict::Integer.constrained(gt: 0)
не дает нам никаких гарантий, что у заказа есть Client, потому что не факт, что у нас есть клиент с таким id. То есть типами мы все равно не избавимся от невалидных объектов.

А теперь что касается типов
Если хочется использовать типы, то IMHO лучше не писать на Ruby)
Для этого прекрасно подойдет тот же Rust, в добавок к типам еще будет очень умный компилятор (который поможет избежать много ошибок) и прирост в скорости на пару порядков.

Ну и на мой взгляд код на Rust с типами намного приятнее, кода на Ruby c типами.
В Ruby с псевдотипами приходится писать сильно больше кода, да еще и зависеть от гемов. Уж лучше тогда писать на Rust или Haskell))
// Код на Rust
extern crate uuid;
use uuid::Uuid;

const IOS: i32 = 1;
const ANDROID: i32 = 2;
const FIRE_OS: i32 = 3;

enum Platform {
    IOS,
    ANDROID,
    FIRE_OS,
}

struct DailyActiveUsersData {
    app_id: i32,
    country_id: i32,
    user_id: i32,
    platform_id: Platform,
    ad_id: Uuid,
    first_request_date: &'static str,
}

fn main() {
    let uuid = Uuid::parse_str("6a2f41a3-c54c-fce8-32d2-0324e1c32e22").unwrap();

    let data = DailyActiveUsersData {
        app_id: 1,
        country_id: 2,
        user_id: 3,
        platform_id: Platform::IOS,
        ad_id: uuid,
        first_request_date: "2018-12-16",
    };

    println!("Data {:?}", data);
}



# Код на Ruby (я обожаю Ruby и пишу на нем каждый день, и мне кажется Ruby не об этом)
require 'dry-types'
require 'dry-struct'

module Types
  include Dry::Types.module

  PLATFORMS = {
    'android' => 1,
    'fire_os' => 2,
    'ios'     => 3
  }.freeze

  UUID_REGEXP = /[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}/

  ENTITY_ID   = Types::Strict::Integer.constrained(gt: 0)
  PLATFORM_ID = Types::Strict::Integer.enum(PLATFORMS.invert)
  UUID        = Types::Strict::String.constrained(format: UUID_REGEXP)
  ZERO        = Types.Constant(0)
end

class DailyActiveUsersData < Dry::Struct
  attribute :app_id,              Types::ENTITY_ID
  attribute :country_id,          Types::ENTITY_ID
  attribute :user_id,             Types::ENTITY_ID
  attribute :platform_id,         Types::PLATFORM_ID
  attribute :ad_id,               Types::UUID
  attribute :first_request_date,  Types::Strict::Date
end

data = DailyActiveUsersData.new(
  app_id: 1,
  country_id: 2,
  user_id: 3,
  platform_id: 1,
  ad_id: '6a2f41a3-c54c-fce8-32d2-0324e1c32e22',
  first_request_date: Date.today
)

puts data
1. Потому что это нам требуется для решения наших задач. Например при создании объекта и для возвращения ошибок валидации. Создается объект, проходит его валидация, если объект не валиден, то мы возвращаем невалидный объект с ошибками в контроллер и рендер формы.
Из данных объекта мы проставляем значения в полях, из объекта ошибок мы выводим ошибки.
Т.е. например если бы наш объект Order не мог бы находиться в невалидном состоянии, нам бы пришлось создать еще какой-то класс NonValidOrder и периодически мы рендерили бы форму/json из Order, а периодически из NonValidOrder, и пришлось бы еще делать какие-то механизмы превращения одного в другое. Зачем, если мы всегда можно вызывать метод valid? везде где оно требуется.

А если валидация в разных ситуациях различается? Или на форме потребуются производные данные (например, опции для поля выбора или форма будет содержать данные нескольких моделей)? Да и слишком много ответсвенности возникает у модели. Не лучше ли для формы рендеринга формы использовать отдельный объект (form object)? А то получается интерфейс тесно связанный с бизнес-моделью (да и, как правило, с БД через модели).
Про валидации
1. Как раз в Ruby это ни разу не проблема — рендерить что-либо из разных классов, лишь бы метод render был у обоих. Вполне нормально для JSON, например, вообще до самого финального рендера готовить объект, а в случае исключения формировать ответ из этого самого исключения.
2. А вот этот момент как-то более распространен не в Ruby… но, на самом деле, решается это через «DTO», «POCO» и прочие страшные названия — т.е. объекты, несущие только данные без поведения (что, кстати, прекрасно и в Ruby реализуется через Hash и Struct). А настоящий объект, с состоянием и поведением уже формируется из такой структуры данных после валидации.
3. Проблема скорее не в системе типов языка, а в структуре типов конкретной системы. Иногда нужно не использовать всё, что удобно.
4. Соглашусь.

А теперь что касается типов
Почти ППКС. Всё-таки и система типов Ruby на многое годится. Один метод respond_to?
чего стоит.
А что делать, если для меня Ruby не есть RoR и RoR не есть Ruby до такой степени, что я люблю Ruby и довольно много использую, но про RoR знаю лишь то, что оно мне не нужно (тут я должен признаться, что не являюсь профессиональным программистом, но вопроса это не отменяет, не снимает и не нивелирует его значимость для меня)?
Так этот вариант как раз для тех, для кого RoR не существует)

RoR не есть Ruby

Как такое может быть? Если не Ruby, то это уже Grails, Sails.js или что-нибудь ещё)
Так этот вариант как раз для тех, для кого RoR не существует)
Значит, я ещё не созрел для того, чтобы это понять
RoR не есть Ruby
Как такое может быть? Если не Ruby, то это уже Grails, Sails.js или что-нибудь ещё)
Я имел в виду что RoR для меня не существует, а Ruby существует, а то, что не существует не может быть тем, что существует
Мне кажется, вашу задачу решил бы обычный Plain Old Ruby Object с обычными валидациями в конструкторе. Это куда более явно, чем код в синтаксисе очередной библиотеки, пусть и популярной.
enum-ы реализуются тривиально, стоит ли ради них тащить библиотеку и её соглашения по синтаксису — неочевидно.
Sign up to leave a comment.

Articles