2

I have started a new project for credit management and I got to a point where I have to handle currency exchange. (example Euro -> Dollar) So I did a bit of brainstorming and got this:

  • currencies are abstract, every new currency is an implementation of an interface
  • built a rate class which stores an array list of pair< pair, double > (I was thinking of something like euro, dollar,1.14 -> the exchange rate of the euro to dollar is 1.14) and has some functionalities like: currency exchange rate storage, a finder for the right rate(it is a function that gets the current currency and the new currency as parameters and performs a search over the list, returning the appropriate currency) and a utility function which determines the index of a pair.

Now, I am thinking about performance. I believe that my design contains a bit of redundancy: if I have euro-to-dollar I have to have dollar-to-euro. Moreover, how efficient is my storage (array list of pair< pair, double >) for a higher number of entries (suppose 100k) and what alternatives do I have? I know there are lots of data structures and I really don't know what to pick.

Now, for the code:

conversion rates class:

package currency;

import javafx.util.Pair;
import loggers.ILogger;

import java.util.ArrayList;

public class ConversionRates implements IConversionRate {
    private ArrayList<Pair<Pair<String, String>, Double>> conversionRates;
    private ILogger log;


    public ConversionRates(ArrayList<Pair<Pair<String, String>, Double>> conversionRates, ILogger log) {
        this.conversionRates = conversionRates;
        this.log = log;
    }

    @Override
    public double find(ICurrency firstCurrency, ICurrency secondCurrency) {
        log.add("Performing rate identification");
        String first = firstCurrency.getId();
        String second = secondCurrency.getId();
        int index = searchPairs(first, second);
        Pair<Pair<String, String>, Double> selectedPair = conversionRates.get(index);
        double rate = selectedPair.getValue();
        return rate;
    }

    private int searchPairs(String first, String second) {
        Pair<String, String> pairContainingRate = new Pair<>(first, second);
        for (int index = 0; index < conversionRates.size(); index++) {
            if (conversionRates.get(index).getKey().equals(pairContainingRate)) {
                log.add("Successfully found target " + first + "/" + second);
                return index;
            }
        }

        log.add("Failed to find target " + first + "/" + second);
        return -1;
    }

    @Override
    public void add(Pair<Pair<String, String>, Double> newRate) {
        log.add("Added new rate " + newRate);
        conversionRates.add(newRate);
    }

    @Override
    public void update(ArrayList<Pair<Pair<String, String>, Double>> newRates) {
        log.add("Updated rates");
        conversionRates.clear();
        conversionRates.addAll(newRates);
    }

    @Override
    public void remove(Pair<Pair<String, String>, Double> redundantRate) {
        log.add("Removed rate " + redundantRate);
        conversionRates.remove(redundantRate);
    }
}

ConversionRates interface:

package currency;

import javafx.util.Pair;

import java.util.ArrayList;

public interface IConversionRate {

    double find(ICurrency euro, ICurrency newCurrency);

    void add(Pair<Pair<String,String>,Double> newRate);

    void update(ArrayList< Pair<Pair<String,String>,Double> > newRates);

    void remove(Pair<Pair<String,String>,Double> redundantRate);
}

Currency interface:

package currency;

public interface ICurrency {


    double convertTo(double amount, ICurrency newCurrency);

    double convert(double oldCurrencyAmount, double conversionRateToNewCurrency);

    String getId();
}

Currency implementation(I will list only the Euro as the euro is exactly the same):

package currency;

public class Euro implements ICurrency {
    private String id;
    private IConversionRate conversionRateFinder;

    public Euro(String id, IConversionRate conversionRateBuilder) {
        this.id = id;
        this.conversionRateFinder = conversionRateBuilder;
    }

    @Override
    public double convertTo(double amount, ICurrency newCurrency) {
        double currentConversionRate  = conversionRateFinder.find(this, newCurrency);
        double newAmount = newCurrency.convert(amount,currentConversionRate);
        return newAmount;
    }

    @Override
    public double convert(double oldCurrencyAmount, double currentConversionRate) {
        return oldCurrencyAmount*currentConversionRate;
    }


    public String getId() {
        return id;
    }
}

I would like to hear from you about the general design as well.

