Before I start Proxy Design pattern, let me explain more about Manager design pattern.Manager is common software design pattern to create a separate “manager” object that coordinates and manages other objects within a system. The responsibilities of a manager class can vary depending on the context, but generally, it is responsible for the high-level coordination of objects and the delegation of responsibilities.
A manager class can be implemented in Go by creating a struct and adding methods to it that coordinate the behaviour of other objects.
There are several design pattern similar to the Manager pattern. For examples
- Facade Pattern — The Facade pattern is used to provide a simplified interface to a complex system, making it easier to use and understand. Like the manager pattern, the Facade provides a single entry point to a set of functionality, but its focus is on simplifying the interface rather than managing the underlying objects.
- Proxy Pattern — The Proxy pattern provides a substitute or placeholder for another object, allowing the proxy to control access to the original object. This can be useful for adding extra functionality or security checks around an object, without modifying the object itself.
- Mediator Pattern — The Mediator pattern is used to reduce dependencies between objects by having them communicate through a central mediator object. The mediator object coordinates the interactions between the other objects, and can help to simplify the design of complex systems.
Proxy Design Pattern
This can be useful for adding extra functionality, like logging, caching, or security checks, without modifying the original object. In Go, the Proxy pattern can be implemented using interfaces and struct types.
Here’s an example of the Proxy pattern in Go:
package main
import (
"database/sql"
"errors"
"fmt"
"time"
_ "github.com/mattn/go-sqlite3"
)
// Database defines the interface that the real database and proxy will implement.
type Database interface {
Connect() (*sql.DB, error)
}
// RealDatabase implements the Database interface and represents the actual database resource.
type RealDatabase struct{}
func (r *RealDatabase) Connect() (*sql.DB, error) {
db, err := sql.Open("sqlite3", "test.db")
if err != nil {
return nil, err
}
// Simulate a slow connection by waiting for a few seconds.
time.Sleep(2 * time.Second)
return db, nil
}
// ProxyDatabase implements the Database interface and provides a surrogate or placeholder for the real database connection.
type ProxyDatabase struct {
realDatabase *RealDatabase
}
func (p *ProxyDatabase) Connect() (*sql.DB, error) {
if p.realDatabase == nil {
p.realDatabase = &RealDatabase{}
}
// Perform some security checks before allowing access to the real database.
// For example, check if the user is authorized to access the database.
if !isUserAuthorized() {
return nil, errors.New("user is not authorized to access the database")
}
// Log the access to the database.
fmt.Println("User accessed the database at", time.Now().Format(time.RFC3339))
return p.realDatabase.Connect()
}
func isUserAuthorized() bool {
// Perform some logic to determine if the user is authorized to access the database.
return true
}
func main() {
var db Database = &ProxyDatabase{}
connection, err := db.Connect()
if err != nil {
fmt.Println("Failed to connect to the database:", err)
return
}
defer connection.Close()
// Use the database connection.
fmt.Println("Successfully connected to the database.")
}
In this example, we define the Database
interface, which both the RealDatabase
and ProxyDatabase
types will implement. The RealDatabase
struct implements the Connect
method to establish a connection to the actual database, while the ProxyDatabase
struct provides a Connect
method that performs some security checks and logs the access to the database before calling the Connect
method of the real database.
In the Connect
method of the ProxyDatabase
struct, we check if the realDatabase
field has been initialized, and if not, we create a new RealDatabase
object. We then perform some security checks to determine if the user is authorized to access the database, and log the access to the database. If everything checks out, we call the Connect
method of the realDatabase
object.
Finally, in the main
function, we create a variable of the Database
interface type, and assign it to a new instance of the ProxyDatabase
struct. We then call the Connect
method on the db
variable, which will call the Connect
method of the ProxyDatabase
object, which in turn will call the Connect
method of the RealDatabase
object.
This example shows how the Proxy pattern can be used to control access to a sensitive resource like a database, by performing security checks and logging access before allowing access to the real resource.