4 ms·
I have mixed feelings about this principle as stated, but strongly favor a similar rule for function parameters (I agree with the author that always Returning a
by eyelidlessness 5y ago
I have mixed feelings about this principle as stated, but strongly favor a similar rule for function parameters (I agree with the author that always Returning an Object is low value generally).
First, mixed feelings:
- Every parameter as an object key is both overkill and can make function signatures less intuitive. If your function follows a common convention eg process(collection, argumentToProcessCollection), that’s going to be easier to understand.
- Bare, mixed-type, ambiguous primitive values are harder to understand in an argument position than clear well-defined types with named semantics.
- Booleans, opaque enums are particular red flags here.
- These ideas things can be combined.
I prefer:
1. Use a functional style, including function parameter position (and always make impure functions call out their impurity).
2. Only accept bare primitive types in positions where their meaning is unambiguous.
3. If you want to accept ambiguous primitives or even optional values with a clear type, consider whether you’re actually writing more than one function.
4. If you’re actually writing one function, everything else goes in a clearly defined options object.
5. If your options parameter starts growing, GOTO 3.
- jwdunne 5y agoYes, this articulates what I was thinking very well. Binary relations / operators are a good example of 2. Things that could be infix operators but can’t because language. It’d be a nightmare if those functions required an object as an interface. As with everything design, there is no hard and fast rule. Keyword arguments / passing an object make wonderfully clear interfaces in a number of situations and it’s helpful to have them. But applying the pattern everywhere would be a complete disaster.
- wruza 5y ago>3, 5 I disagree. By this logic, any fetch-like interface should be decomposed back to fetch(createRequest(new RequestHeaders({…}), …, …), …, …). Big interfaces are there for a reason, and the reason is that we, as developers, are users as well, and we deserve simplicity in our daily work, because there is no deficit in complexity. Structure is only useful if you can remember it and if you use it entirely, naturally (so that you will remember it). If you can’t or you don’t, it’s useless. Why it’s okay to have ls/find/curl/etc in a shell, but having it in the source code is bad, and we have to write pages of readdirs, Regex-compiles and readyState handlers, increasing a mistake probability? I know what your rationale is, and I agree with it: god objects and functions are hard to debug. But nothing stops you from splitting the complexity into simple, testable chunks under the hood of a god interface, which knows how to combine them by following a simple set of options. Whether you’re writing more than one function or not then becomes an implementation detail, which doesn’t land on shoulders of a user of that interface.
- eyelidlessness 5y ago> I disagree. By this logic, any fetch-like interface should be decomposed back to fetch(createRequest(new RequestHeaders({…}), …, …), …, …). This is a weird example because fetch 1. Is constrained by HTTP semantics and has limited control over how it deals with optional values. 2. Has still somehow conflated at least two functions (idempotent reads like GET/HEAD/OPTIONS vs writes) That’s really all of the point I was trying to make with 3. The optionality of Request.body is a signal that fetch is actually expressing multiple functions. And I chose the word “consider” deliberately. If fetch were being written in a vacuum without consideration for HTTP or historical complexities it was meant to address, I’d probably argue for breaking it up. But I would consider those other factors as well. > Big interfaces are there for a reason, and the reason is that we, as developers, are users as well, and we deserve simplicity in our daily work, because there is no deficit in complexity. Big interfaces may or may not provide simplicity. Big coherent interfaces which express the whole of one thing are totally different from interfaces of any size which conflate different things. > Why it’s okay to have ls/find/curl/etc in a shell, but having it in the source code is bad, and we have to write pages of readdirs, Regex-compiles and readyState handlers, increasing a mistake probability? I’m not arguing for consuming small individual functions with a single responsibility as if they’re the final interface. What those small functions allow is composability, and at least a chance to make invalid state unrepresentable. > I know what your rationale is, and I agree with it: god objects and functions are hard to debug. But nothing stops you from splitting the complexity into simple, testable chunks under the hood But those interfaces are hard to debug for users, and > knows how to combine them by following a simple set of options If you know how to do that, there! You’ve defined another function! Now your user can call that higher level function and ignore your lower level units. > Whether you’re writing more than one function or not then becomes an implementation detail, which doesn’t land on shoulders of a user of that interface. Of course. That doesn’t necessarily mean your end user interface is one function with every argument that could possibly be used to influence the behavior of the underlying implementation. That’s just leaking your implementation!
- wruza 5y agoIt may or may not be leaking, that depends on details which are out of my original scope. E.g. http headers is a well-defined structure. It may appear as {[h]:v} to the user, but be represented by a more complex structure in the implementation, e.g. may contain areSent() method or be implemented by a third-party lib, which doesn’t even declare the way it stores them. All of that behind the options={headers:{[h]:v}, …} interface, which does not even modify the options object. How is that leaking?