The original implementation tightly couples traversal logic with operational behavior, making the system rigid and difficult to extend.
To improve maintainability and modularity, we separate concerns into distinct areas:
-
π Data Structure
Defines the hierarchical structure being traversed. -
π Traversal Logic
Responsible for systematically visiting nodes in the structure. Common traversal functions includewalk
,visitAll
, andtraverse
.- π Traversal Context: Metadata derived from traversal, such as
level
,last
, oreven
.
- π Traversal Context: Metadata derived from traversal, such as
-
βοΈ Behavioral Logic
Encapsulates operations performed on the data during traversal. Implemented in dedicated Visitor classes.- π§ Behavioral Context: Internal state maintained by the visitor.
- πΌοΈ Render Logic: Defines how the hierarchical structure is formatted for display.
Note
The source code is simplified to highlight separation of concerns. The original implementation can be found in the problem source folder.
The hierarchical data model follows a tree structure, where each node represents either a department or an employee.
- π Departments: Contain child units (either sub-departments or employees).
- π€ Employees: Contain individual attributes such as role and assigned tasks.
For a detailed breakdown of the data model, refer to tree-structures.md.
π Example Data
We use sample data from src/data.ts
to demonstrate traversal and behavior operations.
To maintain separation of concerns, we first extract the traversal logic, isolating it from any behavioral operations.
The function below recursively visits all units in the hierarchy.
We've marked locations where behavioral logic (such as rendering) was previously intermingled.
export function visitAllUnits(unit: Unit/*, behaviour: unknown*/): void {
if (unit.type === 'department') {
// π’ Processing department behaviour
unit.children.forEach((child, index) => {
// π€ Processing emploee behaviour
visitAll(child);
});
} else if (unit.type === 'employee') {
unit.tasks.forEach((task, index) => {
// π οΈ Processing task behaviour
});
}
}
By separating traversal logic, we gain:
- Modularity π οΈ β The ability to extend the data structure without modifying traversal.
- Reusability π β The same traversal can be applied with different behaviors.
- Customization ποΈ β Allows for specialized traversal strategies.
With traversal logic extracted, we can create specialized traversal functions that target specific types of nodes, such as only visiting tasks or employees.
export function visitAllUnitTasks(unit: Unit/*, behaviour: unknown*/): void {
if (unit.type === 'department') {
unit.children.forEach((employee, index) => {
visitAllTasks(employee);
});
} else if (unit.type === 'employee') {
unit.tasks.forEach((task, index) => {
// π οΈ Processing task behaviour
});
}
}
export function visitAllUnitEmployees(unit: Unit/*, behaviour: unknown*/): void {
if (unit.type === 'department') {
unit.children.forEach((childr, index) => {
visitAllEmployees(child);
});
} else if (unit.type === 'employee') {
// π€ Processing emploee behaviour
}
}
export function visitAllUnitDepartments(unit: Unit/*, behaviour: unknown*/): void {
if (unit.type === 'department') {
// π’ Processing department behaviour
unit.children.forEach((child, index) => {
visitAllDepartments(child);
});
}
}
By structuring traversal logic this way, we maximize flexibility while ensuring the core data structure remains unchanged.
The behavioral logic defines what actions should be performed on each department, employee, or task while traversing the structure. To keep traversal logic independent, we introduce an interface that acts as a contract between traversal and behavior.
export interface UnitVisitor {
visitDepartment?: (department: Department) => void;
visitEmployee?: (employee: Employee) => void;
visitTask?: (task: Task) => void;
}
By using a Visitor pattern, we ensure:
- Each visitor is independent and can be applied to any traversal logic.
- The traversal automatically calls the correct method for each unit type.
- New behaviors can be introduced without modifying existing traversal logic.
- Each visitor can maintain its own state, known as behavioral context.
Visitors maintain their own logic and, if needed, stateful data.
export class TotalWorkVisitor extends UnitVisitor {
private _totalWork = 0; // π behavioral context
get totalWork() {
return this._totalWork;
}
visitTask(task: Task) {
this._totalWork += task.duration;
}
}
export class OrganisationTreeVisitor implements UnitVisitor {
// π behavioral context/state
private _renderedTree = '';
addBranchToTree(branch: string) {
this._renderedTree += `${branch}\n`;
}
get renderedTree() {
return this._renderedTree;
}
visitDepartment(department: Department) {
console.log(department.name);
}
visitEmployee(employee: Employee) {
console.log(employee.name + ' ' + employee.role);
}
// ...
}
Each visitor specializes in a different behavior:
- Calculating Total Work β³ β Aggregates task durations.
- Rendering an Organization Tree π³ β Formats hierarchical output.
By implementing visitors separately, we can swap behaviors easily and test them independently.
import { PUSH_BASED } from './data';
// calculate total work
const totalWorkVisitor = new TotalWorkVisitor();
visitAllUnits(PUSH_BASED, totalWorkVisitor);
console.log(`Total Work: ${totalWorkVisitor.totalWork}h`);
// render organization tree
const orgTreeVisitor = new OrganisationTreeVisitor();
visitAllUnits(PUSH_BASED, orgTreeVisitor);
console.log(orgTreeVisitor.renderedTree); // π missing implementation of the `OrganisationTreeVisitor` for now
With this approach, behavioral concerns remain completely separate from traversal logic, ensuring a scalable and maintainable system.
When extracting the remaining logic from the organization tree visitor, we encounter a challenge:
The tree structure relies on two key states to ensure correct rendering:
childIsLast
β Determines whether to useβββ
(last child) orβββ
(sibling exists).indent
β Determines the indentation level for hierarchical alignment.
These states are not part of the data structure but derived from the traversal process.
To make traversal logic reusable, we generalize these states into a context.
export type TraversalTreeContext = {
level: number;
last: boolean;
// π more contextual information can be added here
// - first
// - even
// - odd
};
This contextual information enables multiple traversal implementations while maintaining structure integrity.
We modify the traversal functions to automatically compute context, passing level and last-child status.
export function visitAllUnits<T extends TraversalTreeContext>(
unit: Unit,
visitor: Visitor<T>,
// π traversal context. Smart default for better DX
context: T = {level: 0, last: false} as T
): void {
if (unit.type === 'department') {
visitor?.visitDepartment?.(unit, context);
unit.children.forEach((child, index) => {
visitAllUnits(
child,
visitor,
// π update traversal context
{
...parentContext,
level: parentContext.level + 1,
last: index === unit.children.length - 1
}
);
});
} else if (unit.type === 'employee') {
visitor?.visitEmployee?.(unit, context);
unit.tasks.forEach((task, index) => {
visitor?.visitTask?.(
task,
// π update traversal context
{
...parentContext,
level: parentContext.level + 1,
last: index === unit.tasks.length - 1
}
);
});
}
}
Each recursive call updates the context:
level
β Increases with tree depth.last
β Identifies the last child at each level.
Now, we modify the visitor interface to receive traversal context.
export interface Visitor<T extends TraversalTreeContext = TraversalTreeContext> {
visitDepartment?: (department: Department, context: Readonly<T>) => void;
visitEmployee?: (employee: Employee, context: Readonly<T>) => void;
visitTask?: (task: Task, context: Readonly<T>) => void;
}
export class OrganisationTreeVisitor implements Visitor {
// ...
// π use traversal context
visitDepartment(department: Department, context: TraversalTreeContext) {
//...
}
// π use traversal context
visitEmployee(employee: Employee, context: TraversalTreeContext) {
//...
}
}
Key Benefits:
β
Traversal logic β remains separate from behavior.
β
Visitors β receive context-aware information for rendering.
β
Multiple visitor implementations β can leverage traversal context without modifying traversal logic.
The OrganisationTreeVisitor was initially responsible for both traversing the organization tree and rendering its content.
To improve modularity and maintainability, we extract render logic into dedicated helper functions.
Unit content rendering is now delegated to the createOrganisationRenderer
helper.
This function encapsulates how departments and employees should be displayed.
organisation-renderer.ts
import { Department, Employee, EmployeeRole } from '../model';
import { bold, dim, greenBright, yellow } from 'ansis';
type OrganisationStyle = {
department: (text: string) => string;
employee: (text: string) => string;
role: (text: string) => string;
task: (text: string) => string;
};
type OrganisationDecoration = {
department: string;
employees: Record<Extract<EmployeeRole, 'A' | 'B' | 'C'>, string>;
unknownEmployee: string;
task: string;
taskSeparator: string;
roleDecorators: { start: string; end: string };
};
type OrganisationTransformation = {
role: (role: EmployeeRole) => string;
};
export type OrganisationConfig = {
style: OrganisationStyle;
decoration: OrganisationDecoration;
transformation: OrganisationTransformation;
};
export type OrganisationRenderer = {
renderDepartment(department: Department): string;
renderEmployee(employee: Employee): string;
};
/**
* Creates an organisation renderer with the given configuration.
*
* @param config
* @returns OrganisationRenderer
*
* @example
*
* const renderer = createOrganisationRenderer();
*
* const department = { name: 'Engineering' };
* console.log(renderer.renderDepartment(department)); // π’ Engineering
*
* const employee = { name: 'Alice', role: 'C', tasks: [1, 2] };
* console.log(renderer.renderEmployee(employee)); // π©βπΌ Alice βΊποΈManagerβ» | 2π οΈ
*
*/
export function createOrganisationRenderer(
config: Partial<OrganisationConfig> = {
style: {
department: bold.gray,
employee: bold,
role: yellow,
task: greenBright
},
decoration: {
department: 'π’',
employees: {
A: 'π©βπ»', // Engineer
B: 'π©ββοΈ', // Supervisor
C: 'π©βπΌ' // Manager
},
unknownEmployee: 'π€',
task: 'π οΈ',
roleDecorators: { start: 'βΊποΈ', end: 'β»' },
taskSeparator: dim('|')
},
transformation: {
role: (role: EmployeeRole) => {
return role === 'A'
? 'Engineer'
: role === 'B'
? 'Supervisor'
: role === 'C'
? 'Manager'
: role === 'X'
? 'Contractor'
: 'Unknown Role';
}
}
}
): OrganisationRenderer {
return {
renderDepartment(department: Department) {
return `${config.style.department(
config.decoration.department + ' ' + department.name
)}`;
},
renderEmployee(employee: Employee) {
const roleEmoji =
config.decoration.employees[employee.role] ||
config.decoration.unknownEmployee;
const roleText = config.style.role(
`${config.decoration.roleDecorators.start}${config.transformation.role(
employee.role
)}${config.decoration.roleDecorators.end}`
);
return `${config.style.employee(
roleEmoji + ' ' + employee.name
)} ${roleText} ${config.decoration.taskSeparator} ${config.style.task(
employee.tasks.length + config.decoration.task
)}`;
}
};
}
Key Benefits:
β
Encapsulation β Keeps formatting logic separate from traversal.
β
Reusability β Different visitors can use the same rendering logic.
β
Customizability β The styles and formatting can be easily configured.
Rendering the tree structure itself requires tracking indentation and branch positions.
This logic is extracted into the createTreeRenderer
helper.
tree-renderer.ts
import { dim } from 'ansis';
import { TraversalContext } from './traversal.type';
export interface TreeConfig {
style: (text: string) => string;
tree: {
end: string;
middle: string;
line: string;
indentSpace: string;
};
}
export type TreeRenderer = {
renderTreeIndent(context: TraversalContext): string;
updateActiveBranchLevels(context: TraversalContext): void;
};
export function createTreeRenderer(
config: Partial<TreeConfig> = {
style: dim,
tree: {
end: 'βββ ',
middle: 'βββ ',
line: 'β ',
indentSpace: ' ', // Space when last item
},
}
): TreeRenderer {
const activeParentLevels = new Set<number>();
return {
/**
* Computes the **tree indentation** string based on the traversal context.
*
* This function ensures that **vertical tree lines (`β`)** are drawn correctly,
* maintaining **proper indentation** for hierarchical structures.
*
* ---
*
* ### π **How It Works**
* - **Loops through all levels** (`context.level`) and:
* - If the level is in `activeParentLevels`, it **adds a vertical tree line (`β`)**.
* - Otherwise, it **adds empty spaces (` `) for alignment**.
* - **Appends the correct tree branch marker (`βββ` or `βββ`)**:
* - If the node is **not the last child** (`isLast === false`), it uses `tree.middle` (`βββ`).
* - If the node **is the last child** (`isLast === true`), it uses `tree.end` (`βββ`).
*
* ---
*
* ### π **Example: How It Affects Indentation**
*
* **Before Processing a Last Node in a Level:**
* ```
* βββ Department A
* β βββ Employee 1
* β βββ Employee 2
* β βββ Employee 3 <-- getTreeIndent() adds `βββ` here
* ```
*
* **Before Processing a Non-Last Node in a Level:**
* ```
* βββ Department A
* β βββ Employee 1 <-- getTreeIndent() adds `βββ` here
* β βββ Employee 2
* β βββ Employee 3
* ```
*
* ---
*
* ### Why This Matters
* - Ensures **correct visual alignment** for hierarchical trees.
* - Prevents **extra or missing** tree lines (`β`) at nested levels.
* - Maintains **professional and structured output**.
*
* ---
* @param context - The current traversal context containing:
* - `level` (number): The **depth level** of the current node in the tree.
* - `isLast` (boolean): Whether this node is the **last child** at its level.
* @returns The computed **tree indentation string**.
*/
renderTreeIndent(context: TraversalContext): string {
return Array.from({ length: context.level })
.map((_, i) =>
activeParentLevels.has(i) ? config.style(config.tree.line) : ' '
)
.join('')
.concat(
config.style(context.last ? config.tree.end : config.tree.middle)
);
},
/**
* Updates the set of active parent levels based on the traversal context.
*
* This method ensures that the **tree structure maintains correct indentation** by
* tracking which parent levels should **continue their vertical lines (`β`)**.
*
* ---
*
* ### How It Works
* - If the **current node is NOT the last child** (`isLast === false`), it means this level
* **has more children** to process. So, we **add** the current `level` to `activeParentLevels`,
* ensuring the tree **continues drawing vertical lines (`β`) at this depth**.
* - If the **current node IS the last child** (`isLast === true`), we **remove** the
* current `level` from `activeParentLevels`, stopping further indentation lines
* at this depth.
*
* This function is called **after processing** a department or employee to update
* indentation behavior for all subsequent sibling nodes.
*
* ---
*
* ### Example: How It Affects Indentation
*
* **Before Processing a Last Node in a Level:**
* ```
* βββ Department A
* β βββ Employee 1
* β βββ Employee 2
* β βββ Employee 3 <-- updateActiveParentLevels removes level here
* ```
*
* **Before Processing a Non-Last Node in a Level:**
* ```
* βββ Department A
* β βββ Employee 1 <-- updateActiveParentLevels adds level here
* β βββ Employee 2
* β βββ Employee 3
* ```
*
* ---
*
* @param context - The current traversal context containing:
* - `level` (number): The **depth level** of the current node in the tree.
* - `isLast` (boolean): Whether this node is the **last child** at its level.
*/
updateActiveBranchLevels(context: TraversalContext) {
if (!context.last) {
activeParentLevels.add(context.level); // Continue the branch
} else {
activeParentLevels.delete(context.level); // Stop indentation at this level
}
},
};
}
Key Benefits:
β
Ensures proper tree alignment β No misplaced branches.
β
Separates structural concerns β Only manages indentation & tree lines, not content.
β
Keeps formatting customizable β Tree structure can be styled independently.
With the traversal logic and render logic extracted, the visitors now focus solely on applying the rendering logic.
The OrganisationTreeVisitor:
- Delegates rendering to the
OrganisationRenderer
andTreeRenderer
. - Traversal context is forwarded for correct indentation.
The full implementation in the organisation-tree.visitor.ts file.
export class OrganisationTreeVisitor implements Visitor {
// ...
constructor(
private readonly organisationRenderer: OrganisationRenderer = createOrganisationRenderer(),
private readonly treeRenderer: TreeRenderer = createTreeRenderer()
) {}
visitDepartment(department: Department, context: TraversalContext) {
// If this is the root department, print it without tree indentation
if (context.level === 0) {
this.addToRenderedTree(
this.organisationRenderer.renderDepartment(department)
);
return;
}
// Otherwise, apply indentation for tree structure
const treeIndent = this.treeRenderer.renderTreeIndent(context);
this.addToRenderedTree(
`${treeIndent}${this.organisationRenderer.renderDepartment(department)}`
);
// Update the active branch levels for the next iteration
this.treeRenderer.updateActiveBranchLevels(context);
}
visitEmployee(employee: Employee, context: TraversalContext) {
// Employees are never root, so always apply indentation
const treeIndent = this.treeRenderer.renderTreeIndent(context);
this.addToRenderedTree(
`${treeIndent}${this.organisationRenderer.renderEmployee(employee)}`
);
}
}
As this visitor was already implemented fully above here just the overview of the implementation.
The TaskCalculationVisitor:
- Calculates total work by summing task durations.
The full implementation in the task-calculation.visitor.ts file.