Комментарии 9
Здравствуйте
Подобную проблему также можно решить и с помощью модуля ActiveModel::Model
Вы можете сбилдить объект, передав все аргументы в new
Либо же наполнять объект в процессе
В тот момент, когда вы решаете что новые данные уже не поступят, вы можете вызвать на объекте #validate!.. Теперь вы можете быть уверенным, что объект содержит корректные данные
Чтобы разработчик мог понять, из чего состоит объект, он может просмотреть правила валидации определенные в классе
Если жы мы хотим быть уверенными в том, что невалидного объекта существовать не должно, мы можем сделать следующее
Ну, или же на крайний случай вот так (правда, я не любитель переопределять такие штуки)
P.S. для ad_type я использовал простой inclusion. Естественно это не полноценная замена, например, enum-а
Надеюсь идея кому-то пригодится, хотя она и не нова. В инете много статей на тему использования form object-ов или чего-то подобного.
Подобную проблему также можно решить и с помощью модуля 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. Ну и последнее самое интересное: тот факт, что у нас в
А теперь что касается типов
Если хочется использовать типы, то IMHO лучше не писать на Ruby)
Для этого прекрасно подойдет тот же Rust, в добавок к типам еще будет очень умный компилятор (который поможет избежать много ошибок) и прирост в скорости на пару порядков.
Ну и на мой взгляд код на Rust с типами намного приятнее, кода на Ruby c типами.
В Ruby с псевдотипами приходится писать сильно больше кода, да еще и зависеть от гемов. Уж лучше тогда писать на Rust или Haskell))
Начну с валидации
Зачем мы разрешаем создавать объекты с невалидным состоянием?
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 на многое годится. Один метод
чего стоит.
1. Как раз в Ruby это ни разу не проблема — рендерить что-либо из разных классов, лишь бы метод render был у обоих. Вполне нормально для JSON, например, вообще до самого финального рендера готовить объект, а в случае исключения формировать ответ из этого самого исключения.
2. А вот этот момент как-то более распространен не в Ruby… но, на самом деле, решается это через «DTO», «POCO» и прочие страшные названия — т.е. объекты, несущие только данные без поведения (что, кстати, прекрасно и в Ruby реализуется через Hash и Struct). А настоящий объект, с состоянием и поведением уже формируется из такой структуры данных после валидации.
3. Проблема скорее не в системе типов языка, а в структуре типов конкретной системы. Иногда нужно не использовать всё, что удобно.
4. Соглашусь.
А теперь что касается типов
Почти ППКС. Всё-таки и система типов Ruby на многое годится. Один метод
respond_to?
чего стоит.
А что делать, если для меня Ruby не есть RoR и RoR не есть Ruby до такой степени, что я люблю Ruby и довольно много использую, но про RoR знаю лишь то, что оно мне не нужно (тут я должен признаться, что не являюсь профессиональным программистом, но вопроса это не отменяет, не снимает и не нивелирует его значимость для меня)?
Так этот вариант как раз для тех, для кого RoR не существует)
Как такое может быть? Если не Ruby, то это уже Grails, Sails.js или что-нибудь ещё)
RoR не есть Ruby
Как такое может быть? Если не Ruby, то это уже Grails, Sails.js или что-нибудь ещё)
Так этот вариант как раз для тех, для кого RoR не существует)Значит, я ещё не созрел для того, чтобы это понять
Я имел в виду что RoR для меня не существует, а Ruby существует, а то, что не существует не может быть тем, что существуетRoR не есть RubyКак такое может быть? Если не Ruby, то это уже Grails, Sails.js или что-нибудь ещё)
Мне кажется, вашу задачу решил бы обычный Plain Old Ruby Object с обычными валидациями в конструкторе. Это куда более явно, чем код в синтаксисе очередной библиотеки, пусть и популярной.
enum-ы реализуются тривиально, стоит ли ради них тащить библиотеку и её соглашения по синтаксису — неочевидно.
enum-ы реализуются тривиально, стоит ли ради них тащить библиотеку и её соглашения по синтаксису — неочевидно.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Решаем проблемы типов данных в Ruby или Make data reliable again