So what exactly is a "leaky abstraction" via getters ? Emphasis on exactly. What is exposing data exactly ?
If you're looking for exact deterministic discussion of big picture software development ideologies, your question fails at the first pass and you're going to have to re-evaluate your expectations.
Guidelines are just that. They are not objectively or empirically measurable or able to be conclusively judged on some kind of absolute scale. Similarly, approaches aren't just right or wrong. A lot of consideration goes into risk mitigation for potential future development effort. Every approach under the sun will hinge on subjective considerations such as (a) your ability to predict future maintenance work (if any) and (b) the consequences of your prediction being wrong.
So what is a "leaky abstraction" via getters?
First, you need to understand what a getter refers to here. As you've pointed out, this can take several different shapes, such as direct read access to a field (which therefore isn't private), a method that effectively gives you the same, or (for some languages) a property that sort of acts like a method getting with a more field-like syntax.
The distinction between these three (or more) interpretations is irrelevant for the advice at hand. The advice, at its very core, is referring to accessing state as opposed to triggering behavior. The details of how you access that state is irrelevant.
This is a secondary argument, following from a previous shift in design philosophy where write access on fields was being revoked specifically because consumers would break things in the object. At the time, not as much attention was devoted to read access, under the supposition that at least this doesn't break the object's internal logic. And that's correct, but there's a different problem with providing excessive read access to state fields.
The overall idea of arguing against "getters" is that generally having access to these fields (or being allowed to open up access to them if you want it) leads consumers to design their own logic based on these fields, as opposed to considering if that object needs to be extended to include this logic so that it can provide a reusable implementation of said logic that is cohesively closer to what it depends on, so that it becomes easier to account for it if future development effort leads you to need to alter those dependencies in some way.
I do generally agree with the spirit of that advice. However, I find myself strongly disliking all three articles on the subject, because in my opinion they fall prey to oversimplification, strawman arguments and conflating different issues and their different causes and solutions.
I don't want this to be an article review piece, but to provide succinct responses to the articles' claims:
- 1 The example is very carefully curated to fit the topic of the post, but its solution opens other doors that it very much ignores.
- Big picture: dealing with one Bob is fine, but dealing with multiple Bobs with very different goals, aspirations and ideas on what Alice should do is going to be a very different experience for Alice. When you have multiple consumers (or are not in direct contact with them), it becomes impossible to continually re-tailor your implementation to whatever your consumer is asking for.
- Just to be clear, I'm not saying no solution was needed in this example scenario. It's good to create an abstraction and make it more change-friendly. But the offered solution creates as many problems as it solves and its underlying assertions are idealistic at best.
- Small picture:
Trade
should not hardcode the specific implementation of Cache
being used, it disables any kind of inversion of control. Static access is similarly horrible for any kind of testing effort, scoping, concurrency or extensibility. And that's not even to discuss the inherent horror of needing to now nest classes and interfaces in one another based on two things being tangentially related. How many classes have you seen that nest their own collection logic inside themselves?
- 2 concluding that "You shouldn't use [..] unless absolutely necessary" is pointless advice on any topic, because it completely sidesteps the core focus (and obvious subsequent question) of how to define when [..] is necessary. Shifting into "well-defined responsibilities", which again defers any concrete information, also conveniently ignores that offering access to state can at times be a useful responsibility.
- 3 I could nitpick this to death but I'll keep it short: the example being discussed is a foisted one, where they're trying to force a decidedly vertical slice into an (equally stubbornly) decidedly horizontally sliced codebase. Yeah, a square peg doesn't fit snugly in a round hole. That's not proof that the peg is the wrong shape, nor that the hole is the wrong shape. The only thing you're proving is that you don't know what you're doing and you end up coming across as a bad workman blaming their tools.
"An example app that requires to redirect an user if he is unauthenticated, does not have an account, does not have an email or does have a blacklisted email"
We should indeed consider extending an object rather than writing some new logic somewhere else (which we can only get away with if we have public access to that object's state in the first place). This is an act of good practice. However, I disagree that the desire to get developers to engage in good practice should manifest itself as telling them to avoid getters.
That's an act of prohibition (or projected dogma, if not outright prohibition), which is a very slapdash way of teaching good practice not by explaining what it is but instructing others on what to do. I don't like it and I never will.
It would be decidedly negative for me to only disagree with others, so here's my take on the information that's presented, and which parts I think you should focus on because they're actually correct. I would phrase it as such:
Avoid blindly relying on the availability of publically readable state, and instead assess whether it would be more appropriate to extend the object's behavior rather than inventing an external way to parse an object's state.
The overall goal here should be to keep dependencies on another component's implementation details as low as is reasonably possible, in order to minimize the risk and size of breakage and regression.
It's not as snappy, but it's significantly more reasonable, in my personal opinion.
getSomething()
,setSomething(val)
). The terms "getter" and "setter" originate from those days. Even today, in languages that do have special syntax, the compiler usually turns those into methods under the hood. It makes no fundamental difference, design wise. The problem is not accessing properties, the problem is that you're making callers rely on the exact structure of your internal fields (assuming you want some flexibility there). That breaks encapsulation.