You are misattributing that to OO programming.
The original edition of Code Complete, written in 1993, makes absolutely no mention of OO programming or any OO principles. However it has a very good discussion of information hiding, and procedural code written in the various styles that it recommends do not create a lot of global state. (And yes, I have worked with such code.)
One of the best things about OO was that it pushed programmers who had not absorbed best practices towards that. However OO encourages combining the principle of information hiding with OO notions of inheritance. Many learned the hard way to prefer composition over inheritance. (Many, unfortunately, never learned that. Just as many procedural programmers never learned about information hiding.)
When you do build programs like this, code gets more and more rigid until it becomes a maintenance nightmare, and extremely expensive to pivot functionality. Sometimes having a complex object hierarchy literally stops you making some new feature.
A lot of mental effort is sucked up in mashing problem spaces into hierarchies, and once there the program is constrained by them.
Implementation inheritance is often justified as a way of "reusing" code, but as it turns out, we've merely introduced undesired coupling instead of the seamless reuse we might have expected.
In principle, with a sufficiently-robust type system, the invariants expected by the base class would be encoded in the typing, and the derived class would only be able to narrow them.
(Of course, OOP languages tend to either be untyped or have insufficiently robust type systems for this; robust type systems are more frequently associated with functional languages.)
why would you make internal methods virtual though?
So, for example, Java's Map is an interface type with, for example, .get() and .put() methods; TreeMap and HashMap implement those methods differently. There are similar things in Smalltalk-80 and the C++ STL.
So clearly you think Java, Smalltalk, and C++ are "a bass-ackward parody of proper OO design." I'm interested to hear what systems you consider paragons of proper OO design.
You might find the book Program Development in Java: Abstraction, Specification, and Object-Oriented Design by Barbara Liskov and John Guttag very useful for understanding OOD/OOP.
You've touched on a whole host of performance issues here too. Not only are we making indirection after indirection when accessing virtual methods and data (my poor cache), but invariably these are all stored in heap memory, so we get the worst possible performance profile baked in.
> the actual code that gets executed depends on whether the object is one of a "derived" class, where that method might have been changed in ways that might break any amount of expected invariants.
On top of that inheritance makes the concrete type unknowable at compile time just to add to the pain.
> Implementation inheritance is often justified as a way of "reusing" code, but as it turns out, we've merely introduced undesired coupling instead of the seamless reuse we might have expected.
Yes this 100%. Inheritance actually stands in the way of code reuse. If I have a class that is kind of similar to another, but different enough to "require" being another class, oh dear they're now incompatible. So you try to kind of fit it into a subclass, but it makes more sense in another, and now you're spending mental effort on a problem created by architecture and not the actual thing you're trying to solve.
This is really clear in the game development world, where game objects often just don't fit into the inheritance format, even with multiple inheritance. This is why most complex games are built with the entity component system pattern as it focuses entirely on composition.
Perhaps the crux of the issue is that inheritance encourages design by "identity" (mental concept) rather than "attribute" (data). Thus to solve problems we must mentally classify them into some taxonomy as if they are animal species before we even write any code. This is just extra work in exchange for all the issues mentioned.
It's not supposed to, and if it does, the design is clearly broken. Now the (rhetorical) question is: does it happens often. The not so rhetorical question is: is it possible to make it happen rarely. The even more interesting question is: is it easy / more practical compared to alternative approaches, and if not then what is the point.
My opinion on SOLID is that there is precisely one hard "principle", and it is worthwhile: the LSP (it derives directly from logic, that's why). I believe that Open-close is even borderline insane, and that if there is a crazy way to make it not insane, this way is probably applied by so few people that it may well be irrelevant -- most people will try to apply it in ways that will quickly put them at risk for LSP violation, but LSP is far more important (or they will try to apply it in bad places, but that is another story). Plus programs designed with complex hierarchies are often missing the point of execution contexts, and then understanding them is absolute hell -- their original authors sometimes do not understand them themselves. (The rest of SOLID are soft attempts at fixing self-inflicted wounds, sometimes even reasonable if you insist on doing Javaesque / old-C++-esque OOO, but I digress.)
I'm not sure why anybody thought that kind of OOO was a good idea, or that the main characteristic in interesting big programs was the usage of "classes". I even find the suggestion of causation instead of mere correlation dubious; they were already quite a good number of big programs before, and what permitted the explosion of program size was more the ever growing capabilities of computers, that happened, in affordable versions, during the Java-like OOO hype. Besides the simplistic reductionism (we can start with: which classes model entities, which classes model values, which classes are controllers, etc.) which is not too much a big deal in practice, modeling with class diagrams is often missing the river in the middle of the forest for some groups of intertwined trees.
The Smalltalk-80 container hierarchy demonstrates that you can get a lot of mileage out of simple single-dispatch virtual methods with inheritance. The Smalltalk-78 system, which you can try at https://lively-web.org/users/bert/Smalltalk-78.html (although it's not working for me at the moment), got a multiwindow GUI with an IDE running usably on an Intel 8086 with 256KiB of RAM, in only about 100 classes and 2000 methods totaling 200 KiB of code. This is not what I would describe as a "big program", but it is a fairly impressive program nonetheless. Seeing that kind of thing is what led people to adopt object-oriented programming.
I firmly agree.
I wrote https://www.perlmonks.org/?node_id=318257 over 15 years ago. I still agree with the fundamental criticism of "OO everywhere" that I offered there.
Though w.r.t. the speculation in your final "Disclaimer", I'd expect the algebraically inclined to prefer either the elegant combinatorical explosion of a J, or the build-it-yourself language of a metaprogrammed Lisp.
I find OOP useful for configuration and library-level APIs (e.g. for injecting dependencies and wrapping things), when there is really zero expectation that a user should have to understand the class definition entirely. But that's about it.
Where it seems to break down for me is when classes are actively used at the application implementation level for general encapsulation. E.g. data access layers. This can be done perfectly fine with modules/packages. Adding inheritance is just asking for trouble in a team setting IMHO.
Trouble comes when there is no clear peer review and style policy to avoid classes for anything other than config or distributing libraries (as separate projects). What ends up happening is a proliferation of subclasses or method overrides when developers are in a rush to ship features without understanding the whole codebase. This is a technical loan with very high interest.
It makes sense rationally in the moment, as classes have an inviting feel to the user as a kind of grab bag of related functionalities that are easily introspected (at first). Compare it to searching through docs for all the different namespaces in a package and learning what types their various functions support, it requires more thought and grasping the concepts of the library. Alternatively, a class with a broadly defined purpose starts to look like a common utility to throw things at. It's a grab bag of stuff that is more amenable to hacking with blinders on, in a way.
Next thing you know the chains of inheritance and method overrides have grown into a very hard to disentangle hydra. The accumulated overhead of maintaining it is compounded by the debugging challenge of knowing where exactly a given instance is coming from and which layers it has been through on its way to a given breakpoint.
The mental effort of building up taxonomies in particular is exactly what put me off inheritance (and fussy type systems in general).
It's too easy to jump down that rabbit hole without considering whether it's a productive activity.
Inheritance is used inappropriately (outside of frameworks) almost everywhere.
When you implement an extended jump table (a table set that extends an existing table def) in C, that is also just inheritance.
There is a good reason the first C++ compiler was just implemented as a bunch of macros in C.
I wrote a blog post earlier this year based on this paper, describing how using the information hiding criterion naturally leads to an improved system structure when compared to using a procedural criterion [1]
I'm an advocate for OOP because it greatly favors information hiding and encapsulation, helping to manage the ever-growing complexity of software projects, making "it possible to develop much larger programs than before, maybe 10x larger" (quoted from the linked article)
[1] https://thomasvilhena.com/2020/03/a-strategy-for-effective-s...
If OOP is so abstract as to be indistinguishable from other paradigms, then what value does it add?
in case anyone's unfamiliar, here's one basic way to emulate objects with closures. i think i saw it in SICP (?), reproducing it here because i just love how simple it is:
// might be easier to first look at
// the usage example at the bottom
let Point = (x, y) => {
let self = (message, args=null) => {
switch (message) {
// getters
case 'x': return x;
case 'y': return y;
// some operations
// (immutable, but that's not required)
case 'toString':
return `Point(x=${x}, y=${y})`;
case 'move':
let [dx, dy] = args;
// use our getters
return Point(self('x')+dx, self('y')+dy);
// let's get DRY!
case 'plus':
let [other] = args;
return self('move', other('x'), other('y'));
default:
throw Error(`unknown message: ${message} ${JSON.stringify(args)}`);
}
};
return self;
};
let p1 = Point(3, 5);
// sending messages
p1('x') === 3;
p1('y') === 5;
p1('move', [1, 2])('toString');
// --> "Point(x=4, y=7)"
let p2 = Point(1, 2);
p1('plus', [p2])('toString');
// --> "Point(x=4, y=7)"
dynamic dispatch & message passing, just like that! and you can easily do `__getattr__/method_missing`-style dynamicism just by looking at `message`.for the other way around, see how Java lambdas desugar to objects with a "call" method and closed-over variables as members.
i remember reading this somewhere: "branching on values is the ultimate dynamic dispatch" :)
Dynamic dispatch implies that you dispatch based on the class of the receiver. For instance, you can't have a Point3 value that uses Point's implementation of 'x' and its own implementation of 'toString' under this example.
let Point3 = (x, y, z) => {
let parent = Point(x, y);
let self = (message, args=null) => {
switch (message) {
// getters
case 'z': return z
// some operations
// (immutable, but that's not required)
case 'toString':
return `Point(x=${x}, y=${y}, z=${z})`;
case 'move':
let [dx, dy, dz] = args;
// use our getters
return Point(self('x')+dx, self('y')+dy, self('z')+dz);
// let's get DRY!
case 'plus':
let [other] = args;
return self('move', other('x'), other('y'), other('z'));
default:
parent(msg, args)
}
};
return self;
};> you can't have a Point3 value that uses Point's implementation of 'x' and its own implementation of 'toString' under this example.
you can't do much of anything under this this example! i didn't think a whole object system would fit in a HN comment, i intended it to be minimal :)
let moveUp = ['move', [0, 1]];
p1(...moveUp)
p2(...moveUp)
but you can't get a `move` that's "bound" to a particular object¹ – the "receiver" is decided when you "send" the message. which reminds me of how JS methods work: `this` is bound to what's "to the left of the dot" when you call a method, so unlike Python, obj.foo(1)
is not the same as let f = obj.foo
f(1) // Error: 'this' is undefined
maybe there's a connection to some language that inspired JS' OO model, with prototypes and all that?---
¹ well, unless you explicitly wrap it in another closure like
(args) => p1('move', args)Edit: To answer your question, the set of all public methods could be seen as 1, but not the only, interpretation of the functional interface.
> the set of all public methods could be seen as 1, but not the only, interpretation of the functional interface.
could you describe another interpretation? "the set of public methods"¹ is the only thing i can think of when i hear about an object's "functional interface". it could be a terminology issue though – i'm reading "functional interface" in a general sort of way, but i could imagine it having some specific definition in OO theory.
---
¹ or recognized messages, in a more smalltalk-ish context
PS. if you're interested in weird perspectives on this, you might find Coinduction (and "codata") interesting. from wikipedia:
> Informally, rather than defining a function by pattern-matching on each of the inductive constructors, one defines each of the "destructors" or "observers" over the function result.
it's a deep rabbit hole, but i remember it being applied OOP-ish things – "destructors" would roughly correspond to exposed methods.
Objects are a poor man's closures. And closures are a poor man's objects.
Modern languages have both. And they serve different purposes.
though i must say, i'm pretty happy in languages that have closures but don't have objects, as long as there's a nice way to do ad-hoc polymorphism (like traits/typeclasses)
let pair = (a, b) => (
(ix) => (
ix === 0 ? a :
ix === 1 ? b :
undefined
)
);
let p = pair(3, "foo");
p(0) // 3
p(1) // "foo"
i've seen sth like this used as the definition of tuples in a math context. (there are also other ways to do it too, like church/scott encoding)however the object has the extra wrinkle of self-referentiality, because the `self` function is defined recursively
Usually this is a hash (dictionary) which is similar to the object with fields that we all know, but it can just as easily be an array or scalar value.
From Alan Kay (who coined the term)[1]:
> OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things. It can be done in Smalltalk and in LISP. There are possibly other systems in which this is possible, but I'm not aware of them.
Almost anyone who has dealt with present-day OOP (Python, Ruby, C++, C#, others, or, God forbid, Java) has had the same thoughts as you, I'm guessing. I've come to the conclusion that OO as originally envisioned had a lot of good ideas, and these are surprisingly compatible with and complementary to a lot of the (almost-)pure FP ideas that have gained traction over the last few years.
OOP also had huge mistakes:
- Class inheritance is the biggest one. Kay himself mentions inheritance appearing only in the later versions of Xerox's Smalltalk. No need to really elaborate. Implement interfaces, compose behavior and don't inherit. He has posted some more of his thoughts on this exact topic. [2] - Java was a giant industry-harming one. Now multiple generations of programmers have been brought up to think that the definition of an object is to write a class (which is not an essential characteristic), declare internal state and then write accessors and mutators for every one of them. We might as well write COBOL. But, having tried to solve problems in Java, I get why it's done. The OO is so weak and awful that I'd rather just crank out slightly-safer C in Java than jump through the horrific hoops. (Just as an aside, I find it sad that Sun had the teams that developed both Java and Self. Self used prototypes instead of classes, which influenced Javascript, but also had a killer JIT that made it super fast and a ground-breaking generational garbage collector. Sun ended up cannibalizing the Self team to assist with Java, and some of the features of the JVM were ports of work that had originally been done for the Self VM.) - C++ was kind of a mistake, because it introduced the interminable "public" and "private" and now the endless litany of stupid access modifiers. They were needed because C++ had this need to be compatible with C and do dynamic dispatch and have some semblance of type safety (though C is only weakly typed) and have it run (and compile) before the heat death of the universe. C++ isn't as horrible as what came after it though IMO (i.e. Java). - Xerox horribly mismanaging Smalltalk was another mistake. Xerox was smart enough to realize that Smalltalk was insanely valuable, but instead of realizing that it should be a loss leader/free drugs, they decided to lock it away in the high tower and then let everyone else pillage their research that could be commercialized (GUIs on the desktop, laser printers, Ethernet, among many others). This literally led Apple to develop Objective-C.
I really went down a rabbit hole on this topic around two years ago. Two ideas really helped crystalize the idea for me, both from Kay.
One is that the Internet is an object-oriented system. It communicates via message-passing, it is fault-tolerant, and all of its behavior is defined at runtime (i.e. dynamically bound). We don't need to bring the entire Internet down when someone develops a new network protocol or when a gateway dies or some other problem happens. It continues to work and has in fact never gone down (though early on during ARPANet, there were instances of synchronous upgrades [3]).
The other, deeper one (and the much more open one) is that "data" is error-prone and should be avoided and is, in fact, "the problem" (i.e. the root of the Software Crisis). This sounds preposterous to most programmers.[4] In fact, I didn't even get it when I first heard it. I think the basic idea is that he uses "data" as a physicist or statistician would use it: quantities measured in the world with varying accuracy/precision. This seemed preposterous or even heretical until I started noticing it in the large codebase I was working on at the time. A lot of the code was in a nominally object-oriented language, but the style was very procedural (i.e. "If a then do X, do Y; else do Z"). I noticed that all of our code was working on large data structures (think a relational database schema), but there was no coherent description of it or what it meant anywhere. If I touched one of the pieces of code, I invariably had to start adding more conditional branches in order to preserve correctness. In order to make any sense of the code, I made a ton of implicit assumptions about what the patterns I saw actually meant, rather than relying on the program itself to communicate this through its code or meaningful constraints on the data. We would be better served with "information" with less noise and more signal than what we get with data and data structures (think Shannon). The only real success we've had with data is a few encoding schemes (e.g. ASCII/Unicode, 8-bit bytes, IEEE-754, etc.), but that approach clearly doesn't scale beyond a dozen or two data encoding schemes. OO (of the high signal-to-noise variety championed by Kay) at least has a story for this, which is that certain patterns do indeed have predictable characteristics, and complex patterns can usually be composed by a handful of simple ones. I have yet to see a system like this actually in practice, but I can imagine something like it could exist, given the appropriate tools (in particular, a good, state-of-the-art language that isn't some rehash of stuff from the 60s - Erlang might fit the bill).
The ideas of DO as articulated by Mike Acton are antithetical to the ideas of OO (although a well-designed future OO system - including late bound hardware - could probably represent and implement a lot of the techniques that Acton talks about to improve performance and quality). This doesn't mean they're wrong, but I see treating data as the _only_ thing that matters as being fundamentally opposed to the idea that data is actually the root of most evil. People adopting DO should be aware that robustly applying the technique in the long run will require having detailed knowledge of every aspect of the system on every change. (I think it's not a coincidence that it was originally developed for game dev, which often doesn't have the same long-term maintenance requirements as other kinds of software, and certainly not software like the Internet.)
FP and OO are more complementary. Immutability and referential transparency can be very valuable in object systems; encapsulation, extreme late binding and message passing can be very valuable in functional systems (in fact, Haskell and Erlang surpass "OO" languages when scored on some of these characteristics). Scala explicitly embraces the complementarity between the two paradigms.
[1] https://userpage.fu-berlin.de/~ram/pub/pub_jf47ht81Ht/doc_ka...
[2] https://www.quora.com/What-does-Alan-Kay-think-about-inherit...
- Class inheritance is the biggest one. Kay himself mentions inheritance appearing only in the later versions of Xerox's Smalltalk. No need to really elaborate. Implement interfaces, compose behavior and don't inherit. He has posted some more of his thoughts on this exact topic.[2]
- Java was a giant industry-harming one. Now multiple generations of programmers have been brought up to think that the definition of an object is to write a class (which is not an essential characteristic), declare internal state and then write accessors and mutators for every one of them. We might as well write COBOL. But, having tried to solve problems in Java, I get why it's done. The OO is so weak and awful that I'd rather just crank out slightly-safer C in Java than jump through the horrific hoops. (Just as an aside, I find it sad that Sun had the teams that developed both Java and Self. Self used prototypes instead of classes, which influenced Javascript, but also had a killer JIT that made it super fast and a ground-breaking generational garbage collector. Sun ended up cannibalizing the Self team to assist with Java, and some of the features of the JVM were ports of work that had originally been done for the Self VM.)
- C++ was kind of a mistake, because it introduced the interminable "public" and "private" and now the endless litany of stupid access modifiers. They were needed because C++ had this need to be compatible with C and do dynamic dispatch and have some semblance of type safety (though C is only weakly typed) and have it run (and compile) before the heat death of the universe. C++ isn't as horrible as what came after it though IMO (i.e. Java).
- Xerox horribly mismanaging Smalltalk was another mistake. Xerox was smart enough to realize that Smalltalk was insanely valuable, but instead of realizing that it should be a loss leader/free drugs, they decided to lock it away in the high tower and then let everyone else pillage their research that could be commercialized (GUIs on the desktop, laser printers, Ethernet, among many others). This literally led Apple to develop Objective-C.
but the linux kernel is definitely OO, except vtables are filled by hand instead than with the help of a compiler.
https://www.kernel.org/doc/html/v4.10/driver-api/infrastruct...
structs with data + function pointers are literally OOP objects. see e.g. https://lwn.net/Articles/444910/
> you'll see that there are object oriented concepts like information hiding and polymorphism all over the place; they just don't formalize them with the "class" or "private" keywords.
yes, that's still OO. python doesn't have a private keyword either and that does not make its classes not OO.
Global variables are called "Singletons". DI injects a reference to all the globals you need to do your work, so you don't feel so grubby. Often DI injects some kind of accessor to the database, a giant store of global variables.
Procedures are called beans, but you can tell they're procedures because they have a single public entry point and are usually called SomeKindOfVerber with a main verb() or do() or execute() method, they have the procedures they call (sorry, I mean "collaborators") injected, and they themselves are injected to where they'll be called from. Their state is ephemeral, and only exists for the purpose of executing a single flow of logic (this is called "bean scope").
All you need to do to describe your code as object-oriented is convert your procedures into beans, and tada!
I also agree that is kinda of funny to constantly so the Verber classes everywhere in Java, but it has at least improved over the last several years with the standardization around Function<T,R>, Supplier<T>, Consumer<T>, etc. Lambda syntax and method references also help. I'd admit the syntax for using a functional style in Java is still a little jenky, but its quite an improvement from where things were back in the day.
It is very hard to do something that is 'good' in technical terms while also being 'good' in product terms. While the discussion often doesn't revolve around the practical implications of taking steps toward a commercially uncommon language or paradigm during business engineering it is pretty much where all the 'stuff gets done'.
And java isn’t where all the ‘stuff gets done’ - well, outside of the enterprise programming bubble. JavaScript and (surprisingly) python are the workhorses of the modern startup ecosystem. I can’t speak with certainty about python, but separating logic and data, and using immutability with stateless components and functions is all very common in the JavaScript world.
C, C++, Java, Python, JavaScript, C#, even PHP are not exactly ideal, even in their special niches, but a lot of real world stuff, if not most 'stuff' gets done using that.
The biggest upside I see for automated DI is to get back concision once you've parameterized your code by the code it depends on. Usually this parameterization is done for unit testing purposes.
Direct calls are hard to exclude or intercept for testing, so you call a method on an interface or a base class or an object instead, and now you need to be initialized with a reference which may be replaced for testing purposes. But the requirement for initialization remains in production, even when there is normally exactly one candidate and it might not even require state if it wasn't for all the other references it in turn needs to call.
The great increase in the burden of initialization creates demand for the IoC / DI container. And once you have a container framework which takes care of gluing objects together, why not let it stick transparent adapters (proxies) around the objects too? Proxies for per-request state, multi-threading, transaction management, caching, all that good Common Lisp advice of before, after and around.
The temptation for clever people can be hard to resist, and before you know it your local code has global side effects unknowable to people who don't have the full picture in their head.
I actually blame unit testing, particularly when combined with a static type system. Static binding, in particular: you could mock static calls if dynamic binding like Emacs Lisp was available!
Unit testing is great at the leaves of the call graph, for self-contained things like container libraries, or things which behave functionally, i.e. they transform things.
Unit testing is like arthritis in the middle of the call graph. It makes rearchitecting much harder by baking in assumptions both upstream and downstream, and the easy temptation of mocking usually overspecifies the interaction with collaborators. The parameterization demands DI which encourages too much cleverness, as discussed. And unit tests don't leave you very confident that things will hold together because test mocks and stubs don't necessarily replicate the behaviour of the real things.
I like integration testing higher up the stack, and trying to convert a program as much as possible into a composition of functional elements - things that belong on or near the leaves of the call graph, and leaving as little "middle" as possible to be infected with unit tests.
Yep, you get all the downsides of coding in Basic with the bonus of the performance hit you take from all the reflective instantiation.
They can help simplify what is essentially a problem of complexity that is self-induced by the original decision to use OOP. But it's rare that they don't also introduce tons of additional other complexity, e.g., at runtime. Debugging these applications is one of the most infuriating things I've ever done as a programmer.
Secondly, you don't even need to use interfaces with one-off implementations if you don't want. You can inject your concrete implementation class instances directly. Using interfaces just makes it easier to swap implementations as needed. The unit testability argument is completely orthogonal (and also in the case of the JVM, you can mock classes as they're open by default, so again, you don't even need to use one-off interfaces).
And I don't see how this issue is limited to OOP. At the end of the day, you're going to have to hook up your components and object graph anyway, either manually or using some kind of DI, so the complexity is always there. Even if you don't write interfaces with single implementors, non-trivial programs quickly become extremely verbose to implement. Maybe something like Scala's `implicit` can be of help here.
> Debugging these applications is one of the most infuriating things I've ever done as a programmer.
Again, Dagger and Micronaut do DI at compile time using codegen, so nothing to debug at runtime.
Non-trivial programs don't have to be verbose. They are generally made so by the addition of unnecessary "features" that, in turn, require even more additions "to reduce complexity."
There are other ways as well and all of them work well for certain problems and not so well for others. Limiting yourself to only one lense with which you look at problems is a bad thing.
Well, agree, but in my experience people who abandon OO are usually the ones who are limiting themselves to only one lens - the top-down/procedural way software was developed in the 60's and 70's which is easier to comprehend but ultimately unsustainable for subtle reasons. (not saying that's you, just saying a lot of people do).
Rejecting OO in favor of FP like Paul Graham does? Great! Hopefully you'll make some headway and functional languages/styles will really catch on!
Rejecting OO in favor of one big function that handles everything and a few global variables (like nearly everybody who uses the Spring "framework" abomination in Java)? Maybe spend some time considering why, if it's so obvious, it's referred to as a "bad practice".
I also don't get the Spring hate and what that has to do with creating "one big function and a few global variables."
I do the same with object-relational mapping--write a thin layer of classes to wrap INSERT, SELECT, UPDATE, DELETE. Not hard to write, easy to test, and you can always slip into SQL if you run into problems.
Use-case specific code should always be considered as an alternative to general frameworks.
Do you use Dagger for your DI needs or do you roll your own container?
Outside of class based inheritance which aspects of Object Oriented is Go missing? I code Go daily and consider it primarily an OO language.
I don't consider inheritance an important feature for a language to be an "Object Oriented Language." Inheritance: none, single, and multiple have been bandied about in the OO world for quite some time now so I understand where you're coming from.
I break out design from the language. While I wouldn't design very deep without knowing the language I was targeting I do consider Go as having a natural bias towards driving me to OO design over, say, functional or procedural. For me Go lends itself naturally to OO design. Sans inheritance, of course.
> "if you don't have these, then OOP is indistinguishable from FP or DOP. If Go is OOP, then which languages aren't, and why?"
Haskel, some Lisps, some Forths, etc. would be more non-OO languages in my mind. C is also a non-OO language to me even though you can certainly code it in an OO style.
I guess I agree with the blog author, OO is simply part of the language landscape these days and isn't going to be supplanted by functional any more than procedural was supplanted by OO. And with my mind-set then I'll say most all of Algol's recent children can be considered OO languages. So for me Go is definitely an object oriented language. But I now understand why you don't.
I mostly agree except for one catch. How do you extend a class which is encapsulated properly? Often this is the only way I use inheritance because I need to do one or two extra things that rely on the internal state of an object which I can't modify directly. So I'm wondering if there is a good way that doesn't rely on modifying the implementation of the original object to be more flexible.
Going back to the "old days" you'd reach for a pattern. Adapter pattern was the most common. Bridge or Facade maybe? But patterns fell out of favor a while back (imo) because they were tough on the coder's mind. A lot of languages added useful bits to create these pattern behaviors more naturally and without reaching for a book. I'd need to see code to be more specific.
So I'd rephrase that I suppose, perhaps we don't need something called Inheritance, but we do need a way to extend objects we don't control without breaking encapsulation.
As an example if you had a method that outputs XML and you want JSON, you can use an adapter object to call it, read the string, and reformat and pass it through. That works (and I've done things just as bad or worse many times from necessity), but it's definitely more hackish and less efficient than a solution that uses object fields to output JSON natively. Inheritance is my familiar way to do this in cases where I don't control the code (in which case injecting a Writer class of some sort probably makes more sense).
I'm not advocating against inheritance. If it works then go for it. I do when I'm coding in languages that have it. This stemmed from a thread discussing whether the lack of inheritance in Go was sufficient to exclude it from being an object-oriented language.
By definition, you can't extend objects you don't control without violating encapsulation. Encapsulation is all about controlling access so that the owner of a class can make changes without breaking downstream code. If downstream code accesses these private APIs, the downstream code may break, regardless of whether the access was via inheritance or composition (as a side note, "protected" makes no sense--who cares if downstream code breaks because it was accessed by inheritance but not by composition?).
> As an example if you had a method that outputs XML and you want JSON, you can use an adapter object to call it, read the string, and reformat and pass it through. That works (and I've done things just as bad or worse many times from necessity), but it's definitely more hackish and less efficient than a solution that uses object fields to output JSON natively. Inheritance is my familiar way to do this in cases where I don't control the code (in which case injecting a Writer class of some sort probably makes more sense).
You can do this just as easily without inheritance. You just need a way to access the member fields--whether you access them via inheritance or otherwise doesn't really matter. E.g.,
class Foo:
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
def xml(self) -> str:
return f"<foo x={self.x} y={self.y} />"
# Via inheritance
class JSONFoo(Foo):
def json(self) -> str:
return json.dumps({"x": self.x, "y": self.y})
# Via composition
class JSONFoo:
def __init__(self, foo: Foo) -> None:
self.foo = foo
def xml(self) -> str:
return self.foo.xml()
def json(self) -> str:
return json.dumps({"x": self.foo.x, "y": self.foo.y})
# Simple
def json_foo(foo: Foo) -> str:
return json.dumps({"x": foo.x, "y": foo.y})
> I'm not familiar with Go but apparently it has "embedding" which is a little different. Maybe that works. But it's largely playing the same role - a way to extend a class you might not control.Go's embedding is exactly composition. It doesn't let you access private member data of the embedded class. It's just syntax sugar for delegation. In other words, you could create the composition version of the JSONFoo class like this:
type Foo struct { X: int; Y: int }
func (f *Foo) XML() string { return fmt.Sprintf("<foo x=%d y=%d />", f.X, f.Y) }
// The compiler will automatically generate this method:
//
// func (jf *JSONFoo) XML() string { return jf.Foo.XML() }
//
// And for a given instance of JSONFoo, the compiler will convert all instances
// of jsonFooInstance.X to jsonFooInstance.Foo.X.
type JSONFoo struct {Foo}
func (jf *JSONFoo) JSON() string { return fmt.Sprintf(`{"x": %d, "y": %d}`, jf.X, jf.Y) }
Note that a `JSONFoo` isn't a `Foo`. This is an error: func printXML(foo *Foo) { fmt.Println(foo.XML()) }
func main() {
jsonFooInstance := JSONFoo{0, 0}
// printXML(jsonFooInstance) // error
printXML(jsonFooInstance.Foo) // correct!
}I tend to lean towards inheritance for functional extensions and composition for everything else, but there are so many ways to skin an OO cat.
If that's what people think OOP is supposed to look like, no wonder they don't like it.
I've yet to find a non boring, yet descriptive, consensual alternate definition of OOP; the first thought people have is more Java than Smalltalk, and if you exclude inheritance and everything virtual, then it boils down to syntactic sugar for single non-dynamic dispatch, and encapsulation. On the other (but still very sweet) hand, is Ruby OOP when you write 10.times { puts "hello" }? I don't know. Or rather: it's completely arbitrary. Even what is the most useful, when used cleanly is not exclusive of "OOP": invariants are also most important, and arguably way more well known by practitioners, in FP.
Natural language is inherently descriptive. If there is a better "OOP", you just have to fight to make it prevail, so that people think of it when they hear "OOP". Maybe we can make the bad teachers stop in other ways too. Like not even using the word, and making "alternate" approaches (maybe even what you are calling OOP) fashionable.
My problem with the example is that Book should not have a sell() method. Instead, BookStore should have a sell(Book) method. The machinery for how to send in a credit card transaction does not belong in Book. You still need it - it has to live somewhere - but BookStore is the place, not Book.
Ugh.
Polymorphism is far from unique to OO, and predates it.
If by encapsulation you mean information hiding, that's as old as the birth of OO. I've also sometimes encountered an alternate definition of encapsulation that more or less means "information hiding in OO", which seems a bit circular to me?
That doesn't mean OO can't have those features, but you didn't say "OOP usually includes loops and conditionals", because it's too obvious to mention.
You even know what the problems are with the design - you described them quite well. But it's not showing that OOP is horribly flawed. It's just showing you that you need a different design.
Off the top of my head you could survey the landscape of OOP books, blog posts, and code bases throughout history. But it doesn’t really matter—even if you don’t believe me and all OOP programmers believe these are design flaws, then my question remains: what distinguishes “good OO design” from data oriented design?
If you're having a hierarchy just to have a hierarchy, that's about as wise as having gotos just to have gotos. Nobody sane would do that today; maybe we'll get there with hierarchy, too.
I'm not sure what your definition of "data-oriented design" is, so it's hard for me to say how OO is different. I'll take a stab at it anyway, but know in advance that my response may be orthogonal to your question.
You've got data - say, data about a book, in the original example. In the structured programming days, that would be an "entity"; now it's an "object". The difference is that, with private methods, nobody can modify the book's data just by having a reference or a pointer to it. (You could kind of do this in C with a source file that would operate on the structure, and some of the methods being file static. But that doesn't keep any other code that has a .h file from modifying the structure without using the functions.)
The philosophical difference with OO might be that OO uses the compiler to enforce that data is only accessed in approved ways. This is a logical extension of static typing. (Of course, for non-static-typed OO languages, it doesn't happen that way. They enforce it at runtime.)
Or is OO encapsulation + inheritance? One problem is that the OOP community seems split about whether or not inheritance is a defining feature of OOP. And since encapsulation isn't a defining feature, surely it's inheritance. Unfortunately, there's no clear consensus, so OO as a term is not very useful.
> The philosophical difference with OO might be that OO uses the compiler to enforce that data is only accessed in approved ways. This is a logical extension of static typing. (Of course, for non-static-typed OO languages, it doesn't happen that way. They enforce it at runtime.)
This is an interesting distinction; ironically C compilers support encapsulation via opaque pointers while Python doesn't enforce encapsulation even at runtime (it's all based on convention--"consenting adults" and all that).
> I didn't say that inheritance hierarchies are bad. They're great... if you've got entities that are actually in a hierarchy.
I think there are cases where inheritance doesn't overtly bite you, but I've never seen a case where inheritance is actually cleaner than composition (with the exception that in many languages, inheritance is the only way to automatically delegate--they lack something like Go's struct embedding). Specifically in your Statement example, you could have gotten your polymorphism from a Statement interface instead of a base class.
Inheritance seems like just a particular subset of composition and polymorphism--there are cases for which it is appropriate, but why does it need to exist at all when you can get the same benefits from composing the constituent components (components being polymorphism and composition via interfaces/first-class-functions and object-/functional-composition, respectively)? It feels like having an add4() function--it's not always bad (sometimes you really need to add 4 to another number), but the more general 2-argument add() function works just as well in the cases where add4() doesn't overtly bite you, and it's much less likely to be abused.
Classic, pre-object Pascal. C. Very little new in the last few decades, because OOP is a very influential paradigm.
I just think more about hoe best to structure my code. Sometimes a OOP-like pattern works well, sometimes something else works better.
Having read "Software Fundamentals" [1], I think David Parnas [2] would beg to differ on that bit. Not the least because he actually invented the concept.
Parnas wrote extensively on information hiding as the basis for modular program construction, abstract interfaces that provide services without revealing implementation, and other software engineering topics.
[1] https://www.goodreads.com/book/show/1416932.Software_Fundame...
Whether or not the original quote meant to imply that I’m not sure, but do see how it could be read that way.
In short, information hiding is a concept from modular procedural programming that was then adopted by OOP, not an original OOP concept.
In general, OOP is really mostly a case of "let's take a bunch of known best practices and encourage (or even enforce) them on the language level". Procedural programming can be as object-oriented (or not) as you like. With minimal amounts of syntactic sugar in the compiler, like e.g. rewriting "foo.bar(...)" to "bar(foo,...)" you won't even be able to easily tell the difference when you see the source code.
While I'm at it, the critique that procedural programming necessarily results in a quagmire of global state is also wrong. It's all up to the programmer. Nobody forces you to use globals. Some OO languages try to force you not to, but usually fail in the face of determined opposition from the coder.
In essence, foo coders will find a way to produce foo code.
Mutate coarse-grained or global state is pretty much similar to functional programming approaches. The global state is a big tree, and there are functions designed for this type, there are also functions and types for every fraction of the tree. It's straight forward, elegant, and extensible.
It's basically what Alan Perlis said in SICP: "It is better to have 100 functions operate on one data structure than to have 10 functions operate on 10 data structures."
The only difference is just functional programming always transact the whole tree instead of mutating it, since they use immutable algebra data type.
It's not far from what Redux/Erlang process/Haskell ST Monad etc does.
IMO OO makes it more obscure and too subjective compared to this approach. It makes people lost the whole picture very quickly, and then accidental complexity comes in play in every corner.
OOP brought the terrible idea of coupling state and behavior, so of course "global state" would be wrong.
Yes this is correct. But data abstraction and encapsulation are found in all object-based code; they're not specific or related to OOP.
That is, it is more about the "how" than the "what".
This lets you isolate where your contract/invariant violations are more easily. Once addressed in the member functions, either it fixes it in the callers, or their use is now invalid and becomes a compile or runtime error.
https://www.gamasutra.com/view/news/169296/Indepth_Functiona...
For example, I have an object... what do I do with it? So I hit the '.' on my keyboard and read the list of things that the object can do.
This is why OOP is popular, it allows better tooling. If people want to move beyond OOP then they need to think about how the new style integrates with the IDE so that the programmer can hit the '.' or equivalent and get a list of useful things to do with the object in question.
Haskell has a powerful enough type system that it can enable "hole-driven development" where you specify the type of your code and then leave a "hole" in the implementation -- anywhere in the implementation, including making the entire implementation a hole! -- and the tools can suggest what should go in the hole, leaving new holes in the code.
Then you can incrementally resolve smaller and smaller holes based on the suggestions you get, and finally you have a completely tool-written program, where you just helped disambiguate when you had to.
This is the '.' on steroids.
Haskell is not popular.
"While >>= and return are the basic monadic sequencing operations, we also need some monadic primitives. A monadic primitive is simply an operation that uses the insides of the monad abstraction and taps into the `wheels and gears' that make the monad work. For example, in the IO monad, operators such as putChar are primitive since they deal with the inner workings of the IO monad. Similarly, our state monad uses two primitives: readSM and updateSM. Note that these depend on the inner structure of the monad..."
Or, possibly because when I make a post on Hacker News about how difficult it is to implement Quicksort in Haskell, the first response is a four-line Haskell implementation of Quicksort, spawning a 50-post subthread which points out that it's not actually an implementation of Quicksort, because it does not have the same performance characteristics as Quicksort, and instead has the performance characteristics of a lethargic water buffalo.
By the end of the thread, the question of whether it is possible to write a Quicksort in Haskell that has the performance characteristics of Quicksort is left as an open research question.
Jane Street uses OCaml, but of course that's not Haskell. All the other finance firms seem to use pretty standard C/C++ because they need the low latency.
Not really. This is all still painfully manual.
Both type hole development and auto completion are enabled by static types. Haskell could theoretically benefit from all these advantages that mainstream statically typed enjoy thanks to IDEA, Visual Studio, or XCode but the Haskell community has this arrogant attitude that they are above such "pragmatic" tools and as such, never took the importance of IDE's seriously.
So when you watch a video on Haskell's awesome type hole development, the developer is often using vim and typing everything by hand. In 2020.
That's just dot-syntax and modules. That has nothing to do with OOP itself. Nothing is keeping you from doing that in functional languages that support proper modules.
As a counterpoint (relevant Rich Hickey rant): https://www.youtube.com/watch?v=aSEQfqNYNAc
Non-trivial data structures are always conformed to some specification, doesn't matter if its Clojure or an OOP-language. If you receive an HTTP request over the wire you will have to check it. Once you conform it to the specification you know exactly how it looks and there's no need to jump through hoops to access the information.
I have a lot of respect for Rich Hickey and I feel like he Understands something that goes straight over my head, but his appreciation for generic, shapeless maps and lists is something I just don't get.
Its construction is dependent on it conforming to a specific form. In this case, your statically typed data comes from a parsed string that is then conformed to a specification to ensure that is has a certain shape. In Clojure you would just do the conforming to specification part and keep the data generic.
I think the appreciation for generic data has to do with the fact the fact that it makes Clojure code very generic and facilitates a lot of functional composition.
And I would assert that every maintainable C program is fundamentally designed in a relatively object-oriented way (functions that operated on structured, polymorphic function pointers, etc.) even if the original author wasn't particularly thinking in terms of classes or inheritance.
C programs aren't functional because C isn't functional (no inherent concept of closures), but Greenspun's tenth rule: Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp" usually applies.
i mean, the fact that you can use class style syntax do define object constructors in javascript and tho result is fairly coherent, seems to suggest that classes are not inherent to oo.
also, the fact that it's called “object oriented” not “classical”.
other examples of non-oo with strict encapsulation includes haskell, where if you lack the constructor, you can only use it according to the public api. it also uses single dispatch for typeclasses.
complex inheritance rare in oo - mostly only single inheritance is supported - and most oo design advocates pretty much say “don't use inheritance”. so it doesn't seem like it's intrinsic.
You don't need closures to do functional programming.
I was recently tinkering in some C++ code and thought "this part should be able to run in parallel." It was simply doing the same operation of a list of objects. It's a complex operation involving some temporary data structures. I had to dig through several layers of functions, but I kept finding that all data was created locally. Functions that returned complex result are passed a class to fill out. It was then that I realized it was written in a functional style - or at least my limited understanding of what that means. The top level function is now run in parallel, which required no changes beneath that level.
Now I prefer that style but like TFA here, I don't think 100 percent functional is a good idea.
That is in the eye of the beholder.
I can well believe that you would look at the code and decide that the author wrote in an OO way. I also believe that there are plenty of authors of such code who would disagree with your assessment.
My mind was blown when I first saw it. https://www.destroyallsoftware.com/screencasts/catalog/funct...
That's not what I'm seeing. The usual discourse around OOP is that some parts of it are bad. In particular, overriding concrete methods, and in general Java-like inheritance brings a lot of pain. "Everything is an object" from C# and Java also gets a lot of well-deserved criticism (https://steve-yegge.blogspot.com/2006/03/execution-in-kingdo...).
Other parts, like polymorphism, incapsulation, binding data with behaviour - I don't see that much criticism against this. For me, modern languages like Go or Rust (yes, I know it's almost a meme at this point, but still) bring sane OOP approach: while not being strictly OOP, they take the parts that work, and remove the parts that don't, while at the same time adopting many practices and abstractions from functional languages that proved useful.
* Data Oriented Programming. There is data, and there are functions operating on the data.
* Functional Programming. Variant of DOP, with a strong emphasis on purism and abstract algebra concepts.
* Procedural Programming. Variant of DOP, lacking language level mechanisms or best practices around polymorphic interfaces. Largely obsolete.
* Object Oriented Programming. Everything is an object.
Information hiding and polymorphism are readily available in all styles, except PP. The trouble with OOP is that insists on two big lies:
* Everything is an object, therefore data is an object, therefore data must be hidden. In fact, data simply 'is' and you can't hide it. Think JSON. There is nothing to hide.
* Everything is a polymorphic object, codified as the popular 'Open/Closed Principle'. In fact, most code is functions that just compute some data. Never needs N different polymorphic variants. If the business cases ever warrant such variants, simply refactor a polymorphic interface into the code base.
To further clarify, one can use DOP style in many languages, including languages that are ostensibly OO. DOP is a style, using data hiding and polymorphism as sparingly as possible, as opposed to OOP style, which strongly encourages using data hiding and polymorphism at every corner.
* Everything is emphatically NOT an object. There is immutable data, there are functions and there are modules. These are not the interchangeable, learn to use them in the proper context.
* Everything is emphatically NOT a message. There are plain functions, there are polymorphic functions, there are RPCs and there are async messages. These are not interchangeable, learn to use them in the proper context.
1. To try to force restrictions on someone who doesn't understand the reasons for these restrictions will not magically ensure good results.
2. To try to force restrictions on someone who does understand the reason for these restrictions is just a pointless nuisance.
It’s all just a bunch of functions and memory
http://www.smashcompany.com/technology/object-oriented-progr...
But it seems to me a more natural scale to use is how the state is managed. In procedural programming state is global, which is messy. OOP cleans it up a bit by splitting global state into pieces and making them local. Functional programming takes this even further by avoiding having to reason about state altogether.
This is a natural way to work on procedural code (in an iterative development fashion) to move from prototype/early code to stable, maintainable code. You create global variables, then encapsulate them in a record (perhaps keeping just one record globally), then you move to passing a record around (permitting multiple states to exist side-by-side). Ideally you'd start with passing the record around, but sometimes you just write code to get the concept implemented and refactor from there.
All programs have state. It's worse if the state is shared. It's worse if the state is mutable. It's worse if the state is global. It's the worst if it's all three.
Functional program still have state. They typically don't have shared state, or mutable state, or (much) global state, though. Those are real wins.
Mirror of that is a driver for a piece of hardware. You got multiple levels of state going on.
The difficulty with that advice is that it doesn't become applicable until you've gotten maybe 5 years worth of humble pies thrown in your face, for moments where you realize in retrospect that you made the wrong call.
Until then you don't have enough of a body of experience to tell two very different scenarios apart: "things are this way because my predecessors already learned the hard lessons" vs. "things are this way because my predecessors didn't know better."
Paradigms are really useful in practice as a shortcut to implementing 80% of the wisdom until you begin to learn the remaining 20%, and one of the roles of senior engineers is to make sure junior engineers don't mistake the 80% for the 20%.
This is just Chesterton's Fence again, and has the same refutation: if you're putting up a fence that serves some long-term purpose, it is your responsibility to put signs on it explaining what that purpose is. Otherwise people are right to assume it's one of the overwhelming majority of fences that do not, in fact, serve any purpose beyond obstructing their movement.
However, when you write the same architecture over and over, you don't write an explanation each time - for various reasons, a not insignificant one being that there's no obvious place to put it. (I use dependency injection in my code. Where should I explain why - on each class? In a separate file in the project? In the documentation? Should I write documentation, then? What if I write it and others continue to "new" services inside their code?)
Your approach absolves yourself of exercising agency to discern purpose in fences that lack signage in a real world where plenty of purposeful fences lack signage. Just because it’s someone else’s responsibility doesn’t mean they actually do it, and just because they didn’t put a sign there doesn’t mean you’re blameless if you cause a service outage.
I work with cryptography and it would take a book to put adequate signage on some fences. Nobody’s going to write why they’re using AES GCM every time they use AES in that code.
Towards the end of the talk note of the stickers on his laptop
it seems all programming paradigms end up devolving into global state by friday. because too many developers cannot recognise global state, instead recognising only its syntax
https://physics.stackexchange.com/questions/29175/why-is-inf...