5 ms·
The analogy I'm using is too big. It's actually a famous quotation but it kind of blinds you to the fundamental issue. Let me put it with a very primitive exam
by Geminidog 6y ago
The analogy I'm using is too big. It's actually a famous quotation but it kind of blinds you to the fundamental issue.
Let me put it with a very primitive example.
class Jungle:
int bananas;
int gorrilas;
removeBananaFromJungle():
banana -= 1;
Literally the example above is the most fundamental example of a class you can write. A class with a method that operates on a member variable. You can't get any simpler than this. There's no alterative pattern here; this isn't some overly complex example you can fix with a pattern. This is objects 101, one of the simplest entities in OOP you can ever define and the problem remains:
I want a banana but I must initialize the jungle and the gorilla in order to remove a banana.
It seems like a good organization scheme because the banana and gorilla live in the jungle. But this is the issue with OOP. We can't predict the future and we can't predict how requirements will change or the perfect design.
So really the best way to program is to assume nothing. Don't assume either the gorilla or the banana are in the jungle:
int gorrila = ...
int banana = ...
map<string, int&> jungleStuff = ...
haveTheGorillaThrowABananaOutOfTheJungle(gorilla, banana, jungleStuff)
What if one day I'm called as a programmer to write a program about Gorillas on the Space station?
With the OOP version I have to create the jungle and the banana just to get the gorilla onto the space station. With the regular procedural version I do not. Basically 99% of accidental technical debt is the result of unintended groupings of logic and data promoted by the OOP pattern because people assume gorillas live in the jungle and they can never account for the fact that one day they will be on the space station.
I'm sure you can come up with some crazy OOP pattern that can account for both the regular Jungle case and the SpaceStation... but what if I start throwing all kinds of crazy requirements at you?
The spacestation now has to account for Ostritches, Gorillas can now live with ostriches in a house.... anything you can think of I will try to break your design with a new requirement (within reason) until you come up with a set of entities that can anticipate anything...
You'll find more and more that your classes will become smaller and smaller until basically you have an OOP pattern that can anticipate any requirement I throw at you:
class Gorrilla
int gorrilas
class Banana
int Bananas
class Jungle
map<string, int> stuffInTheJungle
class SpaceStation
map<string, int> stuffInTheSpaceStation
class House
map<string, int> stuffInTheHouse
class Ostrich:
int ostriches
Which is basically equivalent to this:
int gorrilas
int Bananas
map<string, int> stuffInTheJungle
map<string, int> stuffInTheSpaceStation
map<string, int> stuffInTheHouse
int ostriches
In short, any grouping of logic and variables through the use of free variables or classes reduces the modularity and flexibility of your program pointlessly. There is no reason why you should group logic together and you can never anticipate the future where your grouping will be found to be incorrect. To counter this problem you need to make your groupings smaller and smaller until each class holds one thing. A class that holds one thing is pointless, it's the same as just that thing itself without a class wrapped around it.
- tcgv 6y agoThanks for the detailed explanation, I understand your point now, but I'm still not convinced that it can't be avoided/managed properly. The primitive jungle class example you provided is actually just a condensed view of a jungle, holding counters of 'bananas' and 'gorillas', it only entangles two "counters" which is perfectly fine: class Jungle: int bananaCount; int gorrillaCount; removeBananaFromJungle(): bananaCount -= 1; Now lets say we improve it to add actual 'banana' and 'gorilla' classes, it may look something like this: class Jungle: List<Banana> bananas; List<Gorilla> gorrillas; removeBanana(banana): bananas.remove(banana); In this example 'Jungle' is only an aggregate of 'Banana' and 'Gorilla', whose behavior is abstracted away in their classes. Moreover 'Jungle' only implements association logic. It's completely possible to define another aggregate class for a space station that reuses bananas and gorillas in a different manner: class SpaceStation: List<Banana> bananas; List<Gorilla> gorrillas; removeBanana(banana): bananas.remove(banana); You could even allocate bananas and gorillas separately in your program (or unit tests) and play with them if you desire, without an aggregation class managing them. The important thing here is to promote cohesion and avoid coupling. Sure, you could say that both 'Jungle' and 'SpaceStation' are duplicating logic, but it's simply association logic at this point, nothing that prevents bananas and gorillas to live happily apart from each other. Furthermore, it's highly likely that as the these classes evolve they may handle removal of bananas very differently, for instance, in the space station it will need to be disposed into the void, and in the jungle the disposed banana may be used to fertilize the soil.
- deleted 6y ago[deleted]
- Geminidog 6y agoWhy is changing an integer to a class an improvement? Isn't that like my last example where every tiny thing is encapsulated by a class? >and play with them if you desire, without an aggregation class managing them. This is my point. Within a class you have several entities that are managed by the class. I'm saying take away the management layer all together. Take away classes which is exactly what you said. You say you can do this: List<Banana> bananas; List<Gorilla> gorrillas; removeBanana(banana): bananas.remove(banana); but you can simplify even further and take away the concept of classes all together.: int bananas; int gorrillas; void removeBanana(int& bananas): bananas --; This isn't my point though. The point is in your designs you can never expect your methods to be defined like that. What if for the space station but not jungle... if you remove a banana you have to remove a gorilla too? What do you do? You edit the space station method to account for that. Suddenly the house class requires this too. How do you account for that in the house without copying your logic? How do you account for that without refactoring SpaceStation? Refactoring is simple in these cases but more often then not OOP causes situations where the dependencies are much more tangled and complex. Only if removeBananaAndGorrila(int& banana, int& gorilla) was already written outside the context of a class will you be able to reuse the concept and share it between two classes. So Why not remove the concept of classes all together? That way whenever something needs it, anything can use it. Not to mention that your style requires something called dependency injection which greatly complicates code. removeBanana in all OOP cases can never be reused outside of a class. You must always be redefining it and wrapping banana.remove. You can fix by taking this definition outside the context of the original class with a super class (inheritance) or you can just define it outside of a class all together with no restrictions. Why restrict the ability to reuse something? This is all a class does. Better to let the type parameters of a function to restrict usage.