15

Note: I'm not looking for opinions on whether the authors of the article below are right or wrong. Mainly I'm looking for the exact definition of what they mean by getters, especially since I know some of the authors post here.

In encapsulated object oriented solutions, one of the primary claim is that exposing object data is synonym of a leaky abstraction and one should strive to expose only behaviors. As seen in the idiom "tell, don't ask" or in the following articles by proponents of this pattern:

Strictly speaking, according to them, getters should be avoided and instead one should strive to tell an object what to do, in clear distinction of a more procedural oo approach where one would ask for the data to act on it.

I find the use of the word "getter" loosely defined here: There is no real difference regarding abstraction whether a property is returned via a getter or via a method:

// NOTE: in dart "_" means private
Account {
  final Email _email;
  // those two are effectively equivalent
  Email get email => _email;
  Email giveEmail() => _email;
}

I'll go ahead and assume the authors above count the two methods above as getters. Instead it's about accessing properties of an object, be it with a getter or a even a workaround through a method. However that definition is still blurry, as some method can compute "state" derived from existing "state". For example given the following scenario:

// NOTE: in dart "?" means optional, "_" means private
// NOTE 2: By definition of the authors those objects are not properly encapsulated.


class Account {
  final Email? _email; 

  bool hasEmail() => _email != null; 
  bool hasBlacklistedEmail(Blacklist blacklist) => blacklist.contains(_email);
  
}

Strictly speaking this returns a property of an object. With hasEmail(), even though it's a method, it returns a something about the object. isEmailBlackListed on the other hand is less clear, but it does not obey the "tell, don't ask idiom". In other words it still asks for a inherent property of the object but this time it is in relation with another parameter.

Which is very close to the example given by @Robert Brautigam in the second article, about encapsulation.

// original example in java
class Amount {
   final BigDecimal _value;
   final Currency _currency;
 
   Amount(BigDecimal _value, Currency _currency);
 
 
   bool lessThan(Amount other) {
      // ...
   }
}

Here bool lessThan(Amount other) is structuraly the same as bool hasBlacklistedEmail(Blacklist blacklist). The common denominator here is that returning state when the method takes an argument seems okay.

So what exactly is a "leaky abstraction" via getters ? Emphasis on exactly. What is exposing data exactly ?

10
  • 8
    Robert Bräutigam (author not only of the first, but also of your 3rd referenced article) is an ideologist proposing a school-of-thought where "Tell-Dont-Ask" is taken to the extreme (I think he will agree on this if you ask him). Be aware there are other school-of-thoughts.
    – Doc Brown
    May 10 at 6:14
  • 3
    To add to Doc Brown's point, I also want to be open here and admit that Robert and me have butted ideological heads over these things before. Without trying to devolve it into right and wrong, I think it's fair to say that his take on things is radically different. It is difficult to marry this up to a lot of other guidelines and conventions you're going to find on the subject. Inbetween article 1 and 3 being Robert's, and article 2 being decidedly undecided and vague, it's difficult to distill a general understanding of these concepts from what is essentially one radical position.
    – Flater
    May 10 at 6:28
  • 4
    It's touching that I left a lasting impression, thanks guys :) I wouldn't call myself an "ideologist" though. I'm not an academic. I've been writing "enterprise" software since the late 90s for a living, and I think this is what works. Do not conflate rigorousness with dogmatism. May 10 at 20:58
  • 1
    There was a time when there wasn't special syntax for getters and setters - there were just methods (and people would write 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. May 11 at 20:45
  • 1
    Think of what they are saying as a challenge of sorts. Trying to write working code with no getters whatsoever is a good design exercise (and a somewhat advanced one, I might add), as it forces you to figure out how to get things done by relying on higher-level methods (of your design), and by passing in lambdas or objects as parameters. Avoiding getters is not necessarily something you'll strictly apply in practice, but without this insight, you don't even have the option to strategically chose when to do it, nor the knowledge what to do instead. May 11 at 22:53

9 Answers 9

