Loading learning content...
We've mastered builders—both their architecture and fluent interfaces. But there's one more participant in the classic Builder Pattern that we've only briefly mentioned: the Director.
Consider this scenario: Your application generates PDF reports. These reports follow specific formats—quarterly financial summaries, employee performance reviews, project status updates. Each format has a defined structure: title page, table of contents, specific sections in specific order, appendices.
Without a Director, every caller must know the exact sequence of builder calls for each format. With a Director, you encapsulate these construction algorithms once, and callers simply say "build me a quarterly report."
The Director is the conductor of the construction orchestra. It knows the score (the algorithm); the builders are the musicians (the implementation). Change conductors, change the interpretation. Change musicians, change the sound. Both vary independently.
By the end of this page, you will understand when and why to use a Director in the Builder Pattern. You'll learn how Directors encapsulate construction algorithms, their relationship to builders, and when Directors add value versus unnecessary complexity. You'll recognize scenarios where Directors shine and where they're overkill.
The Director is an optional participant in the Builder Pattern that encapsulates a specific construction algorithm. While the Builder knows how to construct individual parts, the Director knows what parts to construct and in what order.
Formal definition:
A Director defines the order in which to execute building steps, while the Builder provides the implementation for those steps.
Key characteristics:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
// Abstract Builder interfaceinterface MealBuilder { reset(): void; addMain(item: string): void; addSide(item: string): void; addDrink(item: string): void; addDessert(item: string): void; // Note: getResult() is on concrete builders, not here} // Director: encapsulates meal construction algorithmsclass MealDirector { private builder: MealBuilder; constructor(builder: MealBuilder) { this.builder = builder; } // Change the builder at runtime setBuilder(builder: MealBuilder): void { this.builder = builder; } // Algorithm 1: Kids Meal constructKidsMeal(): void { this.builder.reset(); this.builder.addMain('Chicken Nuggets'); this.builder.addSide('French Fries'); this.builder.addDrink('Apple Juice'); this.builder.addDessert('Cookie'); } // Algorithm 2: Healthy Adult Meal constructHealthyMeal(): void { this.builder.reset(); this.builder.addMain('Grilled Chicken Salad'); this.builder.addSide('Steamed Vegetables'); this.builder.addDrink('Water'); // No dessert for healthy meal } // Algorithm 3: Premium Dinner constructPremiumDinner(): void { this.builder.reset(); this.builder.addMain('Filet Mignon'); this.builder.addSide('Truffle Mashed Potatoes'); this.builder.addSide('Asparagus'); // Two sides! this.builder.addDrink('Red Wine'); this.builder.addDessert('Chocolate Lava Cake'); }} // Concrete Builder: Standard meal outputclass StandardMealBuilder implements MealBuilder { private meal: Meal = new Meal(); reset(): void { this.meal = new Meal(); } addMain(item: string): void { this.meal.main = item; } addSide(item: string): void { this.meal.sides.push(item); } addDrink(item: string): void { this.meal.drink = item; } addDessert(item: string): void { this.meal.dessert = item; } getResult(): Meal { return this.meal; }} // Usageconst builder = new StandardMealBuilder();const director = new MealDirector(builder); director.constructKidsMeal();const kidsMeal = builder.getResult(); director.constructPremiumDinner();const premiumDinner = builder.getResult();Without a Director, the client must know the construction sequence. The Director extracts this knowledge into a dedicated class. This is valuable when construction is complex, varies by scenario, or is reused across the codebase.
Directors can be structured in several ways depending on how construction algorithms are discovered and selected. Let's examine the common patterns:
The most common pattern: each construction algorithm is a method on the Director.
123456789101112131415161718192021222324252627282930313233343536
class DocumentDirector { constructor(private builder: DocumentBuilder) {} // Each method is a complete construction algorithm constructCoverLetterDocument(): void { this.builder.reset(); this.builder.setHeader('Personal Cover Letter'); this.builder.addSection('Contact Information', '...'); this.builder.addSection('Opening', '...'); this.builder.addSection('Body', '...'); this.builder.addSection('Closing', '...'); this.builder.setFooter('References available upon request'); } constructTechnicalSpecDocument(): void { this.builder.reset(); this.builder.setHeader('Technical Specification'); this.builder.addTableOfContents(); this.builder.addSection('Overview', '...'); this.builder.addSection('Requirements', '...'); this.builder.addSection('Architecture', '...'); this.builder.addSection('Implementation', '...'); this.builder.addSection('Testing', '...'); this.builder.addAppendix('Glossary'); } constructApiReferenceDocument(): void { this.builder.reset(); this.builder.setHeader('API Reference'); this.builder.addTableOfContents(); this.builder.addSection('Authentication', '...'); this.builder.addSection('Endpoints', '...'); this.builder.addSection('Error Codes', '...'); this.builder.addCodeExamples(); }}When algorithms share structure but vary in details, parameterize the Director method:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
interface ReportConfig { title: string; includeExecutiveSummary: boolean; sections: string[]; includeCharts: boolean; includeAppendix: boolean;} class ReportDirector { constructor(private builder: ReportBuilder) {} // Single parameterized algorithm constructReport(config: ReportConfig): void { this.builder.reset(); this.builder.setTitle(config.title); if (config.includeExecutiveSummary) { this.builder.addExecutiveSummary(); } this.builder.addTableOfContents(); for (const section of config.sections) { this.builder.addSection(section); } if (config.includeCharts) { this.builder.addCharts(); } if (config.includeAppendix) { this.builder.addAppendix(); } this.builder.finalizeDocument(); } // Convenience methods for common configurations constructQuarterlyReport(): void { this.constructReport({ title: 'Quarterly Report', includeExecutiveSummary: true, sections: ['Financial Performance', 'Key Metrics', 'Outlook'], includeCharts: true, includeAppendix: true, }); } constructWeeklyUpdate(): void { this.constructReport({ title: 'Weekly Update', includeExecutiveSummary: false, sections: ['Progress', 'Blockers', 'Next Steps'], includeCharts: false, includeAppendix: false, }); }}For maximum flexibility, load construction algorithms from data:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
// Construction algorithm defined as datainterface DocumentTemplate { name: string; steps: Array<{ action: 'setHeader' | 'addSection' | 'addTableOfContents' | 'addPage'; params: unknown[]; }>;} class TemplateDirector { private templates: Map<string, DocumentTemplate> = new Map(); constructor(private builder: DocumentBuilder) {} // Load templates from config, database, etc. loadTemplate(template: DocumentTemplate): void { this.templates.set(template.name, template); } loadTemplatesFromConfig(config: DocumentTemplate[]): void { for (const template of config) { this.loadTemplate(template); } } // Execute any loaded template constructFromTemplate(templateName: string): void { const template = this.templates.get(templateName); if (!template) { throw new Error(`Template '${templateName}' not found`); } this.builder.reset(); for (const step of template.steps) { switch (step.action) { case 'setHeader': this.builder.setHeader(step.params[0] as string); break; case 'addSection': this.builder.addSection( step.params[0] as string, step.params[1] as string ); break; case 'addTableOfContents': this.builder.addTableOfContents(); break; case 'addPage': this.builder.addPage(step.params[0] as PageConfig); break; } } }} // Templates can be loaded from JSON, database, etc.const templates: DocumentTemplate[] = [ { name: 'invoice', steps: [ { action: 'setHeader', params: ['Invoice'] }, { action: 'addSection', params: ['Billing Details', ''] }, { action: 'addSection', params: ['Line Items', ''] }, { action: 'addSection', params: ['Total', ''] }, ], }, { name: 'receipt', steps: [ { action: 'setHeader', params: ['Receipt'] }, { action: 'addSection', params: ['Items', ''] }, { action: 'addSection', params: ['Payment', ''] }, ], },]; const director = new TemplateDirector(pdfBuilder);director.loadTemplatesFromConfig(templates);director.constructFromTemplate('invoice');Method-per-Algorithm is simplest and best for stable, known variations. Parameterized works when variations differ in details. Data-Driven is powerful when algorithms change at runtime or come from external sources.
The relationship between Director and Builder is a collaboration, not a hierarchy. Understanding their interactions is key to effective implementation:
The communication flow:
| Director Knows | Builder Knows | Neither Knows |
|---|---|---|
| What steps to call | How to implement each step | The other's internal state |
| In what order to call them | How to assemble parts | Specifics of the other's domain |
| What parameters to pass | How to produce the final product | Alternative implementations |
| The construction algorithm | The representation format | Client's usage context |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
// The Director only sees the Builder interfaceinterface VehicleBuilder { reset(): void; buildFrame(type: FrameType): void; installEngine(specs: EngineSpecs): void; addWheels(count: number, type: WheelType): void; installSeats(count: number, material: SeatMaterial): void; addElectronics(features: ElectronicsPackage): void; paintExterior(color: Color, finish: Finish): void;} // Director encapsulates vehicle configuration algorithmsclass VehicleDirector { private builder: VehicleBuilder; constructor(builder: VehicleBuilder) { this.builder = builder; } // Director knows: "A sports car has these characteristics" // Director doesn't know: How to actually build a physical car/render a 3D model/etc. constructSportsCar(): void { this.builder.reset(); this.builder.buildFrame('TUBULAR_SPACE_FRAME'); this.builder.installEngine({ type: 'V8', displacement: 5.0, horsepower: 450, fuelType: 'PREMIUM' }); this.builder.addWheels(4, 'PERFORMANCE'); this.builder.installSeats(2, 'LEATHER_SPORT'); this.builder.addElectronics({ navigation: true, premiumAudio: true, performanceDisplay: true }); this.builder.paintExterior('RED', 'METALLIC'); } constructFamilySuv(): void { this.builder.reset(); this.builder.buildFrame('BODY_ON_FRAME'); this.builder.installEngine({ type: 'V6', displacement: 3.5, horsepower: 280, fuelType: 'REGULAR' }); this.builder.addWheels(4, 'ALL_TERRAIN'); this.builder.installSeats(7, 'CLOTH'); this.builder.addElectronics({ navigation: true, premiumAudio: false, rearEntertainment: true }); this.builder.paintExterior('WHITE', 'PEARL'); }} // Concrete Builder: Physical car manufacturingclass PhysicalCarBuilder implements VehicleBuilder { private car: PhysicalCar = new PhysicalCar(); reset(): void { this.car = new PhysicalCar(); } buildFrame(type: FrameType): void { // Weld actual metal frame this.car.frame = new Frame(type); this.car.frame.weld(); } installEngine(specs: EngineSpecs): void { // Mount physical engine this.car.engine = Engine.manufacture(specs); this.car.engine.mountTo(this.car.frame); } // ... other methods produce physical car parts getResult(): PhysicalCar { return this.car; }} // Concrete Builder: 3D model for visualizationclass CarModelBuilder implements VehicleBuilder { private model: Car3DModel = new Car3DModel(); reset(): void { this.model = new Car3DModel(); } buildFrame(type: FrameType): void { // Generate 3D mesh for frame this.model.addMesh(FrameMeshGenerator.generate(type)); } installEngine(specs: EngineSpecs): void { // Add engine 3D model (simplified) this.model.addMesh(EngineMeshGenerator.generate(specs)); } // ... other methods produce 3D meshes getResult(): Car3DModel { return this.model; }} // Concrete Builder: Specification documentclass CarSpecBuilder implements VehicleBuilder { private spec: CarSpecification = new CarSpecification(); reset(): void { this.spec = new CarSpecification(); } buildFrame(type: FrameType): void { this.spec.frame = { type, weight: FrameWeights[type] }; } installEngine(specs: EngineSpecs): void { this.spec.engine = specs; } // ... other methods populate specification fields getResult(): CarSpecification { return this.spec; }} // SAME Director, THREE different products!const director = new VehicleDirector(new PhysicalCarBuilder());director.constructSportsCar();// -> Physical sports car being manufactured director.setBuilder(new CarModelBuilder());director.constructSportsCar();// -> 3D model of sports car for visualization director.setBuilder(new CarSpecBuilder());director.constructSportsCar();// -> Specification document for sports carThe Director-Builder separation means you can add new vehicle types (new Director methods) without changing any Builder. You can add new output formats (new Builders) without changing the Director. This is the Open/Closed Principle in action.
Beyond basic construction orchestration, Directors can implement sophisticated patterns for complex scenarios:
1234567891011121314151617181920212223242526272829303132333435363738
// Directors can compose other directors for complex assembliesclass WebApplicationDirector { private frontendDirector: FrontendDirector; private backendDirector: BackendDirector; private infrastructureDirector: InfrastructureDirector; constructor( private frontendBuilder: FrontendBuilder, private backendBuilder: BackendBuilder, private infraBuilder: InfrastructureBuilder ) { this.frontendDirector = new FrontendDirector(frontendBuilder); this.backendDirector = new BackendDirector(backendBuilder); this.infrastructureDirector = new InfrastructureDirector(infraBuilder); } // Orchestrate multiple directors for complete application constructEcommerceApplication(): void { // Infrastructure first this.infrastructureDirector.constructProductionCluster(); // Backend services this.backendDirector.constructUserService(); this.backendDirector.constructProductService(); this.backendDirector.constructOrderService(); this.backendDirector.constructPaymentService(); // Frontend this.frontendDirector.constructStorefront(); this.frontendDirector.constructAdminDashboard(); } constructSimpleBlog(): void { this.infrastructureDirector.constructMinimalSetup(); this.backendDirector.constructContentService(); this.frontendDirector.constructBlogTheme(); }}12345678910111213141516171819202122232425262728293031323334353637383940414243444546
class AdaptiveReportDirector { constructor(private builder: ReportBuilder) {} // Director makes runtime decisions based on context constructReport(context: ReportContext): void { this.builder.reset(); // Conditional title based on report type and date if (context.isYearEnd) { this.builder.setTitle(`Annual Report ${context.year}`); this.builder.addCoverPage(); } else { this.builder.setTitle(`${context.quarter} Report ${context.year}`); } // Executive summary for external reports only if (context.audience === 'EXTERNAL') { this.builder.addExecutiveSummary(); } // Always add core sections this.builder.addSection('Performance Metrics'); // Financial details level depends on audience if (context.audience === 'BOARD' || context.audience === 'EXTERNAL') { this.builder.addDetailedFinancials(); this.builder.addAuditorsReport(); } else { this.builder.addFinancialSummary(); } // Projections only for internal/board if (context.audience !== 'EXTERNAL') { this.builder.addSection('Forward Guidance'); this.builder.addConfidentialProjections(); } // Legal disclaimers for external if (context.audience === 'EXTERNAL') { this.builder.addLegalDisclaimers(); this.builder.addSafeHarborStatement(); } this.builder.finalize(); }}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
class ValidatingFormDirector { private stepCount = 0; constructor(private builder: FormBuilder) {} constructRegistrationForm(): void { this.builder.reset(); this.stepCount = 0; // Director validates construction as it proceeds this.addAndValidate(() => { this.builder.addSection('Personal Information'); this.builder.addTextField('firstName', 'First Name', { required: true }); this.builder.addTextField('lastName', 'Last Name', { required: true }); this.builder.addEmailField('email', 'Email', { required: true }); }, 'Personal information section'); this.addAndValidate(() => { this.builder.addSection('Account Details'); this.builder.addPasswordField('password', 'Password', { minLength: 8, requireUppercase: true, requireNumber: true, }); this.builder.addPasswordField('confirmPassword', 'Confirm Password', { mustMatch: 'password', }); }, 'Account details section'); this.addAndValidate(() => { this.builder.addSection('Preferences'); this.builder.addCheckbox('newsletter', 'Subscribe to newsletter'); this.builder.addCheckbox('terms', 'Accept terms', { required: true }); }, 'Preferences section'); this.builder.addSubmitButton('Register'); // Final validation this.validateFormStructure(); } private addAndValidate(buildStep: () => void, stepName: string): void { this.stepCount++; const beforeState = this.builder.getState(); try { buildStep(); } catch (error) { throw new Error( `Form construction failed at step ${this.stepCount} (${stepName}): ${error}` ); } const afterState = this.builder.getState(); if (!this.stateAdvanced(beforeState, afterState)) { console.warn(`Step ${stepName} may not have modified the form`); } } private stateAdvanced(before: FormState, after: FormState): boolean { return after.fieldCount > before.fieldCount || after.sectionCount > before.sectionCount; } private validateFormStructure(): void { const state = this.builder.getState(); if (state.sectionCount === 0) { throw new Error('Form must have at least one section'); } if (!state.hasSubmitButton) { throw new Error('Form must have a submit button'); } if (state.requiredFields.length === 0) { console.warn('Form has no required fields'); } }}Directors aren't just linear method calls. They can implement conditionals, loops, validation, and any other logic to orchestrate complex construction algorithms. The builder stays simple; the director handles complexity.
The Director is the optional part of the Builder Pattern. Many implementations work perfectly without one. Here's how to decide:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
// ============================================// SCENARIO 1: Director adds value// ============================================ // Without Director: repeated, scattered construction logicfunction createQuarterlyReport1() { return new ReportBuilder() .setTitle('Q1 Report') .addExecutiveSummary() .addTableOfContents() .addSection('Financial Performance') .addCharts() .addSection('Key Metrics') .addSection('Outlook') .addAppendix() .build();} function createQuarterlyReport2() { return new ReportBuilder() .setTitle('Q2 Report') .addExecutiveSummary() .addTableOfContents() .addSection('Financial Performance') .addCharts() .addSection('Key Metrics') .addSection('Outlook') // Oops, forgot addAppendix() - inconsistency! .build();} // With Director: centralized, consistentconst director = new ReportDirector(new ReportBuilder());const q1Report = (director.constructQuarterlyReport('Q1'), director.builder.getResult());const q2Report = (director.constructQuarterlyReport('Q2'), director.builder.getResult());// Both reports have identical structure, guaranteed // ============================================// SCENARIO 2: Director is overkill// ============================================ // Simple configuration, varies per use - just use the builderfunction createHttpClient(token: string) { // Each usage is unique; no reusable algorithm return new HttpClientBuilder() .baseUrl('https://api.example.com') .timeout(5000) .bearerAuth(token) .build();} // No Director needed - the fluent builder IS the interface// A Director would just add unnecessary indirectionA Director adds a layer of indirection. If clients just call one Director method that calls the same builder methods they'd call directly, the Director adds complexity without benefit. Use Directors when they genuinely encapsulate valuable, reused construction logic.
In some implementations, the Director works directly with a concrete builder rather than an abstract interface. This is common when:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
// When there's only one builder, skip the interfaceclass QueryDirector { // Works directly with concrete SqlQueryBuilder constructor(private builder: SqlQueryBuilder) {} constructUserSearchQuery(criteria: SearchCriteria): SqlQuery { this.builder.reset(); this.builder .select('id', 'name', 'email', 'created_at') .from('users'); if (criteria.nameContains) { this.builder.where(`name ILIKE '%${criteria.nameContains}%'`); } if (criteria.emailDomain) { this.builder.andWhere(`email LIKE '%@${criteria.emailDomain}'`); } if (criteria.createdAfter) { this.builder.andWhere(`created_at > '${criteria.createdAfter.toISOString()}'`); } this.builder .orderBy('created_at', 'DESC') .limit(criteria.limit ?? 50); // Can access concrete builder's getResult() directly return this.builder.getResult(); } constructAggregationQuery(groupBy: string, metric: string): SqlQuery { this.builder.reset(); this.builder .selectRaw(`${groupBy}, COUNT(*) as count, AVG(${metric}) as avg_${metric}`) .from('events') .groupBy(groupBy) .having('COUNT(*) > 10') .orderBy('count', 'DESC'); return this.builder.getResult(); }} // Usage - simpler when interface not neededconst director = new QueryDirector(new SqlQueryBuilder());const searchQuery = director.constructUserSearchQuery({ nameContains: 'john', limit: 20,});When this makes sense:
The classic Builder Pattern emphasizes the abstract builder interface because it enables the "same process, different representations" benefit. But modern usage often values the Builder Pattern primarily for its fluent construction API, not representation flexibility.
If you're unlikely to ever have multiple builder implementations, the interface is unnecessary ceremony. YAGNI (You Aren't Gonna Need It) applies.
Design patterns are tools, not religions. Use the parts that help; skip the parts that don't. A Director with a concrete builder is still a valid, useful pattern—it still encapsulates construction algorithms and separates concerns.
The separation of Director and Builder creates excellent testing boundaries. Each can be tested independently:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
describe('HtmlDocumentBuilder', () => { let builder: HtmlDocumentBuilder; beforeEach(() => { builder = new HtmlDocumentBuilder(); }); describe('reset()', () => { it('should clear all previous content', () => { builder.setTitle('Test'); builder.addParagraph('Content'); builder.reset(); const result = builder.getResult(); expect(result.html).not.toContain('Test'); expect(result.html).not.toContain('Content'); }); }); describe('setTitle()', () => { it('should add an h1 element', () => { builder.setTitle('My Title'); const result = builder.getResult(); expect(result.html).toContain('<h1>My Title</h1>'); }); it('should escape HTML entities', () => { builder.setTitle('<script>alert("xss")</script>'); const result = builder.getResult(); expect(result.html).not.toContain('<script>'); expect(result.html).toContain('<script>'); }); }); describe('addParagraph()', () => { it('should add a p element', () => { builder.addParagraph('Hello world'); const result = builder.getResult(); expect(result.html).toContain('<p>Hello world</p>'); }); it('should preserve order of multiple paragraphs', () => { builder.addParagraph('First'); builder.addParagraph('Second'); builder.addParagraph('Third'); const result = builder.getResult(); const firstIndex = result.html.indexOf('First'); const secondIndex = result.html.indexOf('Second'); const thirdIndex = result.html.indexOf('Third'); expect(firstIndex).toBeLessThan(secondIndex); expect(secondIndex).toBeLessThan(thirdIndex); }); }); // Test complete product structure describe('getResult()', () => { it('should produce valid HTML document structure', () => { builder.setTitle('Test'); builder.addParagraph('Content'); const result = builder.getResult(); expect(result.html).toMatch(/^<!DOCTYPE html>/); expect(result.html).toContain('<html>'); expect(result.html).toContain('</html>'); expect(result.html).toContain('<body>'); }); });});123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
describe('ReportDirector', () => { let mockBuilder: jest.Mocked<ReportBuilder>; let director: ReportDirector; beforeEach(() => { // Create mock builder that tracks all calls mockBuilder = { reset: jest.fn(), setTitle: jest.fn(), addExecutiveSummary: jest.fn(), addTableOfContents: jest.fn(), addSection: jest.fn(), addCharts: jest.fn(), addAppendix: jest.fn(), finalize: jest.fn(), }; director = new ReportDirector(mockBuilder); }); describe('constructQuarterlyReport()', () => { it('should reset builder before construction', () => { director.constructQuarterlyReport(); expect(mockBuilder.reset).toHaveBeenCalled(); expect(mockBuilder.reset).toHaveBeenCalledBefore(mockBuilder.setTitle); }); it('should set appropriate title', () => { director.constructQuarterlyReport(); expect(mockBuilder.setTitle).toHaveBeenCalledWith( expect.stringContaining('Quarterly Report') ); }); it('should add all required sections in order', () => { director.constructQuarterlyReport(); // Verify all sections present expect(mockBuilder.addExecutiveSummary).toHaveBeenCalled(); expect(mockBuilder.addTableOfContents).toHaveBeenCalled(); expect(mockBuilder.addSection).toHaveBeenCalledWith('Financial Performance'); expect(mockBuilder.addSection).toHaveBeenCalledWith('Key Metrics'); expect(mockBuilder.addSection).toHaveBeenCalledWith('Outlook'); expect(mockBuilder.addCharts).toHaveBeenCalled(); expect(mockBuilder.addAppendix).toHaveBeenCalled(); }); it('should call methods in correct order', () => { director.constructQuarterlyReport(); const callOrder = [ mockBuilder.reset, mockBuilder.setTitle, mockBuilder.addExecutiveSummary, mockBuilder.addTableOfContents, // ... verify complete order ]; // Verify each was called before the next for (let i = 0; i < callOrder.length - 1; i++) { expect(callOrder[i]).toHaveBeenCalledBefore(callOrder[i + 1]); } }); }); describe('constructWeeklyUpdate()', () => { it('should NOT include executive summary', () => { director.constructWeeklyUpdate(); expect(mockBuilder.addExecutiveSummary).not.toHaveBeenCalled(); }); it('should NOT include appendix', () => { director.constructWeeklyUpdate(); expect(mockBuilder.addAppendix).not.toHaveBeenCalled(); }); });});Builder tests verify that construction produces correct output. Director tests verify that the right methods are called in the right order. Neither needs to test the other's behavior. This separation makes tests focused, fast, and maintainable.
We've explored the Director component of the Builder Pattern in depth. Let's consolidate when and how to use Directors effectively:
The Builder Pattern Complete:
With this page, we've covered the complete Builder Pattern:
You now have the knowledge to apply the Builder Pattern thoughtfully, using just the parts appropriate for your situation.
Congratulations! You've mastered the Builder Pattern. You can recognize when complex construction calls for a builder, design fluent interfaces that developers love, and decide when Directors add value. Apply this pattern to create clean, flexible, maintainable object construction in your own systems.