Sparround

Asılılıq qaydası və dörd dairə

"Clean architecture" termini Robert C. Martin-in 2012-ci ildəki məqaləsindən gəlir. Orada dörd konsentrik dairə var — mərkəzdən kənara:

1. Entities — biznesin özünə aid qaydalar. 2. Use Cases — tətbiqə aid qaydalar (bir ssenari, bir əməliyyat). 3. Interface Adapters — çevirici qat: controller, presenter, repository implementasiyası. 4. Frameworks and Drivers — xarici detallar: UI framework-u, verilənlər bazası, şəbəkə.

Bütün quruluşu bir qayda saxlayır — asılılıq qaydası: mənbə kodundaki asılılıqlar yalnız içəriyə yönələ bilər. Daxili dairə xarici dairə haqqında heç nə bilmir. Yəni entity http paketini, use-case Widget-i, domain modeli sqflite-i import etmir.

Bunun praktik nəticəsi Flutter üçün çox konkretdir: biznes məntiqin Flutter-dən asılı olmur. Onu dart test ilə, cihaz və emulator olmadan yoxlayırsan.

DairəFlutter-də qarşılığıNümunə sinifNəyi import edə bilər
EntitiesDomain modeli`Product`, `Order`, `Money`Yalnız `dart:core` və digər domain modelləri
Use CasesUse-case / interactor (opsional qat)`PlaceOrder`, `LoadCatalog`Domain modelləri və repository interfeysləri
Interface AdaptersRepository implementasiyası, DTO, mapper, notifier`ProductRepositoryRemote`, `ProductDto`Domain + service interfeysləri + `json_serializable`
Frameworks and DriversWidget-lər, `http`/`dio`, `sqflite`, platforma kanalları`ProductsPage`, `ApiClient`, `DatabaseService`Hər şeyi — bu, ən kənar dairədir

Burada məntiqi bir sual yaranır: use-case repository-yə müraciət edir, repository isə şəbəkəyə. Onda asılılıq içəridən kənara getmirmi?

Getmir — çünki arada abstraksiya var. Bu, asılılığın tərsinə çevrilməsi (dependency inversion) adlanır və Dart-da bir sətirlik texnikadır:

  • Domain qatında abstract class ProductRepository elan olunur — yalnız metodların imzası.
  • Data qatında class ProductRepositoryRemote implements ProductRepository yazılır.
  • Domain data qatını import etmir; data qatı domain-i import edir.

Ox belə çevrilir: kod yazma vaxtı asılılıq içəriyə (data → domain), icra vaxtı isə çağırış kənara (use-case → konkret implementasiya) gedir. Konkret implementasiyanın hansı olduğunu isə üçüncü tərəf — dependency injection — qərar verir.

dart
// ╔══ QAYDAYI POZUR ══════════════════════════════════════╗
// fayl: lib/domain/product.dart
import 'package:http/http.dart';          // ❌ domain şəbəkəni tanıyır
import 'package:flutter/material.dart';   // ❌ domain UI-nı tanıyır

class Product {
  Product(this.title, this.priceCents);
  final String title;
  final int priceCents;

  // ❌ domain modeli özü sorğu atır
  static Future<Product> fetch(String id) async { /* http.get(...) */ }

  // ❌ domain modeli rəng qaytarır — bu, UI qərarıdır
  Color get badgeColor => priceCents > 10000 ? Colors.red : Colors.green;
}


// ╔══ QAYDAYI SAXLAYIR ═══════════════════════════════════╗
// fayl: lib/domain/models/product.dart  — təmiz Dart, import yoxdur
class Product {
  const Product({required this.id, required this.title, required this.price});
  final String id;
  final String title;
  final double price;      // artıq manatla: çevirmə mapper-də olub

  bool get isExpensive => price > 100;   // biznes qaydası — UI deyil
}

// fayl: lib/domain/repositories/product_repository.dart
// Yalnız müqavilə. Domain "necə" olduğunu bilmir, "nə" olduğunu bilir.
abstract class ProductRepository {
  Future<List<Product>> fetchActive();
  Future<Product> fetchById(String id);
}

// fayl: lib/data/repositories/product_repository_remote.dart
import 'package:my_app/domain/models/product.dart';            // ✅ data → domain
import 'package:my_app/domain/repositories/product_repository.dart';
import 'package:my_app/data/services/product_api_client.dart';

class ProductRepositoryRemote implements ProductRepository {
  ProductRepositoryRemote({required ProductApiClient apiClient})
      : _apiClient = apiClient;

  final ProductApiClient _apiClient;

  @override
  Future<List<Product>> fetchActive() async {
    final dtos = await _apiClient.getActiveProducts();
    return dtos.map((dto) => dto.toDomain()).toList();
  }

  @override
  Future<Product> fetchById(String id) async =>
      (await _apiClient.getProduct(id)).toDomain();
}

Asılılıq qaydası import sətirlərində görünür. Sol tərəf qaydayı pozur, sağ tərəf saxlayır.

Asılılıq qaydasının gözəl tərəfi: o, mexaniki yoxlanıla bilər. Diskussiya lazım deyil — lib/domain qovluğunda flutter/, http, dio, sqflite, shared_preferences sətirlərinin olmaması bir grep addımıdır və CI-də saxlanıla bilər. Belə bir yoxlama komandada "bu import olar, ya olmaz?" mübahisəsini birdəfəlik bitirir.

Praktika. Layihəndə (ya da yeni yaratdığın kiçik nümunədə) lib/domain qovluğu yarat, oraya bir model və bir abstract class repository qoy. Sonra bu əmri işlət:

grep -rE "package:(flutter|http|dio|sqflite)" lib/domain || echo "domain təmizdir"

Hazır sayılır: əmr "domain təmizdir" yazır və repository-nin implementasiyası lib/data altındadır.

📚 Mənbələr və sənədlər

  • The Clean Architecture (Robert C. Martin)blog.cleancoder.com

    Terminin ilk mənbəyi: dörd dairə və asılılıq qaydası. Flutter-ə aid deyil, ümumi prinsipdir — ona görə oxumaq lazımdır.

  • Dart: sinif modifikatorlarırəsmidart.dev

    `abstract`, `interface`, `sealed`, `final`, `base` — asılılığın tərsinə çevrilməsi üçün hansı modifikatorun nə verdiyi.

  • Arxitektura konseptlərirəsmidocs.flutter.dev

    Flutter-in bu prinsipləri hansı dildə ifadə etdiyi — "layers", "separation of concerns", "single source of truth".