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)
[ 'totalPrice': CurrencyUtils.format( purchase.totalPrice, purchase.country ) ]
CurrencyService currencyService
BigDecimal totalPrice = currencyService.convertCurrency( purchase.totalPrice,
purchase.currency, gateway.currency, round )
['totalPrice': purchase.totalPrice.format( Locale.US )]
Money totalPrice = purchase.totalPrice.exchangeTo( gateway.currency )
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
@groovy.transform.CompileStatic
final class Money implements Serializable, Comparable<Money>, MoneyExchange, MoneyFormat {
final BigDecimal amount
final Currency currency
// ...
}
@groovy.transform.PackageScope
trait MoneyExchange {
//...
Money exchangeTo(Currency to, Exchange exchange = getCurrentExchange()) {
//...
}
}
@groovy.transform.PackageScope
trait MoneyFormat {
//...
String format(Locale locale = Locale.default) {
//...
}
}
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,
https://sourcemaking.com/refactoring/smells/primitive-obsession
https://sourcemaking.com/refactoring/smells/primitive-obsession
class Ticket implements Serializable {
BigDecimal totalPrice
Currency currency
//...
}
class Ticket implements Serializable {
Money totalPrice
//...
}
https://sourcemaking.com/refactoring/smells/feature-envy
A method accesses the data of another object more than its own data.
https://sourcemaking.com/refactoring/smells/feature-envy
https://sourcemaking.com/refactoring/smells/feature-envy
https://sourcemaking.com/refactoring/smells/feature-envy
BigDecimal totalPrice = currencyService.convertCurrency( purchase.totalPrice,
purchase.currency, gateway.currency, round )
Money totalPrice = purchase.totalPrice.exchangeTo( gateway.currency )
@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
}
//...
}
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
}
}
class Ticket implements Serializable {
Money totalPrice
//...
static mapping = {
totalPrice type: MoneyUserType, params: [currencyColumn: 'divisa']
}
}
trait MoneyExchange {
//...
Money exchangeTo(Currency to, Exchange exchange = getCurrentExchange()) {
//...
}
}
interface Exchange {
BigDecimal getRate(Currency from, Currency to)
}
Text
trait MoneyFormat {
//...
String format(Locale locale = Locale.default) {
//...
}
//...
}
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 }"/>