Solving data type problems in Ruby or Make data reliable again

 3r3404. 3r3-31. In this article, I would like to talk about what problems with data types exist in Ruby, what problems I encountered, how they can be solved and how to make the data we work with can be relied upon.
 3r3404.
 3r3404. Solving data type problems in Ruby or Make data reliable again
 3r3404. helps to verify this. Rollbar research. where they analyzed more than 1000 Rail applications and identified the most common errors. And 2 out of 10 most frequent errors are connected with the fact that the object cannot respond to a specific message. And therefore, checking the behavior of an object, what duck typing gives us, in many cases may not be enough.
 3r3404.
 3r3404. We can observe how type checking is added to dynamic languages ​​in one form or another: 3r3333387.  3r3404.
 3r3404.
 3r3404.
TypeScript introduced type checking for jаvascript developers
 3r3404.
Type hints were added in Python 3
 3r3404.
Dialyzer does a good job with the type checking task for Erlang /Elixir
 3r3404.
Steep and Sorbet add type checking in Ruby 2.x
 3r3404.
 3r3404. However, before talking about another tool for working more efficiently with types in Ruby, let's look at two more problems for which I would like to find.
 3r3404.
 3r3404. 3r33385. Reason 2. The common problem of developers in various programming languages ​​3r33386.
 3r3404. Let us recall the definition of data types, which I cited at the very beginning of the article: 3r3r114. Types are how you describe the data your program will work with. 3r3115. Those. Types are designed to help us describe the data from our data domain in which our systems operate. However, instead of operating with data types created by us from our data domain, we often use primitive types, such as numbers, strings, arrays, etc., which do not say anything about our data domain. This problem is usually classified as Primitive Obsession (obsession with primitives).
 3r3404.
 3r3404. Here is a typical example of Primitive Obsession:
 3r3404.
 3r3404. 3r33350. price = ???r3r3404. 3r3404. # vs
3r3404. Money = Struct.new (: amount_cents,: currency)
price = Money.new (9_9? 'USD')
3r33333.
 3r3404. Instead of describing the type of data for working with money, ordinary numbers are often used. And this number, like any other primitive types, does not say anything about our subject area. In my opinion, this is the biggest problem of using primitives instead of creating their own type system, where these types will describe data from our subject area. We ourselves give up the advantages that we can gain by using types.
 3r3404.
 3r3404. I will talk about these benefits immediately after covering another problem that our beloved Ruby on Rails framework taught us, thanks to which, I am sure, most of those present here came to Ruby.
 3r3404.
 3r3404. 3r33385. Reason 3. The problem that the Ruby on Rails
framework accustomed us to.
 3r3404. Ruby on Rails, or rather the ORM framework embedded in it. ActiveRecord , taught us to the fact that objects that are in invalid state - this is normal. In my opinion, this is not the best idea. And I will try to explain it.
 3r3404.
 3r3404. Take the following example:
 3r3404.
 3r3404. 3r33350. class App < ApplicationRecord
validates: platform, presence: true
end
3r3404. app = App.new
3r3404. app.valid? 3r3404. # => false
3r33333.
 3r3404. That object app will have invalid status, easy to understand: model validation 3r3333391. App requires that the objects of this model have the attribute platform , and our object has this attribute empty.
 3r3404.
 3r3404. And now we will try to transfer this object in an invalid state to the service that expects the object as an argument. App and performs some actions that depend on the attribute. platform This property:
 3r3404.
 3r3404. 3r33350. class DoSomethingWithAppPlatform
