mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
feat: add anemic model detection and refactor hardcoded values (v0.9.0)
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import { ValueObject } from "./ValueObject"
|
||||
import { ANEMIC_MODEL_MESSAGES } from "../constants/Messages"
|
||||
import { EXAMPLE_CODE_CONSTANTS } from "../../shared/constants"
|
||||
|
||||
interface AnemicModelViolationProps {
|
||||
readonly className: string
|
||||
readonly filePath: string
|
||||
readonly layer: string
|
||||
readonly line?: number
|
||||
readonly methodCount: number
|
||||
readonly propertyCount: number
|
||||
readonly hasOnlyGettersSetters: boolean
|
||||
readonly hasPublicSetters: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an anemic domain model violation in the codebase
|
||||
*
|
||||
* Anemic domain model occurs when entities have only getters/setters
|
||||
* without business logic. This violates Domain-Driven Design principles
|
||||
* and leads to procedural code instead of object-oriented design.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Bad: Anemic model with only getters/setters
|
||||
* const violation = AnemicModelViolation.create(
|
||||
* 'Order',
|
||||
* 'src/domain/entities/Order.ts',
|
||||
* 'domain',
|
||||
* 10,
|
||||
* 4,
|
||||
* 2,
|
||||
* true,
|
||||
* true
|
||||
* )
|
||||
*
|
||||
* console.log(violation.getMessage())
|
||||
* // "Class 'Order' is anemic: 4 methods (all getters/setters) for 2 properties"
|
||||
* ```
|
||||
*/
|
||||
export class AnemicModelViolation extends ValueObject<AnemicModelViolationProps> {
|
||||
private constructor(props: AnemicModelViolationProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
public static create(
|
||||
className: string,
|
||||
filePath: string,
|
||||
layer: string,
|
||||
line: number | undefined,
|
||||
methodCount: number,
|
||||
propertyCount: number,
|
||||
hasOnlyGettersSetters: boolean,
|
||||
hasPublicSetters: boolean,
|
||||
): AnemicModelViolation {
|
||||
return new AnemicModelViolation({
|
||||
className,
|
||||
filePath,
|
||||
layer,
|
||||
line,
|
||||
methodCount,
|
||||
propertyCount,
|
||||
hasOnlyGettersSetters,
|
||||
hasPublicSetters,
|
||||
})
|
||||
}
|
||||
|
||||
public get className(): string {
|
||||
return this.props.className
|
||||
}
|
||||
|
||||
public get filePath(): string {
|
||||
return this.props.filePath
|
||||
}
|
||||
|
||||
public get layer(): string {
|
||||
return this.props.layer
|
||||
}
|
||||
|
||||
public get line(): number | undefined {
|
||||
return this.props.line
|
||||
}
|
||||
|
||||
public get methodCount(): number {
|
||||
return this.props.methodCount
|
||||
}
|
||||
|
||||
public get propertyCount(): number {
|
||||
return this.props.propertyCount
|
||||
}
|
||||
|
||||
public get hasOnlyGettersSetters(): boolean {
|
||||
return this.props.hasOnlyGettersSetters
|
||||
}
|
||||
|
||||
public get hasPublicSetters(): boolean {
|
||||
return this.props.hasPublicSetters
|
||||
}
|
||||
|
||||
public getMessage(): string {
|
||||
if (this.props.hasPublicSetters) {
|
||||
return `Class '${this.props.className}' has public setters (anti-pattern in DDD)`
|
||||
}
|
||||
|
||||
if (this.props.hasOnlyGettersSetters) {
|
||||
return `Class '${this.props.className}' is anemic: ${String(this.props.methodCount)} methods (all getters/setters) for ${String(this.props.propertyCount)} properties`
|
||||
}
|
||||
|
||||
const ratio = this.props.methodCount / Math.max(this.props.propertyCount, 1)
|
||||
return `Class '${this.props.className}' appears anemic: low method-to-property ratio (${ratio.toFixed(1)}:1)`
|
||||
}
|
||||
|
||||
public getSuggestion(): string {
|
||||
const suggestions: string[] = []
|
||||
|
||||
if (this.props.hasPublicSetters) {
|
||||
suggestions.push(ANEMIC_MODEL_MESSAGES.REMOVE_PUBLIC_SETTERS)
|
||||
suggestions.push(ANEMIC_MODEL_MESSAGES.USE_METHODS_FOR_CHANGES)
|
||||
suggestions.push(ANEMIC_MODEL_MESSAGES.ENCAPSULATE_INVARIANTS)
|
||||
}
|
||||
|
||||
if (this.props.hasOnlyGettersSetters || this.props.methodCount < 2) {
|
||||
suggestions.push(ANEMIC_MODEL_MESSAGES.ADD_BUSINESS_METHODS)
|
||||
suggestions.push(ANEMIC_MODEL_MESSAGES.MOVE_LOGIC_FROM_SERVICES)
|
||||
suggestions.push(ANEMIC_MODEL_MESSAGES.ENCAPSULATE_BUSINESS_RULES)
|
||||
suggestions.push(ANEMIC_MODEL_MESSAGES.USE_DOMAIN_EVENTS)
|
||||
}
|
||||
|
||||
return suggestions.join("\n")
|
||||
}
|
||||
|
||||
public getExampleFix(): string {
|
||||
if (this.props.hasPublicSetters) {
|
||||
return `
|
||||
// ❌ Bad: Public setters allow uncontrolled state changes
|
||||
class ${this.props.className} {
|
||||
private status: string
|
||||
|
||||
public setStatus(status: string): void {
|
||||
this.status = status // No validation!
|
||||
}
|
||||
|
||||
public getStatus(): string {
|
||||
return this.status
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Good: Business methods with validation
|
||||
class ${this.props.className} {
|
||||
private status: OrderStatus
|
||||
|
||||
public approve(): void {
|
||||
if (!this.canBeApproved()) {
|
||||
throw new CannotApproveOrderError()
|
||||
}
|
||||
this.status = OrderStatus.APPROVED
|
||||
this.events.push(new OrderApprovedEvent(this.id))
|
||||
}
|
||||
|
||||
public reject(reason: string): void {
|
||||
if (!this.canBeRejected()) {
|
||||
throw new CannotRejectOrderError()
|
||||
}
|
||||
this.status = OrderStatus.REJECTED
|
||||
this.rejectionReason = reason
|
||||
this.events.push(new OrderRejectedEvent(this.id, reason))
|
||||
}
|
||||
|
||||
public getStatus(): OrderStatus {
|
||||
return this.status
|
||||
}
|
||||
|
||||
private canBeApproved(): boolean {
|
||||
return this.status === OrderStatus.PENDING && this.hasItems()
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
||||
return `
|
||||
// ❌ Bad: Anemic model (only getters/setters)
|
||||
class ${this.props.className} {
|
||||
getStatus() { return this.status }
|
||||
setStatus(status: string) { this.status = status }
|
||||
|
||||
getTotal() { return this.total }
|
||||
setTotal(total: number) { this.total = total }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
approve(order: ${this.props.className}): void {
|
||||
if (order.getStatus() !== '${EXAMPLE_CODE_CONSTANTS.ORDER_STATUS_PENDING}') {
|
||||
throw new Error('${EXAMPLE_CODE_CONSTANTS.CANNOT_APPROVE_ERROR}')
|
||||
}
|
||||
order.setStatus('${EXAMPLE_CODE_CONSTANTS.ORDER_STATUS_APPROVED}')
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Good: Rich domain model with business logic
|
||||
class ${this.props.className} {
|
||||
private readonly id: OrderId
|
||||
private status: OrderStatus
|
||||
private items: OrderItem[]
|
||||
private events: DomainEvent[] = []
|
||||
|
||||
public approve(): void {
|
||||
if (!this.isPending()) {
|
||||
throw new CannotApproveOrderError()
|
||||
}
|
||||
this.status = OrderStatus.APPROVED
|
||||
this.events.push(new OrderApprovedEvent(this.id))
|
||||
}
|
||||
|
||||
public calculateTotal(): Money {
|
||||
return this.items.reduce(
|
||||
(sum, item) => sum.add(item.getPrice()),
|
||||
Money.zero()
|
||||
)
|
||||
}
|
||||
|
||||
public addItem(item: OrderItem): void {
|
||||
if (this.isApproved()) {
|
||||
throw new CannotModifyApprovedOrderError()
|
||||
}
|
||||
this.items.push(item)
|
||||
}
|
||||
|
||||
public getStatus(): OrderStatus {
|
||||
return this.status
|
||||
}
|
||||
|
||||
private isPending(): boolean {
|
||||
return this.status === OrderStatus.PENDING
|
||||
}
|
||||
|
||||
private isApproved(): boolean {
|
||||
return this.status === OrderStatus.APPROVED
|
||||
}
|
||||
}`
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user