33

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.

3
  • 12
    Excelllent answer. You got me on "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."
    – Doc Brown
    May 10 at 6:19
  • 8
    You had my upvote after the first paragraph. Naturally, developers look for rules they can strictly follow without thinking, but, as you highlighted in the quoted paragraph at the end of your answer, good software design requires you to think more about each individual case, not less.
    – Heinzi
    May 10 at 9:20
  • 2
    +1, I had a similar reaction to the first example. If the concern is that the definition of Symbol might change, then Trade should not define Symbol at all - Symbol should be its own class so that it can change independently of Trade (and let Bob add a "getMostRecentTrade()" method to Symbol). Once Symbol is its own class, the stated justification for not exposing out of Trade vanishes.
    – Tim C
    May 10 at 16:48
9

A "getter", for the purposes above of talking about code maintainability and design, is any method that returns an object that already existed when the call began, that the caller didn't already had access to (it's not something it passed in, this, things like that).

This is pretty well-defined this way and much inline with the Law of Demeter, which also tries to tell us to avoid exactly these kinds of methods.

As you rightfully pointed out, when it comes to design though, there's lots of gray area. Necessarily so, because all design should be driven by context.

Your example of lessThan and hasBlacklistedEmail are good example of "derived" values/objects of existing ones. It feels as though these are not much better. You're right, they are often not. My rules of thumb are:

  • Is the method business-relevant. Will developers, users, etc. use the word 'lessThan', when speaking about amounts? If it is, it has a right to exist. If it exists, because I need some information somewhere else. I.e. convenience for the developer, then it probably shouldn't exist. This comes (at least for me) from DDD.
  • Who needs this information? Can I conceivably pull the logic that needs this into this object? For lessThan, probably not. There will be a lot of unrelated places where this will be used. For hasBlacklistedEmail, I don't know. If it's one place where I decide to send or not send an email, that logic might be better placed in the Account then, or placed where the blacklist is, for example the Email object, or wherever.

This is a much more subjective topic as you can see.

So what exactly is a "leaky abstraction" via getters?

I struggle with "exactly". All design is context dependent, so I don't think "exact" exists.

On a pragmatic note, it doesn't really matter. Most getters will be "bad" for maintainability, that is why I usually write something like "avoid at all costs".

There are cases for "getters" though. At places where you just don't know how the data will be used, it is external to you, like in a library for example.

Again, since in most current projects almost all getters are wrong, I take the rhetorical freedom to just say "avoid".

