解决Ruby中的数据类型问题或使数据再次可靠

在本文中,我想谈谈Ruby中数据类型存在哪些问题,遇到了哪些问题,如何解决这些问题以及如何确保可以使用我们处理的数据。

图片

首先,您需要确定什么数据类型。 我看到该术语的定义非常成功,可以在HaskellWiki中找到。
类型是您描述程序将使用的数据的方式。
但是Ruby中的数据类型有什么问题? 为了全面地描述问题,我想强调几个原因。

原因1. Ruby本身的问题


如您所知,Ruby使用严格的动态类型以及对所谓的支持。 鸭子打字 。 这是什么意思?

强类型化要求显式转换,并且不能单独产生这种转换,例如在JavaScript中就是这种情况。 因此,Ruby中的以下代码清单将失败:

1 + '1' - 1 #=> TypeError (String can't be coerced into Integer) 

在动态类型化中,类型检查在运行时进行,这使我们不必指定变量的类型,而可以使用相同的变量来存储不同类型的值:

 x = 123 x = "123" x = [1, 2, 3] 

以下语句通常是对“鸭子类型”的解释:如果它看起来像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那么很可能是鸭子。 即 依赖对象的行为的鸭子类型为我们编写系统提供了更多的灵活性。 例如,在下面的示例中,对我们而言,值不是collection参数的类型,而是其响应blank?消息的能力blank?map

 def process(collection) return if collection.blank? collection.map { |item| do_something_with(item) } end 

创建此类“鸭子”的能力是非常强大的工具。 但是,像其他任何强大的工具一样,使用时也需要格外小心。 Rollbar的研究可以证实一点 ,他们分析了1000多个Rail应用程序并确定了最常见的错误。 10个最常见的错误中有2个恰好与对象无法响应特定消息这一事实有关。 因此,在许多情况下,仅检查鸭子类型为我们提供的对象的行为可能还不够。

我们可以观察到如何将类型检查以一种或另一种形式添加到动态语言中:

  • TypeScript为JavaScript开发人员带来了类型检查
  • 在Python 3中添加了类型提示
  • Dialyzer在Erlang / Elixir的类型检查方面做得很好
  • Ruby 2.x中的Steep和Sorbet添加类型检查

但是,在讨论另一种在Ruby中更有效地使用类型的工具之前,让我们看一下我想找到解决方案的另外两个问题。

原因2.各种编程语言中开发人员的普遍问题


让我们回想一下本文开头给出的数据类型的定义:
类型是您描述程序将使用的数据的方式。
即 类型旨在帮助我们描述系统运行所在主题领域的数据。 但是,通常不使用从主题区域创建的数据类型进行操作,而是使用原始类型(例如数字,字符串,数组等),而原始类型并没有说明主题区域。 这个问题通常被归类为“原始痴迷”(对原始的痴迷)。

这是一个典型的原始痴迷示例:

 price = 9.99 # vs Money = Struct.new(:amount_cents, :currency) price = Money.new(9_99, 'USD') 

经常使用常规数字来代替描述使用金钱的数据类型。 与其他任何原始类型一样,该数字也没有说明我们的主题领域。 在我看来,这是使用原语而不是创建自己的类型系统的最大问题,这些类型将描述我们主题领域的数据。 我们自己拒绝使用类型可以获得的优势。

在介绍了我们最喜欢的Ruby on Rails框架教给我们的另一个问题之后,我将立即讨论这些优势。我敢肯定,由于这个原因,我相信其中的大多数都来自Ruby。

原因3. Ruby on Rails框架习惯我们的问题


Ruby on Rails或内置的ActiveRecord ORM框架告诉我们处于无效状态的对象是正常的。 我认为,这绝不是最好的主意。 我会尽力解释。

举个例子:

 class App < ApplicationRecord validates :platform, presence: true end app = App.new app.valid? # => false 

不难理解, app对象将具有无效状态: App模型的验证要求该模型的对象具有platform属性,而我们的对象的此属性为空。

现在,让我们尝试将处于无效状态的该对象传递给期望将App对象作为参数并根据该对象的platform属性执行一些操作的服务:

 class DoSomethingWithAppPlatform # @param [App] app # # @return [void] def call(app) # do something with app.platform end end DoSomethingWithAppPlatform.new.call(app) 

