2

I'm implementing a system in PHP where values are represented with objects that implement an Amount interface. I'm building two different implementations of this interface, one using a simple integer internally to represent the value being stored, and the other using GMP objects.

The interface method that's proving a sticking point is as follows:

/**
 * Get the amount being stored
 * 
 * @return number
 */
public function getAmount ();

(If you're not familiar with PHP documentation conventions, number means any type which can be considered numeric, which would mean either an integer or a float)

The idea is that in most cases the integer implementation will be used for performance and memory usage reasons, and you would only switch to the GMP implementation when number sizes start exceeding what you can put in an integer. Overflow could be handled like this (in psuedo-code):

try
{
    $someAmount -> add ($otherAmount);
} 
catch (OverFlowException $e)
{
    // Replace the overflowed Amount object with a BigAmount
    $someAmount = new BigAmount ($someAmount -> getValue ());

    // Retry the add
    $someAmount -> add ($otherAmount);
}

My integer based implementation is very simple:

/**
 * @return integer
 */
public function getAmount ()
{
    return $this -> amount;
}

However, as I started to implement the GMP version things started getting complicated:

/**
 * Get the amount as an integer or string
 * 
 * This method will attempt to return the current value as an integer. If 
 * the value being represented is beyond the range of a PHP integer then 
 * it will return a string instead. 
 * 
 * @return number|string
 */
public function getAmount ()
{
    // If the value is within integer range then return an int
    if ((gmp_cmp ($this -> amount, PHP_INT_MAX) < 1)
    && (gmp_cmp ($this -> amount, PHP_INT_MAX * -1 - 1) > -1))
    {
        return gmp_intval ($this -> amount);
    } else {
        return gmp_strval ($this -> amount);
    }
}

As I was implementing this, I realised it had some problems:

  • It doesn't always return the same type (resulting in a violation of the principle of least surprise)
  • It doesn't strictly conform to its interface (Also violating least surprise)
  • Getters are supposed to be extremely simple and this isn't

The obvious first solution was to simply return the GMP object, but that would mean that the object would never return what its interface says it should, and that this class would be returning objects instead of primitive types like the integer implementation does.

The second solution was for the getter to always return a string. This would be only slightly more complex than returning the GMP object, and it would mean a primitive type was being returned, but we're still not conforming to the interface.

A third solution would be to have the getter throw an exception if the value can't be returned as an integer and provide an additional method for getting the value as a string. This would fulfil the contract of the interface (if I changed the interface to indicate that an exception could be thrown if the value can't be returned as an integer), but would add an additional burden on programmers using the classes to deal with having to handle cases where numbers are too big for integers.

So in summary, I have a number of choices, none completely ideal:

  1. Stick with the current solution, and have a method that can return different types
  2. Have the method always return a GMP (which is not a primitive type and not the type the interface says)
  3. Have the method always return a string (which is a primitive type but not the type the interface says)
  4. Throw an exception if an integer can't be returned and supply an additional method for those cases (which meets the demands of the interface but would add requirements to handle it in consuming code)

Which approach would you consider to be best (or least worst), or is there another approach I've not considered?

  • If you're processing a lot of numerical values where memory and performance become an issue with PHP. You most likely shouldn't be using PHP. Not trying to criticize PHP, but when you have to do what you're doing just to do something. That's what I call the "writing on the wall" that you're using the wrong language. I bet this is some kind of crontab job that could easily be done with python, java or c# on linux or windows. – Reactgular May 24 '15 at 17:57
  • @MathewFoscarini - I'm a bit surprised by your suggestion to use Python (apart from Java and C#). AFAIK Python doesn't really have significant performance benefit compared to PHP ... – qbd May 24 '15 at 19:44
  • It's more a question of building a library for managing computations with things like money whilst avoiding floating point calculations. – GordonM May 25 '15 at 7:27
  • @qbd Python's a good alternative to PHP because it's available on all the same platforms. Java/C# require the user to install them which they might not have server permissions to do. That's the main reason I recommended it. – Reactgular May 25 '15 at 13:13
1

First 3 options break Liskov's substitution principle. Fourth one complicates the interface for the client.

Personally I think the easiest solution would be to declare the getAmount() method that it returns always string - then it works with both integer-based and GMP-based implementations.

Alternatively this might be a case of premature optimization - maybe it's just fine to compute everything in GMP and return GMP object always (of course interface should be declared accordingly).

0

In the end I decided on option 4. Have the interface specify that getAmount() can throw an exception, and have the BigAmount implementation throw one should it not be possible to express its value as an integer. Then I'll have another method that can return the amount in another format. It would require the programmer using the class to jump through some additional hoops, but at least those hoops are well defined and documented.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.