You can't infer OOP from a line of code
This question is a bit of a semantic puzzle in what it's asking.
But that not every line of a program can be purely object oriented.
Yeah, because object orientation cannot be inferred from a line of code. That's like looking at a brick and asking if it's part of a two-story house. The brick doesn't know. The brick can't prove nor disprove whether it's part of a two-story house.
Object orientation isn't an architecture either, in the sense that e.g. Clean Architecture is (this is not CA-specific, I'm just using it as an example architecture). For architectures, there's an ideal codebase and most of the time your supposedly "Clean Architecture" codebase is a mix of said architecture and some real world compromises you've made along the way. When we argue whether something is or isn't a particular architecture, what we're really disagreeing on what percentage of the codebase adheres to the architecture. Put differently, different people have different opinions on what percentage of impurity (i.e. compromise) they find acceptable.
Object orientation is a modeling philosophy. I want to start from an oversimplified example. You could nitpick this to death, but its intention is only to act as a very simple baseline, not a meticulously precise one.
The measure of OOP-ness is not expressed as a percentage of what parts are(n't) OOP, it's a measure of overall design quality and depth of implementation.
Consider the following brief:
We need an application in which we can save vendors, validate our contracts with them, and purchase items according to these contracts.
An object-oriented programmer is going to see Vendor
, Contract
, Item
. While the focus of this answer isn't on defining functional programming, the contrasting takeaway that a functional programmer would end up with is saveVendor
, validateContract
, purchaseItem
(I believe it would be more correct to omit the noun from these names to be true FP, but I'm more in the OOP camp so I might be biased towards including them).
This isn't a matter of wrong or right, or even of labeling what % of your codebase is or isn't object-oriented.
At the end of the day, that object-oriented programmer is still going to have to write the save
, validate
and purchase
logic, but they will categorize this as part of the objects that they designed. Similarly, a functional programmer is still going to have to create some kind of data container that contains e.g. a vendor's information, but they're going to design it based on how it slots into the functionality they've build.
"Oriented" means "what you used as the first order of structure". It doesn't mean "only does this".
Tangent on statics
Much like you can't write a program free of static methods (you at least need main)
This is a nitpick, but an important one to connect to the rest of this answer. You don't need main. But as long as you don't tell the program what method to run on startup, then you do need a default method that will be run. And statics overlap with defaults in the Highlander sense: there can be only one.
The specific nature of how statics operate as opposed to instances is irrelevant as to how you decide what needs to be executed on startup. There's enough similarity that one can represent the other, but they're not inherently tied together. The compiler could just as easily have defaulted to doing (new Program()).Main()
as opposed to Program.Main
, without really making any different as to the default nature of the startup method being called.
OOP and sharing state
This enables encapsulation. State is not something to share, simply something that changes behavior.
That is not a core OOP tenet. It's good advice, but it's not what drove the concept of OOP to exist in the first place. Experienced OOP devs and FP devs alike will agree with this.
The key distinction being made here that's pro-encapsulation is that processes are significantly more ephemeral than static definitions are. You can more easily adjust the logical implementation of a process (while maintaining its overall input-output structure) than it is to change a static definition while still keeping it compatible with its previous structure.
To put it differently, the fact that a function/method inherently needs to map an input to a process and then (optional) to an output inherently enforces loose coupling between the caller and the implementation. It's possible to achieve similar loose coupling with static definitions (e.g an interface around a class), but it's easier to forget to do this and something it requires more effort.
For OOP specifically, there's an additional consideration here whereby you want thematically linked processed grouped together, e.g. you're going to find the way to contact a vendor and the way to update a vendor's details both in the Vendor
class.
When you expose state externally, you are giving outsiders the ability to write their own logic based on your state fields. But, if this logic hinges on your state fields, the odds are fairly high that this logic should be grouped with you instead of existing in some other consumer of your class.
This leads to a general disapproval of sharing state as a means of preventing others from developing processes that you should be keeping ownership over. By prohibiting state from being shared, you inhibit that from happening.
Should we ban access to state?
Now, I also wrote this in my answer on the question that you answered (which sparked you to post this question), but I very much agree with the spirit (not developing logic external to the class that owns the related fields) while also very much disagreeing with the notion that blanket-banning something is the correct way to enforce that.
So my feedback is going to be more that exposing some state is reasonable, but when consuming said state it should be evaluated whether the consumer is the right place to add this new logic that you're creating.
I preach pragmatism, not blind dogma, which means that I'm generally against any kind of blanket rule unless you can exhaustively prove that there is no counter against it.
However, some boundaries, like library boundaries, make it impossible to move the method and so you move data. [..] And so OOP ideals are compromised in the face of reality.
Just to reiterate, the thing that's suspended in this example scenario of a library isn't an OOP ideal, but it's still something we'd like to be able to do regardless.
A library boundary means meaningfully losing access to the source code in a way that you can adapt it to your extended needs. That's sort of what distinguishes a library from "other files in my codebase". This is also a bit of a semantical puzzle.
Let's explore a world where we could change this - open source libraries have that ability. While it would be nice to be able to extend it, this becomes an issue of ownership. If the library now has a bug, the original authors are not particularly on the hook for it, since they don't understand nor have active support for your extensions of their logic. Similarly, you might not be able to troubleshoot the issue either.
For the example of open source code, you are free to make your own fork of the code, but the library's author has no responsibility over your fork. At best, they might be charitable enough to help you out.
This ropes in another consideration for good development practices: clear ownership. This can only really be ensured if you enforce the separation of logic from different authors.
Note that by "author" I don't mean a specific person, I mean an entity who bears responsibility of any and all people that came before them or worked alongside them and authored code. If Bob leaves MyCompany and Tom now works for them and he inherits Bob's code, then MyCompany is the singular author of this code.
There's other ways to integrate libraries into your codebase that don't require you to break that boundary of ownership. Wrapping is the straightforward solution here. Extension methods can help your logic feel more integrated with the library but they're really just side-loaded helper logic that can only access the public data.
Usually with explicit consent of the author (through their design), it's possible to provide inheritable classes so that you can access protected state and integrate your logic more than just by wrapping it. This works well but it usually requires some design considerations by the library authors so it's up to them whether they really want to take on that additional effort (and if it's work the added users of their library).
Extra credit and examples of why dogma is bad.
For bonus points, please do show me how a OOP purist would design a hash table without getters.
This is more of another semantic puzzle. "Getter" means different things to different people. In a language that has actual property getters (such as C#'s public string MyProp { get; }
), does "getter" refer to specifically that, or also any method like public string GetMyProp() { ... }
?
If the former, then the easy (but in my opinion cop out) answer is that you can replace every property with a private field with accompanying methods to access it, and then you've technically created something without a getter. For obvious reasons, that's not really an answer to your question though.
Additionally, this question paints a scenario that completely ignores the purpose of the advice that sparked the previous question (which in turn sparked this one).
It's impossible for me to conclusively tell you how to design [..] without already being aware of how an external consumer would make use of whatever I expose, if and how they build logic around it, and if that logic would've been more appropriate to add into my class instead of the consumer's.
There is no pure OOP way of solving this problem without knowing the consumer and what they intend to do with your object. Simply put, if you follow to "expose no state, only behavior" guideline (shortened for brevity), then you're going to run into a lot of issues when you realize that OOP starts from an object's definition and then builds its behavior inside of that definition. This inherently means that the author of that object definition must invariably be the author of the behavior included in that type definition.
If it's outside of the type definition, then it violates your "add behavior to the type" goal of exposing behavior.
And if it's a different author, then it would be bad to have one type be designed by two different authors without them at the very least being aware of each other. As we established, you don't know your consumer yet in this scenario, so active knowledge of your consumer is not possible here.
This is a really good example of why I detest blind dogma and consider it one of the bigger issues that plagues software development. The question you asked here is a blind application of the letter of the guideline, and you did not notice that your question actually does not touch on the spirit of the guideline.
getter
here is not used to mean a getter technically but any kind of method (getters included) that returns the internal state of an object.