SHUT UP AND GIVE ME YOUR MONEY!!
Álvaro Salazar
@xala3pa
xala3pa
@xala3pa
Problems to Avoid
Currency exchange and number format logic spread
Using differents round modes
Operations between diffent currencies
Accuracy errors
Design Motivations
COHESION
ENCAPSULATION
PRIMITIVE OBSESSION
FEATURE ENVY
COHESION
Functional cohesion is when parts of a module are grouped because they all contribute to a single well-defined task of the module
https://en.wikipedia.org/wiki/Cohesion_(computer_science)
COHESION
Increased system maintainability
Increased module reusability
COHESION
Before Money:
[ 'totalPrice': CurrencyUtils.format( purchase.totalPrice, purchase.country ) ]
CurrencyService currencyService
BigDecimal totalPrice = currencyService.convertCurrency( purchase.totalPrice,
purchase.currency, gateway.currency, round )
After Money:
['totalPrice': purchase.totalPrice.format( Locale.US )]
Money totalPrice = purchase.totalPrice.exchangeTo( gateway.currency )
ENCAPSULATION
Encapsulation binds together the data and functions that manipulate the data, and keeps both safe from outside interference and misuse
http://www.tutorialspoint.com/cplusplus/cpp_data_encapsulation.htm
ENCAPSULATION
Control the way data is accessed or modified
Makes the class easy to use for clients
Increase reusability
Encapsulation promotes maintenance
ENCAPSULATION
@groovy.transform.CompileStatic
final class Money implements Serializable, Comparable<Money>, MoneyExchange, MoneyFormat {
final BigDecimal amount
final Currency currency
// ...
}
ENCAPSULATION
@groovy.transform.PackageScope
trait MoneyExchange {
//...
Money exchangeTo(Currency to, Exchange exchange = getCurrentExchange()) {
//...
}
}
@groovy.transform.PackageScope
trait MoneyFormat {
//...
String format(Locale locale = Locale.default) {
//...
}
}
PRIMITIVE OBSESSION
http://c2.com/cgi/wiki?PrimitiveObsession
Primitive Obsession is using primitive data types to represent domain ideas. For example, use a String to represent a message or a Big Decimal to represent an amount of money,
PRIMITIVE OBSESSION
https://sourcemaking.com/refactoring/smells/primitive-obsession
Group primitive fields into their own class
Refactor -> Replace Data Value with Object.
Treatment:
PRIMITIVE OBSESSION
https://sourcemaking.com/refactoring/smells/primitive-obsession
Benefits:
-
Code becomes more flexible thanks to use of objects instead of primitives.
-
Better understandability and organization of code.
-
Easier finding of duplicate code.
PRIMITIVE OBSESSION
Before Money:
class Ticket implements Serializable {
BigDecimal totalPrice
Currency currency
//...
}
After Money:
class Ticket implements Serializable {
Money totalPrice
//...
}
FEATURE ENVY
https://sourcemaking.com/refactoring/smells/feature-envy
A method accesses the data of another object more than its own data.
A rule of thumb: If things change at the same time, you should keep them in the same place.
FEATURE ENVY
https://sourcemaking.com/refactoring/smells/feature-envy
Treatment:
Refactor -> Extract Method
Refactor -> Move Method
FEATURE ENVY
https://sourcemaking.com/refactoring/smells/feature-envy
Benefits
-
Less code duplication
-
Better code organization
FEATURE ENVY
https://sourcemaking.com/refactoring/smells/feature-envy
Before Money:
BigDecimal totalPrice = currencyService.convertCurrency( purchase.totalPrice,
purchase.currency, gateway.currency, round )
After Money:
Money totalPrice = purchase.totalPrice.exchangeTo( gateway.currency )
TECHNICAL APPROACH
TECHNICAL APPROACH
TECHNICAL APPROACH
CREATE A CONTAINER TO OUR MONEY OBJECT
@groovy.transform.CompileStatic
final class Money implements Serializable, Comparable<Money>, MoneyExchange, MoneyFormat {
final BigDecimal amount
final Currency currency
private final static MathContext MONETARY_CONTEXT = MathContext.DECIMAL128
final static class CurrencyMismatchException extends RuntimeException {
CurrencyMismatchException(String aMessage) {
super(aMessage)
}
}
//...
Money(Number amount, Currency currency) {
this.amount = (BigDecimal) amount
this.currency = currency
}
//...
}
INTRODUCE MONEY TO GORM
class MoneyUserType implements UserType, ParameterizedType {
private final static String DEFAULT_CURRENCY_COLUMN = 'currency'
private final static int[] SQL_TYPES = [Types.DECIMAL] as int[]
Properties parameterValues
//...
Object nullSafeGet(ResultSet rs, String[] names, Object owner) {
//...
}
void nullSafeSet(PreparedStatement st, Object value, int index) {
//...
}
Class returnedClass() {
Money.class
}
int[] sqlTypes() {
SQL_TYPES
}
}
USE MONEY TYPE IN DOMAIN OBJECTS
class Ticket implements Serializable {
Money totalPrice
//...
static mapping = {
totalPrice type: MoneyUserType, params: [currencyColumn: 'divisa']
}
}
ADD BEHAVIOUR TO OUR MONEY
trait MoneyExchange {
//...
Money exchangeTo(Currency to, Exchange exchange = getCurrentExchange()) {
//...
}
}
interface Exchange {
BigDecimal getRate(Currency from, Currency to)
}
Text
ADD BEHAVIOUR TO OUR MONEY
trait MoneyFormat {
//...
String format(Locale locale = Locale.default) {
//...
}
//...
}
INTRODUCE MONEY TO GRAILS
class StructuredMoneyEditor extends AbstractStructuredBindingEditor<Money> {
private static final String currencyPlaceholder = '¤'
Money getPropertyValue(Map values) {
DecimalFormat formatter = getCustomDecimalFormatter(values)
BigDecimal parsedAmount = getParsedAmount(formatter, (String) values.amount)
new Money(parsedAmount, (String) values.currency)
}
//...
}
def doWithSpring = {
//Custom structured property editor data binding for Money type
moneyEditor com.ticketbis.money.StructuredMoneyEditor
}
class GreaterThanZeroConstraint extends AbstractConstraint {
private static final String DEFAULT_INVALID_MESSAGE_CODE = 'default.gtZero.invalid'
static final String CONSTRAINT_NAME = 'gtZero'
private boolean gtZero
//...
protected void processValidate(Object target, Object propertyValue, Errors errors) {
if (!validate(propertyValue)) {
def args = (Object[]) [constraintPropertyName,
constraintOwningClass, propertyValue]
rejectValue(target, errors,
DEFAULT_INVALID_MESSAGE_CODE, "not.${CONSTRAINT_NAME}", args)
}
}
//...
}
def doWithSpring = {
//...
ConstrainedProperty.registerNewConstraint(
GreaterThanZeroConstraint.CONSTRAINT_NAME,
GreaterThanZeroConstraint)
//...
}
class Ticket implements Serializable {
Money totalPrice
//...
static constraints = {
totalPrice( nullable: false, gtZero: true )
//...
}
class MoneyTagLib {
static namespace = 'money'
def inputField = { attrs ->
def name = attrs.remove('name')
def value = attrs.remove('value')
//...
}
def format = { attrs ->
Money value = new Money(attrs.value)
//...
}
}
<money:inputField name="totalPrice" value="123.45" currency="EUR"/>
<money:inputField name="totalPrice" value="${ money }"/>
<money:format value="${ money }" pattern="¤ ##,##0.00"/>
<money:format value="${ money }" numberFormat="${ formatter }"/>
Summing up
Cleaner code
Less code duplication
Avoid Operations between different currencies
Better Accuracy
Easy to maintain
Increase reusability
https://slides.com/xala3pa
https://github.com/ticketbis/grails-money
Source Code :
Slides :
itjobs@ticketbis.com
http://stackoverflow.com/jobs/companies/ticketbis
Thanks !!
Greach 2016 Money Talk
By xala3pa
Greach 2016 Money Talk
- 1,605