Managing complex software systems can be a challenge, especially when it comes to creating families of related objects. Enter the Abstract Factory design pattern, a tool that brings order to the chaos of object creation. In this Capsule, we'll explore a real-world example, diving into the code, and uncovering the benefits that come with using the Abstract Factory pattern.
Real Use Cases: Consider scenarios like developing a cross-platform GUI library, handling various database systems, or maintaining a consistent look and feel across diverse environments. Abstract Factory shines in such situations, offering a systematic approach to creating families of objects without tying them to specific implementations.
Code Example: To illustrate the Abstract Factory pattern, we implemented a simplified database access system in Node.js. Using concrete factories for MySQL and PostgreSQL, the code showcased how abstract interfaces can create connections, commands, and result sets. The flexibility of the pattern allows for seamless switching between different database systems.
const mysql = require("mysql");
const { Pool } = require("pg");
// Abstract Factory for Database Connections
class DatabaseConnectionFactory {
createConnection(): any {
throw new Error("Method 'createConnection' must be implemented");
}
createCommand(): {
query: (queryString: any) => Promise<any>;
close: () => void;
} {
throw new Error("Method 'createCommand' must be implemented");
}
createResultSet() {
throw new Error("Method 'createResultSet' must be implemented");
}
}
DatabaseConnectionFactory
defines an abstract interface for creating database-related objects.createConnection
,createCommand
, andcreateResultSet
are methods that must be implemented by concrete factories.
// Concrete Factory for MySQL Database
class MySQLDatabaseConnectionFactory extends DatabaseConnectionFactory {
createConnection() {
// Implement MySQL-specific connection creation logic
return mysql.createConnection({
host: "localhost",
user: "root",
password: "password",
database: "mydatabase",
});
}
createCommand() {
// Implement MySQL-specific command creation logic
const connection = this.createConnection();
return {
query: (queryString: string) => {
return new Promise((resolve, reject) => {
connection.query(queryString, (error: any, results: any) => {
if (error) {
reject(error);
} else {
resolve(results);
}
});
});
},
close: () => {
connection.end();
},
};
}
createResultSet() {
// Implement MySQL-specific result set creation logic
return this.createConnection();
}
}
// Concrete Factory for PostgreSQL Database
class PostgreSQLDatabaseConnectionFactory extends DatabaseConnectionFactory {
createConnection() {
// Implement PostgreSQL-specific connection creation logic
const pool = new Pool({
user: "postgres",
host: "localhost",
database: "mydatabase",
password: "password",
port: 5432,
});
return pool;
}
createCommand() {
// Implement PostgreSQL-specific command creation logic
const pool = this.createConnection();
return {
query: async (queryString: any) => {
const client = await pool.connect();
try {
const result = await client.query(queryString);
return result.rows;
} finally {
client.release();
}
},
close: () => {
pool.end();
},
};
}
createResultSet() {
// Implement PostgreSQL-specific result set creation logic
return this.createConnection();
}
}
MySQLDatabaseConnectionFactory
extendsDatabaseConnectionFactory
and provides MySQL-specific implementations.createConnection
method creates a MySQL-specific database connection using themysql
library.PostgreSQLDatabaseConnectionFactory
similarly provides PostgreSQL-specific implementations.createConnection
method creates a PostgreSQL-specific database connection using thepg
library'sPool
.
// Example Usage
async function performDatabaseOperations(factory: DatabaseConnectionFactory) {
const command = factory.createCommand();
// Use the created command to perform database operations
try {
const results = await command.query("SELECT * FROM your_table");
console.log("Query results:", results);
} catch (error) {
console.error("Error executing query:", error);
} finally {
command.close();
}
}
// Example with MySQL Database
const mySQLFactory = new MySQLDatabaseConnectionFactory();
performDatabaseOperations(mySQLFactory);
// Example with PostgreSQL Database
const postgreSQLFactory = new PostgreSQLDatabaseConnectionFactory();
performDatabaseOperations(postgreSQLFactory);
performDatabaseOperations
function takes aDatabaseConnectionFactory
as a parameter.Uses the factory to create a database command and executes a sample query, demonstrating the pattern's flexibility.
Instances of concrete factories (
mySQLFactory
andpostgreSQLFactory
) are created.performDatabaseOperations
is called with each factory, showcasing how the code seamlessly adapts to different database systems.
Gains of Using Abstract Factory:
Organized Code: Grouping related objects together improves code organization.
Flexibility: Easily incorporate new types of objects without disrupting existing code.
Consistency: Objects follow the same rules, ensuring a smooth collaboration.
Testing Friendly: Simplifies testing by allowing easy swapping of components.
Responsibility Division: Clearly separates the creation and usage of objects.
Adaptability: Quickly adjusts to different requirements or scenarios.
Readability: Enhances code readability and understanding for developers.
In the realm of software design, the Abstract Factory pattern emerges as a valuable ally. Its ability to organize, adapt, and simplify the creation of related objects brings a layer of order to complex systems. As demonstrated in our database access example, this pattern proves its worth in real-world scenarios, making code more flexible, modular, and easier to maintain.