في هذه المقالة ، أود أن أتحدث عن المشكلات التي تواجه أنواع البيانات في روبي ، وما هي المشاكل التي واجهتها وكيف يمكن حلها وكيفية التأكد من أن البيانات التي نعمل معها يمكن الاعتماد عليها.

تحتاج أولاً إلى تحديد أنواع البيانات. أرى تعريفًا ناجحًا جدًا للمصطلح ، والذي يمكن العثور عليه في
HaskellWiki .
الأنواع هي الطريقة التي تصف بها البيانات التي سيعمل بها البرنامج.
ولكن ما الخطأ في أنواع البيانات في روبي؟ لوصف المشكلة بشكل شامل ، أود أن أبرز عدة أسباب.
السبب 1. مشاكل روبي نفسها
كما تعلمون ، يستخدم روبي
كتابة ديناميكية صارمة مع دعم لما يسمى. بطة الكتابة . ماذا يعني هذا؟
تتطلب الكتابة القوية صبًا واضحًا ولا تنتج هذا الاختيار من تلقاء نفسها ، كما هو الحال ، على سبيل المثال ، في JavaScript. لذلك ، ستفشل قائمة التعليمات البرمجية التالية في Ruby:
1 + '1' - 1 #=> TypeError (String can't be coerced into Integer)
في الكتابة الديناميكية ، يتم التحقق من الكتابة في وقت التشغيل ، مما يسمح لنا بعدم تحديد أنواع المتغيرات واستخدام المتغير نفسه لتخزين قيم الأنواع المختلفة:
x = 123 x = "123" x = [1, 2, 3]
عادةً ما يتم تقديم العبارة التالية كتفسير لمصطلح "كتابة البطة": إذا كان يبدو وكأنه بطة ، يسبح مثل البطة والدج مثل البطة ، فمن المرجح أن يكون هذا بطة. أي كتابة البط ، بالاعتماد على سلوك الأشياء ، توفر لنا مرونة إضافية في كتابة أنظمتنا. على سبيل المثال ، في المثال أدناه ، ليست القيمة بالنسبة لنا هي نوع وسيطة
collection
، ولكن قدرتها على الرد على الرسائل
blank?
map
:
def process(collection) return if collection.blank? collection.map { |item| do_something_with(item) } end
القدرة على إنشاء مثل هذه "البط" هي أداة قوية للغاية. ومع ذلك ، مثل أي أداة قوية أخرى ، فإنه يتطلب عناية كبيرة عند استخدام. يمكن
التحقق من ذلك من خلال بحث Rollbar ، حيث قاموا بتحليل أكثر من 1000 تطبيق للسكك الحديدية وتحديد الأخطاء الأكثر شيوعًا. 2 من الأخطاء العشرة الأكثر شيوعًا مرتبطة بدقة بحقيقة أن الكائن لا يمكنه الاستجابة لرسالة محددة. وبالتالي ، قد لا يكون التحقق من سلوك الكائن الذي تعطينا كتابته البط في كثير من الحالات كافياً.
يمكننا أن نلاحظ كيف يضاف التحقق من الكتابة إلى اللغات الديناميكية بشكل أو بآخر:
- TypeScript يجلب فحص النوع لمطوري JavaScript
- تمت إضافة تلميحات الكتابة في Python 3
- يقوم Dialyzer بعمل جيد للتحقق من نوع Erlang / Elixir
- إضافة شديدة الانحدار و Sorbet التدقيق في 2.x روبي
ومع ذلك ، قبل أن نتحدث عن أداة أخرى للتعامل مع أنواع أكثر كفاءة في روبي ، دعونا ننظر إلى مشكلتين أخريين أود إيجاد حل لهما.
السبب 2. المشكلة العامة للمطورين في لغات البرمجة المختلفة
دعونا نتذكر تعريف أنواع البيانات التي قدمتها في بداية المقال:
الأنواع هي الطريقة التي تصف بها البيانات التي سيعمل بها البرنامج.
أي تم تصميم الأنواع لمساعدتنا في وصف البيانات من مجال موضوعنا الذي تعمل فيه أنظمتنا. ومع ذلك ، بدلاً من العمل مع أنواع البيانات التي أنشأناها من مجال موضوعنا ، غالبًا ما نستخدم الأنواع البدائية ، مثل الأرقام والسلاسل والمصفوفات وما إلى ذلك ، والتي لا تخبرنا بأي شيء عن مجال موضوعنا. عادة ما تصنف هذه المشكلة على أنها هوس بدائي (هاجس بدائي).
فيما يلي مثال هوس بدائي نموذجي:
price = 9.99 # vs Money = Struct.new(:amount_cents, :currency) price = Money.new(9_99, 'USD')
بدلاً من وصف نوع البيانات للعمل بالمال ، غالبًا ما يتم استخدام الأرقام العادية. وهذا الرقم ، مثل أي أنواع بدائية أخرى ، لا يقول شيئًا عن موضوعنا. في رأيي ، هذه أكبر مشكلة في استخدام البدائل بدلاً من إنشاء نظام الكتابة الخاص بك ، حيث تصف هذه الأنواع البيانات من مجال موضوعنا. نحن أنفسنا نرفض المزايا التي يمكن أن نحصل عليها باستخدام الأنواع.
سأتحدث عن هذه المزايا مباشرة بعد تغطية قضية أخرى علمناها إطار عملنا المفضل لدى روبي أون ريلز ، بفضل ذلك ، أنا متأكد من أن معظم هؤلاء هنا قد وصلوا إلى روبي.
السبب 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
أن يكون للكائنات الموجودة في هذا النموذج سمة نظام أساسي ، وأن كائننا لديه هذه السمة فارغة.
الآن ، دعونا نحاول تمرير هذا الكائن في حالة غير صالحة إلى خدمة تتوقع كائن
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:
- اللغة نفسها لديها آلية للتحقق من السلوك ، ولكن ليس البيانات
- نحن ، مثل المطورين بلغات أخرى ، نميل إلى استخدام أنواع البيانات البدائية بدلاً من إنشاء نظام كتابة لموضوعنا
- اعتادنا القضبان على حقيقة أن وجود الأشياء في حالة غير صالحة أمر طبيعي ، على الرغم من أن مثل هذا الحل يبدو فكرة سيئة للغاية
كيف يمكن حل هذه المشكلات؟
أرغب في التفكير في أحد الحلول للمشكلات الموضحة أعلاه ، وذلك باستخدام مثال لتطبيق ميزة حقيقية في Appodeal. في عملية جمع إحصائيات حول إحصاءات المستخدمين النشطين اليومية (المشار إليها فيما يلي بـ DAU) للتطبيقات التي تستخدم Appodeal لتحقيق الدخل ، توصلنا إلى بنية البيانات التالية التي نحتاج إلى جمعها تقريبًا:
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-struct
. تعد
dry-types
نظامًا بسيطًا وقابل للتوسعة لـ Ruby ، وهو مفيد للصب ، وتطبيق العديد من القيود ، وتحديد الهياكل المعقدة ، وما إلى ذلك.
dry-struct
والبنية
dry-struct
هي مكتبة مبنية على أعلى
dry-types
توفر DSL مناسبًا لتحديد الهياكل المكتوبة / الطبقات.
لوصف بيانات مجال موضوعنا المستخدم في هيكل جمع DAUs ، تم إنشاء نظام النوع التالي:
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
الأنواع
AdTypeId
و
PlatformId
على قيم من مجموعة محددة فقط. كيفية العمل مع هذه الأنواع؟ النظر في
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
من هذا: إذا حاولنا تهيئة البنية بقيم غير صالحة ، فسنحصل على خطأ. وبالنسبة لتلك الحالات التي تكون فيها صحة البيانات ضرورية (وفي حالة جمع DAU ، هذه هي الحالة معنا) ، في رأيي ، الحصول على استثناء أفضل بكثير من محاولة التعامل مع البيانات غير الصالحة لاحقًا. بالإضافة إلى ذلك ، إذا كانت عملية الاختبار مثبتة بشكل جيد بالنسبة لك (وهذا هو الحال تمامًا معنا) ، فعندما يكون هناك احتمال كبير ، فإن الشفرة التي تولد مثل هذه الأخطاء لن تصل ببساطة إلى بيئة الإنتاج.
بالإضافة إلى عدم القدرة على تهيئة الكائنات في حالة غير صالحة ،
dry-struct
أيضًا لا تسمح بتغيير الكائنات بعد التهيئة. بفضل هذين العاملين ، نحصل على ضمان بأن كائنات هذه الهياكل ستكون في حالة صالحة ويمكنك الاعتماد على هذه البيانات بأمان في أي مكان في نظامك.
ملخص
حاولت في هذه المقالة وصف المشكلات التي قد تواجهها أثناء العمل مع البيانات في Ruby ، بالإضافة إلى الحديث عن الأدوات التي نستخدمها لحل هذه المشكلات. وبفضل تطبيق هذه الأدوات ، توقفت تمامًا عن القلق بشأن صحة البيانات التي نعمل معها. ليس هذا الكمال؟ أليس هذا هو الغرض من أي أداة - لتسهيل حياتنا في بعض جوانبها؟ وفي رأيي ،
dry-types
dry-struct
بعملها على أكمل وجه!