Proxy Pattern

Hyung Jun·2020년 12월 5일
0

Design Pattern

목록 보기
9/10
post-thumbnail

This article is lecture note from "Head First Design Patterns" by Elisabeth Freeman and Kathy Sierra and CS course from Kookmin by Inkyu Kim
Especially, this note takes a lot of contents from refectoring.guru and sourcemaking.com

Definition

Proxy is a structural design pattern that lets you provide a substitute or placeholder for another object. A proxy controls access to the original object, allowing you to perform something either before or after the request get through to the original object.

Real World Problem

Why would we want to control access to an object?
Let's think about an example: you have a massive object that consumes a vast amout of system resources. You need it from time to time, but not always.

You could implement lazy initialisation: create this object only when it's actually needed. All of the object's clients would need to execute some deferred initialisation code. Unfortunately, this would probably cause a lot of code duplication.

The Proxy Pattern suggests that you create a new proxy class with the same interface as an original service object. Then you update your app so that it passes the proxy object to all of the original object's clients. Upon receiving a request from a client, the proxy creates a real service object and delegates all the work to it.

The benefit? If you need to execute something either before or after the primary logic of the class, the proxy lets you do this without changing that class. Since the proxy implements the same interface as the original class, it can be passed to any client that expects a real service object.

Structure

First of all, the Service Interface declares the interface of the Service. The proxy must follow this interface to be able to disguise itself as a service object. The Service is a class that provides some useful business logic.
The Proxy class has a reference field that points to a service object. After the proxy finishes its processing(e.g., lazy initialisation, logging, access control, caching, etc.), it passes the request to the service object. Usually, proxies manage the full lifecylce of their service objects. The Client should work with both services and proxies via the same interface. This way you can pass a proxy into any code that expects a service object.

RMI (Remote Method Invocation)

The RMI is an API that provides a mechanism to create distributed application in java. The RMI allows an object to invoke methods on an object running in another JVM.

We are going to make proxy using RMI!

Before we talk about how to use RMI, we need to got the gist of remote method invocation in our head!

Let's say we want to design a system that allows us to call a local object that forwards each request to a remote object. How would we design it?
We need a couple of helper objects that actually do the communication for us. The client then calls a method on the client helper, as if the client helper were the actual service. The client helper takes care of forwarding that request for us.
But the client helper isn't really the remote service. Although the client helper acts like it(because it has the same method that the service is advertising), the client helper doesn't have any of the actual method logic the client is expecting. Instead the client helper contacts the server, transfers information about the method call.
On the server side, the service helper receives the request from the client helper, unpacks the information about the call, and then invokes the real method on the real service object. So, to the service object, the call is local.

Let's understand how to use RMI to enable remote method invocation.

What RMI does for us is build the client and service helper objects. It just right down to creating a client helper object with the same methods as the remote service. The nice hting about RMI is that you don't have to write any of the networking or I/O code ourselves.

RMI also provides all the runtime infrastructure to make it all work, including a lookup service that the client can use to find and access the remote objects.

How to implement RMI?

Make a Remote interface

  • step 1
    Extand java.rmi.Remote
    public interface MyRemote extends Remote{}
    This tells us that the interface is going to be used to support remote calls.
  • step 2
    Declare that all methods throw a RemoteException
import java.rmi.*;

public interface MyRemote extends Remote {
	public String sayHello() throws RemoteException;
}

Every remote method call is considered 'risky'. Declaring RemoteException on every method forces the client to pay attention and acknowledge that things might not work.

  • step 3
    Be sure arguments and return values are primitives or Serialisable
    Arguments and return values of a remote method must be either primitive or Serialisable.
    public String sayHello() throws RemoteException;
    The return value is going to be shipped over the wire from the server back to the client, so it must be Serialisable.

Make a Remote Implementation

  • step 1
    Implement the Remote interface
    Your service has to implement the remote interface.
	public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote{
		public String sayHello() {
        		return "Server says, 'Hey'";
                }
                //more codes
        }

