> safe refactoring ends up breaking format compatibility
I have a moderately strict rule for my teams that API objects must exist separately from normal domain objects (ie, persistence). And any APIs that have separate lifecycles (say, private webclient API vs official published API) get separate DTOs (which is what they really are).
This works fine? It's not much work, not even in Java (thanks to lombok). There's clear migration strategies for these api objects and you can refactor your domain objects without risk of breaking something.
I guess this is the "by hand" mapping that the article concludes with, but honestly it seems like a lot of words just to say "keep your API objects separate from your domain objects".
> JSON cannot directly represent cycles
It's incredibly easy to tweak JSON to allow it, and you don't even need a special parser. I wrote this five years ago: https://github.com/jsog/jsog
> I guess this is the "by hand" mapping that the article concludes with
Not "the", "a". One of the comments under the OP itself links to another blog post [0] describing an alternative to explicit DTOs, which I personally prefer.
The point isn't just to pick some other solution and be done with it; the point is to understand the problem in the first place.
Sure, but that was just an example of a general class of coupled-representation problems. As discussed elsewhere in this thread, the choice of JSON isn't even essential to the problem being discussed.
Also, even though (or precisely because) JSOG is 100% JSON, the fact remains that after your JSON parser finishes reading your JSOG document, you still have to hook up all the cycles. Either you do this by hand (per the article), or you wrap it up into parser-level knowledge (which breaks somewhat from the "100% JSON" intent).
static JObject CreateMessageRepresentation(Customer customer)
{
return new JObject(
new JProperty("customer",
new JObject(
new JProperty("name", customer.Name),
new JProperty("address",
new JObject(
new JProperty("street", customer.Address.Street),
new JProperty("zipCode", customer.Address.Zip),
new JProperty("town", customer.Address.City)
)
)
)
)
)
}
Yuck.
Just make CustomerData and AddressData classes, even if you only use them for that one API response. And even if you have ten other versions of CustomerData and AddressData for ten other methods. You get type safety and your tests refactor nicely.
@Value
static class AddressData {
String street;
String zipCode;
String town;
}
@Value
static class CustomerData {
String name;
AddressData address;
}
@Value
static class Message {
CustomerData customer;
}
public Message createMessage(Customer customer) {
final Address addy = customer.getAddress();
return new Message(new CustomerData(customer.getName(), new AddressData(addy.getStreet(), addy.getZip(), addy.getCity())));
}
You could format this nicer, and adding some constructors would help, but at least the typechecker is doing work for you.
Well you have chosen an example that is little more than an API object in the first place.
class IdealGas implements EquationOfState {
private final double gamma;
public IdealGas(double gamma) {
this.gamma = gamma;
}
public double energyDensity(double pressure, double internalEnergy) {
return (1 + gamma) * pressure * internalEnergy;
}
}
Why create a separate type over this class which is just a projection of its data? You can just use a JSONObject as the API object. You are already going to need some special tricks to deal with the union with other EquationOfStates on top of some out of band type field to designate which class is to be used.
You will have the same sort of boiler plate in either case. Either a `public EquationOfStateData getEOSData()` or a `public JSONObject getJSON()`. In one case you use type safety and the deserializer provides your validation messages but should still do some custom validation on top to handle mismatched unions, in the other case you perform the type check (ordinarily done by using method like `json.getDouble()` and get to give custom messages.
Choose your poison, they really aren't all that different.
This is a parser, not a serializer, but hopefully it's clear how this approach can be applied in the other direction. (I still have the analogous code on a branch somewhere, but the parsing logic needed a cleanup more, and sooner.)
Some of the uncurried function stuff can be cleaned up with dedicated wrappers for `BiFunction` and whatnot. And of course, the address parser could be extracted out if we want to unit test it separately.
I don't find the code I give above to be any worse than the code you gave (for that matter, I don't have the same "yuck" reaction to the C# example, either). I much prefer not needing extra data types that only serve to configure analogous translation code.
We can keep going back and forth, addressing the concerns we have about each other's approach, but it ultimately comes down to preference.
I have a moderately strict rule for my teams that API objects must exist separately from normal domain objects (ie, persistence). And any APIs that have separate lifecycles (say, private webclient API vs official published API) get separate DTOs (which is what they really are).
This works fine? It's not much work, not even in Java (thanks to lombok). There's clear migration strategies for these api objects and you can refactor your domain objects without risk of breaking something.
I guess this is the "by hand" mapping that the article concludes with, but honestly it seems like a lot of words just to say "keep your API objects separate from your domain objects".
> JSON cannot directly represent cycles
It's incredibly easy to tweak JSON to allow it, and you don't even need a special parser. I wrote this five years ago: https://github.com/jsog/jsog