Config Structs Are Awesome

Part of https://discourse.jujucharms.com/t/read-before-contributing/47, an opinionated guide by William Reade

Multiple parameters are pretty nice at times, but the weight of evidence leans in favour of making most of your funcs accept either 0 args or 1. The sensible limit is probably, uh, 3 maybe? …but even then, it’s actually depressingly rare to nail a 3-param signature such that you never need to update it; and callables tend to accumulate parameters, anyway.

So, when you’re exporting any callable that you expect to churn a bit in its lifetime (i.e., pretty much always) just take the few extra seconds to represent the params as a type. There’s a howto wiki page somewhere.

If you defer it until the first instance of actual churn, that’s fine, it’s a judgment call; but if you find yourself rewriting an exported signature even once, just take the time to replace it with a struct. (And then future changes will be much easier.)

Also, take a couple of minutes to see if you’re rediscovering a
widely-used type; and make an effort to give it a name that’s a proper noun: FrobnosticateParams or -Args is generally a pretty terrible choice, it’s super-context-specific and betrays very little intent. For example:

type CreateMachineArgs struct {
    Cloud       string
    Placement   string
    Constraints constraints.Value
}

func CreateMachine(args CreateMachineArgs) (Machine, error)

…is, ehh, comprehensible enough, I suppose. But that’s an awful name! You can immediately make it a bit better by just calling the type what it really is:

type MachineSpec struct {
    Cloud       string
    Placement   string
    Constraints constraints.Value
}

func CreateMachine(spec MachineSpec) (Machine, error)

…and then, as a bonus, you get a type that doesn’t hurt your eyes when it ends up being a generally useful concept and passed around elsewhere.

I have found the following nouns generally better than either Args or Params, in various contexts:

  • Spec (implies creating distant resources, e.g. in the db, perhaps?)
  • Config (implies sufficient dependencies/info to perform a task?)
  • Context (implies you’re a callback?)
  • Request (implies, well, a direct request)
  • Selector (implies request for several things, possibly to set up a
    Request?)

…but please don’t treat this as a prescription: find the right names and use them. e.g. a LeaseClaim, or whatever; and often, indeed, just a string is quite good enough.

func (repo *Repo) Get(name string) (Value, bool)

…is just fine as it is, because it probably really won’t ever change significantly.

One caveat: do not casually modify types that are used as config structs, and pay particular heed to the names-are-for-clients advice above. You may well have several different types that vary in only one or two fields: that certainly shouldn’t result in consolidation to one type alone, but it may help you to discover a single type that is widely used on its own, and occasionally alongside one or two other parameters.

Resist the temptation to consolidate so far that a type’s validity depends upon its context.