DEV Community

nikosst
nikosst

Posted on

Anemic Entities vs Behavior-Rich Entities Από το Anti-Pattern σε σωστό Domain Model (.NET Core / C#)

1. Τι είναι τα Anemic Entities

Anemic Entity (ή Anemic Domain Model) είναι μια κλάση που:

  • Περιέχει μόνο properties
  • Δεν περιέχει επιχειρησιακή λογική
  • Συμπεριφέρεται σαν DTO με ORM annotations
  • Η λογική μεταφέρεται σε Services

Παράδειγμα Anemic Entity

public class Order
{
    public Guid Id { get; set; }
    public decimal TotalAmount { get; set; }
    public bool IsPaid { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Και ένα service:

public class OrderService
{
    public void Pay(Order order)
    {
        if (order.IsPaid)
            throw new InvalidOperationException("Order already paid");

        order.IsPaid = true;
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Γιατί είναι πρόβλημα (Anti-Pattern)

2.1 Παραβίαση OOP

Η αντικειμενοστραφής φιλοσοφία λέει:

Data + Behavior = Object

Στα anemic entities:

  • Τα objects είναι “κουφά”
  • Τα services γίνονται God Objects

2.2 Παραβίαση Encapsulation

Οποιοσδήποτε μπορεί να γράψει:

order.IsPaid = true;
order.TotalAmount = -100;

❌ Δεν υπάρχει έλεγχος εγκυρότητας


2.3 Business Logic σκορπισμένη

  • Validation σε services
  • Rules σε controllers
  • Side effects παντού

➡️ Δύσκολη συντήρηση & refactoring


2.4 Δυσκολία στο Testing

Για να τεστάρεις behavior:

  • Πρέπει να στήσεις services
  • Mock repositories
  • Mock dependencies

Ενώ το domain θα έπρεπε να τεστάρεται αυτόνομα


3. Πώς πρέπει να είναι ένα σωστό Entity

Ένα Behavior-Rich Entity:

  • Κρατά κανόνες και invariants
  • Ελέγχει το state του
  • Δεν επιτρέπει invalid καταστάσεις
  • Εκφράζει έννοιες του domain

4. Μετατροπή Anemic → Behavior-Rich (Step by Step)

4.1 Αρχικό Anemic Model

public class BankAccount
{
    public Guid Id { get; set; }
    public decimal Balance { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Service:

public class BankAccountService
{
    public void Withdraw(BankAccount account, decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException();

        if (account.Balance < amount)
            throw new InvalidOperationException();

        account.Balance -= amount;
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Behavior-Rich Entity (Σωστό Domain Model)

5.1 Entity με Συμπεριφορά

public class BankAccount
{
    public Guid Id { get; private set; }
    public decimal Balance { get; private set; }

    protected BankAccount() { } // EF Core

    public BankAccount(Guid id)
    {
        Id = id;
        Balance = 0;
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new InvalidOperationException("Amount must be positive");

        Balance += amount;
    }

    public void Withdraw(decimal amount)
    {
        if (amount <= 0)
            throw new InvalidOperationException("Amount must be positive");

        if (Balance < amount)
            throw new InvalidOperationException("Insufficient funds");

        Balance -= amount;
    }
}
Enter fullscreen mode Exit fullscreen mode

Τι κερδίσαμε

✅ Encapsulation
✅ Business rules κοντά στα δεδομένα
✅ Καμία invalid κατάσταση
✅ Καθαρό domain logic


6. Πλήρες Παράδειγμα: Order Aggregate (DDD)

6.1 Entity

public class Order
{
    private readonly List<OrderItem> _items = new();

    public Guid Id { get; private set; }
    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
    public bool IsPaid { get; private set; }

    protected Order() { }

    public Order(Guid id)
    {
        Id = id;
    }

    public void AddItem(Guid productId, decimal price, int quantity)
    {
        if (quantity <= 0)
            throw new InvalidOperationException();

        _items.Add(new OrderItem(productId, price, quantity));
    }

    public decimal GetTotal()
    {
        return _items.Sum(i => i.Price * i.Quantity);
    }

    public void Pay()
    {
        if (!_items.Any())
            throw new InvalidOperationException("Cannot pay empty order");

        if (IsPaid)
            throw new InvalidOperationException("Order already paid");

        IsPaid = true;
    }
}
Enter fullscreen mode Exit fullscreen mode

6.2 Value Object

public class OrderItem
{
    public Guid ProductId { get; }
    public decimal Price { get; }
    public int Quantity { get; }

    public OrderItem(Guid productId, decimal price, int quantity)
    {
        if (price <= 0)
            throw new InvalidOperationException();

        ProductId = productId;
        Price = price;
        Quantity = quantity;
    }
}
Enter fullscreen mode Exit fullscreen mode

7. Πώς αλλάζουν τα Services

ΠΡΙΝ

order.IsPaid = true;

ΜΕΤΑ

order.Pay();

Το service πλέον

public class OrderApplicationService
{
    private readonly IOrderRepository _repository;

    public async Task PayOrder(Guid orderId)
    {
        var order = await _repository.GetById(orderId);
        order.Pay();
        await _repository.Save(order);
    }
}
Enter fullscreen mode Exit fullscreen mode

➡️ Το service ορχηστρώνει, δεν σκέφτεται


8. Unit Testing γίνεται απλό

[Fact]
public void Cannot_pay_empty_order()
{
    var order = new Order(Guid.NewGuid());

    Action act = () => order.Pay();

    act.Should().Throw<InvalidOperationException>();
}
Enter fullscreen mode Exit fullscreen mode

❌ Χωρίς mocks
❌ Χωρίς database
✅ Καθαρό domain test


9. EF Core & Anemic Myth

Το EF Core ΔΕΝ απαιτεί anemic entities

Υποστηρίζει:

  • private setters
  • backing fields
  • protected constructors

Άρα:

Το anemic model είναι επιλογή, όχι ανάγκη


10. Πότε ΔΕΝ πειράζει να είναι Anemic

✔ DTOs
✔ Read Models (CQRS)
✔ Projections
✔ ViewModels

❌ ΟΧΙ στο domain


11. Κανόνας Senior Developer

Αν το object σου δεν προστατεύει τον εαυτό του, δεν είναι object.


12. Σύνοψη

Anemic Entity Behavior-Rich Entity
Properties μόνο Data + Logic
Services γεμάτα logic Thin services
Κακή συντήρηση Καθαρό domain
Invalid states Strong invariants

nikosst

Top comments (0)