Pdf Processor
A 1000-Line File Is Not a Code Problem. It Is a Design Problem.
TLDR
Refactoring a 1,000-line file by creating autils.js helper file or wrapping procedural code in a stateful class fails to solve the root problem: implicit coupling across sequential execution passes sharing mutable scope. Document layout classification is a data-flow pipeline. Decoupling the monolith into independent single-function domain modules called by a linear orchestrator converts hidden ordering dependencies into explicit, type-checked function arguments.
| Refactoring Strategy | Code Organization | Scope Isolation | Ordering Dependency Visibility |
|---|---|---|---|
Extract utils.js Helpers | Shorter main file + Utility dumping ground | Low (Shares mutable closure state) | Hidden (Implicit top-to-bottom execution) |
| Wrap in Stateful Class | Methods bound to this instance | Low (Instance properties read/written everywhere) | Hidden (Method invocation order dependency) |
| Decoupled Pipeline Modules | Pure single-function domain modules | High (Explicit arguments & return values) | 100% Explicit (Visible call sequence) |
Problem statement: the myth of the "helper extraction" refactor
When procedural files grow past 1,000 lines, developers frequently apply standard refactorings:
- Extract
utils.js: Move math calculations or regex helpers out of the file. The file shrinks to 900 lines, but the sequential procedural coupling remains completely unchanged. - Wrap in Class: Create a
class DocumentClassifierthat stores state onthis. Methods read and writethis.regionsat arbitrary execution steps, concealing ordering constraints behind object instance state.
Technical architecture: pipelines vs. Stateful objects
Document layout classification is not a stateful object. It is a linear data processing pipeline:
[Raw Items] ---> [Columns] ---> [Tables/Images] ---> [Text Blocks] ---> [Dividers] ---> [Sorted Output]
The decoupled module architecture
Each classification domain resides in its own module exporting a single pure function with explicit parameters:// dividerDetector.js: Takes explicit inputs, returns explicit array
export function detectDividers(lineSegments, existingRegions, viewport) {
return lineSegments.hLines.filter(seg => {
return !existingRegions.some(r => isBBoxContained(r.bbox, seg));
});
}
The function signature explicitly communicates its ordering contract: detectDividers requires existingRegions to be populated with text blocks before it can run.
The pipeline orchestrator defines the sequence:
// orchestrator.js: Sequence is documented directly in function call order
const textBlocks = classifyTextBlocks(textItems, columns);
regions.push(...textBlocks);
// Explicit dependency: detectDividers receives populated textBlocks const dividers = detectDividers(lineSegments, regions, viewport); regions.push(...dividers);
Rule of thumb: Treat multi-pass document classifiers as linear data pipelines with explicit function arguments rather than stateful classes or procedural monoliths.
Read this post in the full Engineering Journal →