feat: add anemic model detection and refactor hardcoded values (v0.9.0)

This commit is contained in:
imfozilbek
2025-11-26 00:09:48 +05:00
parent 1d6c2a0e00
commit a6b4c69b75
21 changed files with 1481 additions and 4 deletions

View File

@@ -0,0 +1,38 @@
/**
* BAD EXAMPLE: Anemic Domain Model
*
* This Order class only has getters and setters without any business logic.
* All business logic is likely scattered in services (procedural approach).
*
* This violates Domain-Driven Design principles.
*/
class Order {
private status: string
private total: number
private items: any[]
getStatus(): string {
return this.status
}
setStatus(status: string): void {
this.status = status
}
getTotal(): number {
return this.total
}
setTotal(total: number): void {
this.total = total
}
getItems(): any[] {
return this.items
}
setItems(items: any[]): void {
this.items = items
}
}

View File

@@ -0,0 +1,34 @@
/**
* BAD EXAMPLE: Anemic Domain Model with Public Setters
*
* This User class has public setters which is an anti-pattern in DDD.
* Public setters allow uncontrolled state changes without validation or business rules.
*
* This violates Domain-Driven Design principles and encapsulation.
*/
class User {
private email: string
private password: string
private status: string
public setEmail(email: string): void {
this.email = email
}
public getEmail(): string {
return this.email
}
public setPassword(password: string): void {
this.password = password
}
public setStatus(status: string): void {
this.status = status
}
public getStatus(): string {
return this.status
}
}

View File

@@ -0,0 +1,139 @@
/**
* GOOD EXAMPLE: Rich Domain Model with Business Logic
*
* This Customer class encapsulates business rules and state transitions.
* No public setters - all changes go through business methods.
*
* This follows Domain-Driven Design and encapsulation principles.
*/
interface Address {
street: string
city: string
country: string
postalCode: string
}
interface DomainEvent {
type: string
data: any
}
class Customer {
private readonly id: string
private email: string
private isActive: boolean
private loyaltyPoints: number
private address: Address | null
private readonly events: DomainEvent[] = []
constructor(id: string, email: string) {
this.id = id
this.email = email
this.isActive = true
this.loyaltyPoints = 0
this.address = null
}
public activate(): void {
if (this.isActive) {
throw new Error("Customer is already active")
}
this.isActive = true
this.events.push({
type: "CustomerActivated",
data: { customerId: this.id },
})
}
public deactivate(reason: string): void {
if (!this.isActive) {
throw new Error("Customer is already inactive")
}
this.isActive = false
this.events.push({
type: "CustomerDeactivated",
data: { customerId: this.id, reason },
})
}
public changeEmail(newEmail: string): void {
if (!this.isValidEmail(newEmail)) {
throw new Error("Invalid email format")
}
if (this.email === newEmail) {
return
}
const oldEmail = this.email
this.email = newEmail
this.events.push({
type: "EmailChanged",
data: { customerId: this.id, oldEmail, newEmail },
})
}
public updateAddress(address: Address): void {
if (!this.isValidAddress(address)) {
throw new Error("Invalid address")
}
this.address = address
this.events.push({
type: "AddressUpdated",
data: { customerId: this.id },
})
}
public addLoyaltyPoints(points: number): void {
if (points <= 0) {
throw new Error("Points must be positive")
}
if (!this.isActive) {
throw new Error("Cannot add points to inactive customer")
}
this.loyaltyPoints += points
this.events.push({
type: "LoyaltyPointsAdded",
data: { customerId: this.id, points },
})
}
public redeemLoyaltyPoints(points: number): void {
if (points <= 0) {
throw new Error("Points must be positive")
}
if (this.loyaltyPoints < points) {
throw new Error("Insufficient loyalty points")
}
this.loyaltyPoints -= points
this.events.push({
type: "LoyaltyPointsRedeemed",
data: { customerId: this.id, points },
})
}
public getEmail(): string {
return this.email
}
public getLoyaltyPoints(): number {
return this.loyaltyPoints
}
public getAddress(): Address | null {
return this.address ? { ...this.address } : null
}
public getEvents(): DomainEvent[] {
return [...this.events]
}
private isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
private isValidAddress(address: Address): boolean {
return !!address.street && !!address.city && !!address.country && !!address.postalCode
}
}
export { Customer }

View File

@@ -0,0 +1,104 @@
/**
* GOOD EXAMPLE: Rich Domain Model
*
* This Order class contains business logic and enforces business rules.
* State changes are made through business methods, not setters.
*
* This follows Domain-Driven Design principles.
*/
type OrderStatus = "pending" | "approved" | "rejected" | "shipped"
interface OrderItem {
productId: string
quantity: number
price: number
}
interface DomainEvent {
type: string
data: any
}
class Order {
private readonly id: string
private status: OrderStatus
private items: OrderItem[]
private readonly events: DomainEvent[] = []
constructor(id: string, items: OrderItem[]) {
this.id = id
this.status = "pending"
this.items = items
}
public approve(): void {
if (!this.canBeApproved()) {
throw new Error("Cannot approve order in current state")
}
this.status = "approved"
this.events.push({
type: "OrderApproved",
data: { orderId: this.id },
})
}
public reject(reason: string): void {
if (!this.canBeRejected()) {
throw new Error("Cannot reject order in current state")
}
this.status = "rejected"
this.events.push({
type: "OrderRejected",
data: { orderId: this.id, reason },
})
}
public ship(): void {
if (!this.canBeShipped()) {
throw new Error("Order must be approved before shipping")
}
this.status = "shipped"
this.events.push({
type: "OrderShipped",
data: { orderId: this.id },
})
}
public addItem(item: OrderItem): void {
if (this.status !== "pending") {
throw new Error("Cannot modify approved or shipped order")
}
this.items.push(item)
}
public calculateTotal(): number {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
public getStatus(): OrderStatus {
return this.status
}
public getItems(): OrderItem[] {
return [...this.items]
}
public getEvents(): DomainEvent[] {
return [...this.events]
}
private canBeApproved(): boolean {
return this.status === "pending" && this.items.length > 0
}
private canBeRejected(): boolean {
return this.status === "pending"
}
private canBeShipped(): boolean {
return this.status === "approved"
}
}
export { Order }