Published on

Learning Design Patterns: Proxy Pattern - The Legal Representative

Authors

The Proxy Pattern offers a new perspective in software architecture design, which is Delegated processing. Instead of business logic being called directly whenever needed, we place an intermediary layer (Proxy) in between to represent important processing from the Caller's side.

This Proxy class will act as a Gatekeeper, deciding whether to allow the call to proceed, temporarily HOLD the command pending processing, or even perform the logic itself and return the result immediately, without accessing the original data source.

This decision depends entirely on the software's load-bearing design problem and the specific requirements for the response speed of each Module / Feature.

Don't mistakenly believe that only code in the Backend or in Multi-Services architectures needs a Proxy. The Proxy Pattern is a software mindset, we will need to use it whenever necessary, whether it's Web, Mobile, Backend, Data, etc., as long as it meets the specifications of the Software / feature being built.

Describing the Proxy Pattern with Kotlin

// 1. Define the Subject Interface
interface Database {
    fun getData(id: Int): String?
    fun saveData(id: Int, data: String): Boolean
}

// 2. The Real Subject: The Actual Database Implementation
class RealDatabase : Database {
    init {
        println("RealDatabase: Initializing database connection...")
        // Simulate a costly initialization (e.g., connection setup)
        Thread.sleep(1000)
        println("RealDatabase: Database connection established.")
    }

    override fun getData(id: Int): String? {
        println("RealDatabase: Reading data with ID: $id")
        // Simulate database read operation
        return "Data for ID $id from the database."
    }

    override fun saveData(id: Int, data: String): Boolean {
        println("RealDatabase: Saving data: $data with ID: $id")
        // Simulate database write operation
        return true
    }
}

// 3. The Proxy: Manages Access to the Real Database
class DatabaseProxy(private val userId: String) : Database {
    private var realDatabase: RealDatabase? = null
    private val cache = mutableMapOf()

    private fun getRealDatabase(): RealDatabase {
        if (realDatabase == null) {
            realDatabase = RealDatabase() // Lazy initialization
        }
        return realDatabase!!
    }

    override fun getData(id: Int): String? {
        if (cache.containsKey(id)) {
            println("DatabaseProxy: Returning data for ID $id from cache.")
            return cache[id]
        }

        if (!authorize(userId, "READ")) {
            println("DatabaseProxy: User $userId is not authorized to read data.")
            return null
        }

        val data = getRealDatabase().getData(id)
        data?.let { cache[id] = it }  // Cache the data
        logAccess("READ", id)
        return data
    }

    override fun saveData(id: Int, data: String): Boolean {
        if (!authorize(userId, "WRITE")) {
            println("DatabaseProxy: User $userId is not authorized to write data.")
            return false
        }

        val success = getRealDatabase().saveData(id, data)
        if (success) {
            cache[id] = data // Update the cache
        }
        logAccess("WRITE", id)
        return success
    }

    private fun authorize(userId: String, permission: String): Boolean {
        // Simulate authorization logic (e.g., check user roles)
        return userId != "guest" || permission != "WRITE"  // Guests can't write
    }

    private fun logAccess(operation: String, id: Int) {
        println("DatabaseProxy: $operation operation on data with ID $id at ${LocalDateTime.now()}")
        // Simulate logging (e.g., write to a file)
    }
}

// 4. The Client
fun main() {
    val user1 = DatabaseProxy("admin")
    val user2 = DatabaseProxy("guest")

    println("\n--- User 1 (Admin) Actions ---")
    user1.saveData(1, "Important Data")
    println("Data read: ${user1.getData(1)}")
    println("Data read again: ${user1.getData(1)}") // From cache

    println("\n--- User 2 (Guest) Actions ---")
    user2.saveData(2, "Attempted Guest Write") // Authorization failure
    println("Data read: ${user2.getData(2)}")
}

Comparing Proxy Pattern with Adapter, Facade, or Decorator

This part is important. We developers should read and remember this to be able to bring it up during interviews.

Reading up to here is okay! @dantech wishes you success!