CyberFox
  • 342
  • 3
  • 16
  • You should care about readability before caring about performance. I have no idea what a ArrayList, Double>> can possibly contain. Define classes, enums, which have names which clearly show what they represent. Note that both Java and Guava don't include a Pair class precisely because it encourages developers to write unreadable code like the one you have there. – JB Nizet Aug 12 '18 at 13:23
  • The first pair represents the currencies and the double is the actual exchange rate from the first to the second. – CyberFox Aug 12 '18 at 13:25
  • 1
    Then define and use a class CurrencyConversion with three fields: Currency sourceCurrency, Currency targetCurrency, double conversionRate. It's way more readable. It can contain methods to actually compute a conversion. It can contain a method which creates a reverse CurrencyConversion simply by swapping the two currencies and inverting the rate. It can validate the validity of the rate at construction time. It can has a toString() method which returns a readable description of the object, etc. etc. – JB Nizet Aug 12 '18 at 13:29
  • That sounds good. So, I am supposed to create that class and inject it into each currency object and perform the conversion. Still, how am I supposed to handle the storage of the rates or how should I correctly perform the detection of the rate exchange rate? Tanks in advance and I apologize for those errors, I am still a rookie. – CyberFox Aug 12 '18 at 13:43
  • 1
    It depends. Is this a real application, where storage actually means persistent storage? If so, use a database. If it's a toy project where everything is in memory, you should probably use a Map, where CurrencyPair contains a source and a target currency, is immutable, and defines proper equals() and hashCode() methods. That would allow an O(1) lookup. – JB Nizet Aug 12 '18 at 13:52
  • This is not a real application, but I would like to take this into consideration. I will be using serialization. – CyberFox Aug 12 '18 at 13:56
  • Perhaps the [*Joda-Money*](http://www.joda.org/joda-money/) project might help? – Basil Bourque Aug 12 '18 at 15:35

2 Answers2

2

I think you should start much simpler. I don't see the reason for an interface for currency especially in this case because I don't see how your implementations will require different providers. Each Currency is denoted by a symbol and a set of exchange rates for that currency.

public class Currency {
    private final String symbol; 
    private final Set<ExchangeRate> rates = new HashSet<>();

    public Currency(String symbol) {
        this.symbol = symbol;
    }
    public BigDecimal convert(Currency currency, BigDecimal amount) {
        return findExchangeRate(currency).getRate().multiply(amount).setScale(2, RoundingMode.HALF_DOWN);
    }
    public String getSymbol() {
        return symbol;
    }
    public ExchangeRate findExchangeRate(Currency currency) {
        for(ExchangeRate rate: rates) {
            if ( rate.getCurrency().equals(currency)) {
                return rate;
            }
        }
        throw new IllegalArgumentException("Currency not found: " + currency);
    }
    public void setExchangeRate(ExchangeRate rate) {
        if ( rates.contains(rate) ) rates.remove(rate);
        rates.add(rate);
    }
    public boolean removeExchangeRate(ExchangeRate rate) {
        return rates.remove(rate);
    }

An ExchangeRate is a rate for the currency that is being exchanged so you don't need to repeat the 1.0 for that currency every time. Don't forget to @Override hashCode and equals so the Set logic will work correctly.

public class ExchangeRate {
    private final Currency currency;
    private final BigDecimal rate;
    public ExchangeRate(Currency currency, BigDecimal rate) {
        this.currency = currency;
        this.rate = rate;
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((currency == null) ? 0 : currency.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        ExchangeRate other = (ExchangeRate) obj;
        if (currency == null) {
            if (other.currency != null)
                return false;
        } else if (!currency.equals(other.currency))
            return false;
        return true;
    }

Using it is pretty straight forward.

Currency usd = new Currency("USD");
Currency eur = new Currency("EUR");
usd.setExchangeRate(new ExchangeRate(eur, new BigDecimal("0.87540")));
eur.setExchangeRate(new ExchangeRate(usd, new BigDecimal("1.14233")));

BigDecimal myMoney = new BigDecimal("1000.00");
myMoney = usd.convert(eur, myMoney);
System.out.println("My Euros: " + myMoney);
myMoney = eur.convert(usd, myMoney);
System.out.println("My Dollars: " + myMoney);

Note the usage of BigDecimal. This, or something like this, is always necessary for money computations because of the failure of accuracy of floats and doubles. You could convert to Long or Integer and keep track of scale yourself but you would be doing the same thing as the BigDecimal class.

K.Nicholas
  • 10,956
  • 4
  • 46
  • 66
  • Your solution is amazing. I never thought I could do that approach, Thank you, sir! – CyberFox Aug 13 '18 at 07:46
  • One more question, is it a good idea to have each currency with its own exchange rates? Wouldn't it be better to keep them separate so I can update them more easily? – CyberFox Aug 13 '18 at 08:45
  • Not sure what you are starting with in terms of data but if you sort it you should have groups of currencies. You will always have to find one currency then the other. If you put all your currencies into a Set then you essentially have pairs of exchange rates. If depends on your real world model. For example a bank might support a certain set of currencies while another bank might support another. So you could make a bank class and have a `Set` of currencies in it. – K.Nicholas Aug 13 '18 at 14:33
  • @k-nicholas Hmm. I have managed to build my own solution, using this as a template. My question is: for the set logic to work, do I have to do something else besides Overriding "equals" and "hashCode" methods? Let's say I want to perform a search, do I have to call "hashCode()"? (PS: I haven't touched Sets and hashing until this project) – CyberFox Aug 16 '18 at 20:31
  • 1
    New question should be a new post. https://stackoverflow.com/questions/5396939/hashcode-and-equals-for-hashset – K.Nicholas Aug 16 '18 at 20:43
1

Actually, Kent Beck in his book about TDD worked on this matter.

I didn't read it to the end but I remember that at some point he had Money abstract class, Sum (or something like this) for storing money and Bank. Banks are responsible for currency rate.

I am not sure if creating classes like Dollar and Euro is a good approach. Maybe it would be easiser just to stick with Money and keep currency in instance variable (KISS). I think I would want currencies to be fully handled by one class (Bank, ExchangeRate or whatever suits you best) and I would make Money instances immutable. And all calculations are made by Sum class (or whatever, the point is to move logic away from money itself).

Anyway, maybe you should just take a look at Beck's book. I am pretty sure he designed it pretty well.

Nondv
  • 769
  • 6
  • 11
  • This does not provide an answer to the question. To critique or request clarification from an author, leave a comment below their post. - [From Review](/review/low-quality-posts/20568367) – David Maze Aug 12 '18 at 20:24
  • 1
    @DavidMaze > I would like to hear from you about the general design as well. – Nondv Aug 12 '18 at 21:10
  • I really appreciate the explanations. I will try the approach you have suggested and see how that would fit my situation. This is not a real project, it is just a personal project I am working on. – CyberFox Aug 13 '18 at 07:44