|
<< Using Caesar | Contents | Observer Example >>
Pricing ExampleOverviewIn this section we will outline how Caesar can be used to implement a reusable pricing component. You can download the full source-code of this example here.Our example application provides information about stock quotes. It consists of a server part and a client part. Let us consider the server part first. public class StockInfo { ... public void addQuote(String stock, Float f) {...} public float getQuote(String stock) {...} } public class StockInfoRequest { ... public StockInfoRequest(String stocks[]) {...} public String[] getStocks() {} } public class StockInformationBroker { private StockInformationBroker() {} private static StockInformationBroker instance = new StockInformationBroker(); public static StockInformationBroker getInstance() { return instance; } private static Hashtable stocksDB = new Hashtable(); static { stocksDB.put("Siemens", new Float(98.23)); stocksDB.put("IBM", new Float(98.73)); ... } public Float getStockQuote(String stock) { return (Float) stocksDB.get(stock); } public StockInfo collectInfo(StockInfoRequest request) { String stocks[] = request.getStocks(); StockInfo info = new StockInfo(); for (int i=0; i<stocks.length; i++) { info.addQuote(stocks[i], getStockQuote(stocks[i])); } return info; } }We have a primitive database storing the information about the stock quotes (see Hashtable stocksDB ). The client interface is provided by the method StockInfo collectInfo(StockInfoRequest) . As parameter the method expects a StockInfoRequest object containing an array of Strings, which are used to identify the requested stocks. As result the method returns a StockInfo object containing the stock quotes.
The client prepares its request in the run method. By calling StockInformationBroker.getInstance().collectInfo(request)) it retrieves the stock quote information from the server.
public class Client { ... public String getName() { ... } public void run(String stocks[]) { StockInfoRequest request = new StockInfoRequest(stocks); StockInfo si = StockInformationBroker.getInstance().collectInfo(request); // print stock info ... } public static void main(String[] args) { Client mira = new Client("Mira"); Client klaus = new Client("Klaus"); mira.run(new String[]{"Microsoft"}); mira.run(new String[]{"BMW", "IBM"}); klaus.run(new String[]{"IBM", "SAP"}); mira.run(new String[]{"IBM"}); klaus.run(new String[]{"BMW", "Microsoft"}); klaus.run(new String[]{"IBM"}); } }So far we have presented the pure business logic of the application. Let us now consider how we can use Caesar to add a reusable pricing service to the stock information broker application. Defining the Collaboration InterfaceCollaboration interfaces describe a set of roles and the relations among them. In the code below we define the Pricing collaboration consisting of a role Customer and a role Item. The Customer has a name, an account, and can charge items. Each Item has a price.abstract public cclass Pricing { abstract public cclass Customer { abstract public /*provided*/ void charge(Pricing.Item it); abstract public /*provided*/ float balance(); abstract public /*expected*/ String name(); } abstract public cclass Item { abstract public /*expected*/ float price(); } }Since we want to write a pricing component, which is reusable in different applications, we have to separate the reusable part from the application specific part. This is achieved by separating the Pricing collaboration in a provided and in an expected part. The provided part is implemented by reusable Caesar components, and the expected part by Caesar bindings. The purpose of a binding is to map the application specific concepts to those defined in the collaboration and to trigger the execution of a component. E.g., a pricing component has to know the price of an item in order to be able to charge it from the customer's account. However, this information is application specific and can not be provided by the component itself. Implementing the provided PartThe component hierarchy implements the provided part. In the pricing example we have two different implementations.RegularPricing always charges from the customer a constant price for the requested item. In DiscountPricing every third item charged is for free.
Note how the virtual class mechanism is applied here. E.g., DiscountPricing refines the RegularPricing collaboration by redefining the charge method in Customer class.
abstract public cclass RegularPricing extends Pricing { abstract public cclass Customer { private float balance; public void charge(Pricing.Item it) { float diff = it.price(); balance -= diff; } public float balance() { return balance; } } } abstract public cclass DiscountPricing extends RegularPricing { abstract public cclass Customer { private int discountstate = 0; public void charge(Pricing.Item it) { if (discountstate++ == 2) { discountstate = 0; // This one was for free } else { super.charge(it); } } } } Implementing the expected PartThe binding hierarchy implements the expected part. In our example we define two different bindings.PerRequestBinding declares a client request as a chargeable item, while in PerStockQuoteBinding every stock quote contained in a request is considered as an item.
The bindings are implemented as follows.
abstract public cclass PerRequestBinding extends Pricing { public cclass Customer wraps Client { public String name() { return $wrappee.getName(); } } public cclass Item wraps StockInfoRequest { public float price() { return 5; } } after(Client c, StockInfoRequest request) :(call(StockInfo collectInfo(StockInfoRequest)) && this(c) && args(request)) { Customer(c).charge( Item(request) ); } } abstract public cclass PerStockQuoteBinding extends Pricing { public cclass Customer wraps Client { public String name() { return $wrappee.getName(); } } public cclass Item wraps String { public float price() { return 2; } } after(Client c, String stock) :(cflow(call(void Client.run(String[])) && target(c))) && (call(Float getStockQuote(String)) && args(stock)) { Customer(c).charge( Item(stock) ); } }In the example code above we can observe two differences to Caesar components, namely the usage of the wraps clause and AspectJ-like pointcuts and advices. The bindings use pointcuts to select joinpoints in the execution control flow, where the execution of the component should be triggered. Moreover, additional context at that joinpoint is collected, necessary to provide the expected information required by the collaboration. E.g., PerRequestBinding interprets the method calls to StockInformationBroker.collectInfo as chargable item, while in the context of a PerStockQuoteBinding a chargeable item is every call to StockInformationBroker.getStockQuote .
The wrappers are used to map an application item to a role defined in the collaboration. E.g., in PerRequestBinding the Item is bound to StockInfoRequest , while in the context of PerStockQuoteBinding it is bound to String (which represents a stock name). In both collaborations Client is bound to the Customer role.
A wrapper always depends on an application object (wrappee). It potentially introduces additional state to the wrappee and it exists till the wrappee is collected by the garbage collector. We can navigate from an application object to its wrapper by using the wrapper recycling calls, e.g., by calling Customer(c) in the advice body of PerRequestBinding . Navigation from wrapper back to the wrappee is achieved via the $wrappee variable defined in the wrapper itself.
Creating the WeavletThe binding and the component hierarchies are useless at its own, since they implement only one part of the collaboration interface and can not be instantiated. Weavlet is the combination of a component and a binding. Since it unites the provided and the expected part, it is complete and ready for the instantiation. Note, if we have n components and m bindings, we can produce n x m weavlets, each of them implementing an individual pricing strategy (see Fig above). Weavlets are constructed by applying the& -operator.
public cclass RegularPricingPerStockQuote extends RegularPricing & PerStockQuoteBinding {} public cclass RegularPricingPerRequest extends RegularPricing & PerRequestBinding {} public cclass DiscountPricingPerStockQuote extends DiscountPricing & PerStockQuoteBinding {} public cclass DiscountPricingPerRequest extends DiscountPricing & PerRequestBinding {} Activating the WeavletIn order to activate the crosscutting code weaved into the application by the binding, we have to deploy it first. This is done in PricingDeployment?, which again uses crosscutting code in order to decorate part of the application control flow with corresponding deploy statement.public deployed cclass PricingDeployment { static Map pricingMapping = new HashMap(); static { // User <-> Pricing Mapping pricingMapping.put("Mira", new PerRequestDiscountPricing()); pricingMapping.put("Klaus", new PerRequestRegularPricing()); } void around(Client c): (execution(void Client.run(String [])) && this(c)) { deploy( pricingMapping.get( c.getName() ) ) { proceed(c); } } }The interesting point is that each client has its individual pricing schema, which is defined in the pricingMapping. The around advice ensures that the pricing weavlet is deployed (activated) before proceed(c) method is called.
In the pricing example, the customer Klaus is charged per stock quote with a discount for every third item, while the customer Mira is charged per request using the regular pricing.
Executing the exampleThe execution of theClient.main method generates the following log.
Mira: request [Microsoft] RegularPricing: charged 5.0 EUR, balance is: -5.0 EUR Mira: request [BMW, IBM] RegularPricing: charged 5.0 EUR, balance is: -10.0 EUR Klaus: request [IBM, SAP] DiscountPricing: charged 2.0 EUR, balance is: -2.0 EUR DiscountPricing: charged 2.0 EUR, balance is: -4.0 EUR Mira: request [IBM] RegularPricing: charged 5.0 EUR, balance is: -15.0 EUR Klaus: request [BMW, Microsoft] DiscountPricing: 3for2 discount - charged nothing, balance is still: -4.0 EUR DiscountPricing: charged 2.0 EUR, balance is: -6.0 EUR Klaus: request [IBM] DiscountPricing: charged 2.0 EUR, balance is: -8.0 EUR<< Using Caesar | Contents | Observer Example >> |