まず、データタイプを決定する価値があります。 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の型チェックの良い仕事をします
- SteepとSorbetは、Ruby 2.xで型チェックを追加します
ただし、Rubyでより効率的に型を操作するための別のツールについて説明する前に、解決策を見つけたい2つの問題を見てみましょう。
理由2.さまざまなプログラミング言語の開発者の一般的な問題
この記事の冒頭で述べたデータ型の定義を思い出してみましょう。
タイプは、プログラムで使用するデータを記述する方法です。つまり タイプは、システムが動作するサブジェクト領域からのデータを記述するのに役立つように設計されています。 ただし、サブジェクト領域から作成したデータ型を操作する代わりに、サブジェクト領域について何も言わない数値、文字列、配列などのプリミティブ型を使用することがよくあります。 この問題は通常、プリミティブオブセッション(プリミティブへの執着)に分類されます。
典型的なプリミティブオブセッションの例を次に示します。
price = 9.99 # vs Money = Struct.new(:amount_cents, :currency) price = Money.new(9_99, 'USD')
お金を扱うためのデータ型を記述する代わりに、通常の数字がよく使用されます。 そして、この数は、他のプリミティブ型と同様に、私たちの主題分野については何も言いません。 私の意見では、これは独自の型システムを作成する代わりにプリミティブを使用することの最大の問題であり、これらの型はサブジェクト領域からのデータを記述します。 私たち自身は、型を使用することで得られる利点を拒否しています。
私たちのお気に入りのRuby on Railsフレームワークが教えてくれた別の問題を取り上げた後、これらの利点についてすぐに話します。
理由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での実際の機能実装の例を使用して、上記の問題に対する解決策の1つを検討したいと思います。 Appodealを使用して収益化するアプリケーションのDaily Active Users(以降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のシンプルで拡張可能なタイプシステムであり、キャスト、さまざまな制約の適用、複雑な構造の定義などに役立ちます。クラス。
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
これで、システムで使用され、構造で使用できるデータの説明を受け取りました。 ご覧のとおり、
EntityId
および
Uuid
はいくつかの制限があり、
AdTypeId
および
PlatformId
AdTypeId
型には特定のセットの値のみを
AdTypeId
ことができます。 これらのタイプを使用する方法は? 例として
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-types
と
dry-struct
を使用することにより、データ型チェックの欠如とデータ記述の欠如に関連する問題を取り除きました。 これで、この構造とそれに使用される型の説明を見た人は誰でも、各属性が取り得る値を理解できます。
無効な状態のオブジェクトの問題に関しては、
dry-struct
はこれから
dry-struct
私たちを救います:無効な値で構造を初期化しようとすると、結果としてエラーが発生します。 データの正確性が不可欠な場合(およびDAU収集の場合はこれが当てはまります)、私の意見では、例外を取得することは、後で無効なデータを処理するよりもはるかに優れています。 さらに、テストプロセスが十分に確立されている場合(そして、これがまさに私たちの場合です)、そのようなエラーを生成するコードは、本番環境に到達しない可能性が高いです。
また、無効な状態のオブジェクトを初期化できないことに加えて、
dry-struct
は初期化後にオブジェクトを変更することもできません。 これら2つの要因のおかげで、このような構造のオブジェクトが有効な状態になり、システムのどこにいてもこのデータに安全に依存できるという保証が得られます。
まとめ
この記事では、Rubyでデータを操作するときに発生する可能性のある問題を説明し、これらの問題を解決するために使用するツールについて説明しました。 そして、これらのツールの実装のおかげで、私たちが作業するデータの正確さについて心配することは絶対になくなりました。 それは完璧ではありませんか? これは、楽器の目的ではありませんか?それを何らかの面で私たちの生活を促進するためですか? そして、私の意見では、
dry-types
と
dry-struct
完璧に仕事をしています!