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; }
}
Και ένα service:
public class OrderService
{
public void Pay(Order order)
{
if (order.IsPaid)
throw new InvalidOperationException("Order already paid");
order.IsPaid = true;
}
}
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; }
}
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;
}
}
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;
}
}
Τι κερδίσαμε
✅ 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;
}
}
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;
}
}
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);
}
}
➡️ Το 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>();
}
❌ Χωρίς 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)