بنية التطبيق وأنماط التصميم

مقدمة إلى هندسة التطبيقات

15 دقيقة الدرس 1 من 12

مقدمة إلى هندسة التطبيقات

مع نمو تطبيق Flutter الخاص بك إلى ما هو أبعد من عدد قليل من الشاشات، ستواجه سريعاً عدواً خفياً: كود السباغيتي (Spaghetti Code). تعيش منطق جلب البيانات وتحويلها وعرضها جميعاً داخل دوال build()، وتصبح الودجات مستحيلة الاختبار، وتُعطِّل تغييرٌ واحد خمسة أشياء غير مترابطة. هندسة التطبيقات (App Architecture) هي مجموعة القرارات الهيكلية التي تتخذها مسبقاً لمنع هذا الانهيار — تحديد كيفية توزيع المسؤوليات وتدفق البيانات وتواصل المكونات.

يقدّم هذا الدرس السبب الكامن وراء أنماط البنية. لا تحتاج إلى حفظ أي نمط محدد بعد؛ بدلاً من ذلك، ركّز على فهم المشكلات الثلاث الأساسية التي يُصمَّم كل نمط لحلها: قابلية الاختبار، وقابلية التوسع، وقابلية الصيانة.

ملاحظة: الهندسة ليست مبالغةً في تعقيد تطبيق عداد بسيط. إنها تتعلق بكتابة كود يظل مقروءاً وقابلاً للتعديل والاختبار مع تزايد تعقيد تطبيقك الحقيقي حتماً.

مشكلة كود السباغيتي

فكّر في شاشة تُحمِّل قائمة مستخدمين من REST API وتُصفّيهم وتعرضهم. بدون هندسة، كثيراً ما يضع المطور كل شيء في مكان واحد:

النمط المضاد: كل شيء داخل الودجت

// سيء: ودجت يقوم بأكثر مما ينبغي
class UserListScreen extends StatefulWidget {
  const UserListScreen({super.key});

  @override
  State<UserListScreen> createState() => _UserListScreenState();
}

class _UserListScreenState extends State<UserListScreen> {
  List<Map<String, dynamic>> _users = [];
  bool _isLoading = false;
  String _errorMessage = '';

  @override
  void initState() {
    super.initState();
    _fetchAndFilter();
  }

  // منطق الأعمال واستدعاءات HTTP ومعالجة الأخطاء كلها متشابكة
  Future<void> _fetchAndFilter() async {
    setState(() => _isLoading = true);
    try {
      final response = await http.get(Uri.parse('https://api.example.com/users'));
      final data = jsonDecode(response.body) as List;
      setState(() {
        _users = data
            .map((e) => e as Map<String, dynamic>)
            .where((u) => u['active'] == true)   // قاعدة عمل مدفونة هنا
            .toList();
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _errorMessage = 'فشل تحميل المستخدمين: $e';
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoading) return const CircularProgressIndicator();
    if (_errorMessage.isNotEmpty) return Text(_errorMessage);
    return ListView.builder(
      itemCount: _users.length,
      itemBuilder: (_, i) => ListTile(title: Text(_users[i]['name'] as String)),
    );
  }
}

هذا الكود يعمل، لكن فكّر فيما يحدث بعد ستة أشهر:

  • تريد كتابة اختبار وحدة لمنطق التصفية — لا تستطيع، لأنه مدفون في ودجت يتطلب بيئة اختبار Flutter كاملة.
  • شاشة ثانية تحتاج أيضاً نفس قائمة المستخدمين — تنسخ وتلصق استدعاء HTTP، مما يُنشئ تكراراً.
  • تتغير نقطة نهاية API — تبحث في ملفات الودجات بدلاً من طبقة بيانات واحدة.
  • يلتحق مطور جديد ولا يعرف أين توجد قواعد الأعمال.

الركائز الثلاث التي يجب أن تخدمها الهندسة

١. قابلية الاختبار

الكود الذي لا يمكن اختباره لا يمكن الوثوق به على نطاق واسع. عندما يكون منطق الأعمال مضمّناً في الودجات، يجب تشغيل شجرة ودجات Flutter بالكامل فقط للتحقق من أن محمول تصفية يعمل. يستخرج التطبيق ذو البنية الجيدة ذلك المنطق إلى فئات Dart عادية — فئات يمكن اختبارها بأمر dart test بسيط، دون الحاجة إلى إطار Flutter.

٢. قابلية التوسع

ميزة تستغرق يوماً واحداً للبناء في تطبيق بخمس شاشات لا ينبغي أن تستغرق أسبوعاً في تطبيق بخمسين شاشة. عندما تكون المسؤوليات مفصولة بوضوح، لا يتطلب إضافة شاشة جديدة فهم أجزاء غير مترابطة من قاعدة الكود أو تعديلها. تستطيع الفرق العمل بالتوازي على طبقات مختلفة دون أن يتعارض عملهم باستمرار.

٣. قابلية الصيانة

تُصان التطبيقات لفترة أطول بكثير مما تُكتب فيها. قابلية الصيانة تعني أن مطوراً — بما فيهم نفسك في المستقبل — يمكنه قراءة ملف وفهم مسؤوليته الواحدة فوراً وتغييره بثقة أن نطاق تأثير ذلك التغيير محدود ويمكن التنبؤ به.

بديل منظّم البنية

نفس شاشة قائمة المستخدمين، مقسّمة عبر طبقات، تصبح أسهل بكثير في التعامل:

نمط أفضل: فصل المسؤوليات

// ١. طبقة البيانات — لا تعرف شيئاً عن ودجات Flutter
class UserRepository {
  final http.Client _client;
  UserRepository(this._client);

  Future<List<User>> fetchActiveUsers() async {
    final response = await _client.get(Uri.parse('https://api.example.com/users'));
    if (response.statusCode != 200) throw Exception('خطأ في الخادم ${response.statusCode}');
    final data = jsonDecode(response.body) as List;
    return data
        .map((e) => User.fromJson(e as Map<String, dynamic>))
        .where((u) => u.isActive)   // قاعدة الأعمال هنا، سهلة الاختبار
        .toList();
  }
}

// ٢. طبقة النطاق — Dart نقي، لا استيرادات Flutter
class User {
  final String id;
  final String name;
  final bool isActive;
  const User({required this.id, required this.name, required this.isActive});

  factory User.fromJson(Map<String, dynamic> json) => User(
    id: json['id'] as String,
    name: json['name'] as String,
    isActive: json['active'] as bool,
  );
}

// ٣. طبقة العرض — الودجت يعرف فقط عن العرض
class UserListScreen extends StatelessWidget {
  final List<User> users;
  const UserListScreen({super.key, required this.users});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: users.length,
      itemBuilder: (_, i) => ListTile(title: Text(users[i].name)),
    );
  }
}
نصيحة: لاحظ أن UserRepository وUser لا يحتويان على أي استيرادات Flutter. يمكنك اختبار كل منطق جلب البيانات والتصفية باختبار Dart عادي وعميل HTTP وهمي — لا WidgetTester، لا pumpWidget، لا عبء إضافي.

