Kodokon kodokon.com

نمذجة بياناتك: الأصناف وJSON وعدم القابلية للتغيير

حوِّل JSON إلى أصناف Dart غير قابلة للتغيير باستخدام fromJson وtoJson وcopyWith، مع مساواة قيمية موثوقة.

9 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

في هذا المستوى، لم يعد الموضوع الحقيقي هو صياغة الأصناف بل الحدّ الفاصل بين العالم الخارجي وكودك. فما دامت استجابة واجهة برمجية (API) تتنقّل على هيئة Map<String, dynamic>، فإن قراءة كل حقل مجازفة أثناء التشغيل. القاعدة الاحترافية: حوِّل JSON إلى كائنات ذات أنواع في اللحظة التي تستقبلها فيها، ثم مرِّر تلك الكائنات وحدها. اجمع هذا مع عدم القابلية للتغيير - حقول final وباني const - فتقضي على عائلة كاملة من العلل: لم يعد بإمكان أحد أن يعدّل كائنًا مشتركًا من وراء ظهرك، كما أن نسختين const متطابقتين يوحّدهما المُصرِّف.

DART
class User {
  const User({
    required this.id,
    required this.name,
    this.email,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: (json['id'] as num).toInt(),
      name: json['name'] as String,
      email: json['email'] as String?,
    );
  }

  final int id;
  final String name;
  final String? email;
}
نموذج غير قابل للتغيير مع تحليل دفاعي لـ JSON.

ثلاثة خيارات تستحق التوضيح. أولًا، (json['id'] as num).toInt(): تُسلسِل كثير من واجهات البرمجة عددًا صحيحًا تارةً على شكل 3 وتارةً على شكل 3.0 حسب الخادم الخلفي؛ والتحويل المباشر إلى int ينهار عند مصادفة double. ثانيًا، يفشل as String فورًا مع TypeError واضح إذا كان الحقل مفقودًا: وهذا مقصود. فالفشل السريع عند التحليل خير من null ينتشر ثم ينفجر بعد ثلاث شاشات. وأخيرًا، يوثّق as String? في النوع أن الحقل اختياري فعلًا.

DART
class User {
  const User({required this.id, this.email});

  final int id;
  final String? email;

  Map<String, dynamic> toJson() => {
        'id': id,
        'email': email,
      };

  User copyWith({int? id, String? email}) {
    return User(
      id: id ?? this.id,
      email: email ?? this.email,
    );
  }
}
toJson وcopyWith في نموذج مُختصَر إلى حقلين.

يستدعي عدم القابلية للتغيير مساواةً قيمية. فبشكل افتراضي، تقارن Dart المراجع: إذ يُعدّ كائنا User متطابقان حقلًا بحقل "مختلفين". وهذه مشكلة ملموسة بمجرد أن تقارن الحالات - في الاختبارات، وفي select الخاص بـ Provider، وفي البحث داخل قائمة. أعِد تعريف == وhashCode معًا، دائمًا.

DART
class Point {
  const Point(this.x, this.y);

  final int x;
  final int y;

  @override
  bool operator ==(Object other) =>
      other is Point && other.x == x && other.y == y;

  @override
  int get hashCode => Object.hash(x, y);
}
المساواة القيمية: == وhashCode يأتيان معًا كزوج.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. لماذا نكتب (json['id'] as num).toInt() بدلًا من json['id'] as int؟
    • لأن as int أبطأ من as num أثناء التشغيل.
    • لأن الخادم الخلفي قد يُسلسِل عددًا صحيحًا على شكل 3.0: فالتحويل المباشر إلى int سيفشل عند مصادفة double.
    • لأن json['id'] سلسلة نصية دائمًا.
  2. ماذا يُنتِج user.copyWith(email: null) مع التنفيذ التقليدي لـ copyWith؟
    • يُعاد الحقل email إلى null كما هو مطلوب.
    • يُطلق استثناء أثناء التشغيل.
    • يحتفظ الكائن بقيمة email الحالية: إذ يرتد null ?? this.email إلى القيمة الموجودة.
    • لا يُصرَّف الكود في الوضع الصارم.
  3. لماذا نعيد تعريف == وhashCode في نموذج غير قابل للتغيير؟
    • لأن Dart تقارن المراجع بشكل افتراضي: وإلا فإن كائنين متطابقين حقلًا بحقل سيكونان "مختلفين".
    • لتسريع إنشاء نسخ const.
    • يصبح إلزاميًا بمجرد التصريح بحقل على أنه final.