Skip to content

Refactoring: Split BasicBody.ts into focused modules #10

@FinClipper

Description

@FinClipper

BasicBody.ts at 1,774 lines is the second largest file in the codebase. It handles multiple responsibilities that should be separated:

  1. Tree node structure and traversal
  2. Value computation and caching
  3. Formula text generation
  4. Serialization/deserialization
  5. Event handling for changes

This violates the Single Responsibility Principle and makes the code difficult to maintain.

Current Class Structure

// BasicBody.ts - 1,774 lines
class BasicBody {
  // Tree structure (~400 lines)
  parent: BasicBody | null
  children: BasicBody[]
  addChild(), removeChild(), getRoot()...

  // Value computation (~500 lines)
  getValue(), computeValue(), evaluateExpression()...

  // Text generation (~300 lines)
  getTextValue(), getCharacterValue(), formatFormula()...

  // Serialization (~250 lines)
  toJSON(), fromJSON(), clone()...

  // Event handling (~150 lines)
  onChange(), notifyParent()...

  // Utility methods (~174 lines)
  // ... various helpers
}

Proposed Architecture

Option A: Composition Pattern (Recommended)

src/data/models/
├── basic-body/
│   ├── index.ts              - Re-exports for backward compatibility
│   ├── BasicBody.ts          (~300 lines) - Core class with composition
│   ├── TreeOperations.ts     (~200 lines) - Tree traversal mixin
│   ├── ValueComputation.ts   (~250 lines) - Calculation logic
│   ├── TextGenerator.ts      (~200 lines) - Formula text formatting
│   ├── Serializer.ts         (~150 lines) - JSON serialization
│   └── types.ts              - Shared interfaces

Option B: Inheritance Hierarchy

src/data/models/
├── AbstractNode.ts           - Base tree node
├── ComputedNode.ts           - Adds computation (extends AbstractNode)
├── BasicBody.ts              - Full implementation (extends ComputedNode)
└── types.ts

Implementation: Option A Details

TreeOperations.ts

export class TreeOperations<T extends TreeNode> {
  getRoot(node: T): T
  getDepth(node: T): number
  traverse(node: T, callback: (n: T) => void): void
  findByPredicate(node: T, predicate: (n: T) => boolean): T | null
  getPath(node: T): T[]
  isAncestorOf(node: T, potential: T): boolean
}

ValueComputation.ts

export class ValueComputation {
  private cache: Map<string, CachedValue>

  compute(node: BasicBody, context: EvaluationContext): number
  invalidateCache(nodeId: string): void
  getCacheStats(): CacheStatistics
}

TextGenerator.ts

export class TextGenerator {
  generateText(node: BasicBody, format: TextFormat): string
  generateCharacterRepresentation(node: BasicBody): string
  formatWithPrecedence(node: BasicBody): string
}

export enum TextFormat {
  PLAIN = 'plain',
  LATEX = 'latex',
  ASCII = 'ascii'
}

Serializer.ts

export class BasicBodySerializer {
  serialize(node: BasicBody): SerializedBasicBody
  deserialize(data: SerializedBasicBody): BasicBody
  clone(node: BasicBody, deep?: boolean): BasicBody
}

Updated BasicBody.ts

import { TreeOperations } from './TreeOperations'
import { ValueComputation } from './ValueComputation'
import { TextGenerator } from './TextGenerator'
import { BasicBodySerializer } from './Serializer'

export class BasicBody {
  private static treeOps = new TreeOperations()
  private static computation = new ValueComputation()
  private static textGen = new TextGenerator()
  private static serializer = new BasicBodySerializer()

  // Delegate to specialized classes
  getRoot() { return BasicBody.treeOps.getRoot(this) }
  getValue() { return BasicBody.computation.compute(this, this.context) }
  getTextValue() { return BasicBody.textGen.generateText(this, TextFormat.PLAIN) }
  toJSON() { return BasicBody.serializer.serialize(this) }
}

Migration Strategy

  1. Create new module files without changing BasicBody
  2. Extract logic method by method with tests
  3. Update BasicBody to delegate to new classes
  4. Ensure 100% backward compatibility
  5. Deprecate direct method usage (optional)
  6. Update documentation

Backward Compatibility

The public API of BasicBody remains unchanged:

const node = new BasicBody()
node.addChild(childNode)      // Still works
node.getValue()               // Still works
node.getTextValue()           // Still works

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions