🚀 Avoid Conditionals in Angular.
Conditionals are powerful, but overusing them can make code harder to read, maintain, and test. The principle of avoiding conditionals encourages the use of polymorphism to simplify code and adhere to the Single Responsibility Principle (SRP). Let’s explore this idea with real-world examples and how it can be applied in Angular applications.
🤔 Why Avoid Conditionals?
- Improved Readability: Each class or function handles a single responsibility.
- Easier Maintenance: Adding new functionality becomes less error-prone.
- Scalability: Avoids long, tangled conditional logic that grows exponentially.
- Testing: Smaller, isolated units are easier to test.
🛠 Real-World Example: Payment Processing System
Bad Example: Conditional Logic
class PaymentProcessor {
process(paymentType, amount) {
switch (paymentType) {
case 'CreditCard':
return this.processCreditCard(amount);
case 'PayPal':
return this.processPayPal(amount);
case 'Crypto':
return this.processCrypto(amount);
default:
throw new Error('Unsupported payment type');
}
}
}
This approach tightly couples the PaymentProcessor
to every payment type. Adding a new payment method requires modifying this class.
Good Example: Using Polymorphism
class PaymentProcessor {
process(amount) {
throw new Error('Method not implemented.');
}
}
class CreditCardProcessor extends PaymentProcessor {
process(amount) {
return `Processing ${amount} with Credit Card`;
}
}
class PayPalProcessor extends PaymentProcessor {
process(amount) {
return `Processing ${amount} with PayPal`;
}
}
class CryptoProcessor extends PaymentProcessor {
process(amount) {
return `Processing ${amount} with Crypto`;
}
}
// Usage
const paymentMethods = {
CreditCard: new CreditCardProcessor(),
PayPal: new PayPalProcessor(),
Crypto: new CryptoProcessor(),
};
function handlePayment(type, amount) {
return paymentMethods[type].process(amount);
}
console.log(handlePayment('Crypto', 100)); // Processing 100 with Crypto
Now, adding a new payment method is as simple as creating a new class and updating the paymentMethods
object.
🌐 Angular Use Case: Dynamic Component Rendering
Problem: A Dashboard with Multiple Widget Types
Suppose you’re building a dashboard where different widgets (e.g., charts, tables, and forms) need to be dynamically rendered based on their type. A conditional approach could involve messy if
or switch
statements.
Bad Example: Conditional Logic in Angular
@Component({
selector: 'app-widget-renderer',
template: `
<ng-container *ngIf="widget.type === 'chart'">
<app-chart [data]="widget.data"></app-chart>
</ng-container>
<ng-container *ngIf="widget.type === 'table'">
<app-table [data]="widget.data"></app-table>
</ng-container>
<ng-container *ngIf="widget.type === 'form'">
<app-form [data]="widget.data"></app-form>
</ng-container>
`,
})
export class WidgetRendererComponent {
@Input() widget!: { type: string; data: any };
}
This approach works for small applications but becomes unmanageable as new widget types are added.
Good Example: Using Polymorphism with a Factory Service
- Abstract Base Class:
export abstract class Widget {
abstract render(data: any): Component;
}
2. Specific Widget Classes:
export class ChartWidget extends Widget {
render(data: any): Component {
return ChartComponent;
}
}
export class TableWidget extends Widget {
render(data: any): Component {
return TableComponent;
}
}
export class FormWidget extends Widget {
render(data: any): Component {
return FormComponent;
}
}
3. Widget Factory Service:
@Injectable({ providedIn: 'root' })
export class WidgetFactory {
getWidget(type: string): Widget {
switch (type) {
case 'chart':
return new ChartWidget();
case 'table':
return new TableWidget();
case 'form':
return new FormWidget();
default:
throw new Error('Unsupported widget type');
}
}
}
4. Dynamic Rendering:
@Component({
selector: 'app-widget-renderer',
template: `
<ng-container *ngComponentOutlet="widgetComponent"></ng-container>
`,
})
export class WidgetRendererComponent {
@Input() widget!: { type: string; data: any };
widgetComponent!: Type<any>;
constructor(private widgetFactory: WidgetFactory) {}
ngOnInit() {
const widget = this.widgetFactory.getWidget(this.widget.type);
this.widgetComponent = widget.render(this.widget.data);
}
}
Switching to polymorphism using a similar factory pattern drastically improved:
- Code Organization: Each layout was encapsulated in its own class.
- Scalability: Adding new layouts no longer affected existing code.
- Testing: Unit tests could be written for each layout class in isolation
👨💻 Takeaway
Refactoring to use polymorphism might feel like extra work initially, but it pays off with cleaner, more maintainable, and scalable code. Whether you’re handling complex business logic or dynamic UI components, avoiding conditionals will make your app easier to grow and debug.