llOOPy lOOPs
Thoughts on Large Language Models, verbose code, encapsulation, and OOP history.
Introduction
Large Language Models (LLMs) have trouble encapsulating classes. The idea of bundling data with operations on that data helped software systems achieve levels of complexity that was cumbersome and error-prone with previous practices of procedures and public records. Encapsulation is the pulse of object-oriented programming (OOP).
What is encapsulation in practice?
Encapsulation
If I want to know whether you’re hungry, I’ll ask; I will not disembowel you to get your digestive tract. Your inner workings are a matter of self-governance, not resources to be harvested out of convenience.
Dr. Alan Kay, considered by some to be OOP’s father, studied biology and mathematics. He knew that macroscopic life, billions of years in the making, and arguably nature’s most complex creation, is composed of building blocks: biological cells. In 1966, he imagined software akin to cells: independent entities communicating via messages that combine to create powerful constructs.
He once wrote, “OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.”
Let’s revisit these pillars of autonomy.
- Messaging. Objects behave as self-contained provinces. They do not know what signals they will receive nor when they’ll arrive. When you’re hungry, your gastrointestinal tract signals your brain via nerve fibers. Your brain decides what to do next, like a subscriber listening to an event bus.
- Local retention. Direct state access is an affront to the long-term maintainability of an indepedently stateful system. Your stomach does not command you to eat; it sends a request, and your brain considers its response. Accessor methods (getters and setters) are precursors to loss of independence. They invite the external objects to treat internal states as their own.
- Hiding state-process. Internal logic is a sanctuary. Your stomach doesn’t share how it knows you’re hungry with your brain; your stomach isn’t even aware that anything will receive its messages. And certainly your stomach can’t control your brain. Objects must not meddle with external, sovereign objects.
- Extreme late-binding. In Dr. Kay’s vision, objects don’t directly depend on the existence, operations, or abilities of other objects.
Anti-patterns
Dr. Kay’s deeper insights were misunderstood or miscommunicated, so it’s unsurprising that LLMs generate anemic classes (having little or no logic) by default:
public class Human {
private String mIntestine;
public String getIntestine() {
return mIntestine;
}
public void setIntestine( String intestine ) {
mIntestine = intestine;
}
}
This anti-pattern appears widely in online repositories, tutorials, and educational materials. Allowing such aggressive external state manipulation treats objects as containers to be plundered by declaring an object’s internal resources as public property. Direct data integration may be efficient at first, but it creates fragile dependencies.
The anti-pattern has a few issues:
- Exposes its internal data type (
String). - Offers its state without regards to limits or security.
- Couples knowledge of internal workings to external classes.
- Makes the system less flexible with respect to changing the data type.
How do we fix them?
Hiding state
Knowing that internal affairs are a matter of private jurisdiction, we could replace the String with an interface:
public class Human {
private Tract mIntestine;
public Tract getIntestine() {
return mIntestine;
}
public void setIntestine( Tract intestine ) {
mIntestine = intestine;
}
}
In theory, this resolves the flexibility issue and provides relatively stable, compile-time safety; in practice, issues remain. Humans have a large and a small intestine. Changing the model to represent our physical characteristics highlights the problem:
public class Human {
private Tract mSmallIntestine;
private Tract mLargeIntestine;
public Tract getIntestine() { return ???; }
}
Breaking API changes are now needed because the original design focused on data not responsibilities. The convenience of having a data carrier already in place can lead down a slippery slope of adding more fields, which compromises the single-responsibility principle. Once you allow neighbours to treat the state of sovereign objects as their own, autonomy is surrendered and the object becomes a mere vassal to the rest of the system.
Local retention
By “getting” object data, local retention is violated. Eliminating accessor methods altogether effectively prevents external state manipulation:
public class Human {
private Tract mSmallIntestine;
private Tract mLargeIntestine;
public boolean isHungry() {
return mSmallIntestine.isEmpty() &&
mLargeIntestine.isEmpty();
}
}
Stripping objects of their private scope treats them like a buffer state—a geography used for passage rather than a place of its own. Even with this improved API, instances of Human would still be tightly coupled to other classes through its isHungry method and therefore lack true independence.
Messages
Exposing fields via accessors degrades objects into anemic domain models, where classes become C-like structs or Java Pascal-esque records.
Programming by direct state queries and updates make systems less maintainable over time: Shared state invariably leads to duplicated logic, complex dependency graphs, and tightly coupled classes. Code ossifies. Even in wholly functional, null-hostile, or immutable-first systems bursting with lambdas, complex dependency graphs are harder to understand than systems where data flows as linearly and directly as possible.
LLMs, constrained by memory, have limited window sizes and token counts. Even with unlimited tokens or huge context windows, verbosity has costs. Code duplication increases the maintenance burden and, for both humans and machines, reduces how much logic can be digested before understanding becomes too arduous, too tenuous, or too expensive.
Dr. Kay’s foresight into message-oriented programming cleanly resolves all the problems with our trite example of a human having exposed intestines.
How do we get there?
Events
Event-based systems align well with message-oriented programming. Consider a class that rejects new orders while a purchase is pending:
public class Order {
private EventBus mBus;
private List<LineItem> mItems = new ArrayList<>();
private ReentrantLock mState = new ReentrantLock();
public Order( EventBus eventBus ) {
mBus = eventBus;
mBus.subscribe( PurchaseEvent, this::handlePurchase );
mBus.subscribe( CompleteEvent, this::handleComplete );
}
private void handlePurchase( PurchaseEvent event ) {
if( mState.tryLock() ) {
event.populate( mItems );
mBus.publish( new OrderAcceptedEvent() );
} else {
mBus.publish( new OrderRejectedEvent() );
}
}
private void handleComplete( CompleteEvent event ) {
mState.unlock();
}
}
We could also avoid the conditional, presuming the event bus takes care of concurrency issues:
public class Order {
private EventBus mBus;
private List<LineItem> mItems = new ArrayList<>();
private Consumer<PurchaseEvent> mState = this::accept;
public Order( EventBus eventBus ) {
mBus = eventBus;
mBus.subscribe( PurchaseEvent, this::handlePurchase );
mBus.subscribe( CompleteEvent, this::handleComplete );
}
private void handlePurchase( PurchaseEvent event ) {
mState.accept( event );
}
private void handleComplete( CompleteEvent event ) {
mState = this::accept;
}
private void accept( PurchaseEvent event ) {
mState = this::reject;
event.populate( mItems );
mBus.publish( new OrderAcceptedEvent() );
}
private void reject( PurchaseEvent event ) {
mBus.publish( new OrderRejectedEvent() );
}
}
How a class uses its resources concerns no other entities, short of introducing corruption. These examples have different implementations, yet the outside world receives the same messages having the same context, irrespective of algorithm.
Logging
In this architecture, every state transition pertains to an event. The narrative can be captured by an event logger that subscribes to all events on the bus. This allows recording the system behaviour without mingling log statements inside of business logic. Instead of multiple log statements woven throughout, classes define a text-based representation of their state:
public class Order {
private static final Consumer<PurchaseEvent> ACCEPT =
Order::accept;
private List<LineItem> mItems = new ArrayList<>();
private Consumer<PurchaseEvent> mState = ACCEPT;
@Override
public String toString() {
return new LogFormatter( this )
.add( "state", toString( mState ) )
.add( "count", mItems.size() )
.format();
}
private String toString( Consumer<PurchaseEvent> state ) {
return state == ACCEPT ? "'accept'" : "'reject'";
}
}
Developers must take care not to parse the text because that would violate encapsulation and impede changing the text format in the future. Logging can be effectively free, architecturally.
Summary
Software developers encounter the truth daily. They see, hear, or otherwise experience right or wrong answers to problems with every line of code written and every algorithm realised. Either the software performs correctly, or it does not. Truth and logic are inescapable.
- Encapsulation. OOP was never about moving variables behind getters; it was about denying external objects the ability to take over internal state.
- Messaging. Objects communicating via events maintain their borders. This prevents tight coupling, making the codebase easier to refactor, scale, and digest without the cognitive overhead of a sprawling, borderless empire.
Upholding encapsulation, using messages instead of direct access, and providing open communication between entities ensures that the system remains a maintainable federation of peers in the long term, rather than a tangled mess of annexable objects straining to breakpoints inside a labyrinthine cacophony of inscrutable rules.
Contact
About the Author
My career has spanned tele- and radio communications, enterprise-level e-commerce solutions, finance, transportation, modernization projects in both health and education, and much more.
