A new problem for the shared model occurs.
1. Typical approach is about to keep them following anemic model approach. Like a property holders. Usually as shared library.
2. That means very soon classes are widely used and modify them can requires huge effort. Pure design hard to improve after it is used in many places.
3. For some microservices some query methods (or even model mutations) are needed and somewhere not.
Consider this case.
We have the VO designed as:
1 2 3 4 5 6 7 | @Data @NoArgsConstructor @AllArgsConstructor public class PriceVO { private String currencyCode; private Long amount; } |
which is used across the components.
As we can see it looks pretty ugly and anemic, no immutability and without operations.
1. No immutability - any code which receives can modify it at any step of invocation
2. This class structure has no sense, this presents Money object, like a 100$ note f.e. Does it make sense if amount is null or currencyCode is also null? It is like 100 of unknown currency or unknown amount of dollars.
3. Maybe null amount is considered as zero? Or currency code "USD" or "usd" should be considered as same and correct values? What if null currency code must be considered as default currency?
4. Very probably it would be nice to add or deduct money. Changing currency has no sense at the scope of this object but different other ops would be nice to have.
5. What's about the currency? Is it a dictionary?
How we can try to fix or eliminate these problems?
First we can change the VO itself, to add immutability or make model (amount or currency) more strict. But is it easy effort? To change and then update artifact deps in many places? And then test it.
A better solution is to use Wrapper pattern. Or any approach which looks like Wrapper. Try to improve it: model, add ops. Better API.
Okay, second variant looks better but how and where we can put this logic? Like a new helper or a service? From my point of view we can develop POJO with ops and following naming convention. Good name could be PriceVOOps.
First let's take care about the code, make it dictionary. If absence is not defined and must be considered as default value the solution could be put this check in the custom restore method (restore Enum constant by the String raw value).
1 2 3 4 | // #3 public enum CurrencyCode { USD, EUR } |
And then develop operations and class, fixing all the mentioned concerns
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | package wkda.api.retail.util; import CurrencyCode; import PriceVO; import java.util.Optional; import java.util.function.Supplier; import static java.util.Objects.requireNonNull; public class PriceVOOps { private long price; private CurrencyCode currencyCode; private PriceOps() { } // #1 immutability // #2 null safety, strict model, enum constant private PriceVOOps(long price, CurrencyCode currencyCode) { this.price = price; this.currencyCode = currencyCode; } // finally get improved value public PriceVO get() { return new PriceVO(currencyCode.name(), price); } // // factory method // public static PriceVOOps zero(String currencyCode) { // implicit not null check return new PriceVOOps(0, CurrencyCode.valueOf(currencyCode.toUpperCase())); } // #5 public static PriceVOOps from(PriceDTO priceDTO) { return new PriceVOOps( amountOrZero(priceDTO.getAmountMinorUnits()), CurrencyCode.valueOf(priceDTO.getCurrencyCode().toUpperCase()) ); } public static PriceVOOps from(Supplier<String> currencyCodeSupplier, Supplier<Long> priceSupplier) { return new PriceVOOps( amountOrZero(priceSupplier.get()), CurrencyCode.valueOf(currencyCodeSupplier.get().toUpperCase()) ); } // #4 // Ops, add more if needed // public PriceVOOps deduct(PriceVOOps priceVOOps) { if(priceOps.currencyCode != currencyCode) { throw new IllegalArgumentException("Operations must be with the same currency: " + priceOps); } return new PriceVOOps(price - priceOps.price, priceOps.currencyCode); } // internal helpers private static Long amountOrZero(Long amountMinorUnits) { return Optional.ofNullable(amountMinorUnits).orElse(0L); } } |