Dart: تصميم المشروع وتطويره — أداة CLI لخط أنابيب البيانات

آخر تحديث: 2026-08-26

المهندس الحقيقي لا يكتب الكود فقط؛ بل يصمم البنى التي تسمح للكود بالنمو. — بوب

1. ما ستتعلمه


2. قصة مطور حقيقية

(1) نقطة الألم: آلام النمو من السكربتات إلى أداة

كتب بوب في البداية عدة سكربتات Dart مستقلة لمعالجة بيانات التجارة الإلكترونية: parse_csv.dart، calc_stats.dart، gen_report.dart. مع نمو المتطلبات، حدث المزيد والمزيد من النسخ واللصق بين السكربتات. تغيير اسم حقل واحد تطلب تعديل 5 ملفات. سألت أليس، "هل يمكننا إضافة مصدر بيانات JSON؟" أدرك بوب أن جميع منطق التحليل كان مقترناً بإحكام مع CSV وكان صعب التعديل.

(2) حل إعادة التصميم

قرر بوب تصميم خط أنابيب البيانات من الصفر، مع اعتماد بنية طبقية، وتجريد مصادر البيانات بفئات مختومة، واستخدام خطوط أنابيب Stream للمعالجة غير المتزامنة. أجرى تشارلي مراجعة الكود لضمان الجودة.

100%
graph TD
  subgraph طبقات البنية
    A[طبقة CLI<br/>تحليل args]
    B[طبقة الخدمة<br/>جدولة خط الأنابيب]
    C[طبقة البيانات<br/>المصدر + المحلل]
    D[الطبقة الأساسية<br/>النماذج + الأدوات]
  end
  A --> B
  B --> C
  C --> D
  B --> E[Isolate Pool]
  B --> F[خط أنابيب Stream]
  C --> G["DataSource مختوم"]
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.

(3) الفوائد


3. تصميم بنية المشروع

(1) البنية الطبقية

الطبقة الدليل المسؤولية التبعيات
طبقة CLI bin/ تحليل الوسائط، توجيه الأوامر طبقة الخدمة
طبقة الخدمة lib/src/services/ جدولة خط الأنابيب، إدارة Isolate طبقة البيانات + الأساسية
طبقة البيانات lib/src/data/ تجريد مصدر البيانات، المحللات الطبقة الأساسية
الطبقة الأساسية lib/src/core/ النماذج، الأدوات، الثوابت بدون تبعيات خارجية

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.

: بنية دليل المشروع

TEXT 📖 للعرض فقط
data_pipeline/
  bin/
    data_pipeline.dart        # نقطة دخول CLI
  lib/
    src/
      core/
        models/
          order.dart
          product.dart
          customer.dart
          analysis_result.dart
        utils/
          formatters.dart
          validators.dart
          constants.dart
      data/
        sources/
          data_source.dart     # صنف مختوم
          csv_source.dart
          json_source.dart
          api_source.dart
        parsers/
          order_parser.dart
          product_parser.dart
      services/
        pipeline.dart
        analyzer.dart
        isolate_pool.dart
        report_generator.dart
    data_pipeline.dart         # تصدير شامل
  test/
    core/
      models_test.dart
      utils_test.dart
    data/
      parsers_test.dart
    services/
      pipeline_test.dart
      analyzer_test.dart
  pubspec.yaml
```text


```text
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.

4. تحليل وسائط CLI والأوامر الفرعية

(1) تصميم الأوامر الفرعية لحزمة args

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.

: نقطة دخول CLI والأوامر الفرعية

DART
import 'package:args/args.dart';
import 'package:args/command_runner.dart';

// أمر فرعي: تحليل
class AnalyzeCommand extends Command {
  @override
  final name = 'analyze';
  @override
  final description = 'Analyze order data and generate statistics';

  AnalyzeCommand() {
    argParser
      ..addOption('source', abbr: 's', defaultsTo: 'csv', allowed: ['csv', 'json', 'api'])
      ..addOption('input', abbr: 'i', mandatory: true)
      ..addOption('output', abbr: 'o', defaultsTo: 'stdout')
      ..addFlag('parallel', abbr: 'p', defaultsTo: false)
      ..addOption('isolate-count', defaultsTo: '4');
  }