أنماط الهندسة الشائعة في Flutter

تقارب نظام Flutter البيئي حول عدة أنماط مسمّاة، تطبّق كل منها مبدأ فصل المخاوف بطرق مختلفة قليلاً:

  • MVC (نموذج-عرض-متحكم) — الثلاثي الكلاسيكي؛ المتحكم يتوسط بين النموذج والعرض.
  • MVP (نموذج-عرض-معروض) — العرض سلبي؛ المعروض يحتفظ بكل منطق العرض.
  • MVVM (نموذج-عرض-نموذج العرض) — نموذج العرض يعرض تدفقات/حالة قابلة للملاحظة؛ العرض يتفاعل. شائع مع Provider وRiverpod.
  • BLoC (مكوّن منطق الأعمال) — أحداث تدخل، حالات تخرج؛ تدفق بيانات أحادي الاتجاه صارم. مُعتمد من فريق Flutter.
  • Clean Architecture (الهندسة النظيفة) — طبقات متحدة المركز (كيانات، حالات استخدام، محوّلات واجهة، أطر عمل) بقواعد تبعية صارمة.
تحذير: لا تُقلِّد نمطاً بشكل أعمى. اختيار Clean Architecture بخمس طبقات لتطبيق بشاشتين يُدخل تعقيداً يُدمّر الإنتاجية. طابق الهندسة مع الحجم الفعلي وحجم الفريق في مشروعك.

الملخص

الهندسة هي انضباط تحديد أين يعيش الكود ولماذا. يخلط نمط السباغيتي المضاد جلب البيانات ومنطق الأعمال وعرض واجهة المستخدم داخل الودجات، مُنتِجاً كوداً يستحيل اختباره بشكل منفصل ومُؤلِماً تغييره. تفصل المقاربة المنظّمة هذه المخاوف إلى طبقات مميزة — عادةً البيانات والنطاق والعرض — كل منها بمسؤولية واحدة محددة بوضوح. كل نمط مُسمَّى ستتعلمه في هذا الدرس (MVVM وBLoC وClean Architecture) هو إجابة محددة على نفس الأسئلة الثلاثة: كيف أجعل هذا قابلاً للاختبار؟ كيف يتوسع هذا؟ كيف يفهم مطور جديد هذا الكود ويعدّله بأمان؟

النقطة الرئيسية: قبل تعلم أي نمط محدد، استوعب الهدف: افصل المخاوف بحيث يكون لكل فئة سبب واحد للتغيير. هذا المبدأ الواحد هو أساس كل نمط هندسي ستصادفه في تطوير Flutter.