0

I am working on a Typescript library (to be used by many users within my org, possibly open-sourced) which provides access to some remote storage. I am contemplating between several variants for the main methods. Let's take the "query" method as an example. Here are several alternatives for its signature:

// alternative (a) - pass a single object (named arguments)
const items[] = await client.query({
    expression: "a1 = :v1",
    filter: "a2 > :v2"
    values: {v1: 'foo', v2: 'bar'}, 
    timeoutMs: 100,
    numItems: 3, 
    consistency: "STRONG"})

// alternative (b) - pass multiple values (positional arguments)
const items[] = await client.query("a1 = :v1", "a2 > :v2", 
    {v1: 'foo', v2: 'bar'}, 100, 3, "STRONG")

// alternative (c), split to to two methods,
//   pass positional arguments
const items[] = client.query("a1 = :v1", "a2 > :v2",
    {v1: 'foo', v2: 'bar'})
    .fetch(100, 3, "STRONG")

// alternative (d) - split to two methods
//   pass positional arguments to the former, named to the latter 
const items[] = client.query("a1 = :v1", "a2 > :v2", 
    {v1: 'foo', v2: 'bar'})
    .fetch({timeoutMs: 100, numItems: 3, consistency: "STRONG"})

// alternative (e) - split to two methods, 
//   pass positional arguments to both 
const items[] = client.query({
    expression: "a1 = :v1",
    filter: "a2 > :v2"
    values: {v1: 'foo', v2: 'bar'})
    .fetch({timeoutMs: 100, numItems: 3, consistency: "STRONG"})

(there probably other alternatives one can think of...)

Contemplation #1

In order to perform a query should the call site call a single method (query() - as shown in (a), (b)) or two methods (query() followed by fetch() - as shown in (c), (d), (e)).

calling a single method is conceptually simpler. OTOH, splitting it to two makes it easy for users of the library to perform "chunk processing": read 10 items, inspect them, read 20 more, inspect them, read some more, etc.

Contemplation #2

named vs. positional? (in either the query() or fetch() methods)

named arguments are more explicit and make it easier to pass any subset of the optional arguments. On the other hand, the call sites are more verbose which is often a major deterring factor.

1

A and B suffer from an issue you already pointed out, which is that you can't reuse the query to do incremental processing. It also splits up the implementation into meaningful parts: generating the query via query() and getting the results via fetch().

When deciding between positional vs. named arguments there are a few considerations:

  1. How many arguments does a function have? Once functions start to have more than 3-4 arguments it becomes a bit unwieldy to keep track of each one. The added verbosity of named arguments helps.
  2. Are any of the arguments optional? Optional positional arguments are a pain to work with when there is more than one optional arg, especially when the default for the argument is something other than null/false.
  3. Will you ever add or remove arguments? If this seems like a hard question to answer, you may want to consider named arguments.

Using a fetch() method eliminates any concerns for #1.

For #2, it seems like there are some reasonable defaults you could apply to the fetch() method. Named arguments here make a lot of sense.

So, assuming we use named args for fetch(), that just leaves query(). Whether you are adding or removing arguments here is something you need to decide. You've reached a fair compromise by mixing both named and positional args for query() which is the right choice.

With all that said, alternative D seems like the best option. For the other alternatives, I find myself asking questions like, "Why am I writing so much?", "What does this argument do?", "Why isn't the timeout set on the client?", etc.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.