在这种情况下,即使类型检查也会通过。 但是,由于该对象的属性为空,因此尚不清楚服务将如何处理这种情况。 无论如何,由于能够创建处于无效状态的对象,我们谴责需要不断处理无效状态泄漏到系统中的情况。

但是,让我们考虑一个更深层次的问题。 通常,为什么我们要检查数据的有效性? 通常,要确保无效状态不会泄漏到我们的系统中。 如果确保不允许无效状态非常重要,那么为什么要允许创建具有无效状态的对象呢? 特别是当我们处理诸如ActiveRecord模型之类的重要对象时,它通常指的是根业务逻辑。 我认为,这听起来像是一个非常糟糕的主意。

因此,总结以上所有内容,我们在Ruby / Rails中使用数据时遇到以下问题:

  • 语言本身具有验证行为的机制,但不能验证数据
  • 与其他语言的开发人员一样,我们倾向于使用原始数据类型,而不是为我们的主题领域创建类型系统
  • Rails习惯了这样一个事实,即处于无效状态的对象的存在是正常的,尽管这样的解决方案似乎是一个非常糟糕的主意

这些问题如何解决?


我想使用一个在Appodeal中实现真实功能的示例来考虑上述问题的一种解决方案。 在收集有关使用Appodeal获利的应用程序的每日活动用户(以下称DAU)统计信息的统计信息的过程中,我们大致得出了以下需要收集的数据结构:

 DailyActiveUsersData = Struct.new( :app_id, :country_id, :user_id, :ad_type, :platform_id, :ad_id, :first_request_date, keyword_init: true ) 

这个结构具有我上面写过的所有相同的问题:

  • 完全不存在任何类型检查,这使得不清楚此结构的属性可以采用什么值
  • 没有对该结构中使用的数据的描述,并且使用了原语而不是我们领域专用的类型
  • 结构可能以无效状态存在

为了解决这些问题,我们决定使用dry-types库和dry-struct库。 dry-types是用于Ruby的简单且可扩展的类型系统,可用于强制转换,施加各种约束,定义复杂的结构等dry-struct是建立在dry-types之上的库,它为定义类型化的结构提供了便捷的DSL类。

为了描述用于收集DAU的结构中使用的主题领域的数据,创建了以下类型系统:

 module Types include Dry::Types.module 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) end 

现在,我们已经收到了对系统中使用的数据的描述,并且可以在结构中使用它们。 如您所见,类型EntityIdUuid有一些限制,可枚举类型AdTypeIdPlatformId只能具有特定集合中的值。 如何使用这些类型? 以PlatformId为例:

 #     enumerable- PLATFORMS = { 'android' => 1, 'fire_os' => 2, 'ios' => 3 }.freeze #       , #     Types::PlatformId[1] == Types::PlatformId['android'] #    ,    #   ,     Types::PlatformId['fire_os'] # => 2 #     ,   Types::PlatformId['windows'] # => Dry::Types::ConstraintError 

因此,使用自己找出的类型。 现在,将它们应用于我们的结构。 结果,我们得到了:

 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 

我们现在在DAU的数据结构中看到什么? 通过使用dry-typesdry-struct我们摆脱了与缺少数据类型检查和数据描述有关的问题。 现在,任何人只要查看了此结构以及其中使用的类型的说明,都可以理解每个属性可以采用的值。

至于对象处于无效状态的问题, dry-struct我们免于此:如果我们尝试使用无效值初始化结构,则将得到一个错误。 在我看来,对于那些必须保证数据正确性的情况(对于DAU收集来说,情况就是如此),我认为,获得异常比以后再处理无效数据要好得多。 此外,如果为您建立了良好的测试过程(我们就是这种情况),那么产生此类错误的代码很可能不会到达生产环境。

除无法初始化处于无效状态的对象外, dry-struct还不允许在初始化后更改对象。 由于这两个因素,我们保证此类结构的对象将处于有效状态,并且您可以在系统中的任何地方安全地依赖此数据。

总结


在本文中,我试图描述您在Ruby中使用数据时可能遇到的问题,并讨论了用于解决这些问题的工具。 而且由于有了这些工具的实现,我绝对不再担心我们正在使用的数据的正确性。 那不是完美的吗? 这不是任何工具的目的-在某些方面改善我们的生活吗? 在我看来, dry-typesdry-struct完美地dry-struct工作!

Source: https://habr.com/ru/post/zh-CN433180/


All Articles