Comments 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-ов или чего-то подобного.
+2
# если передать не корректное значение, получим ошибку
Types::PlatformId['windows']
# => Dry::Types::ConstraintError
а fetch зачем придумали? нафигачат костылей нафиг не нужных никому и потом пытайся сообразить как с этим говном работать.
0
Я бы разделил две проблемы: проблему валидации и проблему типов.
Начну с валидации
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
1. Потому что это нам требуется для решения наших задач. Например при создании объекта и для возвращения ошибок валидации. Создается объект, проходит его валидация, если объект не валиден, то мы возвращаем невалидный объект с ошибками в контроллер и рендер формы.
Из данных объекта мы проставляем значения в полях, из объекта ошибок мы выводим ошибки.
Т.е. например если бы наш объект Order не мог бы находиться в невалидном состоянии, нам бы пришлось создать еще какой-то класс NonValidOrder и периодически мы рендерили бы форму/json из Order, а периодически из NonValidOrder, и пришлось бы еще делать какие-то механизмы превращения одного в другое. Зачем, если мы всегда можно вызывать метод valid? везде где оно требуется.
А если валидация в разных ситуациях различается? Или на форме потребуются производные данные (например, опции для поля выбора или форма будет содержать данные нескольких моделей)? Да и слишком много ответсвенности возникает у модели. Не лучше ли для формы рендеринга формы использовать отдельный объект (form object)? А то получается интерфейс тесно связанный с бизнес-моделью (да и, как правило, с БД через модели).
+1
Про валидации
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?
чего стоит.
0
А что делать, если для меня Ruby не есть RoR и RoR не есть Ruby до такой степени, что я люблю Ruby и довольно много использую, но про RoR знаю лишь то, что оно мне не нужно (тут я должен признаться, что не являюсь профессиональным программистом, но вопроса это не отменяет, не снимает и не нивелирует его значимость для меня)?
0
Так этот вариант как раз для тех, для кого RoR не существует)
Как такое может быть? Если не Ruby, то это уже Grails, Sails.js или что-нибудь ещё)
RoR не есть Ruby
Как такое может быть? Если не Ruby, то это уже Grails, Sails.js или что-нибудь ещё)
0
Так этот вариант как раз для тех, для кого RoR не существует)Значит, я ещё не созрел для того, чтобы это понять
Я имел в виду что RoR для меня не существует, а Ruby существует, а то, что не существует не может быть тем, что существуетRoR не есть RubyКак такое может быть? Если не Ruby, то это уже Grails, Sails.js или что-нибудь ещё)
0
Мне кажется, вашу задачу решил бы обычный Plain Old Ruby Object с обычными валидациями в конструкторе. Это куда более явно, чем код в синтаксисе очередной библиотеки, пусть и популярной.
enum-ы реализуются тривиально, стоит ли ради них тащить библиотеку и её соглашения по синтаксису — неочевидно.
enum-ы реализуются тривиально, стоит ли ради них тащить библиотеку и её соглашения по синтаксису — неочевидно.
0
Sign up to leave a comment.
Решаем проблемы типов данных в Ruby или Make data reliable again