Symmetry versus DRY

2023-04-06

We are building a fairly complex JSON serializer and deserializer for a telecommunication application that exchanges messages over the WebSocket protocol.

To keep the format flexible, support API usability, and also backwards compatibility, we decided early on to hand craft the serialization code using Thoth in F#.

Several of the serialized JSON objects contained the same fields and so - strictly adhering to DRY - we started to make parts of a JSON object into reusable functions.

For example, in addition to a encoder and decoder of a type, we also provided readers and writers, that combined the field's name with the encoder or decoder, these looked like.

let decoder : Decoder<Channel> =
    Decode.string |> Decode.map Channel

let write (channel: Channel) : string * Json =
    FieldName, channel |> string |> Encode.string

let read : Decoder<Channel> =
    Decode.object (fun get ->
        decoder |> get.Required.Field FieldName
    )

Based on these functions, we were able to compose these functions in more complex JSON objects:

let toJson (details: InboundSessionDetails) : (string * Json) list = [
    SessionId.write details.Session
    CustomerId.write details.Customer
    "service", Encode.string details.Service
    Channel.write details.Channel
    "identity", CallerIdentity.toJson details.Identity
]

A few days ago while porting the code to Rust and therefore re-reading it, I recognized that this was probably a bad idea. Even though we reduced some redundancy, the symmetry and aesthetics of the composite functions were broken. There were intermixed invocations to read and write functions with other encoders / decoders defined inline. And then I've wondered if we went too strict on the DRY principle and therefore neglected the readability and symmetry aspect of the functions.

Finally, I've decided to remove all read and write functions in Rust, thereby eliminating a large number of functions and a concept that produced more accidental complexity. This way he field names are only specified in the composite encoders and decoders, like:

let toJson (details: InboundSessionDetails) : (string * Json) list = [
    "session", Session.toJson details.Session
    "customer", Customer.toJson details.Customer
    "service", Encode.string details.Service
    "channel", Channel.toJson details.Channel
    "identity", CallerIdentity.toJson details.Identity
]

Does symmetry and aesthetics trump the DRY principle?