|
Design pattern questions are very popular with the job interviewers, and this post covers Java strategy and factory design pattern in a tutorial style. This blog takes you through a scenario where you will be learning 4 key things.
1. Factory design pattern
2. Strategy design pattern
3. Applying logic to calculate variance
4. Making use of the BigDecimal class for the financial calculations. The floating point variables must not be used as they can cause rounding issues.
|
Q. The scenario is narrated with the diagram below. It is financial application where an investor builds portfolio by buying and selling managed funds and listed securities.
The additional business rules are:
1. The managed funds that are not of type daily, must be funded from the available cash. Which means, you cannot use the proceeds you receive from selling managed funds or listed securities.
2. The daily managed funds and listed securities can be switched. This means you sell some daily managed funds to buy some listed securities.
3. The variance is calculated as total sell amount - total buy amount. The funded from cash amount also needs to be calculated to show the investor.
Let's look at the code samples:
Step 1: Define the
InvestmentDetail value object (i.e. a POJO) that defines each investment detail as shown above.
import java.math.BigDecimal;
public class InvestmentDetail {
enum TRADE_TYPE {BUY, SELL};
enum FUND_TYPE {MONTHLY, DAILY, NOT_APPLICABLE}
private String investmentCode;
private String investmentName;
private TRADE_TYPE tradeType;
private FUND_TYPE fundType;
private BigDecimal orderAmount;
public InvestmentDetail(String investmentCode, String investmentName,
TRADE_TYPE tradeType, FUND_TYPE fundType, BigDecimal orderAmount) {
this.investmentCode = investmentCode;
this.investmentName = investmentName;
this.tradeType = tradeType;
this.fundType = fundType;
this.orderAmount = orderAmount;
}
public boolean isDailyManagedFund() {
return fundType != null && fundType == FUND_TYPE.DAILY;
}
public boolean isMonthlyManagedFund() {
if(fundType == null ) {
throw new IllegalArgumentException("fundType cannot be null");
}
return fundType == FUND_TYPE.MONTHLY;
}
public boolean isListedSecurity() {
return fundType != null && fundType == FUND_TYPE.NOT_APPLICABLE;
}
public boolean isBuy() {
if(tradeType == null ) {
throw new IllegalArgumentException("tradeType cannot be null");
}
return tradeType == TRADE_TYPE.BUY;
}
public boolean isSell() {
if(tradeType == null ) {
throw new IllegalArgumentException("tradeType cannot be null");
}
return tradeType == TRADE_TYPE.SELL;
}
public BigDecimal getOrderAmount() {
return orderAmount;
}
//setters, getters, equals(), hashCode(), and toString() omitted for brevity
}
Step 2: The
CashVariance value object (i.e. a
POJO) for storing the calculated values like funded from cash and proceed to cash amounts and the variance is derived from these two amounts.
import java.math.BigDecimal;
public class CashVariance {
BigDecimal fundedFromCash;
BigDecimal proceedsToCash;
public CashVariance(BigDecimal fundedFromCash, BigDecimal proceedsToCash) {
super();
this.fundedFromCash = fundedFromCash;
this.proceedsToCash = proceedsToCash;
}
public BigDecimal getVariance() {
return this.proceedsToCash.subtract(this.fundedFromCash);
}
public void setFundedFromCash(BigDecimal fundedFromCash) {
this.fundedFromCash = fundedFromCash;
}
public void setProceedsToCash(BigDecimal proceedsToCash) {
this.proceedsToCash = proceedsToCash;
}
public String toString() {
StringBuilder buffer = new StringBuilder();
buffer.append("fromCash:").append(this.fundedFromCash);
buffer.append("|toCash:").append(this.proceedsToCash);
buffer.append("|variance:").append(getVariance());
return buffer.toString();
}
//getters, equals(), and hashCode()omitted for brevity
}
Step 3: Define a factory class that decides which cash variance calculation strategy to use depending on the product type.
//cannot be extended as it is final
public final class VarianceCalculationFactory {
//can't instantiate from outside
private VarianceCalculationFactory(){}
//factory that creates and return a relevant strategy
public static CashVarianceCalculationStrategy getCashVarianceStrategy(String productType) {
if(productType != null ) {
return new SpecificCashVarianceCalculationStrategy();
}
else {
return new DefaultCashVarianceCalculationStrategy();
}
}
}
Q. Why use a
factory design pattern?
A. Factory pattern returns an instance of several subclasses (like
DefaultCashVarianceCalculationStrategy,
ManagedInvestmentsCashVarianceCalculationStrategy, etc), but the calling code is unaware of the actual implementation class. The calling code invokes the method on the interface for example
CashVarianceCalculationStrategy and using polymorphism the relevant class and correct method gets invoked. So, as you can see, the factory pattern reduces the coupling or the dependencies between the calling code and called objects like
DefaultCashVarianceCalculationStrategy,
ManagedInvestmentsCashVarianceCalculationStrategy, etc. This is a very powerful and common feature in many frameworks. You do not have to create a new
DefaultCashVarianceCalculationStrategy or a new
ManagedInvestmentsCashVarianceCalculationStrategy on each invocation. In future, to conserve memory, you can decide to cache objects or reuse objects in your factory with no changes required to your calling code. You can also load objects in your factory based on attribute(s) read from an external properties file or some other condition. Another benefit going for the factory is that unlike calling constructors directly, factory patterns have more meaningful names like
getShape(…),
getInstance(…),
getStrategy(…) etc, which may make calling code much clear.
Step 4: Define an interface each strategy class will be implementing.
import java.util.List;
//strategy pattern interface
public interface CashVarianceCalculationStrategy {
CashVariance calculate (List<InvestmentDetail> investments);
}
Step 5: This is the default implementation of the above interface, and the class that is responsible for calculating the funded from cash and proceed to cash values.
import java.math.BigDecimal;
import java.util.LinkedList;
import java.util.List;
public class DefaultCashVarianceCalculationStrategy implements CashVarianceCalculationStrategy {
@Override
public CashVariance calculate(List<InvestmentDetail> investments) {
//input validation
if(investments == null){
throw new IllegalArgumentException("No investments founds!!!");
}
List<BigDecimal> buys = new LinkedList<BigDecimal>();
List<BigDecimal> sells = new LinkedList<BigDecimal>();
List<BigDecimal> switchables = new LinkedList<BigDecimal>(); //pay for buys from the sells
for (InvestmentDetail detail : investments) {
if(detail.isBuy()){
processBuy(detail, buys, switchables);
}
else if(detail.isSell()){
processSell(detail, sells, switchables);
}
}
CashVariance variance = calculateVariance(buys, sells, switchables);
return variance;
}
private void processSell(InvestmentDetail detail, List<BigDecimal> sells, List<BigDecimal> switchables) {
if(isSwitch(detail)){
switchables.add(detail.getOrderAmount()); //+ve amount
}
else{
sells.add(detail.getOrderAmount());
}
}
private void processBuy(InvestmentDetail detail, List<BigDecimal> buys, List<BigDecimal> switchables) {
if(isSwitch(detail)){
switchables.add(detail.getOrderAmount().negate()); //-ve amount
}
else{
buys.add(detail.getOrderAmount());
}
}
//protected so that it can be overridden by another strategy
protected boolean isSwitch(InvestmentDetail detail){
return detail != null && (detail.isDailyManagedFund() || detail.isListedSecurity()); //return false if it is a monthly managed fund
//This has to be funded from cash.
}
protected CashVariance calculateVariance(List<BigDecimal> buys, List<BigDecimal> sells, List<BigDecimal> switchables) {
BigDecimal switchedSum = sumAll(switchables);
BigDecimal fundedFromCash = calculateFundedFromCash(buys, switchedSum);
BigDecimal proceedsToCash = calculateProceedsToCash(sells, switchedSum);
return new CashVariance(fundedFromCash, proceedsToCash);
}
private BigDecimal calculateFundedFromCash(List<BigDecimal> buys, BigDecimal switchedSum) {
BigDecimal buysSum = sumAll(buys);
//if switchedSum has more buy amount (i.e negative) add the switchedSum to buy
return buysSum.add((switchedSum.signum() == -1) ? switchedSum.abs() : BigDecimal.ZERO);
}
private BigDecimal calculateProceedsToCash(List<BigDecimal> sells, BigDecimal switchedSum) {
BigDecimal sellsSum = sumAll(sells);
//if switchedSum has more sell amount (i.e positive) add the switchedSum to sell
return sellsSum.add((switchedSum.signum() == 1) ? switchedSum : BigDecimal.ZERO);
}
private BigDecimal sumAll(List<BigDecimal> values) {
BigDecimal total = BigDecimal.ZERO;
for (BigDecimal val : values) {
total = total.add(val);
}
return total;
}
}
Q. Why use a strategy design pattern?
A. A strategy design pattern allows you to choose different algorithms at run time depending certain run time conditions like product type. Different algorithms can be isolated in different implementation classes that implement the same interface.
Different client classes or invokees can execute different strategies
(i.e. algorithms
).
Step 6: Finally, the tester class that demonstrates the above scenario by constructing investment details and calculating the variance as shown below.
import java.math.BigDecimal;
import java.util.LinkedList;
import java.util.List;
public class VarianceCalcTester {
public static void main(String[] args) {
InvestmentDetail ab0001 = new InvestmentDetail("AB0001", "AB Manged Fund",
InvestmentDetail.TRADE_TYPE.BUY, InvestmentDetail.FUND_TYPE.MONTHLY,
new BigDecimal("2500.00"));
InvestmentDetail cx0002 = new InvestmentDetail("CX0002", "CX Managed Fund",
InvestmentDetail.TRADE_TYPE.BUY, InvestmentDetail.FUND_TYPE.DAILY,
new BigDecimal("4500.00"));
InvestmentDetail by007 = new InvestmentDetail("BY007", "BY Managed Fund",
InvestmentDetail.TRADE_TYPE.BUY, InvestmentDetail.FUND_TYPE.DAILY,
new BigDecimal("2000.00"));
InvestmentDetail sh0008 = new InvestmentDetail("SH0008", "BY Managed Fund",
InvestmentDetail.TRADE_TYPE.SELL, InvestmentDetail.FUND_TYPE.NOT_APPLICABLE,
new BigDecimal("8000.00"));
List<InvestmentDetail> investments = new LinkedList<InvestmentDetail>();
investments.add(ab0001);
investments.add(cx0002);
investments.add(by007);
investments.add(sh0008);
//get the variance calculation strategy from the factory
CashVarianceCalculationStrategy cvCalcStaregy = VarianceCalculationFactory.getCashVarianceStrategy(null); // return the default strategy
CashVariance cv = cvCalcStaregy.calculate(investments);
System.out.println(cv); // -ve variance Debit and +ve variance Credit
}
}
Labels: design pattern