Tough Situations In OOP
“Object-oriented programming (OOP) is a programming language model organized around objects rather than “actions” and data rather than logic. Historically, a program has been viewed as a logical procedure that takes input data, processes it, and produces output data.” ~ techtarget.com
OOP clearly has detractors, some of whom are definitely worth listening to. And even among those who like the paradigm, there are spots where they will admit that OOP makes life difficult. It’s not that the code is particularly difficult (although it can be), but that system models get more complex over time as your understanding of things improves. As a result, it’s very easy for an early, naive implementation to paint you into a corner later on.
“Object oriented programs are offered as alternatives to correct ones” and “Object-oriented programming is an exceptionally bad idea which could only have originated in California” – Edsger W. Dijkstra
OOP is a commonly used paradigm, but there are pitfalls out there for the unprepared. Not only can such issues point towards problems that you should avoid even if you are going to build things using OOP, but they can also help you avoid using OOP in situations where it isn’t appropriate. Like most other paradigms, some issues are intrinsic to the practice, while others are problems with common means of implementing it. There is a lot to be learned from critics.
“C++ is a horrible language. … C++ leads to really, really bad design choices. … In other words, the only way to do good, efficient, and system-level and portable C++ ends up to limit yourself to all the things that are basically available in C. And limiting your project to C means that people don’t screw that up, and also means that you get a lot of programmers that do actually understand low-level issues and don’t screw things up with any idiotic “object model” crap.” – Linus Torvalds
10:10 Misuse of Inheritance (The Mammal Problem)
Inheritance heirarchies are fuzzy in the real world, yet OOP tends to require them to be more strictly defined. For instance, a veternary clinic might use a “Mammal” abstraction in the context of the practice. A confused developer would also apply it to the receptionist. Everyone listening to this podcast is the product of two parents, an educational system, and a set of life experiences. It would be hard to conceive of a base class that could apply to just those. Lots of people have kids of their own, different careers and aspirations that add additional data, etc. The real world is messy, especially across contextual boundaries. The meaning of words has to be agreed upon. A goat-farming schoolteacher “has kids” in three different contexts, which have varying degrees of overlap. An abstraction trying to cover all three is going to be a failure.
Deep hierarchies make code brittle. Let’s think of a hierarchy of living things and assume “mammal” is a base class, on top of which a series of other classes is layered. What happens when the first developer didn’t know about marsupials (keep their young in pouches) and monotremes (lay eggs) and you have to backfill that behavior? How do you handle cases that don’t fit neatly? For this hierarchy, what would you do when your software suddenly has to support things like Synapsids and Therapsids (extinct mammal-like reptiles / proto mammals)?
Multiple Inheritance is also tricky to deal with. What do you do when the same entity is a customer AND a vendor? The most obvious option is to simply make them separate entities, but that gets tricky when you need the features of both. The next option is to create a chimera that somehow understands its context and presents the correct behavior. The next option is for the consumer to have deep knowledge of your object. In which case, why use one?
15:35 Misuse of Polymorphism (The Electrical Outlet Problem)
It’s easy to get consumers of objects and the objects themselves out of sync in regards to expectations. For this example, we’ll use an electrical outlet as an example of something that exhibits polymorphic behavior. Polymorphism allows you to treat different things the same, as long as they don’t ACT different. With multiple people touching things, that guarantee is hard to maintain.
Uses of polymorphism can expose large portions of a codebase to changes that would otherwise be isolated. If a piece of functionality has to be added to two types that don’t have the same parent type, it’s common to add it further up the chain for “polymorphism”. This can force you to account for it at everything below that type and every consumer of the same. This can bleed far further out into the codebase that you might think. If the input amperage of the building goes up, all the wires, circuit breakers, etc. in the building may have to change. This might be true of everything plugged into it, and everything plugged into them.
Polymorphic code trends towards handling things in general, when specificity might be more reasonable. When polymorphic calls make polymorphic calls of their own, the temptation is for the first call to take parameters that are as general as possible. This means that the second call is either forced to deal with a general case, or is forced to probe for more specific cases and then dispatch accordingly, possibly meaning that it will break polymorphism when new, more specific behavior is added. Let’s assume your outer call takes an enumerable bunch of objects because it just needs to loop through them. Let’s assume the inner call needs to get a count, that it dutifully returns as an integer. Using an enumerable instead of a list means that a caller of the outer method could pass something in that has a count greater than an integer can hold, or that’s subject to network errors. BOOM!
21:00 Bad Encapsulation (The dinner discussion problem)
The innards of a class must be protected. However different degrees of protection are appropriate in different contexts. For instance, in conversation with a spouse, there is a lot of stuff you can talk about. The same is not true at a table in a crowded restaurant. Nor is it true with some random person you just met at a gas station. In other words, exposure levels of various fields vary by context.
The granularity of exposed data does not match the way that data is validated. Similarly, the way properties are exposed does not correspond to how they are altered. There are multiple components to an address. However, it’s almost certain that if you move to a different state, that your street name probably changed. If you do not break state mutations into reasonable chunks, your objects will transit invalid states on the way to valid ones. Which means lots of validation code.
Encapsulation plays havoc with inheritance, especially across library boundaries. You may want other classes that you wrote that inherit from a given class to be able to get to an encryption key or other protected data, but you don’t want just anybody to be able to inherit and get at it. You might also want to expose this class to classes in other libraries, which means that even if you have library-specific protection levels, you still can’t enforce your rules because the language won’t let you.
24:55 Database and Persistence Issues (the family filing cabinet problem)
You have to consider how an object will be stored in either a relational context or in a file. This is ok when it is a single object, but it gets interesting with polymorphic members and nulls. With polymorphic deserialization, you are also going to have to have metadata that tells you what you are deserializing.
This means you also have to deal with versioning issues in storage. The serialized form of an object from a version of your app a year ago is different than the one today. This means you have to fill in missing things, and ignore extra things.
Complex object graphs will also have the risk of circular references or of containing things than can’t be serialized and deserialized. Let’s say you are recording a family tree. You have a couple parents, their kids, and their kids. If the parents have a list of their kids and the kids have references to parents, you can traverse the graph in a loop forever. You also can’t just keep track of object ids and try to avoid duplicates. Far enough back, every family tree is a directed acyclic graph.
Inheritance makes serialization even more fun, especially with polymorphism thrown in. A parent is also someone’s child, possibly someone’s sibling, likely someone’s spouse. If you model this poorly and your language doesn’t transparently cast things, you’ll have to preserve metadata so you can.
How do you serialize a diff? Let’s say a wife passes and the husband remarries and there are kids from both marriages. Let’s say the husband then dies and the wife remarries. How do you store this such that the now orphaned nodes get serialized/deserialized? How do you tell when something has been altered from the last time it was stored? You’ll need this to cut down on audit trail noise.
29:35 Concurrency nightmares (The Joint Property Problem)
If more than one thread has access to your object and the object’s state is mutated on one thread with synchronization around it, it can cause weird behavior on another thread. This is how you get race conditions. A good example of this is a couple’s bank account. Dad buys a boat and mom buys a car. Depending on which transaction hits first, you could have an inconsistent number of overdraft charges. Bank of America actually took advantage of this a few years ago and put transactions in an order that would maximize their overdraft charges.
When multiple threads are accessing multiple objects and using synchronization mechanisms, it requires a lot of care to avoid deadlocks. Visualize two lanes of traffic going in opposite directions, where the road space is the shared resource. Two cars simultaneously start to turn left. Each car is depending on traffic in the other lane to clear to be able to turn left. One lane gets blocked further ahead, and no one lets the car turn in that lane, until there is no space to move. Barring a median (deadlock “victim”), both lanes are now blocked because they accessed the same resources in a different order.
This can also make it tricky to know when to destroy the memory used by an object. If you are doing manual memory management, this means keeping a counter of how many things reference an object. This counter will have to be thread-safe, and objects typically have no awareness that they are being referenced. This means threading problems. If you are using a memory-managed language, this still happens in some manner, but it’s not your problem
34:50 Viewpoint Issues (The “No, You” problem)
There is no such thing as a single, universal understanding of an entity type across an organization, especially over time. A client is different for accounting than it is for shipping. A client is different from a small business serving small businesses than it is for a large business serving large businesses with subsidiaries, but the second may evolve from the first and live side-by-side.
Developers are second-order ignorant of a problem domain when they first start working in it. You have reasonable guesses of what’s going on in a problem domain. They will be wrong until they are less wrong. It’s very easy to paint yourself into a corner with initial wrong understandings and it can be a lot of refactoring to get out of them.
Viewpoint is mostly subjective and can be an organizational fault line. If accounting views things one way and shipping views them another, and both are in the meeting with you, there will be friction. This can make you a pawn of organizational politics, even though you were just trying to design an object model. Now your code is political.
38:55 Behavioral Issues (The “Criminal Minds” problem)
Modeling behavior can be difficult to get right in an object-oriented paradigm. Who “owns” behavior that involves two entities? Think of your English class in school. Is something passive voice or active? It changes the model. This won’t be the same across departments either.
Behaviors can sometimes be verbs and sometimes be nouns. A murder is a noun, but it was a verb whenever whomever framed OJ Simpson did it. Behaviors may have their own data associated with them, which may include other objects.
Behaviors have a tendency to become persistent objects and to involve multiple objects themselves. If something impacts system state in a way that costs money, impacts security, touches personal data, or otherwise involves regulatory compliance, it has to be stored. This makes a verb into a noun all on its own.
42:20 Relational Issues (The “Indecent Prepositions” Problem)
Similar to behaviors, relationships between two objects can be quite fluid. “Is a” versus “Has a” will change on at least some of your objects at some point during your application lifecycle.
Where do you store a relationship between two objects? This can impact your ability to navigate between objects from anywhere in the graph. But you can introduce circular references as well. Removing references is also a complication when they are dual-sided.
How do you handle cardinality of object relationships when there are more than two involved? Things get nastier when an object “has” 1 to some limited number of some other object. Validation is particularly interesting if the relationship is between several polymorphic classes.
46:25 Speed and Memory Issues (The traffic problem)
OOP is going to be slower than direct function calls in some cases. Every abstraction has a cost. When you are writing an OS, you don’t accept any costs that you don’t have to. Low-power computing also makes OOP difficult.
You may find yourself creating a large object when you only need one or two of its properties because it’s simpler. This is especially true of situations where you are using an Object Relational Mapper (ORM) to pull things from the database. While you could easily write something where you aren’t loading a large object just to get one or two of its properties, in practice developers are too lazy to do this.
You’ll find yourself using flyweight patterns and the like for heavy objects, adding more complexity. If an object is particularly difficult to construct and you don’t need the parts that are difficult to construct, you’ll end up using other patterns to avoid doing so. These patterns will complicate your code, especially if they are done frequently.
49:45 Advocacy Issues (The loudmouth issue)
Pushing OOP on non-developers is tricky in most non-technical organizations. Nobody cares about your object model. Remember that the OOP part of the project is your interface, not the one that clients are using. Thus it’s not going to be a focus and you’re going to annoy people by trying to make it the focus.
You will be refactoring as your understanding improves. Clients won’t understand that. Pretty much any developer has to refactor code as they go along, but certain types of changes in OOP create significantly more refactoring work than a client would expect. If you put “refactoring” as a line item on an invoice, clients are going to ask you not to do it any more. It’s very easy for OOP refactoring to be the result of a semantic difference, rather than a required feature- set change.
You’re going to be dealing with a lot of people who are doing it wrong, including (especially) yourself six months ago. A lot folks got an introduction to OOP and that’s all they got. Consequently, a lot of really idealistic notions persist in this sort of design, and you’ll be arguing with these people. Couple this with management not caring about your object model and you can see the potential for this to blow up in your face.
Keen Smart Vents
These IoT enabled vents adjust the airflow to a room based on metrics from that room specifically. It’s like having individual AC controls per room but at the vent level. The sensors and an app on your phone allow you to set up schedules for rooms to open up vents and even set room temperatures based on the air flowing into the rooms. While it won’t control the temperature of the air flowing into the room it can control when and how much flows in.
Tricks of the Trade
Consider criticism, but don’t be ruled by it.