The compiler will make sure that you've implemented all the methods from the interface you implement.

  • step 2
    Extend UnicastRemoteObject
    In order to work as a remote service object, your object needs some functionalility related to 'being remote'. The simplest way is to extend UnicastRemoteObject and let the class work for you.
    public class MyRemoteImpl extends UnicastRemoteObject implements Myremote{
  • step 3
    Write a no-arg constructor that declares a RemoteException
    Our new superclass, UnicastRemoteObject, has on little problem-its constructor throws a RemoteException. The only way to deal with this is to declare a constructor for your remote implementation, just so that you have a place to declare the RemoteException.
    public MyRemoteImpl() throws RemoteException{} You don't have to put anything in the constructor. You just need a way to declare that your superclass constructor throws an exception.
  • step 4
    Register the service with the RMI registry
    Now that you've got a remote service, you have to make it available to remote clients. You do this by instantiating it and putting it into the RMI registry. When you register the implementation object, the RMI system actually puts the stub in the registry, since that's what the client really need. Register your service using the static rebind() method of the java.rmi.Naming class.
try {
	MyRemote servicce = new MyRmoteImple();
	Naming.rebind("RemoteHello", service);
} catch(Exception ex) {...}

The client has to get the stub object (our proxy), since that's the thing the client will call methods on. And that's where the RMI registry comes in.

Source Code

Let's get back to our GumballMachine remote proxy.

We'll start with the remote interface:
<GumballMachineRemote.java>

import java.rmi.*;
 
public interface GumballMachineRemote extends Remote {
	public int getCount() throws RemoteException;
	public String getLocation() throws RemoteException;
	public State getState() throws RemoteException;
}

Here are the methods were goint to support. Each one throws RemoteEcxeption. And all return types need to be primitive or Serialisable.

<State.java>

import java.io.*;
  
public interface State extends Serializable {
	public void insertQuarter();
	public void ejectQuarter();
	public void turnCrank();
	public void dispense();
}

The State class is originally not a serialisable. So in State class, we extends Serializable interface(which has no methods in it). And now State in all the subclasses can be transferred over the network.

Actually, we're not done with Serialisable yet; we have one problem with State. As you may remember, each State object maintains a reference to a gumball machine so that it can call the gumball machine's methods and change its state.

public class NoQuarterState implements State {
	private static final long serialVersionUID = 2L;
    transient GumballMachine gumballMachine;
 
    // all other methods here
}

In each implementation of State, we add the transient keyword to the GumballMachine instance variable. This tells that JVM not to serialise this field. Note that this can be slightly dangerous if you try to access this field once its been serialised and transferred.

<GumballMachine.java>

import java.rmi.*;
import java.rmi.server.*;
 
public class GumballMachine
		extends UnicastRemoteObject implements GumballMachineRemote 
{
	// instance variables here

	public GumballMachine(String location, int numberGumballs) throws RemoteException {
		// code here
	}
 
 
	public void insertQuarter() {
		state.insertQuarter();
	}
 
	public void ejectQuarter() {
		state.ejectQuarter();
	}
 
	public void turnCrank() {
		state.turnCrank();
		state.dispense();
	}

	void setState(State state) {
		this.state = state;
	}
 
	void releaseBall() {
		System.out.println("A gumball comes rolling out the slot...");
		if (count != 0) {
			count = count - 1;
		}
	}

	// other methods here
}

The only things that has changed are just import java.rmi.* and server then extends UnicastRemoteObject so that the GumballMachine is able to act as a remote service. Of course we need to throw a remote exception in the constructor because the superclass does.

Now we just need to make sure that GumballMachine is registered in RMI registry so that clients can locate it.

<GumballMachineTestDrive.java>

import java.rmi.*;

public class GumballMachineTestDrive {
 
	public static void main(String[] args) {
		GumballMachineRemote gumballMachine = null;
		int count;

		if (args.length < 2) {
			System.out.println("GumballMachine <name> <inventory>");
 			System.exit(1);
		}

		try {
			count = Integer.parseInt(args[1]);

			gumballMachine = 
				new GumballMachine(args[0], count);
			Naming.rebind("//" + args[0] + "/gumballmachine", gumballMachine);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

We added the call to Naming.rebind which publishes the GumballMachien stub under the name gumballmachine.

Now for the GumballMonitor client. We wanted to reuse it without having to rewrite it to work over a network.

<GumballMonitor.java>

import java.rmi.*;
 
public class GumballMonitor {
	GumballMachineRemote machine;
 
	public GumballMonitor(GumballMachineRemote machine) {
		this.machine = machine;
	}
 
	public void report() {
		try {
			System.out.println("Gumball Machine: " + machine.getLocation());
			System.out.println("Current inventory: " + machine.getCount() + " gumballs");
			System.out.println("Current state: " + machine.getState());
		} catch (RemoteException e) {
			e.printStackTrace();
		}
	}
}

First we need to import the RMI package because we are using the RemoteException class.
Then we're going to rely on the remote interface rather than the concrete GumballMachine class. We also need to catch any remote exceptions that might happen as we try to invoke methods that are ultimately happening over the network.

Real Final has come we've got to test our GumballMonitor. 🤓

<GumballMonitorTestDrive.java>

import java.rmi.*;
 
public class GumballMonitorTestDrive {
 
	public static void main(String[] args) {
		String[] location = {"rmi://santafe.mightygumball.com/gumballmachine",
		                     "rmi://boulder.mightygumball.com/gumballmachine",
		                     "rmi://austin.mightygumball.com/gumballmachine"}; 
		
		if (args.length >= 0)
        {
            location = new String[1];
            location[0] = "rmi://" + args[0] + "/gumballmachine";
        }
		
		GumballMonitor[] monitor = new GumballMonitor[location.length];
		
		
		for (int i=0;i < location.length; i++) {
			try {
           		GumballMachineRemote machine = 
						(GumballMachineRemote) Naming.lookup(location[i]);
           		monitor[i] = new GumballMonitor(machine);
				System.out.println(monitor[i]);
        	} catch (Exception e) {
            	e.printStackTrace();
        	}
		}
 
		for (int i=0; i < monitor.length; i++) {
			monitor[i].report();
		}
	}
}

We can see that the locations we're going to monitor. Ane we create an array of locations one for each machine. We also create an array of monitors.
Through Naming.lookup(location[i]); we get a proxy to each remote machine.

Why is it good?🧐

It was a long journey from the beginning...

The Proxy passes the client request over the network, handling all of the nasty details of working the network. This is so called Remote Proxy. This is when the service object is located on a remote server.

In addition, Proxy Pattern has a dozens of ways to utilise it. So, let's go over the most popular uses.

  • Lazy initialsation (virtual proxy). This is when you have a heavyweight service object that wastes system resources by being always up, even though you only need it from time to time. Instead of creating the object when the app launches, you can delay the object's initialisation to a time when it's really needed.
  • Access control (protection proxy). This is when you want only specific clients to be able to use the service object; for instance, when your objects are crucial parts of an operating system and clients are various launched applications.
  • Logging requests (logging proxy). This is when you want to keep a history of requests to the service object. The logging proxy can log each request before passing it to the service.
  • Caching request results (caching proxy). This is when you need to cache results of client requests and manage the life cycle of this cache, especially if results are quite large. The proxy can implement caching for recurring requests that always yield the same results. The proxy may use the parameters of requests as the cache keys.
  • Smart reference. This is when you need to be able to dismiss a heavyweight object once there are no clients that use it. The proxy can keep track of clients that obtained a reference to the service object or its results. From time to time, the proxy may go over the clients and check whether they are still active. If the client list gets empty, the proxy might dismiss the service object and free the underlying system resources.
profile
Keep Learning👨🏻‍💻

0개의 댓글