3 ms·
This seems close to illustrating a thought I've been toying with turning into a blog post but need some more examples of before I do: "Abstract over data, not
by UglyToad 5y ago
This seems close to illustrating a thought I've been toying with turning into a blog post but need some more examples of before I do:
"Abstract over data, not behaviour"
This seems to fall into the trap of abstracting over behaviour.
The best other example I've thought of for this is the 'generic repository pattern' common in C# and I'd guess Java. Just because CRUD on types is all similar behaviour you can't really build a generic/abstracted way of doing it, because at some point you need different behaviour in some update/insert depending on how the domain concept should act and then you're in a world of pain with a crappy abstraction.
One's code actually 'doing something' isn't a sign of unclean code, it's why one writes it. Stop trying to build abstractions that simplify the doing of what your code does.
- datavirtue 5y agoIn this case the abstraction should be designed so as to invite/welcome custom functionality. The domain/entity repo should extend a generic crud repo--as they usually do in my experience.
- thegeomaster 5y agoAh, thank you. I never managed to put it quite clearly as that. But thinking back and considering the intuition I built around when an abstraction makes sense and when it's just going to be unhelpful cruft, boils down to this. Abstracting over behavior gets messy very quickly. Special cases will probably arise on the next requirement change. Unless they're already there and you missed the subtle interaction. And if you didn't, and actually handled that properly, your abstraction will have a lot of hooks and bells and whistles to support the different behaviors, and it'll just be hell to maintain. Data can change underneath you, sure, but I think we as humans have a much better intuition about concrete things rather than algorithms, so it's easier to abstract data in a useful way. Which is why I think that works better.
- ThaJay 5y agoThe abstraction is the standard case and should invite custom behaviour to be added dynamically. This extra behaviour should be added by the caller / user of the abstraction. Especially in a CRUD situation, every custom procedure will probably have a few basic steps that stay the same. All you usually need is a pre and a post hook.
- thegeomaster 5y agoSee, but that's exactly what I'm saying does not work. It's tempting to think that for e.g. a CRUD situation, pre- and post-hooks are all you need. And it may be that way at the beginning, although hardly so, except for the simplest applications. But very soon, you start to run into things like (not an exhaustive list, but all are things I actually encountered while trying to do precisely what you're saying, many years ago): * Transactions, when multiple things have to happen atomically. Your pre-hook must start a transaction, and your post-hook must commit it. But what if there's an error somewhere? Python and JS don't have RAII, so you need some kind of catch block to abort the transaction. Where does that happen? * Tricky validation, e.g. needing to do queries against the data store to check the request is well-formed. So if you're using an event-based language, your pre-hook also needs to be asynchronous. * Data transformations (what the user sends will hardly be what needs to end up in your data store), so your pre-hook needs to be able to return a new object. * Tracing across service calls, so you need to pass some kind of request ID as well to your hooks. * An "update" request needs to return something (e.g. an ID) to avoid another round trip. But sometimes it needs to return more stuff, so your post hook must be able to return data. Oh, but your ORM or DB usually returns the created object, so you'd like to use that instead of re-fetching in your post-hook, so now your post-hook must accept that as well. These just keep coming up. The first three in particular are usually guaranteed to happen before the first release of the product, since requirements always change. Soon you have an unmaintainable monstrosity. A little copy-paste is tame in comparison.
- ptx 5y agoSome of those could perhaps be solved by an "around" hook, in addition to pre- and post-hooks, like advice[1] in Emacs. Although in OOP maybe that would be represented by a decorator class (or a subclass) that overrides the method and calls the original method? [1] https://www.gnu.org/software/emacs/manual/html_node/elisp/Advice-Combinators.html https://www.gnu.org/software/emacs/manual/html_node/elisp/Ad...
- Jtsummers 5y agohttps://en.wikipedia.org/wiki/Aspect-oriented_programming https://en.wikipedia.org/wiki/Aspect-oriented_programming A further elaboration on that idea. I like it, but have used it in a disciplined way. There's some valid criticism of its ability to obfuscate a program. Particularly with the ability to add and remove advice dynamically, making static analysis (to determine why something happens) practically impossible.
- TehShrike 5y agoAbsolutely – I was sad that the post didn't point out the better (correct?) way to clean up that code: leave the objects alone, but write pure helper functions that implement the most common expressions inside the mathy parts, and have all the similar-looking objects use those helper functions.