3 ms·
> I disagree. By this logic, any fetch-like interface should be decomposed back to fetch(createRequest(new RequestHeaders({…}), …, …), …, …). This is a weird e
by 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?
- eyelidlessness 5y agoI really think fetch may be a poor example for the purposes of this discussion. Fetch is roughly analogous to HTTP, its semantics are well known and pretty clear… and importantly, its scope is unlikely to significantly grow branchy optional junk. Again, I would not object to fetch if I encountered it in a design document or code review. That implicitly means I would not object to headers as represented in its signature (it’s just Record<string, string> after all… a very common type in plenty of well-defined interfaces). An example that would better illustrate when to “consider whether you’re writing more than one function” would be a generalized data access function that may use the following data sources, directly accepting whatever details needed/appropriate for the specific usage: - HTTP - SSH - Postgres - Redis - Local file system - In-memory POJO or equivalent Sure, you can define such a function. But can you clearly define its usage? And sure, it might defer to smaller units as an implementation detail, but is that single function actually providing any benefit to users? Won’t they (1) find it easier to reason about the different interfaces directly and (2) be able to consume or produce a more usable general function which defers to a single, consistent interface abstraction? I mean, yeah sure now you need to reference both that function and the implementation satisfying its signature. That’s twice as many imports, surely it’s twice as bad right? Or… maybe it’s better not to have to remember (or even try to document) all the different variations of parameters you must/may pass depending on which data source you’re accessing.
- wruza 5y agoWe could use webpack-dev-middleware as an example, cause it has much more options and tool diversity. Or Intl.NumberFormatter, for a lesser example. If these APIs were built in a composable way, it would be even harder to wrap your head around their configuration and just too wordy to both write and read. Sure, you can define such a function. But can you clearly define its usage? Yes! In your example I see many db drivers for a single db/orm-like sort of api (not exactly orm, but very similar). It’s much easier to learn one common interface, and then only learn/discover differences in connection strings and subtle functions that do or do not work for a specific access method. While db/orm is not a single function, it is usually a collection of functions that target multiple very different storages (the same convergence which you find wrong with GET vs POST etc). If I were to use these native APIs, that would be mental overload x 6 for me. Libraries like sequelize and sqlalchemy are very usable because you may use them in both serious postgre projects and your pocket sqlite tools, by changing only the connection string. Of course we discussed functions, not APIs, but if I were to design dropbox-like thing for developers, e.g. store/access your foo config anywhere, it could be: f(data, options) options: { conn_string: “…”, // driver-specific opts… } Not because my users are too dumb or too lazy, but because in the scope of this function all of these accessors minutiae details are irrelevant to them. They just want f(data, {conn:postgreconn, table:”FooConfigs”}) or f(data, {conn:”webdav://…”, method:”put”}), or just f(data, config.foo_options), the entire options imported from devops-aware config. All of them would be described in one github readme.md page and usable in 15 minutes instead of hours of reading on manuals. This all only works because I reduced the scope of that function to a single operation compatible with all access methods, but that’s exactly my counterpoint for 3, 5. It’s almost like cat data > /dir/file, where <dir> is a mountpoint of hdd, cdrom, nfs, webdav, mongodb, aws kms, mailbox (file is mail folder), sshfs, postgrefs (file is table name) and so on. But for developers, not shell users.