The SOLID Principles
SOLID is an acronym for five other class-design principles:
The Dependency Inversion Principle (DIP)
☑️ Topic: Objects and Data Structures
☑️ Idea: High-level modules should not depend on low-level modules and both should depend on abstractions. Abstractions should not depend upon details, but details should depend on abstractions. Classes shouldn’t have to know implementation details from their dependencies.
☑️ Benefits: Low coupling, Maintainability.
☑️ Guideline: If your class depends on a lower-level module, you should change that module for an interface. If your class depends on implementation details from another class, you should encapsulate those behind an interface and use the interface instead.
Benefits Explained
- Lowers coupling: It keeps high-level modules from knowing the details of their low-level modules and setting them up, reducing coupling between modules.
- Less maintenance: If some implementation details change, no changes are needed on the clients because it only depends on an interface.
Interfaces in JavaScript and Typescript
JavaScript doesn't have explicit interfaces. Interfaces are implicit contracts in JavaScript because of duck typing.
Typescript has interfaces and the Dependency Inversion Principle can be applied directly to it. I’ll use Typescript to explain this principle.
Example in Typescript
Let’s look at a design example that applies the Dependency Inversion Principle to databases.
BAD
Here
RecordService
depends on a specific database class called MySQLiteDatabase
. If we wanted to use another database we’d have to change the RecordService
implementation.// BAD class RecordService { database: MySQLiteDatabase; // constructor save(record: Record): void { if (record.id === undefined) { this.database.insert(record); } else { this.database.update(record); } } } class SQLiteDatabase{ insert(record: Record) { // insert } update(record: Record) { // update } }
The dependency is direct
GOOD
Here
RecordService
depends on a Database
interface. We can use a SqliteDatabase
or a different database without changes in RecordService
.// GOOD class RecordService { database: Database; // constructor save(record: Record): void { this.database.save(record); } } interface Database { save(record: Record): void; } class SQLiteDatabase implements Database { save(record: Record) { if (record.id === undefined) { // insert } else { // update } } }
The dependency was inverted
Example in JavaScript
Let’s look at a design example that uses the Dependency Inversion Principle in JavaScript. This is a bit more complex because interfaces are implicit.
BAD
// BAD class InventoryRequester { constructor() { this.REQ_METHODS = ["HTTP"]; } requestItem(item) { // ... } } class InventoryTracker { constructor(items) { this.items = items; // BAD: We have created a dependency on a specific request implementation. // We should just have requestItems depend on a request method: `request` this.requester = new InventoryRequester(); } requestItems() { this.items.forEach(item => { this.requester.requestItem(item); }); } } const inventoryTracker = new InventoryTracker(["apples", "bananas"]); inventoryTracker.requestItems();
GOOD
//GOOD class InventoryTracker { constructor(items, requester) { this.items = items; this.requester = requester; } requestItems() { this.items.forEach(item => { this.requester.requestItem(item); }); } } class InventoryRequesterV1 { constructor() { this.REQ_METHODS = ["HTTP"]; } requestItem(item) { // ... } } class InventoryRequesterV2 { constructor() { this.REQ_METHODS = ["WS"]; } requestItem(item) { // ... } } // By constructing our dependencies externally and injecting them, we can easily // substitute our request module for a fancy new one that uses WebSockets. const inventoryTracker = new InventoryTracker( ["apples", "bananas"], new InventoryRequesterV2() ); inventoryTracker.requestItems();