  @override
  Future<void> run() async {
    final source = argResults!['source'] as String;
    final input = argResults!['input'] as String;
    final output = argResults!['output'] as String;
    final parallel = argResults!['parallel'] as bool;
    final isolateCount = int.parse(argResults!['isolate-count'] as String);

    print('Source: $source | Input: $input | Parallel: $parallel');
  }
}

// أمر فرعي: تصدير
class ExportCommand extends Command {
  @override
  final name = 'export';
  @override
  final description = 'Export analysis results to file';

  ExportCommand() {
    argParser
      ..addOption('format', defaultsTo: 'json', allowed: ['json', 'csv', 'markdown'])
      ..addOption('input', abbr: 'i', mandatory: true)
      ..addOption('output', abbr: 'o', mandatory: true);
  }

  @override
  Future<void> run() async {
    final format = argResults!['format'] as String;
    final input = argResults!['input'] as String;
    final output = argResults!['output'] as String;

    print('Export: $format | $input -> $output');
  }
}

void main(List<String> args) async {
  final runner = CommandRunner('data_pipeline', 'DataPipeline - E-commerce data analytics CLI')
    ..addCommand(AnalyzeCommand())
    ..addCommand(ExportCommand());

  try {
    await runner.run(args);
  } on UsageException catch (e) {
    print(e);
  }
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
الأمر الفرعي الوظيفة المعاملات الرئيسية
analyze تحليل البيانات والإحصائيات --source, --input, --parallel
export تصدير التقرير --format, --input, --output
validate التحقق من البيانات --input, --strict

5. تجريد مصدر البيانات — صنف مختوم

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.

: DataSource مختوم

DART
sealed class DataSource {
  const DataSource();

  String get displayName;

  Stream<String> readLines();
}

class CsvSource extends DataSource {
  final String path;
  final String delimiter;

  const CsvSource({required this.path, this.delimiter = ','});

  @override
  String get displayName => 'CSV: $path';

  @override
  Stream<String> readLines() => File(path).openRead().transform(utf8.decoder).transform(const LineSplitter());
}

class JsonSource extends DataSource {
  final String path;

  const JsonSource({required this.path});

  @override
  String get displayName => 'JSON: $path';

  @override
  Stream<String> readLines() async* {
    final content = await File(path).readAsString();
    final jsonList = jsonDecode(content) as List;
    for (final item in jsonList) {
      yield jsonEncode(item);
    }
  }
}

class ApiSource extends DataSource {
  final String endpoint;
  final Map<String, String> headers;

  const ApiSource({required this.endpoint, this.headers = const {}});

  @override
  String get displayName => 'API: $endpoint';

  @override
  Stream<String> readLines() async* {
    final client = Client();
    final response = await client.get(Uri.parse(endpoint), headers: headers);
    final jsonList = jsonDecode(response.body) as List;
    for (final item in jsonList) {
      yield jsonEncode(item);
    }
    client.close();
  }
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.

: مطابقة أنماط لاستهلاك DataSource

DART
DataSource createSource(String type, String input) => switch (type) {
  'csv' => CsvSource(path: input),
  'json' => JsonSource(path: input),
  'api' => ApiSource(endpoint: input),
  _ => throw ArgumentError('Unknown source type: $type'),
};

String sourceIcon(DataSource source) => switch (source) {
  CsvSource() => '📄',
  JsonSource() => '📋',
  ApiSource() => '🌐',
};
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
النمط الغرض الميزة
صنف مختوم أنواع مصادر البيانات ضمان switch الشامل وقت الترجمة
مطابقة الأنماط استهلاك مصادر البيانات بدون سلاسل if-else
دالة المصنع إنشاء مصادر البيانات نقطة دخول موحدة

6. محللات متعددة التنسيقات

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.

: محلل الطلبات

DART
abstract class OrderParser {
  const OrderParser();

  Order parse(String raw);
  List<Order> parseBatch(List<String> raws) => raws.map(parse).toList();
}

class CsvOrderParser extends OrderParser {
  final String delimiter;

  const CsvOrderParser({this.delimiter = ','});

  @override
  Order parse(String raw) {
    final parts = raw.split(delimiter);
    if (parts.length < 4) {
      throw FormatException('Invalid CSV row: $raw');
    }
    return Order(
      id: parts[0].trim(),
      amount: double.parse(parts[1].trim()),
      status: parts[2].trim(),
      category: parts[3].trim(),
      region: parts.length > 4 ? parts[4].trim() : 'US',
    );
  }
}

class JsonOrderParser extends OrderParser {
  const JsonOrderParser();

  @override
  Order parse(String raw) {
    final json = jsonDecode(raw) as Map<String, dynamic>;
    return Order(
      id: json['id'] as String,
      amount: (json['amount'] as num).toDouble(),
      status: json['status'] as String,
      category: json['category'] as String,
      region: (json['region'] as String?) ?? 'US',
    );
  }
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.

7. خط أنابيب غير متزامن قائم على Stream

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.

: معالجة Stream لخط الأنابيب

DART
import 'dart:async';
import 'dart:convert';
import 'dart:io';

class Pipeline {
  final DataSource _source;
  final OrderParser _parser;
  final int _chunkSize;

  Pipeline({
    required DataSource source,
    required OrderParser parser,
    int chunkSize = 1000,
  })  : _source = source,
        _parser = parser,
        _chunkSize = chunkSize;

  Future<AnalysisResult> execute() async {
    final orders = <Order>[];
    var processed = 0;
    var skipped = 0;

    await for (final line in _source.readLines()) {
      try {
        final order = _parser.parse(line);
        orders.add(order);
        processed++;
      } on FormatException {
        skipped++;
      }

      if (processed % _chunkSize == 0) {
        stdout.writeln('Progress: $processed orders processed, $skipped skipped');
      }
    }

    stdout.writeln('Total: $processed processed, $skipped skipped');

    final analyzer = OrderAnalyzer();
    return analyzer.analyze(orders);
  }

  Stream<Order> streamOrders() async* {
    await for (final line in _source.readLines()) {
      try {
        yield _parser.parse(line);
      } on FormatException {
        continue;
      }
    }
  }

  Stream<AnalysisResult> streamByCategory() {
    final controller = StreamController<AnalysisResult>();

    streamOrders().fold<Map<String, List<Order>>>(
      {},
      (groups, order) {
        groups.update(order.category, (v) => v..add(order), ifAbsent: () => [order]);
        return groups;
      },
    ).then((groups) {
      final analyzer = OrderAnalyzer();
      for (final entry in groups.entries) {
        controller.add(analyzer.analyze(entry.value));
      }
      controller.close();
    });

    return controller.stream;
  }
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.

8. المعالجة المتوازية بـ Isolate

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.

: Isolate Pool

DART
import 'dart:isolate';

class IsolatePool {
  final int _poolSize;
  final List<Isolate> _isolates = [];
  final List<SendPort> _sendPorts = [];

  IsolatePool({int poolSize = 4}) : _poolSize = poolSize;

  Future<void> initialize() async {
    for (var i = 0; i < _poolSize; i++) {
      final receivePort = ReceivePort();
      final isolate = await Isolate.spawn(
        _isolateEntryPoint,
        receivePort.sendPort,
      );
      final sendPort = await receivePort.first as SendPort;
      _isolates.add(isolate);
      _sendPorts.add(sendPort);
    }
  }

  static void _isolateEntryPoint(SendPort mainSendPort) {
    final receivePort = ReceivePort();
    mainSendPort.send(receivePort.sendPort);

    receivePort.listen((message) {
      if (message is _IsolateTask) {
        final result = _processChunk(message.orders, message.taxRate);
        message.responsePort.send(result);
      }
    });
  }

  static AnalysisResult _processChunk(List<Map<String, dynamic>> rawOrders, double taxRate) {
    final orders = rawOrders.map((o) => Order(
      id: o['id'] as String,
      amount: (o['amount'] as num).toDouble(),
      status: o['status'] as String,
      category: o['category'] as String,
      region: (o['region'] as String?) ?? 'US',
    )).toList();

    final analyzer = OrderAnalyzer(taxRate: taxRate);
    return analyzer.analyze(orders);
  }

  Future<List<AnalysisResult>> processInParallel(
    List<Map<String, dynamic>> allOrders,
    double taxRate,
  ) async {
    final chunkSize = (allOrders.length / _poolSize).ceil();
    final results = <AnalysisResult>[];
    final completers = <Completer<AnalysisResult>>[];

    for (var i = 0; i < _poolSize; i++) {
      final start = i * chunkSize;
      final end = (start + chunkSize).clamp(0, allOrders.length);
      if (start >= allOrders.length) break;

      final chunk = allOrders.sublist(start, end);
      final completer = Completer<AnalysisResult>();
      completers.add(completer);

      _sendPorts[i].send(_IsolateTask(
        orders: chunk,
        taxRate: taxRate,
        responsePort: completer.future as dynamic,
      ));
    }

    for (final completer in completers) {
      results.add(await completer.future);
    }

    return results;
  }

  void dispose() {
    for (final isolate in _isolates) {
      isolate.kill(priority: Isolate.immediate);
    }
  }
}

class _IsolateTask {
  final List<Map<String, dynamic>> orders;
  final double taxRate;
  final SendPort responsePort;

  const _IsolateTask({
    required this.orders,
    required this.taxRate,
    required this.responsePort,
  });
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
النموذج المتوازي السيناريو المناسب الميزة
Isolate.run حساب لمرة واحدة API بسيط
Isolate.spawn عامل طويل الأمد قابل لإعادة الاستخدام
Isolate Pool تقسيم متساوي استخدام كامل لأنوية متعددة
compute (Flutter) بدون تعليق واجهة المستخدم خاص بـ Flutter

9. حقن التبعية

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.

: نمط Service Locator

DART
class ServiceLocator {
  static final _instances = <Type, dynamic>{};

  static void register<T>(T instance) {
    _instances[T] = instance;
  }

  static T get<T>() {
    final instance = _instances[T];
    if (instance == null) {
      throw StateError('Service not registered: $T');
    }
    return instance as T;
  }

  static void reset() {
    _instances.clear();
  }
}

// التسجيل عند البدء
void setupServices({required String sourceType, required String inputPath}) {
  final source = createSource(sourceType, inputPath);

  ServiceLocator.register<DataSource>(source);

  final parser = switch (source) {
    CsvSource() => const CsvOrderParser() as OrderParser,
    JsonSource() => const JsonOrderParser(),
    ApiSource() => const JsonOrderParser(),
  };

  ServiceLocator.register<OrderParser>(parser);
  ServiceLocator.register<OrderAnalyzer>(const OrderAnalyzer());
  ServiceLocator.register<Pipeline>(Pipeline(source: source, parser: parser));
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.

10. المثال الكامل: أداة CLI لخط أنابيب البيانات

DART
// ============================================
// أداة CLI لخط أنابيب البيانات - التنفيذ الكامل
// أداة تحليل بيانات التجارة الإلكترونية لبوب
// ============================================

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';

// ---- النماذج الأساسية ----

class Order {
  final String id;
  final double amount;
  final String status;
  final String category;
  final String region;

  const Order({
    required this.id,
    required this.amount,
    required this.status,
    required this.category,
    this.region = 'US',
  });

  double get tax => amount * _taxRate(region);
  double get total => amount + tax;

  static double _taxRate(String region) => switch (region) {
    'US' => 0.08,
    'EU' => 0.20,
    'UK' => 0.15,
    'JP' => 0.10,
    _ => 0.10,
  };

  Map<String, dynamic> toJson() => {
    'id': id,
    'amount': amount,
    'status': status,
    'category': category,
    'region': region,
  };
}

class AnalysisResult {
  final int totalOrders;
  final int completedOrders;
  final double revenue;
  final double tax;
  final double total;
  final Map<String, double> revenueByCategory;

  const AnalysisResult({
    required this.totalOrders,
    required this.completedOrders,
    required this.revenue,
    required this.tax,
    required this.total,
    required this.revenueByCategory,
  });

  double get averageOrderValue => completedOrders > 0 ? revenue / completedOrders : 0;

  @override
  String toString() => '''
=== DataPipeline Analytics ===
Orders:    $completedOrders/$totalOrders completed
Revenue:   \$${revenue.toStringAsFixed(2)} USD
Tax:       \$${tax.toStringAsFixed(2)} USD
Total:     \$${total.toStringAsFixed(2)} USD
Average:   \$${averageOrderValue.toStringAsFixed(2)} USD
Categories: ${revenueByCategory.keys.join(', ')}
''';
}

// ---- الخدمات الأساسية ----

class OrderAnalyzer {
  final double taxRate;

  const OrderAnalyzer({this.taxRate = 0.08});

  AnalysisResult analyze(List<Order> orders) {
    final completed = orders.where((o) => o.status == 'completed').toList();
    final revenue = completed.fold<double>(0, (s, o) => s + o.amount);
    final tax = completed.fold<double>(0, (s, o) => s + o.tax);
    final total = revenue + tax;

    final byCategory = <String, double>{};
    for (final o in completed) {
      byCategory.update(o.category, (v) => v + o.amount, ifAbsent: () => o.amount);
    }

    return AnalysisResult(
      totalOrders: orders.length,
      completedOrders: completed.length,
      revenue: revenue,
      tax: tax,
      total: total,
      revenueByCategory: byCategory,
    );
  }
}

// ---- مصدر البيانات (مختوم) ----

sealed class DataSource {
  const DataSource();
  String get displayName;
  Stream<String> readLines();
}

class CsvSource extends DataSource {
  final String path;
  const CsvSource({required this.path});

  @override
  String get displayName => 'CSV: $path';

  @override
  Stream<String> readLines() =>
    File(path).openRead().transform(utf8.decoder).transform(const LineSplitter());
}

class JsonSource extends DataSource {
  final String path;
  const JsonSource({required this.path});

  @override
  String get displayName => 'JSON: $path';

  @override
  Stream<String> readLines() async* {
    final content = await File(path).readAsString();
    final list = jsonDecode(content) as List;
    for (final item in list) {
      yield jsonEncode(item);
    }
  }
}

// ---- المحلل ----

abstract class OrderParser {
  const OrderParser();
  Order parse(String raw);
}

class CsvOrderParser extends OrderParser {
  const CsvOrderParser();

  @override
  Order parse(String raw) {
    final parts = raw.split(',');
    if (parts.length < 4) throw FormatException('Invalid row: $raw');
    return Order(
      id: parts[0].trim(),
      amount: double.parse(parts[1].trim()),
      status: parts[2].trim(),
      category: parts[3].trim(),
    );
  }
}

class JsonOrderParser extends OrderParser {
  const JsonOrderParser();

  @override
  Order parse(String raw) {
    final json = jsonDecode(raw) as Map<String, dynamic>;
    return Order(
      id: json['id'] as String,
      amount: (json['amount'] as num).toDouble(),
      status: json['status'] as String,
      category: json['category'] as String,
    );
  }
}

// ---- خط الأنابيب ----

class Pipeline {
  final DataSource source;
  final OrderParser parser;

  const Pipeline({required this.source, required this.parser});

  Future<AnalysisResult> execute() async {
    final orders = <Order>[];
    var count = 0;

    await for (final line in source.readLines()) {
      try {
        orders.add(parser.parse(line));
        count++;
        if (count % 100000 == 0) {
          stdout.writeln('Progress: ${(count / 1000).toStringAsFixed(0)}K orders');
        }
      } on FormatException {
        continue;
      }
    }

    stdout.writeln('Loaded: ${(count / 1000).toStringAsFixed(0)}K orders from ${source.displayName}');
    return const OrderAnalyzer().analyze(orders);
  }
}

// ---- مولد البيانات التجريبي ----

List<String> generateDemoOrders(int count) {
  final categories = ['Electronics', 'Clothing', 'Books', 'Home', 'Sports'];
  final statuses = ['completed', 'completed', 'completed', 'pending', 'cancelled'];
  final regions = ['US', 'EU', 'UK', 'JP'];

  return List.generate(count, (i) {
    final category = categories[i % categories.length];
    final status = statuses[i % statuses.length];
    final region = regions[i % regions.length];
    final amount = (100 + (i * 37) % 5000).toDouble();
    return 'ORD-${(i + 1).toString().padLeft(5, '0')},$amount,$status,$category,$region';
  });
}

// ---- الرئيسية ----

void main() async {
  print('=== DataPipeline CLI v1.0 ===\n');

  final demoData = generateDemoOrders(1000);
  final tempFile = File('${Directory.systemTemp.path}/demo_orders.csv');
  await tempFile.writeAsString(demoData.join('\n'));

  final source = CsvSource(path: tempFile.path);
  final parser = const CsvOrderParser();
  final pipeline = Pipeline(source: source, parser: parser);

  print('Source: ${source.displayName}');
  print('Processing...\n');

  final result = await pipeline.execute();
  print(result);

  final sorted = result.revenueByCategory.entries.toList()
    ..sort((a, b) => b.value.compareTo(a.value));

  print('Revenue by Category:');
  for (final entry in sorted) {
    print('  ${entry.key.padRight(12)}: \$${entry.value.toStringAsFixed(2)} USD');
  }

  await tempFile.delete();
  print('\nDone. Temporary file cleaned up.');
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو باستخدام `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.

11. مراجعة كود تشارلي

(1) ▶ نقاط المراجعة

البُعد معايير المراجعة قائمة مراجعة خط أنابيب البيانات
أمان النوع بدون dynamic، switch شامل DataSource مختوم + مطابقة الأنماط
قابلية الاختبار المنطق الأساسي مستقل عن I/O OrderParser دالة خالصة، Pipeline يقبل الواجهة
معالجة الأخطاء لا تبتلع الاستثناءات، لها بدائل تخطي FormatException + العد
الأداء التحكم في الذاكرة، غير متزامن غير معرق Stream سطر بسطر + تقسيم Isolate
قابلية التوسع الميزات الجديدة لا تغير الكود القديم فئة فرعية جديدة من DataSource بدون تعديل

تشارلي: "الصنف المختوم + مطابقة الأنماط يتيح للمترجم التحقق من الإغفالات، أكثر أماناً بكثير من if-else. OrderParser دالة خالصة، لذا لا يتطلب الاختبار محاكاة نظام الملفات."


❓ أسئلة شائعة

س: لماذا استخدام صنف مختوم بدلاً من enums لمصادر البيانات؟ ج: لا يمكن لـ enums حمل البيانات (مثل path، endpoint). يمكن للفئات المختومة أن يكون لها حقول وطرق مع الحفاظ على ضمان switch الشامل وقت الترجمة.

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

س: هل تواصل Isolate يمرر فقط الأنواع البدائية؟ ج: يجب أن تكون البيانات الممررة عبر SendPort قابلة للتسلسل. تحتاج الفئات المخصصة إلى التحويل إلى خرائط أو استخدام jsonEncode/Decode. سجلات Dart 3 مدعومة أيضاً للتمرير.

س: أيهما أفضل، حزمة args أم حزمة dcli؟ ج: args هي حزمة رسمية، مناسبة لأنماط الأوامر الفرعية القياسية. توفر dcli أدوات CLI أغنى (عمليات الملفات، إدارة العمليات، إلخ) لكنها غير رسمية. يستخدم هذا البرنامج التعليمي args.

س: هل الإطار ضروري لحقن التبعية؟ ج: ليس بالضرورة. للمشاريع الصغيرة، يكفي Service Locator أو حقن المُنشئ. أطر مثل flutter_bloc و get_it مناسبة للمشاريع الكبيرة. يستخدم خط أنابيب البيانات ServiceLocator بسيط.

س: كيف أختبر تدفق خط الأنابيب من النهاية إلى النهاية؟ ج: استخدم ملفاً مؤقتاً (Directory.systemTemp) كمصدر بيانات اختبار وتحقق من قيم كل حقل في AnalysisResult الناتج. انظر مثال الاختبار التكامل في الدرس L20.


📖 ملخص


📝 تمارين

  1. أساسي (صعوبة ⭐): أضف أمر validate فرعي إلى خط أنابيب البيانات يقبل معامل --input، ويعد عدد الأسطر ذات التنسيق غير الصحيح في ملف CSV، ويبلغ عنه.
  2. متقدم (صعوبة ⭐⭐): نفّذ JsonSource + JsonOrderParser. استخدم مصفوفة JSON كمدخلات، شغّل خط الأنابيب، وأخرج نتائج التحليل. تحقق مما إذا كان switch الشامل للصنف المختوم يغطي جميع الفئات الفرعية.
  3. تحدي (صعوبة ⭐⭐⭐): نفّذ IsolatePool. قسم 1000 سجل بيانات تجريبي إلى 4 أجزاء، استخدم 4 Isolates للتحليل بالتوازي، وأخيراً ادمج 4 كائنات AnalysisResult (اجمع الإيرادات وعدد الطلبات بشكل منفصل). قِس فرق الوقت بين التنفيذ المتوازي والمتسلسل.

← الدرس السابق | الدرس التالي →

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%