Clean Architecture: Entities, Data Models, and View Models - momshaddinury/flutter_template GitHub Wiki

Understanding Models in Clean Architecture: Entities, Data Models, and View Models

In software development, especially when working with architectural patterns like Clean Architecture, terms like "model," "entity," and "DTO" (Data Transfer Object) can sometimes cause confusion. This document aims to clarify the distinct roles of Entities, Data Models, and View Models within a Clean Architecture paradigm.

Understanding these distinctions is crucial for building maintainable, testable, and scalable applications by enforcing strict separation of concerns.


1. The Entity (Core Business Logic / Domain Layer)

What is it? An Entity represents the core business concept or object in your application's domain. It encapsulates the fundamental business rules and data that are independent of any external concerns like databases, user interfaces, or specific technologies.

Key Characteristics:

  • Pure Business Logic: Contains methods that implement and enforce the invariant business rules related to that concept.
  • Independent: Has no knowledge of how it's stored (database), how it's displayed (UI), or how it's transferred (API). It's a plain object (POJO/POCO).
  • Stable: The business rules it embodies are considered the most stable part of your application. They rarely change, even if technologies or user interfaces evolve.
  • Contains Behavior: Entities are not just data bags; they have behaviors (methods) that operate on their own data to ensure business invariants are always met.
  • Resides In: The innermost layer of Clean Architecture – the Domain Layer.

When do we use it?

  • Whenever we are implementing core business rules that define the behavior and state of a fundamental business object (e.g., deducting stock, calculating a discount, validating user registration).
  • When passing data within the Domain Layer or between the Use Cases and Entities.

Example: A Product entity might have methods like deductStock(quantity) which contains the logic to prevent negative stock, or calculateDiscountedPrice(percentage) to apply business-specific pricing rules.

// lib/domain/entities/product.dart
// Represents the core business concept of a Product.
class Product {
  final String id;
  final String name;
  final String description;
  final double price;
  int stock; // Stock can change, hence not final

  Product({
    required this.id,
    required this.name,
    required this.description,
    required this.price,
    required this.stock,
  }) {
    // --- Invariant Business Rules ---
    if (id.isEmpty) {
      throw ArgumentError('Product ID cannot be empty.');
    }
    if (name.isEmpty) {
      throw ArgumentError('Product name cannot be empty.');
    }
    if (price <= 0) {
      throw ArgumentError('Product price must be positive.');
    }
    if (stock < 0) {
      throw ArgumentError('Product stock cannot be negative.');
    }
  }

  // --- Core Business Logic Methods ---

  /// Reduces the stock of the product.
  /// Throws a StateError if there's insufficient stock.
  void deductStock(int quantity) {
    if (quantity <= 0) {
      throw ArgumentError('Quantity to deduct must be positive.');
    }
    if (stock < quantity) {
      throw StateError('Insufficient stock for product "$name". Current stock: $stock, requested: $quantity');
    }
    stock -= quantity;
    print('DEBUG: Stock for "$name" (ID: $id) reduced by $quantity. New stock: $stock');
  }

  /// Calculates the final price after applying a discount.
  /// Throws an ArgumentError if the discount percentage is invalid.
  double calculateDiscountedPrice(double discountPercentage) {
    if (discountPercentage < 0 || discountPercentage > 100) {
      throw ArgumentError('Discount percentage must be between 0 and 100.');
    }
    return price * (1 - discountPercentage / 100);
  }

  /// Checks if the product is currently available for purchase.
  bool isAvailable() => stock > 0;

  @override
  String toString() {
    return 'Product(id: $id, name: $name, price: \$${price.toStringAsFixed(2)}, stock: $stock)';
  }
}

2. The Data Model (Persistence / Data Layer)

What is it? A Data Model (often referred to as a "DTO" - Data Transfer Object, specifically for data persistence) is a data structure designed to represent how data is stored in a specific persistence mechanism (e.g., a database table, a JSON structure from a REST API, or a document in a NoSQL database).

Key Characteristics:

  • Specific to Data Source: Its structure is often optimized for the database schema or external API contract. Field names, types, and relationships might differ from the Entity.
  • Serialization/Deserialization: Typically includes methods to convert to/from the data source's format (e.g., toJson/toMap, fromJson/fromMap).
  • No Business Logic: Should be a pure data container with little to no behavior beyond simple getters/setters or conversion methods.
  • Resides In: The Data Layer (or Infrastructure Layer).

When do we use it?

  • When retrieving data from a database or external API.
  • When sending data to be persisted in a database or to an external API.
  • Within the Repository implementation, which is responsible for mapping between Data Models and Entities.

Example: A ProductDataModel might have product_name instead of name and include a created_at timestamp, reflecting the database column names and specific data types.

// lib/data/models/product_data_model.dart
// Represents how a product is stored in a NoSQL database (e.g., Firestore document).
// Note: Field names might differ from the Entity to match the database schema.
class ProductDataModel {
  final String id;
  final String productName;        // Matches 'product_name' in DB
  final String productDescription;
  final double unitPrice;          // Matches 'unit_price' in DB
  final int availableStock;
  final DateTime createdAt;        // Database-specific field (e.g., for auditing)
  final DateTime? lastUpdatedAt;   // Database-specific field (nullable)

  ProductDataModel({
    required this.id,
    required this.productName,
    required this.productDescription,
    required this.unitPrice,
    required this.availableStock,
    required this.createdAt,
    this.lastUpdatedAt,
  });