# @param[App]app 3r3404. #
# @return[void]3r3404. def call (app) 3r3404. # do something with app.platform
end
end
3r3404. DoSomethingWithAppPlatform.new.call (app)
3r33333.
 3r3404. In this case, even a type check would pass. However, since this attribute of an object is empty, it is not clear how the service will handle this case. In any case, having the ability to create objects in an invalid state, we doom ourselves to the need to constantly handle cases where invalid states have leaked into our system.
 3r3404.
 3r3404. But let's think about a deeper problem. In general, why do we check the validity of the data? As a rule, to ensure that an unacceptable state does not leak into our systems. If it is so important to ensure that the invalid state is not allowed, then why do we allow the creation of objects with an invalid state? Especially when we deal with such important objects as the ActiveRecord model, which often belongs to the root business logic. In my opinion, this sounds like a very bad idea.
 3r3404.
 3r3404. So, summarizing all of the above, we get the following problems in working with data in Ruby /Rails: 3r33387.  3r3404.
 3r3404.
  •  3r3404.
  • in the language itself, there is a mechanism for checking behavior, but not data 3r33269.  3r3404.
  • we, as well as developers in other languages, tend to use primitive data types instead of creating a type system of our subject area 3r-3269.  3r3404.
  • Rails taught us that the presence of objects in an invalid state is normal, although this solution seems like a pretty bad idea to  3r3404.

 3r3404. 3r33385. How can you solve these problems?
 3r3404. I would like to consider one of the solutions to the problems described above, on the example of implementing a real feature in Appodeal. In the process of implementing the Daily Active Users statistics collection (hereinafter referred to as DAU) for applications that use Appodeal for monetization, we have come to the following data structure that we need to collect: 3r33387.  3r3404.
 3r3404. 3r33350. DailyActiveUsersData = Struct.new (3r3404.: App_id, 3r3404: a ??? ???: user_id, 3r??? ak.ch. ch. . 3r33333.
 3r3404. This structure has all the same problems that I wrote about above:
 3r3404.
 3r3404.
  •  3r3404.
  • any type checking is completely absent, due to which it is unclear what values ​​the attributes of this structure can take on 3r-3269.  3r3404.
  • there is no description of the data used in this structure, and instead of the types specific for our domain, the primitives 3r-3269 are used.  3r3404.
  • the structure may exist in an invalid state  3r3404.

 3r3404. To solve these problems, we decided to use the libraries. dry-types and dry-struct 3r3r92. . dry-types Is a simple and extensible type system for Ruby, useful for type casting, applying various constraints, defining complex structures, etc. 3r33391. dry-struct 3r3r92. Is a library built on top of dry-types which provides a convenient DSL for defining typed structures /classes.
 3r3404.
 3r3404. To describe the data of our subject area used in the structure for the collection of DAU, the following type system was created: 3r33387.  3r3404.
 3r3404. 3r33350. module Types
include Dry :: Types.module
3r3404. AdTypeId = Types :: Strict :: Integer.enum (AD_TYPES.invert)
EntityId = Types :: Strict :: Integer.constrained (gt: 0)
PlatformId = Types :: Strict :: Integer.enum (PLATFORMS.invert)
Uuid = Types :: Strict :: String.constrained (format: UUID_REGEX)
Zero = Types.Constant (0) 3r3404. end
3r33333.
 3r3404. Now we have received a description of the data that is used in our system and which we can use in the structure. As can be seen, types EntityId and Uuid have some restrictions, and enumerable-types AdTypeId and PlatformId can only matter from a specific set. How to work with these types? Consider the example of PlatformId :
 3r3404.
 3r3404. 3r33350. # set of valid values ​​for enumerable-type
PLATFORMS = {
'android' => ?
'fire_os' => ? 3r3404. 'ios' => 3 3r3404.} .freeze
3r3404. # we can use both the values ​​themselves,
# and their designation
Types :: PlatformId[1]== Types :: PlatformId['android']3r3404. 3r3404. # if you pass the correct value, as a result 3r3404. # we get the value of the primitive on which the 3r3404 type is built. Types :: PlatformId['fire_os']3r3404. # => 2 3r3404. 3r3404. # if you pass an incorrect value, we get an error 3r3404. Types :: PlatformId['windows']3r3404. # => Dry :: Types :: ConstraintError
3r33333.
 3r3404. So, with use of types understood. Now let's apply them to our structure. As a result, we got this:
 3r3404.
 3r3404. 3r33350. class DailyActiveUsersData < Dry::Struct
attribute: app_id, Types :: EntityId
attribute: country_id, Types :: EntityId
attribute: user_id, Types :: EntityId
attribute: ad_type, (Types :: AdTypeId Types :: Zero)
attribute: platform_id, Types :: PlarformId
attribute: ad_id, Types :: Uuid
attribute: first_request_date, Types :: Strict :: Date
end
3r33333.
 3r3404. What we see now in the data structure for DAU? Through the use of dry-types and dry-struct 3r3r92. we got rid of the problems associated with the lack of data type checking and the lack of data descriptions. Now, anyone looking at this structure and the description of the types used in it, can understand what values ​​each attribute can take.
 3r3404.
 3r3404. As for the problem with objects in an invalid state, then dry-struct 3r3r92. it also relieves us of this: if we try to initialize the structure with invalid values, we will get an error as a result. And for those cases where the correctness of the data is essential (and in the case of the DAU collection, things are exactly like this), in my opinion, getting an exception is much better than trying to deal with invalid data. In addition, if the testing process is well established (and this is how it is with us), then with high probability the code that generates such errors will not reach the production environment.
 3r3404.
 3r3404. And besides the inability to initialize objects in an invalid state, dry-struct 3r3r92. also does not allow to change aboutObjects after initialization. Thanks to these two factors, we get an assurance that the objects of such structures will be in a valid state and you can safely rely on this data anywhere else in your system.
 3r3404.
 3r3404. 3r33385. Outcome
 3r3404. In this article, I tried to describe the problems that you may encounter when working with data in Ruby, and also talk about the tools we use to solve these problems. And thanks to the introduction of these tools, I absolutely stopped worrying about the correctness of the data with which we work. Isn't that great? Isn't this the goal of any instrument - to make our life easier in some of its aspects? And in my opinion, dry-types and dry-struct 3r3r92. in this task perfectly cope! 3r33400. 3r3404. 3r3404. 3r3404.
3r3404. 3r33400. 3r3404. 3r3404. 3r3404. 3r3404.
+ 0 -

Add comment