3
  • 2
    +1; I might disagree with the design principals expressed in this answer, but the question was asked specifically about the intent of a term used by this answer's author (and clarified as such in the comments), and this answer explains the term.
    – Tim C
    May 10 at 22:05
  • Although I was looking primarily for your answer, I decided to answer my own question as well as I felt like some things could be added and I've a feeling that the fact that a method has no parameter may be a big hint that it is in fact leaking too much, but I can't put my finger on the why. (note: I realize you are not taking as much of a literal approach, but it's interesting nonetheless).
    – Ced
    May 13 at 13:55
  • 1
    @Ced Sure. I think having a parameter doesn't make a difference though. Remember, the point of all this is, that we want to retain control over our own internal state to make it easy to change and reason about. Things you publish you lose control over. Regarding no parameters, it's perfectly fine to have createReport(): Report, etc. That's why the somewhat awkward definition inspired by LoD. May 13 at 18:50
6

A getter is a failure to design an object. It violates encapsulation which is a core principle of object oriented programing.

Now please tell me, how do you design a libraries hash table collection under that philosophy?

The brutal truth here is, much like you can't have pure functions everywhere, you also can't have pure objects everywhere. Even SmallTalk had to break down and have you construct things with a menu system because it solved problems the idealized language couldn't handle on it's own.

What you can have is an awareness of when you've stepped out of an ideal and be ready to face the consequences. I've never seen a single purely object oriented program in my life. They all leak.

But, what I have seen is procedural code used to create a safe space for object oriented code, which in turn is used to create a safe space for functional code.

Give me that and I do not object to the purist ideals at all. These rules about what is object oriented are not prohibitions. They're definitions.

2
  • 3
    "Now please tell me, how do you design a libraries hash table collection under that philosophy?" - I feel a negative vibe here. I'm not judging, I do that sometimes too. I more than happy to answer, if I feel there's an honest effort to try to understand. May 10 at 21:06
  • 2
    @RobertBräutigam please find my effort here May 11 at 20:40
4
  • A getter is a function that returns a value (usually one that's stored in an object's member variable).
  • A setter is a function that takes a value as a parameter and uses it to modify an object's state. Or at least, it could modify the state: It can refuse to make the change if the new value fails a validation constraint.

Some languages have syntactic sugar (e.g., C# with its “properties”), where you can write obj.getProp() as obj.Prop and obj.setProp(value) as obj.Prop = value, but behind the scenes it's still a pair of functions.

Often, a getter/setter pair corresponds to a private variable, but this isn't required. For example, you could pack multiple bool properties into one int bitfield to save space.


IHMO, there's nothing wrong with getters/setters or properties per se. Sometimes, it's perfectly natural to think in nouns (properties), and sometimes it's natural to think in verbs (methods).

Perhaps what people are actually complaining about is the newbie programmer mistake of assuming that every variable needs a getter and setter, effectively creating public variables with extra steps. You should only expose getters and setters when it makes logical sense to do so.

For example, in banking software, it makes perfect sense to have an Account.getBalance method, since your customers will often want to check the balances of their accounts. But Account.setBalance would be meaningless, because you can't just declare “I want $1000 in my checking account” without more context. Account balances are updated by moving money to or from somewhere (either from another of your accounts, someone else's account, or the teller/ATM's cash drawere), and you have to ensure that every decrease in acct1.balance is balanced by a decrease in acct2.balance. So instead of a setBalance() method, you use transfer(), deposit(), or withdraw() to update account balances.

Also, you should avoid unnecessarily exposing irrelevant implementation details. The user of a DateTime class shouldn't care whether the object internally contains a 64-bit integer count of the number of microseconds since the Unix epoch, separate year/month/day/hour/minute/second fields, or a String in ISO 8601 format. What matters is whether they can use the class to answer questions like “Is today Friday?” or “How old is a person who was born on December 21, 1982?”. If an internal state variable isn't relevant to the information that your class's users would care about, then it shouldn't be exposed with get/set.

2
  • This answer, while valid, is not related to the question. The original claim of the author is that there is another way of going about Account.getBalance(), EG: Account.displayBalance() and that by not sharing balance you are decoupling the call site from the implementation detail.
    – Ced
    May 10 at 18:16
  • Which makes sense, if you have a Account.displayBalance() your whole use case will start from that method, from accessing the data to the UI, instead of having the logic and data spread out. (Which, also mean that you'll have ui related stuff in domain objects, but that's out of scope of the question.)
    – Ced
    May 10 at 18:26
3

I'm late to the party but going to throw this anyway since its ended up a massive discussion.

There is no definition of a "getter" because its not what it does, but how you use it that's the problem.

When people say "getters are bad" what they mean is you are writing in an Anemic Domain Model style where you put your methods on services and read data from your objects. ie.

public class Account
{
   public int Amount {get;set;}
   public string CurrencyCode {get;set;}
}

public class CashMachine
{
   public AddFunds(accountId, amount)
   {
      var a = repo.GetAccount(accountId)
      a.Amount += amount;
      repo.Save(account);
   }
}

And you should be writing in an OOP way. ie.

public class Account
{
   private int Amount
   private string CurrencyCode
 
   public AddFunds(amount)
   {
      this.Amount += amount;
   }
}

With no public Amount getter, you would be forced to use the OOP approach. So "avoid getters" is a good general rule to flag up when you might be straying from OOP.

However.

If you really don't expose any properties at all you start doing other things which are also considered bad. ie.

public class Account
{
   private int Amount
   private string CurrencyCode
 
   public Save() //ooo coupling to datalayer!
   {
      this.database.execute("insert into...");
   }

   public Display() //ooo coupling to presentation layer!
   {
      this.detailView.Update(...
   }
}

What's happening is that how people program in real life is a mishmash of different styles which all have a bunch of simple rules like "avoid getters" meant to help you understand the principle behind that style. They aren't meant to be taken to the nth degree and they aren't supposed to be non-contradictory with other style rules or even each other.

Robert Bräutigam has answered the question directly about his articles, so you can read the truth there, but it seems to me like he's saying "You can take the rule further than you think, push yourself harder to write in the OOP style"

This is really a challenge to the reader rather than a proof of OOPs superiority over other styles. Try putting Display() on Account is it really that bad? look at the advantages, don't assume it won't work.

I'm my view people often don't try things out, they take a bit of each style that they like and dismiss the rest "it would never work!", "the example is a strawman!". You need to take some time and try out what the article is suggesting before going back to everyone's favourite the superior ADM style

14
  • 1
    Though there are ways to mitigate coupling to the other layers, this answer succinctly points out the principals and problems of stricter OOP.
    – user949300
    May 12 at 19:32
  • 1
    which kind? ADM or OOP
    – Ewan
    May 13 at 15:18
  • 3
    This is a really good answer overall. My only note is that a Display() method on Account could/would be a great solution if it took an abstract DisplayTarget (name negotiable) interface and implement the, (IMO underrated) GoF Builder pattern.
    – JimmyJames
    May 13 at 15:35
  • 1
    @DocBrown Presuming the 'great solution' is in reference to my comment, what about it isn't great? I've used it to very good effect to eliminate all the mess of MVC, MVP, ... the revolving door of flawed UI solutions. The biggest problem I've seen with it is that developers don't get it or how effective it can be.
    – JimmyJames
    May 13 at 15:45
  • 1
    DB proving my point there :) but I agree with you Jimmy, it's got nothing to do with getter eradication. It seems to me that people get caught up in tribalism. and use the no true scotsman argument "I am on the side of OOP, but i don't like this! so that thing isn't true OOP" cant we just say "OOP isn't that great"?
    – Ewan
    May 13 at 16:02
2

So what exactly is a "leaky abstraction" via getters

For added emphasis, let's change your Account code to

final String _email;
String get email => _email;

This leaks the information that

  1. Account contains the email (imperfect, but not too shocking)
  2. The email is stored as a String (this is bad)

Users of Account know that email is concrete, a String, and will write code to check the length and split on an @ sign and stuff like that. It will be very difficult to ever change _email to something "better", like an Email. Your abstraction has leaked and it is no longer abstract.

Even if you had used an Email type, there is some leakage, though obviously not as bad as for a String.

I think this falls under a couple of bullets from Joshua Bloch: Bumper-Sticker API Design

Public APIs, like diamonds, are forever. You have one chance to get it right so give it your best.

When in doubt, leave it out. ... You can always add things later, but you can't take them away.

Leaving out getters, to a "reasonable" extent, is good OOP, and good API design. And setters are even worse - in general, avoid them like the plague.

2
  • _email is private there. In dart (in the language used) this means library private
    – Ced
    May 13 at 14:22
  • @Ced Thanks, edited
    – user949300
    May 13 at 15:51
1

Your issue appears to stem from a foundational problem of object-oriented analysis, and the confusion with getters might be a symptom of deeper design considerations. Having encountered similar scenarios throughout my career, but without knowing your exact requirements, I can offer insights that might clarify your approach:

  1. The "User" often represents an external actor rather than a system-managed entity. Typically, it should not be modeled as an object in your system.

  2. If the "Account" is unauthenticated, or does not have an email, then it should be the one redirecting the user.

  3. It's beneficial to employ polymorphism where object behavior varies significantly:

    • UnauthenticatedAccount()
    • InvalidAccount()
    • EncryptedAccount(encoding: BasicAccount())

let account: Account = UnauthenticatedAccount()
account.showProfile() // redirect

Again, this depends on your specific requirements, however.

Anyway, the key is not just about eliminating getters but ensuring that objects manage their own data and behavior. This means designing your objects' interfaces around what they can do for clients, rather than exposing their internal states via getters or indirect methods.

1
  • +1 for the last paragraph.
    – user949300
    May 12 at 17:10
1

Plenty of answers why getters are good or bad. But not what they are, which is the question.

In C, structs have fields that anyone can access. Same for structs and classes in C++, except they are called “instance variables”, and in classes they are usually private, so they cannot be used outside the class.

The simplest getters or setters are public functions that set the instance variables or read them and return their values. So no difference whatsoever to instance variables except slightly different syntax and they are public. And there comes big criticism: If it’s a bad idea to access the instance variable, then using a getter or setter instead doesn’t make it any better.

Slightly more complicated, you may have a getter that returns a reference or a const reference to the instance variable. Which allows you to modify the instance variable even later, after you may have forgotten the reference to the object. Just as bad as another getter or setter.

Then people write more complicated getters and setters. Take a rectangle with width, height and area. Setting the height makes the area invalid. So the setter for height could change the height and area instance variable. Or the getter for area might have no instance variable at all but return width * height. Or a button object might have a setter for “title” which stores the title, but would also redisplay the button on the screen, possibly changing the screen layout. That’s a functionality that you want, and there is no reason to call it anything other than setTitle. Still a setter.

So getters/setters are functions with the right names, and making a standalone change to the object, that is a change that can be performed on its own, and doesn’t require other setters to work correctly.

1
  • It's no wonder that architectural concepts are so subjective when people cannot agree on the basics. Roberts gave an answer, that at least has the merit of being rigorous: it is any method that returns an object that already existed when the call began,
    – Ced
    May 14 at 15:25
0

A getter in this context is any method that returns an object that already existed when the call began. (From Robert's answer)

statement

By definition a method on an object that has a return value will leak state or derived state from either:

  1. The object
  2. The argument
  3. The global scope

Assuming you are not crossing your program boundaries (with example: network calls), we can order data encapsulation as such:

0. direct access to state

Here there is no encapsulation method used.

class A {
  final public B b;
}

This allows direct access with a.b

1. Getters access to state

( ## 1. Technical pure getters access to state)

Here a getter in technical terms (as defined by the language you use) is used.

class A {
  final private B _b;
  B get b => _b;
}

This still allows a.b (or variants from your language).

( ## 1. Pure getters access to state)

This is the same as technical getters but uses a method with no arguments to "hide" the fact that this is a getter

class A {
  final private B _b;
  B giveB() => _b;
}

You access state via a.giveB()

( ## 1. introspectional access to state)

For the sake of completness:

class A {
  final private B _b;
  dynamic giveProperty(String propertyName) => propertyName == 'b' ? _b : null;
}

2. Derived state access

Here we are not directly accessing the state of an object but computing a new object based on the object own property because there is no argument.

If we did not use the object properties, then the method would not be a good fit for the object (it could have been static method for example).

So, because a new value is created we can't say that the object is leaking state directly. However some breadcrumbs of information is still leaking through the computed result, by definition, since the object properties are used to compute a new value.

class A {
  final private B? _b;
  Boolean hasB() => _b != null;
}

This would be in line with Robert explanation, inspired by the law of Demeter, if you consider a Boolean an object (it does not matter if it's a primitive technically in your language).

class A {
  final private B? _b;
  final private Boolean _hasB;
  Boolean get hasB => _hasB;
}

2. Derived state access through parameters

Here the object properties and method parameters are both used to compute a new value:

class A {
  final private B? _b;
  D computeD(C c) => _b.doSomething(c);
}
1
  • 1
    But what is the answer then? Which of these is a getter? And how bad are the leaks?
    – Basilevs
    May 15 at 23:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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