  // Factory constructor to create ProductDataModel from a Map (e.g., from a Firestore document).
  factory ProductDataModel.fromMap(Map<String, dynamic> map) {
    return ProductDataModel(
      id: map['id'] as String,
      productName: map['product_name'] as String,
      productDescription: map['product_description'] as String,
      unitPrice: (map['unit_price'] as num).toDouble(),
      availableStock: map['available_stock'] as int,
      createdAt: DateTime.parse(map['created_at'] as String), // Assuming ISO 8601 string
      lastUpdatedAt: map['last_updated_at'] != null
          ? DateTime.parse(map['last_updated_at'] as String)
          : null,
    );
  }

  // Convert ProductDataModel to a Map for storage (e.g., to a Firestore document).
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'product_name': productName,
      'product_description': productDescription,
      'unit_price': unitPrice,
      'available_stock': availableStock,
      'created_at': createdAt.toIso8601String(),
      'last_updated_at': lastUpdatedAt?.toIso8601String(),
    };
  }
}

3. The View Model (Presentation / UI Layer)

What is it? A View Model (sometimes also called a "Presentation Model" or "UI Model") is a data structure specifically designed to present data to the User Interface (UI). It contains only the data that a particular screen or UI component needs, often formatted or combined for immediate display.

Key Characteristics:

  • UI-Specific: Its structure is tailored for a specific UI view. It might combine data from multiple entities or present derived properties suitable for display.
  • Formatted Data: Data is often pre-formatted for direct rendering (e.g., currency strings, dates in a specific format, status messages).
  • No Business Logic: Should not contain core business rules. Its primary purpose is to hold data for rendering. It might contain very simple UI-related logic (e.g., get buttonColorBasedOnState()).
  • Resides In: The Presentation Layer (or UI Layer).

When do we use it?

  • Whenever we need to display data on a screen.
  • When passing data from the Presenter/Controller/ViewModel to the actual UI widgets/components.

Example: A ProductListItemViewModel might include a formattedPrice string and a stockStatus message ("In Stock", "Out of Stock") rather than raw numbers, making it easier for the UI to consume directly.

// lib/presentation/models/product_view_model.dart
import 'package:intl/intl.dart'; // For currency formatting, typically from pubspec.yaml

// Represents how a product is displayed on a list screen item.
class ProductListItemViewModel {
  final String id;
  final String title;
  final String descriptionPreview;
  final String formattedPrice;
  final String stockStatus;          // e.g., "In Stock", "Low Stock", "Out of Stock"
  final bool isAvailableForPurchase;
  final String? thumbnailUrl;        // UI-specific: path to image, not directly in core entity

  ProductListItemViewModel({
    required this.id,
    required this.title,
    required this.descriptionPreview,
    required this.formattedPrice,
    required this.stockStatus,
    required this.isAvailableForPurchase,
    this.thumbnailUrl,
  });
}

// Represents how a product is displayed on a detailed product page.
class ProductDetailViewModel {
  final String id;
  final String name;
  final String fullDescription;
  final String formattedPrice;
  final String stockAvailabilityMessage; // e.g., "Only 5 left!", "Currently Unavailable"
  final String mainImageUrl;             // UI-specific: larger image for detail page
  final List<String> tags;               // UI-specific: might be derived from description or a separate field

  ProductDetailViewModel({
    required this.id,
    required this.name,
    required this.fullDescription,
    required this.formattedPrice,
    required this.stockAvailabilityMessage,
    required this.mainImageUrl,
    required this.tags,
  });
}

How They Interact: The Role of Mapping

The clear separation between these models is enforced by mapping processes:

  1. Repository Layer (Data Model <-> Entity):

    • When retrieving data from the database, the Repository implementation takes the Data Model (e.g., ProductDataModel) received from the data source and maps it into an Entity (e.g., Product). This ensures the Domain Layer only ever works with pure Entities.
    • When saving data, the Repository takes an Entity and maps it into a Data Model suitable for persistence.
  2. Presentation Layer (Entity -> View Model):

    • After a Use Case has executed and returned Entities, the Presenter/Controller/UI-ViewModel takes these Entities and maps them into one or more View Models (e.g., ProductListItemViewModel or ProductDetailViewModel) specifically tailored for the UI.

This explicit mapping prevents direct dependencies between layers and ensures that changes in one layer (e.g., database schema changes) do not ripple through to other layers (e.g., UI), as long as the core business rules (Entities) remain consistent.


Why this Separation Matters (Benefits of Clean Architecture)

  • Testability: Each layer can be tested in isolation. Entities can be tested without needing a database or UI.
  • Maintainability: Changes in the UI or database technology have minimal impact on the core business logic.
  • Flexibility: It's easier to swap out persistence mechanisms (e.g., from SQL to NoSQL) or UI frameworks without rewriting large parts of the application.
  • Scalability: Clear boundaries and responsibilities make it easier for larger teams to work on different parts of the system concurrently.
  • Reduced Complexity: By focusing each model on a single responsibility, the cognitive load for developers is reduced.

By consistently applying these distinctions, we ensure our projects adhere to Clean Architecture principles, leading to more robust, adaptable, and understandable software. If you have further questions, please don't hesitate to ask!

⚠️ **GitHub.com Fallback** ⚠️