Case against OOP is understated, not overstated (2020)
boxbase.org
boxbase.org
In accordance with this view, I think project architecture should be approached with an emphasis around how much state is necessary for it to run. This is why simulations like say someone making a game or simcity with like relatively independent entities that map to something in real life use OOP. If you're writing a service doing requests, you want as minimal state as possible. Singletons are state. Initialized/non-static objects are state. The smaller amount you have the easier it is to reason about the system.
As I write this however, I worry a little that my view is overly simplistic, or maybe applicable only to domains that I have worked in. If anyone wouldn't mind poking holes in this argument or offering examples I would appreciate it.
It seems that that's called a state machine, and OOP objects should come with state charts, but they don't.
> and this is where newer PL formalisms like "homotopy types" might end up being quite helpful.
PL research would actually get adopted if they didn't insist on using the worst possible names for everything. If they're not calling something an "intuitionistic type theory in the calculus of constructions" they're calling it a "pi-calculus".
If the alternative is stuff like "FactoryFactoryFactory", I'm not sure that's better.
That is because the state chart would so quickly explode into uncountable states or so difficult to understand transitions from state to state, that such a diagram would become instantly useless. Which only goes to show, how unrealistic the idea is, that you can really fully understand such a system and that shows what the problem is. Granted, there may be certain areas of related state, that are separated from other areas, but when the program becomes non-trivial, the lines usually blur, unless some kind of approach is used, which reminds me very much of FP, only that it wraps functions uselessly in classes and objects, instead of making use of modules and functions only.
In an FP style, ideally each function would be a thing you can look at separated from the whole system, if you know what its input can be (which may be difficult). That makes for testable code. I should be able to test every function separately, without having to use ten other classes to make instances to set up an environment, in which I just hope that what I wanted to test, can actually be tested.
That is theoretically impossible - complexity is fundamentally not decomposable into parts in the general case. That is, given a complex function, you may not be able to extract one more meaningfully separate part, leaving the core function still too complex.
OOP’s general idea is to encapsulate just enough of the complexity to make it possible to reason about its outside API, while the complexity will live inside, allowing the class to enforce some of its invariants.
Don’t get me wrong, I’m not saying that FP is bad, hell, I think that both paradigms are essential, they are not either-or choices.
What I want to express is, that I can call every function of the program separately. I might have to put effort into preparing the call's arguments, of course, but I can in the end look at its inputs and outputs in a unit test, separate from the setup of an environment. The environment is basically in the arguments of the function.
The FP paradigm encourages people to avoid global state and state mutation, which helps with reducing the setup effort required to make the arguments for the function call.
In an OOP style program, I cannot simply call and test every part separately. I will have to create a kind of landscape of objects, which experienced the set of state mutations, which hopefully sets up an environment, in which I can test for one specific case of a method doing what it should do in that case. That is the moment, when the state diagram has already exploded into uncountable states, usually impossible to keep all in your head. It might also be the case, that the constructor of an object interferes with the actual setup, that you want to have. Then you will need to apply mutations to change the state to get there, doing more work than ideally would be necessary.
I see OOP maybe still in things like GUI. People are trying to get declarative there as well or functional, but there it seems like a normal thing to have some widget really change state, to avoid overhead of creating a new widget and re-displaying it. But maybe in the future FP will invade this territory as well somehow.
And yes, you can combine FP and OOP, but many common practices used in OOP are detrimental to the advantages FP can bring. I thing it would be best to limit OOP to parts of the system, where it makes sense and then wrap it in an API, which protects the rest of the system from having to use mutation all the time. The question becomes again "What is OOP?". Is it still OOP, if I work with structs and functions working on structs, instead of objects? Do we use Alan Kay's definition with message passing and each object being its own little machine? I think in Erlang we have some combination of it. Like Joe Armstrong said in a talk with Alan Kay, it is either the most or the least OO lang. Well maybe nowadays we have different candidates for that as well.
But then a lot of proving (small p prove) an object in c++ is in a valid state amounts to hoare predicates and other spark-ada like expressions.
Certainly FP could do same and like c++ define that away when callers and callees think undefined behavior is gone?
This is most easily seen if you consider a TON of the domain specific languages out there. Logo, PostScript, DVI, GCode, etc. Many of these are "move to X" "put down pen", "move to Y", "pick up pen", etc. Very imperative and how you would talk to someone on how to do something.
So, if your objects give meaningful verbs to control the state that they maintain, it works rather naturally to reduce the code that you have.
Now, most OO today, that I see, embraced objects as records. And goes out of its way to not encode any language of behavior in the code they let you write. But, I don't think that is enough of an argument to say that abstracting some active objects into an OO paradigm is a waste and can never help.
Wrap the behavior in classes and have data as data. Hopefully this will be indeed the direction taken by Java with its records and other new TBD features.
This is primarily because of inheritance, which seems counter-intuitive. In a meta-analysis of OOP-based designs, inheritance is used as the primary form of composition with other strategies being either last-resort or added later when the inheritance is already deeply embedded as part of the design.
Inheritance is a brittle form of a composition (no-reinherit) that nests state in a deep tree-like type system, rather than isolating it into attachable modules. Most OOP-based languages have slowly had to adopt additional forms of composition, as inheritance is not suited well for cross cutting concerns. Ironically, almost anything added after the base class (and maybe some abstracts above that) is a cross cutting concern added after the core functionality is established.
I agree that inheritance creates a lot more problems, but the usages of non-static methods and internal state even in classes with no usage of inheritance can feel just as bad, when you have a high level method utilizing instantiated objects. Internal state as a whole can be avoided fairly often
some_state.do_stuff()
do_stuff(some_state)OTOH, the second one would be much better (and imply immutability) if it were written as
new_state = do_stuff(some_state)
But it'd also allocate a new state.let's have do_stuff=square and some_state=2.
What does square(2) imply?
I know there are mechanisms to avoid this, but many times they are opt-in rather than opt-out, and so it encourages this access-to-state propagation through the codebase where you have far reaching consequences
Whose meta-analysis came up with that? Like to see that.
> Ironically, almost anything added after the base class (and maybe some abstracts above that) is a cross cutting concern added after the core functionality is established.
That's a bold statement (unless you have a novel definition of "cross cutting concerns") and actually backwards: The super provides the generalization and subs specialize. A cross cutting concern is a 'general' concern. AFAIK, cross cutting concern is a term originated by the inventor of AOP, and the typical garden variety CCC deals with matters that rarely have anything to do with the types to which it is applied. (Debug log in-args is a garden variety example.)
You'll have to dig into each language that has expanded it's composition capability and the reasoning, but the outcome is self-evident. Many languages started with simple inheritance (eg PHP, Java, VB, C++, et al) and expanded composability mechanisms over time.
> That's a bold statement (unless you have a novel definition of "cross cutting concerns") and actually backwards: ... A cross cutting concern is a 'general' concern.
I'm not going to argue about how you wish to redefine things.
Good luck with whatever.
For example, Unity is designed around GameObjects but is now adding building out the DOTS framework stack that’s more of a first party ECS system.
The only real diversion from objects in games that I’ve seen is the work in Andrew Kennedy’s thesis on FRP for games.
The EC part of ECS is definitely object oriented - more so than mainstream object-oriented systems / languages, in that it favors composition over inheritance (i.e. entities are compositions of components).
But the S in ECS breaks the most fundamental tenet of object-oriented programming - encapsulation of state. And this separation between entities/components and systems is what makes data oriented programming so much cleaner and more powerful than OOP.
The issue isn’t about how you store the objects but about whether the framework logically encapsulates object data with its associated logic.
No it doesn't. There are plenty of examples in OO where that isn't true or no physics engine that's been build in the last two decades would work at all. The only core thing needed for OO is that objects have unique identities that then enable a notion of associated state at all. A pure system would not allow for an unbounded number of unique identities at all, you would not really be able to talk about objects at all (or entities or whatever object synonym is preferred).
Yes, yes it does.
> There are plenty of examples in OO where that isn't true or no physics engine that's been build in the last two decades would work at all.
It would be surprising if most physics engines were built in an object-oriented way. The only one I'm familiar with, ODE, certainly isn't object-oriented.
> The only core thing needed for OO is that objects have unique identities that then enable a notion of associated state at all. A pure system would not allow for an unbounded number of unique identities at all, you would not really be able to talk about objects at all (or entities or whatever object synonym is preferred).
No, objects in OOP have not just their own unique identities but have their own behavior. Encapsulation is the most fundamental pillar of object-oriented programming. Without that, any procedural C code that makes use of structs could be called object-oriented, since structs would then be synonymous with "objects".
One of my "ahhh moments" with ECS was when someone pointed out. The ECS has a very 'strict' boundary on DATA(attributes,entities,components) VS BEHAVIOR (methods,systems,functions).
The 'traditional OOP' usually 'smash/hide/design/encapsulate' these two conflicting powers into one 'Entity'
I think it's 'easier' to write a ball-of-mud in OOP for games, then it is to write ball-of-mud with ECS.
*NOTICE I said 'easier' not 'impossible'
I love the distinctions between BEHAVIOR and DATA, from an organizational point of view. The bigger the whole system is the more value I think a good programmer can get with ECS.
Imagine having to "implement" gravity for 100 different types of 'Objects'. Sure The ECS way would be to have a System(Gravity) that executes the 'same computing' on all entities with a 'MassComponents' and 'PosComponents' for example. The Entity would usually not need to know ANYTHING about Gravity. Sure the OOP way one can inherit but eventually you inherit yourself into a corner (diamond-problem).
My 2cents - YMMY
TL;DR (just squint hard enough)
ECS ~= (Data | Behavior)
OOP ~ (Data + Behavior)
EDIT: just to make it clearer, both Simula Style OO and Smalltalk Style OO bundle data and behavior together. The Entity owns the Components and the System.
ECSs are much more like relational databases where an entity is a primary key, components are tables and systems are queries (that also mutate the database). The entity is subservient to the systems, not the other way around.
If somehow a relational database is in any way equivalent to OO then we've reached peak clown world as far as terminology goes.
My understanding of data oriented systems is there's a desire to put related data physical close, in memory. It doesn't remove state, it just packs it differently.
To be honest, I'm confused by most of the comments here.
Really, the primary benefit is that you get a much cleaner paradigm to work with. Here's a good basic overview of the point of an ECS system: https://iolivia.me/posts/entity-component-system-explained/
In particular I'll point to the key quote in that post that distinguishes ECS from OOP encapsulation: "The whole idea is separating behaviour from logic, so all the data goes in components and all the behaviour goes into systems."
Intuitively, yes.
In practice, I think once you actually use an ECS for a game you won't want to program them in the traditional OOP way.
Except games rarely have that rigid a structure, and maybe you want a sword that also has laser gun features, or an axe that's also a magic staff. That's a lot of subclassing and further dividing up your game objects into rigid hierarchies that become difficult to modify later without unintentionally breaking stuff (was it the logic in the Sword class that grants that damage bonus, or in the Weapon class?)
It's nicer to be able to say "I have a weapon entity, I'm going to attach the `SliceDamage" component, the "MagicAttack" component, and so on, and call it a magic axe. It's easier to maintain, it can change at runtime, and the behaviour code only exists in a component file that only does that behaviour, rather than nestled in some lowest common denominator of a hierarchy of classes.
OOP itself could be good for problems where you need state machines. The Erlang Actor model is successful for a reason, but I wouldn't apply OTP to general programming.
People love to hate on Java, but a Spring app with everything set up into a graph of interacting interfaces is about the most well-organized code you'll ever see, and it encourages modifications that maintain thoughtful organization.
I don't know if it's just me but I watched that talk a couple years into my career and it was like something clicked into place in my brain. It changed the way I think about software.
In some sense, the only distinction a "pure" function has over "non-pure" is that it declares all its inputs/outputs (as function parameters and result). We say that a non-pure function has "side effects", but all that actually means is that we don't readily see all its inputs/outputs.
Even a function that depends on time could be converted to a pure function which accepts a time parameter - this is conceptually the same as a function which accepts a file, or an HTTP request or anything else from the "outside world".
The trouble, of course, comes from the tendency of the outside world to change outside of our program's control. What do we do when time changes (which is all the time!) or file, or when the HTTP request comes and goes never to be seen again?
Or when the user clicks on something in the UI? Can we politely ask the outside world for the history of all past clicks and then "replay" the UI from scratch? Of course not. We cache the result of all these clicks (and file reads and network communications and database queries...) and call it "state". When the new click comes, we calculate new state based on the previous state and the characteristics of the click itself (e.g. which button was clicked on). This is a form of caching and keeping a cache consistent is hard, no matter what paradigm we choose to implement on top of it.
The real-world example of this would be React. It helps us implement the `UI = f(state)` paradigm beautifully, but doesn't do all that much for the `state` part of that equation which is where the real complexity lies.
That looks like a terrible mess.
The problem is not state, but messy access to it.
(I'm not saying that OOP doesn't have its place, but it has clearly turned from a way of structuring code to universally strive for into something to avoid if possible)
Chapter 3 of SICP deals with this topic in great detail.
Hickey's fundamental contention is that whether something is easy is an extrinsic property whereas whether something is simple is an intrinsic property. Whether something is easy is dictated often by whether it is familiar, whereas simplicity lends us the more ultimately useful property of being understandable.
To which I'll counter with Von Neumann's famous quote about mathematics : "You don't understand things [simple]. You just get used to them [easy]."
There is no fundamental difference between ease and simplicity. Simplicity (of finite systems) is ultimately a function of familiarity. There's a formal version of this argument (which is effectively that most properties of Kolgomorov complexity when applied to finite strings are defined by your choice of complexity function, even in the presence of an asymptotically optimal universal language. In particular there is not a unique asymptotically optimal universal language, that is the Invariance Theorem is overhyped), but the informal version is that both simplicity and easiness arise from familiarity.
Indeed the fact that there is "ramp-up" speed for simplicity suggests that in fact what is going on is familiarity. E.g. splitting state into "value" and "time" is one way of thinking about it. But I could easily claim that in fact "time" complects "cause" and "state." Rather state machines where the essential primitives are "cause" and "effect" are the proper foundations from which "value" and "time" then flow (you can think of "effect" nondeterministically, a la infinite universes, and then "value" and "time" fall out as a way of identifying a single path among a set of infinite universes). Likewise Hickey claims that syntax mixes together "meaning" and "order" whereas I would could just as easily say that "order" complects syntax and semantics!
What of the idea of "being bogged down?" That "simple" systems allow you to continue composing and building whereas merely "easy" systems collapse and are impossible to make progress on past a certain threshold? I claim that these are not intrinsic properties of a system. They are rather extrinsic properties that demonstrate that the system no longer aligns well with the mental organization of a human programmer. However this is dependent on the human! A different human might have no problem scaling it.
Now hold on, perhaps, while simplicity is perhaps dependent on the human mind and humans all more or less have the same mental faculties. Perhaps we can't find a truly intrinsic property that we call simplicity, but perhaps there's one that's "intrinsic enough" and relies only on the mental faculties common to all humans. That is, returning to the idea of "being bogged down," there are systems whose complexity puts them beyond the reach of all, or at least most, humans. We can then use that as our differentiator between "simple" and "easy."
To which I would reply that this is probably true in broad strokes. There are probably systems which are are so arcane as to be un-understandable by any human even after a lifetime of study. But at a more specific level, the way humans think is very varied. The ways we learn, the ways we develop are hugely different from person to person. Hence I find this criteria of "bogging down" far too weak to support Hickey's more concrete theses, e.g. that queues are simpler than loops or folds.
When you're talking about things like love, hate, and fear, sure maybe those are universal enough among humans to be called "objective" or to have associated "intrinsic properties," but when you're talking about whether a programming language should have a built-in switch statement, I don't buy it.
For the purposes of programming languages, simple is not made easy. Simple is easy. Easy is simple. The search for the Platonic ideal of software, one that relies on a notion of intrinsic simplicity, is a false god. Code is an artifact made for consumption by humans and execution by machines and therefore any measure of its quality must be extrinsic to the humans that consume it.
Sometimes X is simple. Sometimes it's not. It all depends on the person.
As empirical evidence of this I leave this final exchange between Alan Kay and Rich Hickey where the two keep talking past each other, no matter how simple their own system is: https://news.ycombinator.com/item?id=11945722
Every bit of state creates potentially exponentially more possible entity states. So therefore limiting potential changes in state limits the amount of working memory necessary to understand the system. Its starting with "can't" and then building a "can" when necessary, which is a lot better on memory, comprehension and feeling safe/secure to make changes then starting with a collection of 10^n "can"'s and adding in "can't"'s.
But that's immaterial to your main point, which is that adding state into the mix of things makes things hard. Which I agree with, but again to steelman the point, I could turn around and say that values allow for exponentially more possible values as well! When I see a map passed into a Clojure function I have no idea what could be in that map!
I think the main objection here which you are alluding to is one of "global" vs "local" reasoning. With a value I just need to worry about the body of my function, whereas with (global) state I need to worry about every function everywhere! But what if that's just a problem with our tools rather than an intrinsic issue? What if I had a tool that could automatically present all the mutable state of your system that is publicly accessible as a single screen and automatically link to different procedures that link to different parts of it? At that point I don't see much of a difference between state strewn everywhere and nice orderly values plumbed everywhere. In fact maybe it's nicer to have that implicit state strewn everywhere instead of having to carry around values which are irrelevant for the bulk of a function body and only relevant for a single part of a subfunction. What if it's all just a matter of not having the right IDE?
Working memory is definitely a hard limitation and universal enough among humans, but it's not clear to me it's a specific enough concern to convincingly justify certain programming language features which may just be crutches for inadequate visualizations or different educational backgrounds.
> In fact maybe it's nicer to have that implicit state strewn everywhere instead of having to carry around values which are irrelevant for the bulk of a function body and only relevant for a single part of a subfunction.
I would call this an anti-pattern in FP. It's often a symptom of trying to replicate more imperative styles like OOP in a pure language. Threading mostly-irrelevant state through a bunch of different functions is a sign that your program is under-abstracted. If you think of all the function calls in your functional application as a tree, state should stay as close to the root of the tree as possible, kept in nodes it's relevant to, and the children and especially leaves of these nodes should be decoupled from it to the greatest extent possible.
The problem is that often you do want fairly complex state in the leaves of the tree, but want very little of it in anything else. Web browsers are a classic example of this. Pure FP solutions such as Elm that completely eschew the idea of local mutable state require a lot more ceremony to implement something like a form (the classic thorn for Elm users). By forcibly moving up the state to the root, you sometimes end up needing to pull some fairly severe contortions.
E.g. the usual answer to move the state back up to the root in the land of statically-typed, pure FP is to express it in a return type (e.g. a reader or state monad, culminating in the famous ReaderT handler strategy in Haskell) or in the limit bolt on an effect system instead. The usual answer in impure FP is to accept some amount of mutable state and just rely on programmers not to "overdo" it.
But from a certain point of view, writing an elaborate effect system whose very elaborateness might cause performance issues and inscrutable error messages sounds suspiciously like trying to work around a problem in visualization with an over-engineered code solution. And from another perspective it feels a bit like a trick. If some function has a lot of state, then I would hope by opening up the definition of the function I'd see how it all works, but with an effect system all of a sudden I've split things up into an interpreter that actually performs the mutation and an interface that merely marks what mutation is to be done. It feels like I've strewn logic around in even more places than if I just had direct stateful, mutable calls there!
I’m also very pragmatic and to the example of a web browser I would say, most applications are not web browsers. The overwhelming majority aren’t, in fact. I’ve chosen at this point in my career to mostly focus on enterprise software development, which I believe was Rich’s original field as well, and I’ve seen an enormous number of solutions with too much state cast about everywhere that benefit massively from centralizing the state high in the tree and really thinking through the data model carefully. So I stand by the principle I advocated originally, but it’s not universally applicable. It’s my belief that one of the core virtues of software development is knowing when to apply which principles.
I should've clarified. I meant developing a web page to run on a web browser, hence the form example.
I also have a separate service that the server makes calls to, which doesn't run on this server in production (it has its own production server), but does run in dev and test. Each dev/test system runs a separate instance of this service, which has its own separate connection pool(s), and setting this up was trivial.
Needless to say, failures are reproducible and meaningful. There is no mocking -- we test against real local services with real local DBs. (There are still some remote service calls which I'm slowly replacing, and some flakey, unavoidable remote dependencies in a few browser tests).
I didn't do anything special to make this possible other than naming the config files "service-name-config" instead of just "config". It is just the natural result of passing state in explicit arguments. The same is not true of global state.
> It is just the natural result of passing state as explicit arguments.
But nothing you've mentioned here is intrinsic to mutable state. It seems like all that's happened is you identified a part of your program that you wanted to be configurable and exposed a configuration knob. If for example you wanted to make it so that there is a test mode that where you want to prefix "test-" to every string written to the DB that would also probably involve a new argument somewhere. There's nothing here special about the mutable state part of it.
The world needs this. I think Pernosco has a workable technical foundation, but the GUI is a debugger and I need a code exploration tool to "find my way" in big unfamiliar codebases. Encouraging developers to pick up and hack around in others' codebases is the only way to get enough eyeballs to make all bugs shallow.
> maybe it's nicer to have that implicit state strewn everywhere instead of having to carry around values which are irrelevant for the bulk of a function body and only relevant for a single part of a subfunction.
I think global state (which is unusually bad) or shared mutable state (which is omnipresent outside of Rust) is a mental overhead (more things to keep in mind). I don't think tooling can eliminate the overhead of worrying about moving parts, only make it faster to look up (and hopefully document) what touches each bit of state.
In the case of Clojure, the map that you pass to a function is a value. It is guaranteed not to change underneath you and it can be freely shared with anybody.
> concurrency plus parallelism into the mix
The hard part of concurrency is writing or writing+reading, not just reading, so an immutable map isn't going to solve everything. Instead the hope is that you confine the mutability to one place with various transactional guarantees (in Clojure's case, this is usually atoms) and then everywhere else you don't have to worry about it.
But then again why couldn't the same analysis be performed on mutable state? How are we sure this isn't just a tooling issue? If we knew exactly what parts of mutable state were being touched by what we could identify what critical sections needed various guards.
Taking my hat off and going back closer to my own views, I actually think Clojure's combo of maps+atoms are an arguable case where Clojure has in fact complected things together in a way that e.g. STM doesn't (and Clojure's implementation and use of STM has its own problems). Namely it's complected committing a transaction with modifying an element in a transaction.
To illustrate the problem, right now Clojure atoms basically give up parallelism entirely. If you have a map in an atom with two threads modifying different keys, then those threads have to come one after another. It's actually kind of a waste of resources compared to the single thread case because work done in one thread will be thrown away and retried if the other thread wins.
So if you want true parallelism when modifying different keys you can use a ConcurrentHashmap. But that then gives up atomic updates of multiple keys at once! (Or you can have nested atoms but that has its own problems and doesn't solve the inter-key atomicity issue).
It looks like an all or nothing proposition where you either get non-parallel but fully atomic map updates or parallel per-key updates but nothing in-between. These kinds of false dilemmas are a classic symptom of complection.
The way other languages with an STM system deal with this is to build concurrent maps out of STMs refs. That way you get exactly the amount of parallelism you can relative to the amount of atomicity you need. If you have a transaction that touches two keys at once then both of those keys are atomically updated together and those two keys form one unit of parallelism. If you have a transaction that only touches one key then you have per-key parallelism. If you have a transaction that touches all the keys at once then you just collapse to the normal case of a map inside an atom.
As far as I can tell the reason Clojure doesn't do this (but other languages have) is that its STM API is a bit clunky and missing some interesting combinators.
All this is to say that maybe indeed simplicity and ease aren't all that different if from one perspective atoms are simple and from another merely easy.
I'm not going to delve into STM because that can be a whole book worth of discussion :). It's a fascinating universe, I've spent many hours (weeks, months?) exploring it, and I don't consider myself even close to an expert.
You are absolutely correct about the trade-off about atoms in Clojure.
Practically speaking, to start seeing retries you'd have to have a big number of updates going on at the same time. You can push a huge number of updates through a single thread. If you do have the need to do big throughput, you can explore not-so-idiomatic options like atoms-in-atoms, like you said.
IMO, the biggest unique benefit of combining atoms with immutable persistent data structures, comes from the fact that you can get unlimited number of consistent readers virtually for free. Any thread can look at (aka, deref) an atom, while the state/world keeps moving forward. I don't think any amount of tooling can solve that case for mutable data. A snapshot of a mutable data structure would require copying the whole data structure while using some sort of a locking strategy to stop writers while the read is taking place.
This bit is wher ehe says that about magnets: https://youtu.be/P1ww1IXRfTA?t=1300
>>> It all depends on the person.
Based on what I've recently learned about neuroscience and optogenetics, I don't think there's much evidence to support this sort of relativism. On the contrary, many processes in mammalian brains have common mechanisms.
To explore more, this is a great podcast https://peterattiamd.com/karldeisseroth/
Disclaimer: I am a complete layman on the topic, so please correct me if I'm wrong.
I’m fairly sure this great quote is about mathematical “objects” in that you will never be able to truly “understand” or have a “real feeling” for more complex ones, like higher dimensions. Yet, by applying some simpler rules we can use and transform them, and after a bit of practice that will make it feel “close to us”, or “real”.
> Simplicity (of finite systems) is ultimately a function of familiarity.
I really don’t believe it would be true. Maybe I’m misunderstanding, but no matter how familiar I am with a given crud program vs JIT compiler technology, the latter will always be complex - but as you later refer to, I’m sure you know the difference between essential and accidental complexity. But in this view I would rather say that simple things are ones with minimal accidental complexity, while the easy-hard axis is about the essential part of that, that is irreducible.
While the 'simplest' (in the physics sense) description of something is elegant, it can also be extremely hard to understand and work with. Maxwell's equations are used in engineering for a reason - and not their simpler theoretical physics underpinnings.
But where are the examples? Not a single example of something easy versus simple, or how something "easy" would resist change or be harder to debug. All of these concepts sound fantastic until you begin to write code. How do I apply it? It's a great notion to carry around, but I often wonder if this is just someone's experience/opinion boiled down to a really well done talk, and not much else.
The system is made more simple if you remove the function, though.
This is more so if only part of the behaviour of a function is no longer desired - the function becomes easier to understand when it's trimmed down, but it's harder to make that change.
Simplicity in a vacuum isn't a good thing. Ideally your solution targets the exact level of simplicity vs complexity required for your problem. Obviously you won't always hit or know the target.
The value in simplicity is greater composability. It's especially important for the building blocks of our systems - of which programming languages make a huge portion. It doesn't sound too controversial to say that it's easier to take multiple simple things and make a more complex thing, than it is to take a complex thing and distill it down to the simpler thing you need. I say this because regardless of what programming paradigm you adhere to, the "kitchen sink" unit of code is universally derided, be it god modules or god classes that does shit you don't need.
It's not that Clojure is all simple, all the time. There is mutable state in Clojure - atoms, refs, etc. They also have interfaces. And multimethods. And so on.
But the simplicity floor is lower in Clojure than most other languages I've used. More than those other languages, you can target the level of simplicity you need. And it provides for more complex elements if you need them. And in my experience, a lot of the time, you don't need those more complex elements.
Steve wrote a simple CRUD API that gets some data and returns it. Bob tried to be clever and write a loosly typed declarative cluster fuck that nobody understands, but it's "easy" if you dont do anything interesting or useful with it.
Or like an improv exercise where you have to improvise a dialogue, but only by using questions, no afirmations.
Can it be done? Sure, but not by most people, not in real time. Again, wonderful when you see it done right.
Improvisation. A constrained dialogue. Affirm? No. Question.
Can it be done? Sure. Most people struggle slowly. When right? Wonderful.
I submit that if we did that, we would have excellent, elegant, simple software, but following the process would be incredibly hard. So hard, in fact, that it couldn't possibly be distilled into a conference talk.
Once you get traction, you can start to afford to have the crazy vision. IMO, at that point it's easily worth the risk. A decent research team will probably discover something, and potentially extremely valuable knowledge.
If you were James Clerk Maxwell before he published his equations, how much would they be worth to you, especially if you had paying customers?
So instead we've had to make a system which makes it less painful when bugs occur.
For us that means making it trivial to run older major and minor versions our software, and an automated update mechanism which delivers new builds to customers on-premise in less than an hour, updating the DB schema as well.
(Also, as one of my past companies enshrined as an engineering axiom: "write software to be debugged". Most programmers write waaay too few logs. You know the print statements you add to your code when it's buggy, to track down what's going wrong? Well, do that all the time, and if there are too many then fix that problem with adequate tooling. If it's running on your customers' computers - whether servers or PCs or phones - then store them locally for N days / N logs and allow them to be submitted when a bug occurs. Stack traces - even good ones - are not nearly enough.)
Even for domains that are stable and knowable, I have to wonder what businesses can afford that kind of up-front investment before the first feature ships.
Ultimately, I think we have to make a trade-off between simplicity and easiness. The approach I outlined would be incredibly expensive because the tooling for that approach isn't quite good enough yet, and stakeholders wouldn't even understand it. They wouldn't realize that you were building a pitch for your product not as a PowerPoint deck, but as executable code!
A lot of our complexity today is from constructing software itself over layer upon layer of previous complex software (CSS, I'm looking at you), not due to the intrinsic "business cases" our software is meant to solve. Some of that complexity cannot be avoided, and some of it could be but at significant cost. To use an analogy, it's also cheaper to build a traffic light-controlled intersection, but overpasses are simpler.
Coincidentally, almost all of the tools I've seen that try to make simplicity cheaper come either from the Scheme/Racket/Lisp world that Hickey himself hails from or from Alan Kay and his sphere of influence. (The two groups have quite a bit of overlap, both in terms of ideas and even people.)
I know about Smalltalk (Squeak) so I guess that is the playground for Kay’s. Would just playing with Clojure do the same for the Hickey’s?
But even there, I suspect adaptation has to happen. Python's had how many versions over the years? Indeed, I could argue that it's one of the world's most successful languages precisely because it keeps responding to user need. Or look at tax software, which is going to change at least every year, and more often in emergencies.
So I suspect at best these other domains have a slower iteration clock. Which might be slow enough for the sort of formal modelling that is described. But then I think there's an open question: do other methods also work just as well with slow iteration clocks?
Coming up with the right abstractions and the right domain model is difficult (especially if you just sit down and try to come up with stuff, you're likely to get it wrong the first time around). Knowing about some of those things could help you come up with better abstractions, but it's neither necessary nor sufficient to ensure that you will.
Take dependent types for example. They allow you to express more program invariants or correctness properties in your types. But actually using them requires you to write proofs (at least, if you're using them to their full potential). And I do think that in general System F like type systems hit a nice sweet spot and are generally good enough for the stuff that you might actually want to handle on the type system level.
I've also run into similar "proof-like" situations with much simpler type systems like those of Haskell and Rust, where I was structuring my types to "make illegal states unrepresentable", but in the process ended up complicating my program due to having to match the structure of my program to the expected structure of the types. Sometimes it is nice to _not_ to have the type system enforce some of your invariants. (Such things are also doable with dependent types of course, but this is just an example of some of the tradeoffs involved).
You can also still have a shitty domain model even if you use all of those fancy tools. They just allow you to be very formal/precise about the domain model (and do perhaps encourage some more uniformity by making it more annoying to express ugly or complicated things).
I think it's hard to provide examples since they would all be implementation dependent.
simple to me is a stage of the thought process that will become apparent only after putting in the extra work. It's not just applying "this 1 trick". Making it simple is its own unique challenge. E.g. my first iteration of an idea is always a mess. Then I rework it enough times to make it presentable (a state where it "works" and I can reason about it with others). But on the job nobody pays me to make things simple because that means spending another 10-30% of the budget on it. making things "simple" at work is nearly impossible to sell because people quickly through arguments at you like "perfect is the enemy of good", and few jobs give you a "definition of done" where making things simple is part of it.
Another reason why it's impossible is that the best time to rewrite a greenfield project or an MVP is before you add additional features. But at that point people will not allow it because the expectation usually is to build on top what you (they) invested in previously.
What state takes away is access to a given value at any other time but now.
It's always now; every value is the current value and no other version of that value exists.
Of course, Simple Made Easy is excellent too, probably his most influential talk.
Reminds me of deterministic finite automaton. Is that what you mean?
The serious comment here is that the real world imposes a minimum floor on the amount of mutable state that you have to model. Databases are giant piles of mutable state. Maybe we should start talking about "essential state" and "accidental state" the way we talk about complexity.
At the end of the day, users interact with state. We need languages and techniques that manage it well.
You end up get some states that should just the same but in multi places.
And this is almost one of a biggest source of bugs. Because you WILL do it wrong. As the code grows, the place you need to manual synchronize data grows. You end up miss it, create a lot of bugs.
On the other end, in most FP languages. It is pointless to dupe state most of the time, so this kind of problems did not happen so much in the first place.
BTW: Personally I like the idea of the [computed](https://v3.cn.vuejs.org/api/computed-watch-api.html#computed) primitive of vue, because it make the getter/setters first party encouraged. And getter/setters never add states. If you use it properly, states can be reduced by a lot and with much shorter code. While it looks the same as manually dupe them on surface, so you don't need refactor everything just to use it.
Beginners want to keep functions short and the way to do that is to chunk up a bigger method into several smaller, then realize oops, that you needed that variable in both functions. Store it into “this” and now instead of one decoupled function you have two coupled functions.
Contrived example written on phone but code like below is extremely common, especially from Java coders who have been mislead to make classes for everything and haven’t learned the static keyword yet. Here obviously the self.stuff is the accidental state creating coupling between the functions, that now carefully have to be called in correct order and any of their mutations to self can impact the other.
class Worker:
def init()
self.setup()
self.foo()
self.bar()
def foo()
self.stuff = fluff
def bar():
do_work(self.stuff)
Rather than just do_work(bar(foo(fluff))).I don't think this is the case, or at least it hasn't been for quite awhile.
Any gamedev I've known in the last decade or so would reach for an ECS[0] if they wanted to clone SimCity, in other words they would design it somewhat like a normalized database.
Each character, building, zone, or whatever in the game would be an Entity, represented by a unique ID.
Then there would be collections of Components, which are basic structs like Position or Sprite. A set of components that are all tied to the same ID would represent a single Entity the same as if it were an instance of an Entity class. These components together hold all of the game data, to the point where a naive save game system could just serialize all the (non-pointer) component data and be done. How these are stored varies by implementation and configuration, but a table in a database is a reasonable mental model to understand the core concept.
The game logic is executed in Systems, which are functions that read and write component data on some schedule. The simplest example is a velocity system, that would find all the entities with both a Position component and a Velocity component and update the Position accordingly.
In the case of a SimCity style game, the ECS approach is much more cache friendly, for both instructions and data, because you're handling all of the same work at the same time, instead of updating each entity one at a time which leads to cache miss after cache miss. This can bump the max number of agents in your simulation by multiple orders of magnitude.
Some other benefits are:
* Empower designers to iterate more quickly by giving them an editor where they can change components out without changing code. Say you have Players and Enemies and they both have Health, and Walls which are simply props with Collision. If you want to try destructible environments you can simply add a Health component to your Walls in the editor rather than try to move Walls into your LivingEntity inheritance chain or modify everywhere that does damage to check for WallEntity in addition to LivingEntity.
* Easier to parallelize. If you're using objects and you want to start multithreading you quickly start feeling like mutexes are the only answer. But with an ECS if your Systems only operate on their arguments, then you can run any systems in parallel that you want, as long as anything you mutate is not referenced in any other currently running systems. For instance every Rust-based ECS I've ever seen does this out of the box, because they can tell what fields are mutable from the function signature.
* Easier to test. If all your movement system cares about are entities with both Position and Velocity, then that's all you need to setup to perform a test. No MockPlayerInput or headless rendering required, except where those are actually the thing under test.
This sort of begs the question: where does classical OOP, the one taught to all undergrad CS majors in programs that use Java or C++, really fit in nowadays?
I have no idea. The smalltalk ideal of OOP lives on in all sorts of ways, but the deep inheritance chain version that gets taught in classes? I don't think it has a place apart from maintaining existing code.
Smalltalk style OOP has its own issues, but there's lots of good aspects to it and you really have to experience Smalltalk as a complete system to really get it, it's nothing like Simula/C++/Java-style OOP.
That said, best thing you can learn in your programming career is to get rid of "Customer" classes (or whatever the equivalent is for your particular problem). A customer is a unique entity represented by an ID, a private key if you will, which links data in various systems.
Need to split some part of your codebase that handles authentication into a separate microservice for whatever reason? No problem, the same key can be used to refer to data in that system.
Your program magically becomes more modular, more maintainable, easier to understand, and more performant, all in one go.
Use OOP when you need an abstract interface to something that can be implemented in various different ways, e.g. data structures or plugin systems, stuff like that. Any time I see "PODs" full of getters and setters I cry on the inside (and sometimes on the outside).
"That said, best thing you can learn in your programming career is to get rid of "Customer" classes (or whatever the equivalent is for your particular problem). A customer is a unique entity represented by an ID, a private key if you will, which links data in various systems."
What methods should it have? What does a Customer do exactly? Or is it just a POD which simply stores customer data? If so which data? Does it mix authentication information with the user's purchase history? Maybe not that but what about the user's little Avatar in the UI?
The answer is none of the above, the single responsibility of the Customer class is to identify a user, that's it. In the end that's just a number, no need for a class. The purchase history of a user is only relevant to the system that manages the purchase history. The avatar is only relevant to the UI. The authentication information to the authentication service.
A Customer is not in and of itself an "entity" of some sort with associated behavior in the OO sense. But you see this all the time with companies trying to model these taxonomies of their business as class hierarchies.
It's an unhelpful practice that imposes a structure on your code that's not relevant in a any way to the actual functionality of your software.
But I cannot fully comprehend in which scenarios this would be a clear winner and which it would add too much overhead. There is something to it though, so thanks for opening my eyes.
What is the deepest chain we find in SmallTalk itself? I can't check it right now, but I'm sure it's not as deep as some classroom examples I've seen. Inheritance makes a lot of sense when you are modelling the world, but most structures we see in the real world aren't that deep.
Honestly OOP did a lot better in those than the procedural toolkits they replaced in many ways. It was generally "better".
So if we're going to dump OOP for ??? I would say that a really amazing GUI toolkit that is pretty clearly better than the classical OOP inheritance model would really underscore the point. I can see a compositional interface GUI toolkit that is great.
I have since those days not done a native UI, so all of the modern ones, from KDE/Qt, GNOME, whatever the hell replaced MFC on windows, etc I am clueless about.
Plus all UIs are kind of dominated by HTML/CSS/javascript style of code, which is its own special evolutionary tree at this point.
Hot take: nowhere. I mostly kid. But really I don't know of any domain in which strict inheritance and data hiding beats mixins/composition, interfaces, and dataclasses. It's so much easier to reason about the behavior of an interface in a given role, than an "Object" which spans all kinds of scopes.
It is not clear that such strawman OOP was ever really taught, let alone ever practiced. Mainly it seems to exist as something to be disparaged.
Edit: It's actually the iOS app, not the Android app, crazy either way. But yeah in university I was taught the whole "a cat is a feline which is a mammal which is an animal" shtick. Completely useless in the real world.
A square is a kind of rectangle but the "API" of a Square is more restrictive than that of a Rectangle (where both width and height can change independently). I've seen this issue discussed in either this year's or last year's CPP-CON...
In OOP systems that support predicate-style dynamic inheritance, rectangles pick up the square trait only when their width is equal to their height. We don't talk about such systems very much today (JavaScript does not have a notion of type, and updating an object's type is a mutable operation rather than one driven by a rule), but there were experimental systems from the 90s that looked at this.
Either way, the problem in my view is that the question is silly to begin with, Squares and Rectangles should be PODs (or, even better, simple structures), and not have any associated behavior. The decision of whether they are mutable or not then depends solely on the needs of the software, and not on irrelevant modelling constraints unrelated to the problem.
There's no need for them to have a taxonomy relation unless the problem being solved involves taxonomies of shapes.
Either way, it's simply not true that you can't treat them the same way because you've made them PODs, there are more kinds of polymorphism beyond subtype polymorphism. Even for the latter modern languages split the data (structs) from the behavior (traits/protocols), see Rust or Swift for example.
If you explicitly need to treat squares and rectangles as a single entity to draw them (alongside other shapes), rather than thinking of each of them having a "draw()" function (likely breaking the single responsibility principle), you should instead have a function square_to_drawable, rectangle_to_drawable etc, where a drawable is also a POD but one that has the relevant drawing data (textures, triangles, whatever). Then a drawing system is responsible for actually rendering these drawables.
That's how it's done on modern game engines. It takes a while to wrap your head around but it works really well. This style is used a lot by modern game engines because performance is way better with this style.
PODs + independent functions is not OOP. ECS-style designs are not OOP because ECS-style designs are PODs + independent functions (it's a relational model). OOP and the relational model are not equivalent, if they were the object-relational impedance mismatch wouldn't exist.
Polymorphism exists independently of OOP. OOP is not the only way to model relationships. OOP is not the only way to encapsulate data. OOP is not the only way to abstract things. Sometimes it is by far the best way to do all three. That's when you use it, not because of some silly attempt at purity of style.
People have a really distorted view of procedural-style programming where it's all global variables and copy pasted code. Couldn't be further from the truth. Even within OO codebases you have tons of procedural-style code that just happens to be stored in a class with the little "static" keyword in front. Many "procedural" codebases (i.e. stuff written in C) have lots of OO-style code with structs full of function pointers.
But it's wrong to say that it's all just OOP in disguise. If behavior and data are not bundled it's not OOP, and most code doesn't need that, not even when you need abstraction + polymorphism. Encapsulation is the main one where OOP is often the best approach.
“Rectangle" is a superclass of “Square”, if you are referring to the entities in geometry.
> A square is a kind of rectangle but the "API" of a Square is more restrictive than that of a Rectangle (where both width and height can change independently)
Neither the width nor the height of either can change without changing the identity, just as neither the whole or fractional part of a real number can change without changing what number it is.
If you've got something with mutable side lengths, it's no longer a Square or Rectangle, it's probably some kind of Drawable that might have a mutable state variable for position and a mutable state variable for a shape, but just as numbers, but geometric shapes themselves, like numbers, are immutable values (they can be in a mutable container, but then you are changing which one is in the container, not changing the shape while retaining it's identity.)
Yes, it's an example of a problem that has nothing to do with OOP and everything to do with applying model concepts from one domain to a different domain.
> They shouldn't even be modeled as a hierarchy they should just be simple structs.
Whether they are structs or objects with state and attached methods is an orthogonal concern to whether they form a type heirarchy. It's true that geometric squares and rectangles make sense as value types like structs. They also form part of a natural heirarchy that it is perfectly useful to leverage in code.
> It's just an example on how OO taxonomies are a silly (and detrimental) exercise.
Except that isn't what it is an example of. It's an example of why you can't apply a heirarchy from geometry to things which don't represent the concepts that heirarchy applies to. It doesn't show that OO taxonomies are silly or detrimental.
Even in the immutable case if you have a Square inherit from Rectangle it still stores 2 separate fields for width and height (that it inherited), which are unnecessary. The only winning move is not to play.
I think this generalizes to any "this is a more constrained that".
They still do btw. Universities still teach that Mammals have a walk() method that you implement for Dog and Cat, except then you need to add a Whale and now you're screwed. Silly example, but real world cases are much more subtle. I have seen plenty of NotImplementedExceptions strung across various codebases.
If you haven't done Domain-Driven Design, with cute UML diagrams and everything, then we aren't talking about the same thing (and I have no idea what you are talking about).
Reflecting the business domain into class hierarchies and structuring your codebase around that structure has ruined more codebases than NULL ever did. Code structure should not be dictated by business taxonomy concerns, only by concrete business data.
It is, but modelling the taxonomy of a different domain than the one you are operating in is not. “An mutable object whose current state will always correspond to some square and can be mutated to correspond to any square” and “A mutable object whose current state will always correspond to some rectangle and can be mutated to represent any rectangle” (which are the entities in the domain being discussed) are not the same things as “a square” and “a rectangle” (entities in the domain of geometry), and taking the is-a relationship that holds between the latter and trying to apply it to the former is bad modelling at a level prior to how it reduces to implementation in any particular programming paradigm.
> Universities still teach that Mammals have a walk() method that you implement for Dog and Cat, except then you need to add a Whale and now you're screwed.
To the extent this is true, it's a pedagogical problem not an paradigmatic one.
> If you haven't done Domain-Driven Design, with cute UML diagrams and everything
DDD is at least 3 decades more recent than OOP, and is not equivalent to it.
What I'm trying to get across is that this happens all the time. All the freaking time. I will concede that attributing this to a flaw in OOP might be unfair to OOP, but this comment thread started from a comment on how this style of OOP modelling is a strawman. It's not a strawman because I've seen this happen all the time. Universities still teach it really poorly. It is still discussed in conferences.
Now, you can do proper OOP modelling that is actually useful. My approach is to use inheritance exclusively for the purpose of code reuse and even then only when it is trivially correct (if you need to think about it, that's enough of a sign to not use it). Interfaces are for is-a relationships so you can have a DAG instead of a tree (which is too limiting in practice). The other 3 pillars of OOP are very useful, meaning abstraction, subtype polymorphism and encapsulation.
But I only use OOP to model software entities that have actual behavior + data, not business concerns (I will rant about how "Customer" classes with associated methods are a bad idea till the cows come home, they break the single responsibility principle by default).
But, and this may be more of DDD problem than a OOP problem, I've seen people invest tons of effort into modeling UML diagrams where every class corresponds to some business concept, and then they think of all the little methods these classes should have, and then this gets turned into code.
The design is *always* wrong because the behavior is associated to the wrong data, and then you end up with the single responsibility principle broken, horrifically, everytime. The performance is abysmal because state is spread across RAM like my cats spread sand all over the house.
Maintenance is also a pain because you feel like you need to maintain this utterly suboptimal class hierarchy to fit the "business taxonomy" even if it doesn't help in any way with transforming data A into data B, so you add all these additional classes and abstractions and dependency injection to make it somewhat usable in practice.
Things get a lot easier when you focus on using paradigms as tools rather than ideals.
You are still focused on a mathematical property of a square being a special case of a rectangle. Inheritance is not the tool to model such a relationship. Again it has nothing to do with taxonomy.
>Universities still teach,...
The same people teach functional programming with silly recursion examples and linked lists being the most important data structures.
OOP code can be convoluted, but a large imperative mess is worse. I haven't seen any large collaborative project written in a purely functional language, so I can't comment about that.
You're too hung up on the square vs rectangle example. It's just an example. Replace it with a big tree-shaped UML model encoding business concepts, and it's the same problem. Business concept relationships are a DAG or even an undirected graph and no edge in that graph should be given "preferential treatment" (as is the case for inheritance relationships, since they form a tree). The moment you do, you screwed up, and people do all the time.
It does not. The Liskov substitution principle states, as it applies here, that a property that holds for all rectangles has to hold for all squares. This is true, as every square is a rectangle.
As everyone is telling you, it’s a poor paradigm that makes the developer’s job harder compared to both non-OOP approaches and better approaches (mixins / multiple inheritance).
That said, one should be cognizant of the reason it was created in the first place - single inheritance allows C++ to implement fast polymorphism via virtual function tables.
Multiple inheritance can be implemented to be as fast as single inheritance regarding virtual function calls, and typically is in C++ by keeping multiple virtual table pointers per object. The problem this brings is that we can have pointers/references to the different parts of the same same object. A Child* may point to the start of the object, but Parent* may point to somewhere in the middle (because that's where the parent's vtable pointer happens to be). This also means that casting a pointer can change it. This can introduce some "interesting" bugs, as you can imagine.
OTOH, there are languages like C# that don't do that, but require 2 "hops" when calling an interface method, causing a slight performance penalty (a class can inherit from at most one other class, but can implement multiple interfaces). But a reference to an object always points to the object's start, which is very important for garbage collector.
the long term answer is probably a refactor, but what's the common quick fix? copying/duplicating data between component types? systems that examine other components as well as their native ones? merging systems?
asking the hard question... how does it stand up to the unpleasant cases?
It's the best tool I know of for a certain class of problem, that's all I'm claiming. Not evangelizing it as a magical cure-all for all domains, just explaining it in enough detail that it can be understood by someone outside the domain of gaming.
> what about when the abstractions age and a new feature for a system needs the data from the components from another system?
> the long term answer is probably a refactor, but what's the common quick fix? copying/duplicating data between component types? systems that examine other components as well as their native ones? merging systems?
I apologize if I gave the impression that things are so tightly coupled that a system owns a component or vice versa. (Though if I had to pick one, it'd be that components have associated systems)
In reality your components are just plain structs, and any systems that want access can query for entities based on any combination of components. (and good ECS impls allow for exclusion as well)
For instance I mentioned a Position component. This would be used by a movement system that checks for (Position, Velocity). It would also be used by a rendering system, which could query for (Position, Mesh) or (Position, Sprite) as appropriate. It could be used for collision by querying for (Position, Bounds).
If later you want to unload entities that are too far away from a player, you could perform two queries: (Player, Position) and (Position), filter out entities in the second query that are within n units of an entity in the first query, and then despawn what remains.
No existing data or systems need to change to allow multiple systems access to the same data. The only thing that might change is if the engine provides automatic parallelization, and you have two systems that mutate the same data, you may need to define an explicit ordering for them and they would not run simultaneously. If you don't need explicit ordering you may not even need a code change in this case.
***
In the spirit of what you're asking though, let's say you've been making a tile-based game where the player can move between discrete spaces like in chess, but you decide you'd rather have free movement like Mario. Your Position component uses integers instead of floating point values and you don't want to change your world's scaling. Here you have a few options:
0. You could just bite the bullet and change the types on the Position component to floats instead of ints, and let your type system guide you to any errors. Then you run your test suite again and make sure that everything is still behaving as expected. I'd also plan on creating several new tests based on analyzing existing uses of Position. And of course play your game again to make sure it feels right.
1. You can store the fractional position in a separate component, PositionFraction, and create/update systems as needed. Movement would need to be updated to look for (Position, PositionFraction, Velocity) and rendering would need to be updated to look for (Position, PositionFraction, Sprite). Meanwhile pathfinding could still just look for (Position, Goal).
2. You can create a second component that holds the full float value called PositionFine. Like above you update the systems that care about fine-grained positioning to use PositionFine instead of Position. Then you create a system to update Position based on PositionFine's value or vice versa, and log anytime there's a discrepancy. Once you're confident that you can drop Position, you replace every use with PositionFine. Rename afterward as desired.
If I'm changing the meaning of an existing component like this, #0 would be my strong choice, but if the game is already in production and the migration needs to go over super smoothly I'd consider #2 as well. In particular you can treat the existing logic as the source of truth, but run the new logic side-by-side and log out any variation between the two for analysis before flipping the switch and preferring the new logic.
However if the new data is purely additional and makes sense being split off from the existing data, then #1 is the solution to use. Think migrating from displaying usernames over players' heads to letting them choose a custom name. You still need the username for authentication, save games, friends lists, and the like. But a new component for the display name lets you reference that when it makes sense.
so it seems like then, that the discipline in building a system in this fashion revolves around responsibility for updates (which system is the sole updater of a given type of component) and the sequencing of those systems (ensuring that all the systems that update components that are used by a given system have completed their updates, possibly with some sort of record level update indication that allows for downstream systems to begin processing before upstream systems have completed all their records).
do any of the ecs libraries provide facilities for these problems, or are they typically built into a game engine framework?
Usually there will be some way to sort systems into steps and define the order of those steps. Some require you to hardcode the order, others define constraints like “after input” or “before physics” and attempt to solve those constraints, and some let you define groups that can run together and you define the order of the groups. Explicit signaling is not typically built in, but you may be able to implement it yourself if desired.
In environments with an expressive type system, most tend to favor the constraints model. Otherwise ordering tends toward the hardcoded approach, typically implicitly in registration order.
As to managing what systems are responsible for updating a given component’s state, that tends to be left to the game developer.
Sometimes there is a concept of events and if it’s not obvious who should mutate something then systems that want to mutate will send events that later systems can consume. For instance an input system and an AI system might both send a Move(entity, direction) event that a later system validates and applies.
And because it’s cute to do so, often times those events will be implemented as components themselves, with convenience wrappers to make it feel more natural to end user developers. This can come in handy for networked games and debugging in editor. You could also use it in game, such as displaying a unit’s next planned move in a turn-based game.
in the automatic constraint solver variants (which is more interesting to me) are the schedules static (precomputed at compile time) or do they run as a dynamic scheduler of sorts and are the constraints statically checked for deadlock ahead of time?
this architecture is exciting!
Don't get too caught up in the hype, it never ends well no matter what the architecture. But it's certainly useful and fun to play with.
Unity has not shipped DOTS. They actually removed it from the Package Manager last year. Joachim says it has a bright future, but I suspect it will never ship in Unity itself.
Epic has nothing in Unreal yet, though apparently a few months ago somebody spotted some changes in repository that suggests one may be on the way.
The major engines may not have ECS built in, but some of them are supported by ECS systems that are readily available. Your implicit restriction of professional gamedevs to include only people who both use a major game engine but don't use third party components not supplied by the engine is, I think, overly restrictive.
A traditional OO architecture will provide all the performance you need to create a modern Simcity style game without having to take on the additional problems of potential bugs in the underlying code, training everybody to understand and use it, and not even really knowing if the end result will be significantly faster or not.
Where I work we think long and hard about adding any third-party packages to the project. The benefits have to be very real, for the budget or for the player.
The benefit is to the developer, arguably the most important part of the game development process. It can save a lot of headaches that OO hierarchies can create, makes things more easily concurrent, allows for more flexibility in behaviour and emergent gameplay.
ECS helps to prevent bugs in game object logic, by keeping state and behaviour separate from its corresponding entity. If I'm looking for some behavior, I don't have to start thinking about which ring of the inheritance chain it ended up on, I just find the component or system that does that thing. It encourages you to write state and behaviour in a way that works with any entity and can be attached to anything at any time without crashes and state mutation problems or race conditions.
That Unity and Unreal are lacking (even after Unity's public efforts in the area) is because they are licensed en masse and retrofitting each with an ECS would be no small feat. And is that refactor worth losing revenue from obsoleted marketplace content? Or does the ECS need to interoperate perfectly with existing code from endless numbers of existing projects while still providing the benefits of a dedicated solution?
Unity and Unreal are not the only engines. In house engines are shaped by the needs of the immediate users, and not the sales pitch for hobbyists or third party studios.
[0]: https://www.gamedevs.org/uploads/data-driven-game-object-sys...
Many of us are 'stucked' wring the same old CRUD-Variation-Apps stuff. Learning about ECS is a great way to get those "original-programming-excitement-juices" flowing :)
The gaming industry is renowned for some fantastic programming solutions. To ekt out every bit of performance.
Anywhoo - makes for a nice change to arguing with colleagues over ORMs, Fat-Vs-Thing models :P
ECS sounds somewhat like how I was thinking of a civ-like game engine while falling asleep a couple months ago when I was thinking why Civ scaled so poorly under some circumstances: traditional games used to have everything in memory, but civ type stuff could just have a database with some indexes and spatial indexes for quick lookups, but otherwise just sweep the various tables. Thus the size of your game was more constrained by disk space than RAM.
Per your discussions with cache conservation/thrash avoidance, does ECS work well for mapping entities to specific processors so that cache-hopping doesn't occur in modern multicore processors and NUMA stuff?
I think this line alone is proof that you're doing this right.
if you're building a thing where state is required, then yes, it should be minimized. but i'd argue against dogmatic use of immutable data records and really think about what that sort of design is attempting to achieve: reduction of sprawl in places in the code where a piece of data is updated.
the goal is to put all that stuff in one easy to find place. that can be manifested or violated (sometimes with great acrobatics) regardless of whatever rules are followed. (even oop!)
Check out Hasura, Postgraphile and Postgrest.
But, consider, OO grew in a time when the likes of Logo was strong. How do you draw a square in Logo? Usually, some form of:
pen down
repeat 4
straight 10
right 90 degrees
But this /only/ works if you keep track of the state of the system in your mind, when your are figuring it out. Which, works really well if you are being taught that your program doesn't exist in and of itself, but to manipulate something else.Functional advocates lose many learners because they don't allow that writing a local function can be done with the more global state in mind.
A square is a polygon with four lines of the same length, forming 4 equal angles of 90°
A drawing can be made given a pen and a shape
The output is a drawing of square made with pen
That is, yes, to an extent you can expand your vocabulary to include more shapes and find ways to compose them. Or you could peel back and literally write point values. Both have uses. And both are used.
Consider, we aren't forcing all art assets to be created and described using elementary shapes. For many of those, we are closer to literally writing the canvas by hand. And then retroactively glueing to program.
It is a shame we often force a single paradigm with our code bases.
Better to separate the shape data, which is immutable (and basically declarative), and the rendering method, which does need to know about the previous work which was already completed and what it is doing right now.
There are of course also historical reasons, when it would be a central mainframe that would generate the gcode, and then it could be executed many times by cheaper computers attached to the machines. There was even a point where a lot of gcode was written, or at least edited, by hand. In these modern days of compute excess, gcode probably wouldn't have developed to the extent it did, and we'd be distributing STLs with some metadata around tolerances, materials and primary stress directions and the machines would figure it out themselves. The equivalent of gcode would just be used as a communication protocol between the interface and the motor controllers.
Which part of the state, and when?
Is this your program?
// change anything, anywhere, in the entire database, the biggest state
execute_sql(user_input)
You might want controls around that state so not anyone can change it. It might be read only for some users - that is, immutable.Is this your program?
log_to_database(financial_event for $5.00)
You probably want your financial logs to be immutable. Nobody should be able to mutate that $5.00 event to be $500.00.Is this your program?
o.foo = complex_function(...)
... 1000 lines later ...
o.foo = null
... 1000 lines later in another file ...
o.foo.do_stuff() // oops! foo was set to null somehow - but where?
The above scenario has shared state with "foo". Somewhere it was set to null somewhere. It could be set to null anywhere in your program. Good luck tracking the bug. If "foo" was immutable, you would know immediately where null came from, because it can only be initialized one time. It lowers the cognitive load, knowing that certain actions are impossible makes it easier to focus on what matters.Is this your program?
thing = computation()
return thing + 2
Many program are a series of computations - fresh state is created on each line, nothing is mutated. There is no reason not to be using immutability. In fact, if immutability were throughout, a compiler wouldn't have to worry about things like aliasing.This is all the tip of the iceberg, there are many reasons to enjoy using immutability.
I guess what you are saying is that foo should not have been changed, but a new variable created called "can_do_stuff" and it should have been checked before the call do_stuff()
I think the old Carmack article linked somewhere below makes a very good case for why pure functions are a good thing, and I see the value of making small atomic changes to an apps state, but ultimately, almost every applications job is to mutate data.
Other sorts of streaming operations --- message in, transform to new object which is logged or put into a data store most certainly do not need mutation so long as they are cache friendly, performant. FP and friends might even argue: yah ok it's some 10 pct slower but no race conditions, no weird sync. Formal methods are easier there to deploy. Those secondary arguments can be effective too; I'd bite
You sir - should write flyers and product-copy :)
import Data.List (nubBy)
refuteGoldbach :: Integer
refuteGoldbach = head $ [ n
| n <- [4,6..]
, not $ n `elem` [ p1 + p2 | p1 <- primesTo n, p2 <- primesTo n ]
]
where primesTo n = takeWhile (< n) $ nubBy isMultiple [2..]
isMultiple m n = n `rem` m == 0
If you think you already know the answer to this computation, get yourself a Field's medal.And then there are pure functions. Every time you compute a function using an input no-one has tried before, you are probably computing something that is not already known. You do this routinely even with a calculator.
For example, a REPL keeps state in the messages it prints to the terminal and this state isn't visible in the pure function definitions and expression the program is concerned with.
Unfortunately I'm over 40, otherwise I would try
No one was saying "don't use state" they were saying we need to adjust how we use it.
The right way to handle mutable state is not to pretend it doesn't exist but to accept it as a reality of complex systems and to encode its management in the type system of the language. And that's exactly what Rust does.
With Rust, I no longer feel dirty whenever I have mutable state and I trust Rust to not just keep my code bug free but also to make me think carefully about mutable state and how to design my code with it in mind.
In the beginning of my career I did a lot of engineering simulations (Simulink), to me signal flow diagrams have always been a very obvious way to model programs. All the state is explicit in that it becomes a delayed output->input mapping of the signal flow graph. Each block behaves the same, because it has no internal state.
I always thought about programs in the same way. What goes in, what goes out, what goes back in (if multiple iterations). Only later did I find out about functional programming, which basically is the same idea, and that instantly clicked.
Except for the auto-completion after writing the . on an object, I've never really seen OOP (in the Java way, not the Erlang way) be intuitive or simple. Always keeping state in mind, class hierarchies spanning tens of files where the only way to know what your object really does is step through with a debugger, interfaces for everything because otherwise you can't mock the classes for the test, the list goes on.
The funny effect is - if you want to minimize state and you're serious about it - keep everything you can in database and make a 2-layered (relatively thin) client <-> relational db architecture. With stored procedures. We were there in 90s and we moved away because web and OOP became fashionable.
So we took our clean, normalized, minimal state from db and made it messy and complicated with ORMs to satisfy OOP gurus :)
DB <-> Front end
to DB <-(ORM)-> APP SERVER <-> FRONT END
I don't think it's any faster :)OOP does unfortunately encourage introducing mutable state into the domain model. The canonical example being the back account, with a mutable back balance!
The good parts of OOP are interfaces and first-class modules. Obviously we should try and keep those.
This is literally how private/public keywords work, so I think your criticism is unfounded. However, I do agree with the overall sentiment that OOP implementations tend to "leak" way too much state than they need to.
There is no getting away from essential state, from a theoretical point of view. In my opinion the often leaky partial encapsulation of state is still one of the better ways to deal with it. And immutability is another axis so of course the frequent case where it makes sense should be adhered to, but not religiously.
The problem with "leaky" encapsulation as you put it, is the combinational explosion of the state space as many stateful objects are composed.
Most mainatream programming languages are still unfortunately not well geared up for working with immutable data, they lack even persistent collections. Functional languages are of course ideal for this.
I don’t think it has to be necessarily more than with an immutable approach. Like, there is an essential amount of state you will have to have either case and it is not clear to me that immutability is always the better choice.
But don’t get me wrong, I also default to immutable data structures, I’m just saying that 1) OOP is not incompatible with FP 2) not every problem is solved better with FP-idioms. It’s not accidental that haskell has state monads as well.
- minimal (amount and lifetime)
- well conceptualized (~= easy to understand the organization)
- well named
- minimally exposed
- coherent by construction (make inconsistency impossible by design of the format or by offering updating functions that ensure the invariants)
OOP can actually help with some of these things! I develop mainly in C++, which doesn't encourage a purely OOP style like Java.
Wat are your thoughts on (super simplified example):
* 1 state-var with 3 values ?
* 2 state-vars with 2 values each ?
Sometimes I steer my design too much to first example and then other times to the last example. Both extremes can make things ugly
enum AuthConnectionState
{
WaitingForConfig = 0,
Disabled,
Connecting,
Connected,
TimedOut
};
where the value of the corresponding variable is derived (in just one place that is called when any input changes!) from many inputs, and it's the authoritative source of information. If you want to know whether the current state allows to proceed with login (which can be local if so configured or the connection definitely failed), call: static bool connectionStateAllowsLogin(AuthConnectionState state)
{
return state == Disabled || state == Connected || state == TimedOut;
}
(Note for people who don't know C++: this is a file-static function, which is basically as private as it gets in C++, and it's also a pure function, not by any language feature though. It could access globals.)It has a couple of sister functions like isWaitingForWhatever() or isLocalLogin().
The naive alternative is a very nasty and error-prone forest of booleans, each of which you must remember to update when something about the connection changes, and to make sure it's all consistent. It's almost impossible to get right without exhaustive testing.
I also like to use enums, but these are widely used anyway.
It's hard to say something general because the answer is "it depends".
I'd say that not just state is the real enemy. It's all about life time of a particular bytes. `(x, y) => (x + y)` is a good function. Its state is discarded pretty fast. `(x, y) => (z) => (x + y * z)` probably is a bad function, its state is preserved for a long time. Global variable state is preserved during the whole program run. Database state is preserved during the whole software lifecycle or may be even more.
And that's not even about state is enemy. It's about how much time do you need to design this particular code. When you're thinking about database structure, that's a real deal. Take a lot of time, that's important and decisions will have impact for years or even decades. When you're designing pure function which does not leak any state, you don't need to think at all, just slap something and move on, you can easily replace it later if need arises.
TLDR: prefer state with short life time, be very careful with code that works with long time state.
I think there are much more interesting things to be said about state than just to minimize it. For example, you can limit stateful computations inside a function in such a way that the function itself is still referentially transparent (it behaves as if it wouldn't have state). In this way, you can still do a for-loop, or do a quick-sort on a copy of the input data, without losing the benefits of pure functions. In the D programming language, this can be expressed in the type system.
We need to 'deal with' all the risks that state involve, reducing it is the first thing to do but then there are a lot of other options as well.
This is the real issue with OOP. Abusing a type system, using inheritance when composition or interfaces would be more appropriate, etc.
I've seen pretty much every programming silver bullet implemented in the most horrifying ways by people who didn't understand the reasoning behind each approach.
You can write great FORTH and terrible LISP. You can write readable FORTRAN or APL (I'm stretching it a bit here) and elegant 6502 assembly. You can even write resilient and reusable JavaScript and PHP if you have the discipline to do it.
> If you're writing a service doing requests, you want as minimal state as possible.
The service can have a lot of state. What you really don't want is your client trying to keep track of it. When tempted to do so, you need to change the service.
Put state on an object only if there is a hard requirement for it. The occurrence is incredibly rare, state is mostly introduced to save re-typing method arguments...
Abstract Data Types are sort of a subset of OOP and are massively useful, I certainly don't think it's a good idea to expose the internal implementation of a data structure most of the time. Any sort of plugin system works in an OO matter. It is a useful tool, no question.
Even things like Actors like in Erlang (which are much closer to the Alan Kay style OO) are massively useful in a distributed setting where state is naturally distributed.
Where you get into trouble is when you go the whole "lets model a taxonomy of the world" style with inheritance hierarchies or go ham on design patterns, layers upon layers of abstraction and other nonsense like that.
The saddest thing about the "object–relational impedance mismatch" is that the focus went totally to the wrong side. The relational model is a much nicer way to model relations than a graph of objects (that's the whole point after all). SQL sucks but that's a separate issue, Entity Component Systems are a form of relational modeling for example and work really well, or even better Datalog.
Inheritance is not the only problem w/ OO either, many things are straight up awkward to express when you must couple data and code together. Many 2+-argument function w/ disparate parameter types create confusion about which class "owns" the definition of the function.
You also don't need to marry data and code to get the benefits of encapsulation. Many functional languages in the ML family have developed clever solutions to encapsulate the definition of a datatype while exposing it to module-local functions. There's no need for the datatype to carry a method around with it to support this.
I found the same thing. When I was using ORMs I always found them clunky for all but the simplest tasks, where I would long for an easy way to use SQL and have it "just work" for objects, so I created this:
https://github.com/iaindooley/PluSQL
It's obviously not been maintained but I think it's a model that has legs: that is, simply the creation of SQL with some convenience methods, allow the use of completely arbitrary SQL, and then intuit the object mapping automatically without loading the entire result set into memory.
My preference is to have sum types modeled separately from relations as just a complex type that can be related as well. This is the approach taken by Souffle, a C++ Datalog implementation which supports sum types.
For example:
Stakeholder(X) :- Shareholder(X); Customer(X); CEO(X).
It's still a lot better to model them separately from the relations, for the same reason that you want to model relations between points as actual relations between points not between X, Y and X, Y.
I don't take issue with the authors, with their insights, or anything related to the content of the book. It's that the book exists at all: it's a book filled with solutions to imaginary problems.
When using a procedural language the first thing you do is start implementing a solution. When using OOP, you first have to solve the imaginary problems created by OOP, only then can you start down the path of typing out a solution.
It's significantly more difficult to refactor OOP (even with great tools) than it is procedural.
If you could spend 20% less brain power, possibly 40% fewer keystrokes to describe the exact same solution, why wouldn't you?
This is a fundamental misunderstanding of what patterns are. The GOF book is used to this though.
A design pattern is someting that will naturally crop up if you adhere to certain design principles. If you follow a principle of separating instantiation logic from other logic then you will start to see factories. If you combine multiple complex parts of your code into simpler ones then you will see facades.
GOF is a reference book for some patterns that have been observed as being common, with examples that are essentially academic. It is not a how-to guide for OOP.
In e.g. FP there are no objects and therefore no instantiation but you can separate this kind of logic all the same, except you wouldn't call it anything, or would name it like any other function.
For the record I've been doing mainly OOP and have dabbled in F# so I'm happy to learn something today.
If you want a simple function to create objects, you have those in OOP languages. They're called constructors. They don't even have names independent of the thing they construct, so they're as lightweight as you can get. And if you do want names, you have static methods or top level named functions (depending on language).
Factories exist for a few different reasons:
1. When there are more things to configure about the newly created objects than makes sense to pass as function properties.
2. When an API designer wishes to provide backwards compatibility to his clients, which adding function parameters doesn't do.
3. When there's a need to separate how something is constructed from where it is constructed, i.e. you create a "factory" or "builder", configure what you want it to do, but then don't directly use it. Instead you pass it off to some other code that uses it when it needs to.
These are fundamental concepts. They aren't some side effect of how OO languages work. I think the disdain for them comes largely from programmers who haven't actually done many different types of programming. If all you've ever done is write web apps then yeah they're sometimes going to seem a bit useless. If you've shipped a type safe library API that you've needed to maintain compatibility for over a period of many years, maybe that will be used in ways you didn't anticipate so you can't just arbitrary refactor yourself out of a hole, suddenly these sorts of abstractions start to look pretty good.
() -> new Thing(foo, bar);
Behind the scenes it's a class, but you don't write it as such and you don't name it. Nonetheless, it's still a factory.Also you're leveraging anonymous function syntax which is, interesting, to say the least. In Java 8- you'd have to write boilerplate class named after a pattern. Lastly creating something from outside information was done since forever, it's trivial when you don't invent ceremony around it. Hence my conclusion, it's a waste of time and effort.
Yes, in Java 7 or below it'd have required more verbose syntax. So what? That came out 8 years ago and Java isn't the only OO language with a notion of factories. C# had delegates much longer than Java had lambdas, and it also needs a way to talk about "a bit of code that produces objects complying with a contract", so also talks about factories.
Other OO languages don't have lots of explicit factories. I'm thinking Objective-C (abstract classes can choose the appropriate subclass for new objects), Python (packages tend to have instantiation functions), Ruby (very flexible), CLOS, etc. So the factory is really a design pattern that popped up to address a specific language deficiency caused by losing a lesson discovered in the late 70s (metaclasses).
Other design patterns are really quite useful and I don't see how they can be called deficiencies. Facades, for example, are widely used in OO and non-OO systems. Template method is duplicated similarly in procedural languages that allow dynamic dispatch, in any place where you need a policy that delegates parts of its fulfillment to a more dynamically selectable part. Iterators, strategies, these things have widespread use. Builder and Prototype are frequently used outside of OO. Adapter, Bridge and Proxy also appear in various ways without OO. Many of the other patterns are OO specific mainly because there is no desire to send a message to an object in other languages. For example, Flyweight is unnecessary if you don't send messages to objects.
Without OO, a lot of domains would end up with some sort of implementation of the same thing. OO itself is a pattern in this sense. You can write Javascript in a functional way, but it makes sense to encapsulate the HTML AST and not have its memory manipulated directly by the language. Does it make a lot of difference whether you put the node as the first parameter to a function or whether you use it as the target of a message? As some functions apply to only certain kinds of nodes, does it make more sense to document those functions by the list of known nodes that they apply to, or as a hierarchy of nodes?
People often say that inheritance is the problem, but you look at a successful (in terms of utility) object system like Smalltalk and it has quite a lot of inheritance. Refactoring leads to that. Sometimes a parent class might exist just to provide one implementation of a single method for two different kinds of object. Whether this is accomplished through inheritance, mix-ins, monkey-patching, protocols, multiple dispatch, or whatever, is somewhat immaterial. But when a language makes that difficult, and you have to copy-paste to get the same functionality in multiple places, that can be a problem.
My issue is that the book is useful. It helps solve the artificial complexity introduced by OOP.
I own a car. Great, now I have maintenance concerns. But it's still a net positive, which is why we do it.
If oop creates more work than the value of brings, then we should scrap it. But I've seen some pretty bad procedural code, so I don't think that's a given.
Have you even read it? I'd say it has more deep real world examples than almost any other SW engineering book I've read.
I always have a good proportion of the class coming up with many patterns entirely on their own without any prior exposure, and I love their looks when I show them that the nebulous concept they came up with actually has a name and is well defined (and the best solution to the problems they encounter given the tools they have).
Regardless, the true weak point of OOP is arguably implementation inheritance, which just doesn't leave you with a consistent semantics that's open to extension and changes in the basic/derived classes (that is, the well-known "fragile base class" problem is still a showstopper). But that has always been a pretty ad-hoc feature anyway. The other components of what people call "OOP", including encapsulation and interface inheritance. are all pretty well defined and not as prone to misuse.
Why is OOP so bad, anybody? With scenarios, code samples or alternate implementations?
A few years ago OOP was supposed the be the magic bullet for everything, which it was apparently not. And now people critizice OOP for not delivering magic bullets. At the same time everybody seems to mean different things with OOP.
So no, it is not bad. Certain practices from OOP, like deep inheritance turned out to be worse in reality than expected in theory. But nothing dramatic. And by my definition of OOP - it is still a major part of every major language.
So if you have a Vehicle base class with Car and Truck that inherit from it people will naturally externally do things like pass around List<Vehicle> and will extend functionality with Lorry : Vehicle and start using it. This creates a problem because you cannot change the base class of a Car or else it will no longer fit in a List<Vehicle> but if you modify Vehicle you may break people who inherit from Vehicle.
If you wrote it out explicitly without using inheritence though you'd have something a bit more sane and split up into the three different concerns:
class Car : IPublicVehicle {
IProtectedVehicle _vehicle = new Vehicle();
public void Drive() {
_vehicle.Drive()
}
}
Now you have a public interface where you'd create List<IPublicVehicle>'s and toss them around, but you would be free to write a VehicleV2 class that could be injected into _vehicle (you could even do it dynamically and go nuts with dependency injection). Then other people using Vehicle() wouldn't have their behavior changed and everyone would still live happily inside of List<IPublicVehicles>'s next to each other.At the same time by having to write down the interface IProtectedVehicle (not shown) you'd be more likely to do actual design about the interfaces between your subclasses and your base class. This is the real problem which is that people do lazy shit design over the base class interface and just use it to shove shared code into the base and don't consider it a private interface and then go and break behavior as they mess around with more crap in there. And the "DRY" principle pushes software devs to ALWAYS push shit into their base classes, without even thinking beyond "because DRY", which is guaranteed to be bad design. With good design you may have to tolerate some level of necessary repetition in your base classes. Then you wind up realizing that you need derived classes that do not share behavior (e.g. consider adding a Boat subclass of Vehicle when previously you had taken the SinksInWater() implementation of Car(), Truck() and Lorry() and pushed that into the Vehicle() baseclass. Oops. Now your boats all SinksInWater() unless you override that. And if you override it you may be heading down the road of violating Liskov.
So that's the source of the objection that OO programmers will just tell you to write interfaces everywhere for loose coupling and only depend on interfaces not types (not abstract/virtual base classes).
But so far I don't know what Go or rust programmers do that is any different. And Go has a really fairly slick system for delegating interfaces to contained structs by composition which is just the same model I outlined above but where the two interfaces are the same. I'm still searching for where other programming languages produce a fundamentally better model. And as far as I've been able to ascertain Go's model is to just replace inheritance with interfaces and delegation -- which you can do in any OO model just by not using inheritance and writing interfaces and delegating. But then the complaint against OO is that proponents will just tell you to write interfaces, and for some reason that is bad. IDK if I'm missing something here...
In which case, it would be nice if the language supported ADTs
Rust doesn't have object inheritance. So Car and Truck can't inherit from a class named Vehicle. However its Traits have inheritance, so for Car and Truck you could write implementations of a trait named Vehicle or a trait MotorTransport which inherits from Vehicle (and so you'd need to implement Vehicle too in this case as MotorTransport relies on that).
Rust only allows the author of Boat or the author of the trait Vehicle to implement Vehicle for Boat, so if you were to add a sinks_in_water() predicate to Vehicle either:
* As author of Vehicle you're responsible for implementing sinks_in_water() for Boats OR
* You've made a backwards incompatible change, anybody who uses the existing Boat can't upgrade to your revised Vehicle OR
* inside your own system where nothing external can force the version requirement, now it doesn't compile because Boat claims to implement Vehicle but it lacks sinks_in_water()
Which gets to the point of if we should be talking about avoiding inheritance specifically? Because "OO" is somewhat ill-defined.
For example std::cmp::Eq inherits from std::cmp::PartialEq - if there's an Equivalence relation between things of this type then necessarily there is also a Partial Equivalence relation between some of those things (specifically: all of them) so you implement std::cmp::PartialEq and then just say actually it's also Eq (ie this equivalence applies to the whole type).
If you make some types which you implement std::cmp::Eq for, and all I need is std::cmp::PartialEq, I can use your types, because of the inheritance. But the fact your types have std::cmp::Eq (and thus also std::cmp::PartialEq) does not prevent them from being quite different in every other respect to other types, nothing about the types themselves is inherited.
So this means thinking about inheritance in a different way but it doesn't mean the concept is gone from the language. A typical toy Rust type might implement half a dozen or more Traits, some of them inherited from others and some not, "eagerly" implementing common Traits is encouraged.
As to just not using subtype inheritance in languages which have that, you're likely to immediately run into an existential crisis when the language - not unreasonably - depends upon this feature in its own design. In Java for example you can't go anywhere without tripping over Object, the supertype of all user-defined types. Java expects you to use inheritance so avoiding it comes with needless penalties.
But I still don't understand why it's good! Why is it so good? It is said to be better than (non-OOP) alternatives. Where's the evidence for that?
Something like ECS (data-orientated). Very explicit separates DATA(state) and BEHAVIOR (System, Functions, Methods etc).
For me at least it makes it easier and more reliable to "reason" and have a "mental model" about the big-complex system. If I know that for example in a game project ANYTHING with "gravity" is about the ONE Gravity-System(which runs 1x60 seconds in possible sep.thread) and that it Operates(change state) for ALL entities with the "Mass + Pos Components."
That said, any principle including FP, taken to an extreme can produce a very tiring codebase (I’m guilty of doing this :p). So its not really OOP’s fault
I just wish Julia had a succinct way of saying "This object needs to have implementations for functions X,Y,Z", rather than duck typing everything and just seeing if it works. Maybe it isn't too bad in practice I just don't like it when a function can fail because the implementation changed even though the type signature didn't.
Anyway at the very least the approach of keeping methods separate helps to prevent objects that do not have any state and do not in fact represent any entity at all. Seriously who came up with naming a class "MyObjectHelper"? What on earth is it? It could represent literally anything. Does it even have state? And why?
I might have to give this a try. Is there any language other than assembly that can beat the performance of C?
But vanilla C/C++ is already obsolete for performance - if you're writing high performance code it needs to be in CUDA, or some language / library that compiles down to CUDA.
As a wild example of that - just the other day I heard about BOLT in a podcast. It optimises the performance of LLVM/GCC binaries by moving around the object code after compilation. https://www.phoronix.com/scan.php?page=news_item&px=LLVM-Lan...
On modern hardware, you will struggle to write large programs that have decent performance in C. It lacks a bunch of obvious intrinsics - things the hardware can trivially do, but which you can't really express in C, and so you end up maybe writing a bunch of macros to try to persuade your C compiler to emit the desired machine instructions. Now your code is harder to maintain, either you encapsulate all this, losing performance, or it gets very difficult to write more of the software and while your notional "performance" is good for the parts that work, the system as a whole doesn't work, so you don't have any performance.
Languages like C++ and Rust provide better intrinsics which means that actual human programmers can write the more sophisticated program that would technically be possible and just as fast in C except you'd never have written it.
As an extreme end of what's possible, WUFFS has much faster image codecs than are available as C libraries. But, WUFFS is under the hood just a transpiler, the output of WUFFS-the-language is horrible spaghetti C. So, in theory a human programmer could have written say, a C PNG decoder that's just as fast as the one in WUFFS-the-library, after all the C code is in theory code a human (a completely insane human) could write. But humans wouldn't do that because unlike the machine they can't keep a thousand step proof of correctness in their heads and be quite sure that variable can't overflow, they'd chicken out and write the overflow check and lose performance.
A lot of OOP's success it was timing - OO showed up right around the time that we started building large gui apps. Now we have a lot of languages that do a great job doing encapsulation at the module level, which seems to be "good enough" and functional and procedural code seems to work pretty well, and be pretty easy to maintain at the module level.
Recently, I've enjoyed the simple utility of python's functools library. Small-inheritance-like functionality seems to find a happy medium in a lot of cases, while avoiding a lot of unnecessary abstraction and boilerplate.
Were there ever any real objective [hah] studies done about how much it improved software development? And did they show a significant improvement? Even if you're still pro-OOP today, you would have to admit it fell vastly short of its promises even if it does help a little bit.
Today it seems like there's been very little accountability or learning from all this. Some people have sheepishly climbed down off the bandwagon, but there's been very little overall reflection. I'm not talking about witch hunts -- there will always be more snake oil salesmen -- I mean learning as individuals and an industry to demand data and reason rather than handwaving and assertions. The sad thing too is that a lot of the baseless hype came out of academia too (microkernels are another one that comes to mind).
I still see this today. The new languages and language features. New database concepts like NoSQL. "AI". Blockchain. All the way down to the CPU (transactional memory, various "security" features, etc). Proponents can make extremely compelling-sounding cases for these things, and make it sound like they'll solve all the world's problems. And some may well turn out to be a net win in the end. But the only thing that actually matters is the real world results, and you can only evaluate that by studying the data.
In general, if something sounds too good to be true, it usually is. Maybe the incredible trajectory of the computing industry has dulled peoples' common sense when it comes to detecting this kind of hype. It's absolutely rife in the computing industry and academia.
A bit annoying when people shove a relational DB into a NoSQL schema though.
The point is how uncritically some of these things get taken, and how easily people will believe fantastic, unfounded claims. And not just a few gullible idiots, but huge swaths of academia and industry.
For a few years back there, it was going to take over the world and we were all going to throw away 'old fashioned' DBMSs because they were slow, clunky and overcomplicated.
Like many of these overhyped technologies, when the dust cleared about 5 years down the line, we are left with something useful that definitely has its place, but isn't like wow huge it's taken over everything maaaaan. Meanwhile SQL is still with us and still good at what it does too.
This is what is annoying with NoSQL the same as it was with OOP and now with FP.
People learn this as the new better way of doing something mostly because they heard at a conference a FAANG dev sharing it and then everything should be built with it.
I saw a lot of projects where the developer(s) used NoSQL just because it was available or it was hot or it was what they learned in a bootcamp/article. But then they added relations so now a User has Projects and each project has categories and with constraints on relations and more ...and everything is glued together with NoSQL and suddenly they are reimplementing relational DBs logic in code with NoSQL being only a pure data storage.
Now, what's harder is to provide some of the stronger ACID guarantees, say, fully atomic distributed commits. Most of the time it's just a question of time it takes to reach full concensus in a distributed context.
But this has nothing to do with the relational data model itself, which is just tables of uniform rows referencing each other. Say what you like about SQL, but the core model is perfectly fine.
Where I've seen it used is to delay the decision making process of adding structure to data, or a prototype database, before you are certain what your application's needs are. For simple disconnected data in low performance applications, they provide a low barrier to entry. But eventually people start embedding foreign keys into documents and the whole thing goes South.
I think years of hard experience across the industry found out that, for example, multiple inheritance and operator overloading caused more problems than they solved. Both features were taught and advocated back in the day, and now "there be dragons" signs have sprung up and most of the literature today warns the journeyman programmer to avoid them.
It's mind boggling to me when we see the kinds of people in the industry and their demands for data and evidence when it comes to other subjects.
This is a propaganda war, people. We are being told we are too dumb to handle knives. And the truth is, our industry lets incompetents play our roles, and we (those smart enough to use knives) must suffer the ramifications of those who stab themselves repeatedly and they cry out "it's the language!"
[citation needed]
Just because operator overloading can be abused doesn't mean that it isn't a massive boon in certain problem spaces (e.g. math libraries, SIMD libraries, etc.)
1. translating format strings to other languages is extremely difficult because the position of expressions in the message is fixed.
2. modifying how things are printed requires modifying global state, and it's easy to forget to reset the flags on std::cout after setting the precision of floats or something.
There's also the famous question of "what does the multiplication operator do on vectors?" problem, but that's something that could be solved by simply having a standard "vector" interface that defines it in a particular way. Overall I don't fully disagree, but seeing as it happened once with C++, I can imagine it can happen again in some equally widespread language (Javascript with it's + operator on strings maybe?).
This only applies to std:: cout and std::cerr. Other stream interfaces, like std::fstream or std::stringstream don't have this problem. Also, it is orthogonal to operator overloading.
The same will happen with every technique or process that becomes popular and consultants, authors and mediocre but loud people take over. Happened with OOP, Agile and will happen with other things too.
When I look at the Kubernetes, microservices, "need to scale just in case we may grow by 100000% soon" monstrosities we are building to deploy simple CRUD apps I don't think we have learned much.
People still take useful techniques, make them into a religion and push the techniques to the point where they are becoming a liability.
A language like Erlang indeed seems to align more closely with the spirit of the OOP goals - especially regarding encapsulation and avoiding shared state.
I would argue one of the best implementations of OOP is found in OCaml, where a nominal type system lives side-by-side with a structural type system, the latter which is used for objects and classes [1]. You still can use shared/mutable state, but functional styles are usually preferred - including a pattern known as "functional objects" [2], which allows the use of OOP but avoids shared mutability and its associated shortcomings.
[1] https://en.wikipedia.org/wiki/Structural_type_system#Example
[2] https://ocaml.org/manual/objectexamples.html#s%3Afunctional-...
Indeed, Alan Kay himself said, to Joe Armstrong, that Erlang was closer to his idea of OOP than any contemporary language that claims to be OOP.
It's taken over the front end. The react paradigm is FP.
SQL read queries are FP.
https://reactjs.org/ homepage writes "Build encapsulated components that manage their own state" That's a textbook definition of OOP.
Yes.
> You can do this with FP too. But FP isn't used so much...
So close....
The problem is single paradigm tools. No single paradigm fits all of a problem. And in my experience, OOP is particularly bad at it. Not Prolog levels of bad, but also not much better. We have a huge number of very popular single paradigm OOP languages, because in the '90s the programming gods set forth the decree that OOP is the One True Way to design a system.
The only reason OOP wins is because it doesn't enforce a single paradigm. You write 100% or 99% procedural code, you can write a functional program and encapsulate it. You can write a shell script and pop a GUI on top (it's marvelous in a horrible sort of way). OOP's lack of constraints is what lets it be used all sorts of places.
Prolog is homoiconic like lisp, and support metaprogramming, so in theory there's nothing stopping Prolog from being great at most or all of a problem.
Ouch, not even unit tests to catch regressions?
So many of the bugs that I deal with in $BIG_FOSS_PROJECT are regressions where a bug fixed in version X.Y.5 was reintroduced in X.Y+1.0 by someone's cool shiny new feature.
And yes, there's a significant lack of unit tests in some of these errors, plus some hard to test code. (While I'm happy I can use Mockito to mock static methods now, the fact that I have to is a very definite code smell).
Which is why I recommend never installing version A.B.0 of $BIG_FOSS_PROJECT. I always wait until everyone else finds the new bugs for you, then install A.B.1.
I also don't like unit tests and very rarely unit test. I think they provide a false sense of security and were invented by corporate software shops to better quantify "units of work" (oh, how many times I've had unit tests assigned to me in tickets!).
If you can't formally prove something doesn't break (in your head or with pen & paper or via pseudocode), your code is too complex. There are a few exceptions to this, but they are highly technical (e.g. FFT, cryptographic, physics/math implementations, tricky pointer arithmetic, regular expressions, etc.) where a hard-to-see typo can actually break things in non-obvious ways.
Most code is not that -- it's just written poorly (because code standards aren't enforced). Linux is a great example of a project where coding standards are annoyingly enforced (and there's no real "unit" testing) and lo and behold, the code is of exceptional quality.
And more importantly, if JIRAISSUE-15295 is reproducible, then a unit test that a) reproduces it and b) verifies that the bug no longer occurs, is invaluable to prevent someone bringing JIRAISSUE-15295 back from the dead.
Of course, if the unit test is too tightly coupled, and has too many insights into code it shouldn't, then it's worthless as JIRAISSUE-15295 will most likely reoccur via a different code path.
But, poorly written unit tests aside, I have found significant value in unit tests when maintaining a rapidly changing code base.
If you have discovered a way to write unit tests that can tell the difference between a regression and an enhancement, please let me know.
This is a clever way of solving this problem, and I've ran into it before, as well. Applications where state is very deep (like a game) and you need to verify certain operations on that state is quite tricky. IMO, I'd probably call these integration tests, not unit tests.
Then when running the actual app, the data is not exactly like on the unit test and you have bugs.
To cover those scenarios, we use integration/acceptance tests.
So I always come back to the same question: then why should I even bother with unit tests? Especially the unit tests that people normalized (test per class/public method).
You end up with unit tests that are extremely coupled to the implementation, and without proper integration tests, you can't guarantee it will all work anyways. It only works on a bubble.
I believe tests should be much more about the broader behaviors than the implementation details, but TDD and evangelists of today will have you writing tests for every small class you create. I personally still use unit tests from time to time when there are a ton of edge cases on a single behavior that I want to test, but that's the exception not the rule.
By avoiding unit tests, or to put it differently making the units tested larger, you get more space to refactor, less coupling between tests and implementation as well as more meaningful tests.
Somewhere along the way we lost the meaning of "unit" and it became "class/methods", when it was originally supposed to be more at a module level.
I feel that it’s important for me to note that this is because almost all the projects I’ve been around have been build in imperfect scenarios. Sometimes projects had years worth of terrible unit tests that were terrible because they were written by people who didn’t really know how to test correctly. Sometimes because they were added waaaaaaay late in the process. Sometimes because they were sometimes skipped due to time pressure. Sometimes because the pipelines weren’t really effective or set up correctly. And so on.
The issue I personally have with them is the same issue that I have with a lot of other dogmas around software development. All our theoretic tools are nice, if people actually adhere to them and know how to utilise them.
But as soon as things like Test Driven Developmebt, Agile, SCRUM, Enterprise Architecture or even Unit Testing meets reality, they break in most of the cases because the theories aren’t fascist enough. By that I mean is that you can implement things in so many different ways that almost nobody manages to make them work, not really.
This is anecdotal of course, and I’m sure there are much more talented people, teams and companies that derive a benefit from these things than what I’ve seen, but the only thing that’s ever, really, worked for me is too keep things as simple and single responsibility as possible.
That sometimes require OOP or Unit Testing though. We wrote our general ODATA API with inheritance as an example. We do so because it lets us have a unified way of handling auditing and code-first SQL database IDs and convention and because it lets us write a single API controller and then use it in, every, other controller instead of writing the same 90 lines or code 10.000 times.
You can likely do that without OOP but it’s just easy with OOP in C#.
So I see a lot of these things as, do what is necessary, what works, and, what is easily maintainable. If that’s Unit Testing for you, then do it, but I can assure you that it won’t be unit testing for a lot of people out there for whatever reason.
At least with single responsibility you still have a somewhat general idea of what goes wrong simply by where it happens if it isn’t caught but automated tests.
> I find it much easier to formally verify things correct than to test that they're correct.
I don't have anything else to say but I wanted to put it here for context.
I'm not sure how formally verifying things continue to be correct after a change (which could be to a dependency) can be automated and scaled.
class PersonnelRecord {
public:
char* employeeName() const;
int employeeSocialSecurityNumber() const;
char* employeeDepartment() const;
protected:
char name[100];
int socialSecurityNumber;
char department[10];
float salary;
}
As written, PersonnelRecord class will inevitably lead to code duplication, tightly coupled classes, and other maintainability issues. An improvement that's still not OOP, but exposes a more flexible contract, resembles: class Employee {
public:
Name name() const;
SocialSecurityNumber socialSecurityNumber() const;
Department department() const;
private:
Name name;
SocialSecurityNumber socialSecurityNumber;
Department department;
Salary salary;
}
OOP is more about the actionable messages that objects understand to carry out tasks on behalf of other objects. Wrapping immutable data exposed via accessors reaps few benefits. Rather, OOP strives to model behaviours that relate to the problem domain: class Employee {
public:
void hire();
void fire();
void kill();
void raise( float percentage );
void promote( Position position );
void transfer( Department department );
private:
Name name;
SocialSecurityNumber socialSecurityNumber;
Department department;
Salary salary;
}
This allows for writing the following code: employee.transfer( department );
I don't know how to "transfer" an employee given the code from the article, but it would not be nearly as elegant. transferredEmployee = employeeService.transfer(employee, department);
IMO this forces a certain style of coding that ages better and requires keeping less stuff in your head.My two cents. Have a good one!
xferEmployee = employee.transfer( department )
xferEmployee = company.transfer( employee, department )
xferEmployee = department.transfer( employee )
exEmployee = humanResources.fire( employee )
Or, using an event-based architecture: new EmployeeTransferEvent( employee, department ).publish()He also ends up with the commands/actions/events/rules (basically emphasize relation over object) as main types in an interesting[0] example of a rule-based game.
One of my issues with “mainstream OOP” (at this point I’m not even sure what “real” OOP is) is that it apparently tempts people to base their models around objects and subjects not verbs.
I’m not sure if it’s due to prevalent nominal type systems or due to how we traditionally teach class hierarchies (dogs/cats <- animals) but I think centering the model around predicates and relations works much better. [1]
[0] Interesting because while it is a toy example it is one that could be real; not like examples with cars and animals.
[1] Just reminded me a bit of Wittgenstein’s Tractatus; didn’t want to get too philosophical but I think the ontological views we have influence this kind of modelling a lot
struct PersonnelRecord {
char name[100];
int socialSecurityNumber;
char department[10];
float salary;
}
and be done with it.- They're small and single-purposed
- They don't inherit (or at the very least, extremely light inheritance)
- There's a very clear relationship between the state and the methods (e.g. a counter class, or a future/promise)
For example in JavaScript, I think you can still uses classes and end up with functional-ish code: https://bluepnume.medium.com/functional-ish-javascript-205c0...
Procedural is "linear" while OOP is "layered". One could write code either way but OOP better resembles the physical world and since brains have grown up in the physical world, layered code is easier to grok. It can hide/show pieces such that you can inspect functionality in a controlled way. Of course procedural can do the same with functions but it's coarser.
State is also a property of the physical world. Things persist, stuff accumulates, that's just how it is. Since applications ultimately model things in the physical world, they also have to persist things and accumulate things. State is inevitable.
OOP is a sharp tool and of course it's possible to use it well or cut one's self really badly
The part about OOP which doesn't resemble the physical world though is encapsulation. Encapsulation is like pretending the world functions in passive voice, with objects doing things to themselves.
They guy who hammers nails all day thinks screwdrivers are worthless.
Lenses solve the nested mutation problem for immutable structures but then go past what you can do with getters and setters. For example, you can compose bigger lenses out of smaller ones.
The biggest UI framework of the past decade is decisively anti-OOP in its philosophy
Switch it to a state with the methods and you got your object. Polymorphism here is not a requirement. It is a feature to be used when needed. Some time in the 80s I was doing exactly the same - operating on state with functions. Same shit different color.
Programmers just love going on crusades. I find it a waste of time and intolerance breeding ground. Instead of heating the air use whatever the fuck suits one. Just do not stalk other people who happen to have different preferences.
Agreed hooks are… something else.
I think it looks functional because it's only easy to reason about if you write functional code.
I suppose it tries to solve some of the same problems as OO, like related state and private transitions. But it feels like React itself is the God Object.
This gives a good breakdown: https://overreacted.io/algebraic-effects-for-the-rest-of-us/
Lots of use of context managers in Python are for accomplishing this. Smalltalk error handlers are like this (different than try/catch). Or Scheme's with-output-to-file... I got the sense that variables that start and end with stars in Common Lisp are used for this, but I was never clear if it was convention or an actual language feature.
I think I'd like it in React too if they just hadn't tried to be so clever.
You could achieve the same effects by passing down callbacks.
I thought the reason why this is good for Errors, Themes and Loading (Suspense), is because they're so common, that you can always expect someone up in the hierarchy to consider it, and therefore sacrifice some type safety for less verbosity.
The article mentions that "usually the fact that a function can perform an effect would be encoded into its type signature", which is why I don't understand the need in this example.
Most of the even more OO frameworks around the same time, like Taligent, failed.
Of course it did win over procedural frameworks like classic MacOS, but some of those are still popular in games or embedded systems.
It could have been a single Python file using Pandas to build a DataFrame and this change would have been so much easier. And I'm saying that as someone with at least 10x more Java development than Python development.
It'd be like carpenters saying "spruce is terrible if you make anything from spruce its fucked, you have to use oak." ; A good carpenter will be able to make something amazing from spruce.
Like carpentry, metalworking etc. the quality of the product of programming is heavily based on the skill of the tradesman.
If a paradigm (whether functional, OOP, or whatever label makes you feel better) isn't working for you when it has worked for many others, maybe it's not because the paradigm sucks... it's because you might just suck at the paradigm?
Obviously there are cases where things were pigeonholed into the wrong paradigm for the job. Then it's a case of the person choosing the paradigm sucking at choosing the paradigm. Whatever happened to personal responsibility?
But I struggle with reading it, and I struggle with writing it, and I struggle to build a conceptual framework of how FP style components fit together. I've been banging my head against the FP wall for _years_ now, and I still can't seem get a level of fluency that where I'm comfortable using it regularly.
That doesn't mean FP sucks, it just means my brain isn't wired that way.
OOP is a massive foot gun for many programmers who are okay at best. And that effect is exponential as the project and their team grows.
I believe people dislike OOP because the vast majority of code is written using OOP. If everything were written using data oriented programming or whatever, people would have the same reaction as they have regarding OOP.
There are definite degrees to this. But something like FP that declares f(x: X) => Y, instead of something like OOP where given context: x: X, y: Y and f() => void (where there is an effect on y) those are massively different scopes of problems.
It's not that you cannot do the previous in the latter, but that people -- especially those that are inexperienced and haven't dealt with the hell of a stateful programming -- routinely reach for as a solution to their problem.
>> I believe people dislike OOP because the vast majority of code is written using OOP.
Why do you believe that's the reason why? There's no implication here -- just a genuine curiosity as to how that was the conclusion.
>> If everything were written using data oriented programming or whatever, people would have the same reaction as they have regarding OOP.
DOP especially given one with a static language forces you to move from one type to your next to solve your problem, and handle your edge cases.
From experience DOP and FP can be intimidating, but usually end up being easier, and the development time, along with the time to stability are hugely reduced -- because it's inherently dumb.
> There are definite degrees to this. Agree. The question is, does OOP leads to more buggy code? Maybe yes, so we ban OOP? Because that's the sentiment I get whenever someone is against it. In your example, is it ok allocating a new object every time? If you can't afford to do it, what do you do?
> Why do you believe that's the reason why? I think in terms of proportions. For example, if you have 80% of all code written is OOP, chances that you'll find a bad OOP is greater than finding bad code in any other paradigm.
What if people learn programming using FP or DOP? There would be less buggy code? I don't know, but I think in the end, OOP would become the popular choice because, in my opinion, it fits very well the way we think about our world and we would end having this same conversation.
I've been guilty of over-abstraction and I've been guilty of under-abstraction.
There's a lot of blaming the tool for what amounts to lack of experience. Everyone has lacked that experience at some point in their career.
What FP programmers are arguing about is that all code becomes shit if you do OOP. It doesn't matter how good you are.
They argue that if you do FP your code is much less likely (note the word likely) to be shit.
The analogy to carpentry or craftsmanship is bad. Programming is about managing complexity to a degree no one can fully hold in their head or understand. You can't just be a "master" craftsman and write an entire OS with zero technical debt.
The argument here is that if you use FP over OOP your code will have magnitudes less technical debt.
Also, code has barely changed at all since C. It's all just some variables, loops, flow control, some math operations and doing stuff with strings sometimes.
One can write functionally in an OO language. No paradigm or design pattern or whatever will save you from making a mess. For me, the more experienced I've become, the less those things matter.
I understand every craft. They are analogous in the simple minded way you're thinking about it. However complexity in these crafts does not rise to the heights like the way it does in programming. That is the key difference.
>No paradigm or design pattern or whatever will save you from making a mess
This is fundamentally backwards. The FP style saves you from all errors related to mutation. This single statement already proves you wrong. You can't mutate a variable so that's one mess literally taken off the table. There are patterns that can provably prevent errors from happening and you can create languages that strictly enforce certain patterns.
Elm for example, cannot have runtime errors. The language strictly prevents that "mess" from happening.
Outside of strictly enforced patterns the FP pattern promotes better organization. You can still make a mess. But you are less likely to make a mess. Additionally for extremely large programs the proportion of technical debt will be significantly less for an FP styled program than an OOP styled program. See below for one instance of this.
http://wiki.haskell.org/Why_Haskell_just_works
>For me, the more experienced I've become, the less those things matter.
I've found that people who brag about experience doesn't make them smart or good programmers. You have limited intelligence and no matter your level of experience there are just design mistakes you make all the time in extremely large programs. Technical debt is an unsolved issue. Though the claim here is that FP programs tend to have less debt than non-FP programs. The keyword is "tend" as there are exceptions but the generality is mostly true.
The real goal should be to untangle state and decouple it from computations by putting it all in global shared state (e.g. SQL, Redux, ECS frameworks, etc.) while using stateless functional computations to compute changes to the global state. Pure functions are composable in a way that objects simply aren’t.
It's the code monkeys, stupid. Given a powerful enough machinery most code will look like junk. That's why some "beautiful" languages today tie your hands and dumb everything down so you aren't free to do as you like, whereas assembly was just fine 40 years ago.
> The real goal should be to untangle state and decouple it from computations by putting it all in global shared state (e.g. SQL, Redux, data oriented game frameworks, etc.) while using stateless functional computations to compute changes to the global state.
I kind of presume that you're in a single-threaded world. Shared mutable state plus multithreading is a recipe for disaster.
I was skeptical of functional for years and was actually a big OO zealot. I finally brought myself to give functional a serious go over 1.5 years ago. I now write it professionally and don't miss OO even a tiny bit. It's really just something you have to try and have an open mind about. I actually still find myself sometimes thinking, "Crap, maybe I should make a copy of this in case something else tries to touch... OH WAIT! IMMUTABILITY! I'M SAFE!" It's a really nice feeling :)
If I don't have immutability, then if I can call foo(thing), then I can write bar(), and then call bar(thing). Bar can now alter thing. So my encapsulation can be broken by someone just writing a function. (This is the same problem that C had with structs - anyone could write a function to alter the data in your struct, and thereby put it in an inconsistent state.)
Now, with something like C++, you can still do that. You have to go to thing, though, and write a new thing.bar(). It therefore becomes much clearer that bar() may break the consistency guarantees of thing.
So I don't think that just functional gives you the guarantees that OO encapsulation does.
Now, immutability changes things... a little bit. But with immutability, the problem only moves, it doesn't go away. Someone can now write a bar() that returns a new/altered thing, and then pass it to me. I can still get a thing that is in a state that violates the rules for what a thing is supposed to be.
And, once again, the same thing can happen with OO. It's just that, if it happens, you have a lot less code to look through to try to figure out how and where it happened.
You may have noticed that I care a lot about data being in a consistent, valid state. If you don't care about that, then my arguments may not resonate with you.
To the question of consistency though, OOP gets you very little because consistency rarely maps directly to objects. So if you end in a situation where object A is inconsistent with the object B, you still have to trace out all the locations where object A and object B might have been mutated and figure out what combination of code causes the issue.
At least in the global state system you can get runtime consistency by running consistency checks on each attempted transaction.
1. Internal/private data -- things like a pointer to a string's contents, its length, and the capacity of the content buffer.
2. Public "data" (API) -- best provided as accessor methods/properties (and public methods) to ensure that the class' invariants hold if these can be used to mutate state. You don't care if the object has been mutated through this public API as this is the contract for how the class/object instances should be used.
A properly encapsulated string class is free to change its internal state/data (e.g. start+length+capacity, or start+end+endOfBuffer), as long as it keeps the contract defined by the public API intact. The same applies for data structures, mathematical objects like complex and rational numbers.
You could store a complex number in polar (r, theta) or cartesian (x, y) form and provide public accessors for all of those values. If you had setters for those, the native representation would be a simple assignment, while the other representation would do the necessary polar <=> cartesian conversion. This would maintain the invariant that the two representations are equivalent, such that if you set r then the angle (theta) does not change, but the magnitude changes such that it is equal to r.
Note that if the complex number (or string) was modelled in a procedural or functional language, you would have to choose and stick to one or the other representation. That structure or type definition is leaking the state, such that a program could modify the length of the string without updating its contents.
Q: Can you give an example of where having object A inconsistent with object B is an issue? That would help me to understand the issue/problem you are referencing.
[1] Many OOP languages also allow protected data, which can be seen by implementations but not any other code. These are risky, as they can allow the derived class to break any invariants on that protected data like you described in your second paragraph. As such, I try to avoid them wherever possible in my own code, but will run into them in thirdparty or system classes.
Echoing my sibling comment: immutability is a defining feature of FP, so there is no way to put it aside if we're talking about FP.
As for the rest of your argument, I'm a bit of meathead and maybe haven't followed it fully. My thought is that in OO, an instance of an object can be passed some data that it processes along with its own internal state. If that data is a reference to another object, then that object could be changed while the receiving object is doing its thing. The receiver then would go and store its results within itself.
In functional, functions receive everything as parameters and all that data is guaranteed not to change for duration that the function runs. Once it returns, it will update the global state and then, again as my sibling comment points out, it's far easier to check validity of the whole shebang as it's in one place.
As for "you have a lot less code to look through", I feel this is a common thing said by folks who have not really grokked how FP works. I say this because I used to make this argument ;P While FP can be slightly more verbose than OO, I've found FP way easier to figure out where things came from.
All in all, I don't have a vendetta against OO. I was really just trying to get across that I was a big OO enthusiast and went in FP perhaps even with a closed mind and it didn't take much to win me over. Having said that, I'm in the Elixir/Erlang world, which is a bit of its own beast.
(Edit: your/you’re/yore)
Well, no it doesn't. I've worked with too many codebases that has a declaration of `void foo (sometype_t &instance);`. Then when you read the code and see a function call of the form `someFunc(localInstance);` you have no idea if someFunc can change localInstance.
And then there is this article who just goes through more articles trying to re-ignite the debate. It’s silly.
Both have good techniques, that shine for certain types of problems.
The more important thing is how you do it, how good you are at it.
It's the same thing with e.g. Agile, some people make a mess of anything including agile, some people do it well, and the vast majority of average people get average results with Agile.
So don't waste time getting stuck on one particular school of evangelism, try a range of techniques and get good enough at all so you can match the ideal technique to the specific solution you're creating.
In this system, the models (records) in the domain model are more like classic OOP (objects like "car", "banana") and they have encapsulated state, but only to have a centralized place where you make sure object invariants are upheld whenever their state is mutated (by services), and nothing else.
The hard part is interoperation/invariant validation between multiple objects (aggregates or just related data) and we haven't solved it yet in a consistent manner (everyone comes up with their own approach, for example state propagation via events).
OOP like everything, using it incorrectly will lead to trouble, and it has its merits when done right. plus, it's used widely in practice that is really the best evidence in that, it's not bad at all.
use FP all the way IMHO will lead more spaghetti code, use it to complement OOP could be great however.
Last, what's the alternative? if you don't have a better alternative, you don't solve any existing problem.
“… Because the problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.“ —Joe Armstrong, creator of Erlang programming language
State2 = update(State1, ArgX),
State3 = update(State2, ArgY),
As opposed to, say: obj.update(arg_x);
obj.update(arg_y);
obj holds a gorilla and the jungle, and it may be hard to know how update method works because there is a complicated diamond shaped class hierarchy, and then a thread may concurrently modify parts of it behind our back. You're just trying to add a method or fix a bug, but it's almost impossible because we don't know it affects the gorilla and the whole jungle.State2 = obj.update(argx)
State3 = State2.update(argy)
But if you do this people will argue this is inefficient, because you are copying a lot of objects.
Syntactically it is annoying to some extent. But, just like the most important data gets saved to a database with lots of "ceremony" involved -- separate protocols, transactions, SQL statements, etc, because it's pretty important to track updates carefully. Here ,it's a bit like that, but on a smaller scale as in memory program state is also important, and has to be tracked explicitly, and for that some "ceremony" is acceptable.
Implementation-wise, because of immutability, there is copying but it is often not 100% duplication. The updated version and the previous one behind the scenes (in heap) might share a lot of common structures. For example, if we have a 1000 element list L, and we updated it with a new element at the front L1 = [H | L], then L1 and is not a complete copy of L, but instead is just element H and a pointed to the shared tail L. For dictionary data structures (maps) something similar happens but there it's a O(log n) order of updates with everything else being shared. Definitely not as efficient as in-place updates in say C++ or Java but that's a price I'd happily pay.
Most computer programs have an internal state that you interact with through some API. And sometimes you need to compartmentalize a process and have it work independent of another process. There's literally no way to get away from programs with an internal state and API access since that's how computers, and electronics in general, work.
To see a language that is purely OOP but also forces you to be very explicit about accessing state, check out Pony language.
i think it's the other way- an OOP object is a poor imitation of monadic algebra that cannot be generalized with mathematical rules to guarantee certain properties.
1. New code base is needed
2. Some developer comes up with the master oop abstraction to solve the problem
3. Years of dev effort spent with the abstraction at the core
4. Dev on step 2 is now the expert in some overly convoluted complex system
5. Is determined a genius since new comers cant easily grasp the complexity, gets promoted to a high ranking dev job
My number one rule of coding politics:
He who creates the OOP abstraction rules the team
When we are getting to "only a Principle engineer who is an expert in scala with a history of strong mathematics can work on this" is about when a codebase gets a fail.
To be fair, OP’s original phrasing is still a good rule of thumb. And nowhere does the OP imply it’s an absolute.
Your entire team could be highly experienced and have PhDs in exotic subjects and I still think it would be a good approach to take.
Yes.
The simplest code isn't always the most immediately approachable though. At least due to the way universities and bootcamps teach things. OOP only smears complexity around and should be avoided at all costs, but the "simple" alternatives that most people have in mind (usually procedural Python/Go) aren't much better. Simplicity shouldn't be synonymous with repetitive, error-prone nonsense like for loops and null checks just because they are familiar. Junior engineers should be schooled up on mapping, folding, optional types, ADTs in general, etc, because these things make code radically simpler.
TBF that one is really going to depend on the domain addressed by the code.
Lesson: If you're going to code your way into being the only developer who understands the core abstraction behind your employer's critical code, do not overplay your hand.
:-/
None of these articles mention 'cohesion' or 'coupling'... It's not possible to critique OOP unless you understand the principle of loose coupling (ease of substitution) and high cohesion (clear separation of concerns/responsibilities).
Without loose coupling and high cohesion, your classes will not be composable which is the whole point of OOP...
These characteristics ensure that state is fully encapsulated and does not leak between multiple components (that's when it gets ugly).
I can look at any project's code and assign it a score in terms of cohesion and coupling of the classes/modules/components. Other people who are experienced with OOP can look at the same code and they will come up with a similar score.
Some people, myself included, just try to avoid this complication altogether, by separating data from logic.
IMO, the biggest problem I often see with FP code bases is poor separations of concerns which leads to spaghetti code which is hard to read and maintain. When some state is not co-located the logic which is supposed to be operating on it, you're already throwing high cohesion out the window... And when you do that, it makes is harder to separate the responsibilities of different components because there is no clear ownership relationship between the logic and various bits of state... With FP state can end up being mutated all over the place and it's hard to know who did what.
In practice, OOP basically exists to take some horribly messy program, put an interface around it, and make it slightly less terrible to deal with. Similarly, it also involves creating an interface to a thing a programmer barely understands and letting the programmer do some things with it. Which is to say is about letting the programmer be wrong and only fail moderately - as opposed to making the programmer is right.
Inheritance, one of the worst feature of OOP, has wormed it's way because it's also incredibly convenient.
Everything that OOP is compared to (notably functional programming), is based on a "green fields" paradigm where everything can be controlled. And maybe those approaches work great (though you can't exhibit stuff with they replaced in OO in the trenches and maybe things great).
It's got the tone of "this suspension bridge is far superior to your roll of duct tape" - yeah, maybe you're right but that won't make the duct tape go away even slightly.
I wish someone would come up with a good and immediately applicable alternative to OOP but I can't this sort of critique leading there.
I don't think any of this is unique to OOP; rather, this applies generally to the concept of abstraction.
That's not really "the right way" to do programming or something generic abstraction gets you. The calling function should just know what it's doing. The Dijkstra quote "Object oriented programs are offered as alternatives to correct ones..." is correct. If you set a wrong parameter, it's ignored and your program works, it might have a more subtle bug from what you thought your wrong parameter would accomplish.
But this is still useful for programs produced by large teams where some people knowledge is limited. It's a mess but no one has put forward an alternative to the mess.
Adding new methods to an object to tame complexity seems paradoxical to me. When something becomes too large, I generally prefer to investigate other means of abstraction/extraction/organization available in a given language.
This is the sort of hype that has followed OOP around everywhere though. Well that's great, of course we would want to take a horribly messy program and make it less messy! Who wouldn't?
But where is the evidence that it actually performs as advertised? Does it make software easier to deal with? Is it superior to alternative ways to achieve that?
What are those alternative ways to make a huge system manageable you allude to? As I said, none of the paradigms put forward as alternatives even claim to operate like this. It's serious question, what are the alternatives in this kind of situation?
And I'll admit, a "horrible messy situation" that we'd look at today is going to be an earlier OOP system.
Mind you there are countless millions of lines of COBOL, C, etc out there in production so existence alone does not provide any evidence one way or another.
My view is that software are like fractals. That is, self-similar and there is no difference between the micro and macro scales. What is right on the micro scale is right on the macro scale and vice versa. What is wrong on the macro scale is wrong on the micro scale and vice versa. Is it "wrong" to implement sorting algorithms using OOP? Yes. Then it is also "wrong" to implement music players using OOP.
That's quite an interesting way of framing it, I've never considered whether or not software has a "you shouldn't mix your micro- and macroeconomics" problem or not.
Is your conclusion that they are the same at scale based on gut feeling/experience (which I don't mean in a dismissive way, experience is a valuable source of insight) or do you have some concrete examples to elaborate why you think that?
https://blog.klipse.tech/databook/2020/09/25/data-book-chap0...
This tends to have other benefits besides performance as the simplification of code that results from focusing on the right thing also tends to help with architecture (until you go ham on optimization).
What it most certainly is not about is immutable data since that's counter to efficiency. It's hard to beat a big fat buffer you poke at directly. Not entangling code with data and focusing on immutable data is Functional Programming, pretty much its definition, not Data Oriented Programming.
When you create a .patch-file with `git diff`, you are essentially creating an inheritance hierarchy. Imagine having 5 patch files that are applied to the same code file in sequence. This is analogous to an inheritance hierarchy with 5 levels.
patches/inheritance is a very useful tool when you want to make some changes to third party code you depend on before using it - while minimizing the maintenance burden of these changes.
Inheritance/patches is also a very useful tool for describing/representing changes to your own code, but only as a temporary measure - you want to flatten out the levels of inheritance/patching for readability. With .patch-files, this can be done automatically. With OOP/inheritance, this is more of a manual (although still straight forward) process.
A git repository with 1000 commits is essentially an inheritance hierarchy with 1000 levels. Note that git will automatically flatten out these patches when you check out a certain commit - which is what makes git such a great/usable tool. Imagine if git could not do this flattening automatically. Code bases would quickly become unmaintainable if they wanted to retain their change history.
I don't hate OOP. I don't love it either. I tend to write my code in a mix of procedural and diet-OOP paradigms. It tends to produce less state, less inter-dependencies, and doesn't require abandoning stuff like mutable state.
Why do I want mutable state so bad? Because it's the easiest, least abstracted solution to the problem much of the time, and closer to what the compiler will actually want to generate.
I think trying hard to stick to a single paradigm is folly. I like mixing and matching according to the task I'm attempting to accomplish, and I have a firm conviction that my code benefits from this ideology. Encapsulation I do value, but not as a "hard no". I use access specifiers to tell you "you probably don't want to mess with the guts of this thing, it can take care of itself better than you can."
As for inheritance, I don't use it very often. I like how C++ has no base "Object" class. A lot of the time I'm basically writing structs with methods, and I think that's often the right approach.
Ha nice, I love the idea of calling it diet-OOP. "Diet-OOP, same great productivity with 99% less inheritance!"
I'm aware that you can write good OO code, that inheritance can be useful (e.g. in shallow hierarchies), etc. but I'm interested in exploring this other side of programming to improve my code
You now have a new language which you could call a procedural- or functional language based on the set of features that were present in the language you started from. But it is no longer an Object-Oriented language.
To make it more concrete start with Java compiler but modify it so it only allows max one method per class.
You now have NOOP-Java. (Non Object-Oriented Java).
Are you happy? Is this NOOP-Java somehow better than the plain old Java? If not really then removing the OOP-ness did not really help did it?. OOP is good. You want to keep OOP, not remove it.
OOP lets you have multiple functions associated with a data-structure the details of which you hide behind those functions (a.k.a. methods). I think THAT is the essence of OOP. You can have multiple functions attached to the same data-structure and only those specific functions can read and write that data-structure.
Isn't that something that really is so useful that getting rid of that feature, getting rid of "OOPness", would be crazy?
It really struck the confidence I had in C++. The code structure had so much technical debt and non-sense, that most developers never really dare say how awful it was. I was impressed by the blind humility of all of this, it seemed like a very elaborate game of hot-potato-passing and it-s-not-my-fault. Of course this software team was thriving, because it was in a monopolistic domain that would always rain money to write this software, even if it was bad. The manager was a highly motivated guy, very humble, but he also seemed a bit delusional, because fixing this software was going to take at least 5 or 10 more years.
OOP sounds like an utopian abstraction, some kind of "smartest guy in the room" stuff. It's like pure mathematics trying to do practical physics and engineering. It just doesn't work.
It's really time academics start to highlight the dangers of abstraction, and start writing some philosophical approach to software engineering and how to solve problem IN PRACTICE.
Personally, I mostly use namespaces to "encapsulate" data, behavior and code. I just write functions and use data oriented programming, even in python. It's funny how people finds this inadequate and bad practice. I never share code because I'm afraid to go in an argument because of it.
Code that use classes, inheritance, private/protected access, is just impossible to read and follow. It's just obfuscation to me. Unless you're writing a library that is used by many, meaning it requires well-structured inheritance, 99% of developers should not write OOP.
I will never forget this hackernews comment mocking people who were presenting their project, praising how complex it was. OOP seems like it's the main method used by "hostage takers" to make their code only readable to them.
In any kind of engineering, simplicity MUST PURSUED AT ALL COST. Only use complexity as a last resort, and FIRE developers who are unknowingly becoming hostage takers, and who keep innocently arguing that if they can understand their code, anyone can. Advocate of OOP will cost a lot of pain to future developers.
> “I invented the term object oriented, and I can tell you that C++ wasn't what I had in mind.” —Alan Kay.
Read here for more info: http://xahlee.info/comp/Alan_Kay_on_object_oriented_programi...
That's silly and not how definitions work.
definition noun def· i· ni· tion | \ ˌde-fə-ˈni-shən
an explanation of the meaning of a word, phrase, etc. : a statement that defines a word, phrase, etc.
In other words: words or phrases have actual meanings, regardless of what people call them.
To give you an example, people use "their" instead of "they're" all the time. It doesn't mean that "their" means "they're".
I'm confused by this argument against multiple dispatch. Can't there be a third module that references both?
However, i do love a library like pandas with their data frames and series etc. which are two really cool and powerful abstractions / classes.
I also noticed this trend in Python/ Data Developers (including myself),that they start writing mostly procedural code and at some point they feel like they "have to" move to classes to be more sophisticated.
Maybe I am also just bad at finding good and easy to understand abstractions that you can re-use like a data frame. But maybe when it comes to data this is just quite hard? Would love to hear about other data-peoples perspectives about OOP.
Also the thing with data engineering the state lives in a DB or a file and not really in my class (because its soo large).
"That you don't understand something doesn't mean it's flawed or bad."
Precisely. Many of the arguments against OO are from academics that don't write real-world, large-scale software.
More arguments are from beginner programmers with a mere 2-5 years of experience that have never personally encountered the mess that programming at scale was before OO made it manageable.
Even more arguments are from people that assume that "precisely what language X happens to do" is exactly what OO is or isn't, and then argues from that straw man position.
Those same people then cheerfully embrace microservice or K8s, even though they are object-oriented! In case this is not clear, let me define OO for you:
"Object oriented design is all about encapsulated private state with abstract interfaces that have varying implementations that clients don't need to know the specifics of -- even the type names -- ahead of time."
Sounds like microservices? Or K8s? Or REST? Or service bus? Or event streams? Or any large-scale programming pattern? Yes. That's because abstract interfaces and decoupled components are critical to enabling large teams to collaborate on software too big to hold in the head of any one person.
OO is not intended to solve manager-human-employee-customer categorisations of real-world entities. That's a stupid straw-man argument born of toy textbook examples that aren't representative of real-world programs to begin with.
OO is intended to allow programming at the scale where simple procedural programming no longer works. I didn't understand the need for OO either until I worked at that scale myself. It seemed overcomplicated and unnecessary.
This is why it cracks me up to see language designers (and random Internet bloggers) talk about how "they don't need OO". Yes... you. Singular. One person doesn't ever need OO. Many people do. You're not many people!
Has anyone seen large scale development done successfully in an an "anti-OO" language like Rust? I haven't. The single largest codebase is probably Mozilla's Servo, which is still a toy compared to what's been written in C++ or Java! It was written by a few people, mostly in one building. Notably development of it slowed down and was abandoned. It was never shipped as intended.
How would you even envision a Rust project working with, say, 1000 developers? What happens when there's some new functionality added? Does developer #587 have to go notify the other 999 developers to update their switch statements, pattern matching code, etc... ?
Right now... generally speaking, yes. You'd have to go tell other people to go update their code. As you can imagine, 1000 developers telling 1000 developers to update things is a 1M-level organisational scaling problem.
That's why automated tooling was invented to handle this problem automatically, even at runtime, let alone compile time.
If you think automation is bad, then we no longer agree on the most basic concepts and this conversation is over.
If you have a better method for automating this problem, then I'd love to hear about it! Just make sure it's not OO with a different name...
Certainly there are none with more than 1K developers collaborating.
I'm aware that you can have "dyn Trait" in Rust and a Box of a trait also provides runtime polymorphism. However, it just has too much friction due to explicit "no more OO!" decisions by the Rust core language team to enable truly large-scale programming, at least in my humble opinion.
I think such decisions stem from logic like this:
"I saw OO used extensively at <insert large project> and the <project> was a huge mess and nobody was happy, hence OO is bad and should be avoided."
Meanwhile the logic is more like:
"<large project> needed OO because it was large, and it was unpleasant to work on because it was large, not because it was OO. It would have been worse if it wasn't OO."
So given the pervading reality, colloquially when we say the term "Object Oriented Language," we are not referring lisp or C or haskell even though all of those language are technically OO.
The same can be said for Rust. Rust is NOT OO.
> "Object oriented design is all about encapsulated private state with abstract interfaces that have varying implementations that clients don't need to know the specifics of -- even the type names -- ahead of time."
"OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things." - Alan Kay
You're only using 1/3 there. First-class messages were more important than encapsulation to him.
It is as if you sent me a letter by "first class mail". Great I can read your letter. But your letter can not directly alter the arrangement of furniture in my house. Only I can do that. And perhaps when I read your letter I decide to take such action. Or maybe I decide not to.
Taking "anti-OO" to mean languages that specifically don't have OO capabilities (rather than languages where you can choose not to use OO). Any of the large C code bases?
The Linux kernel is huge, yes, and is written in a "pre-OO" language. It's also full of OO paradigms.
For example, the core kernel developers don't want to have to write huge "switch" statements to handle the thousands of different drivers written by tens of thousands of third-party developers.
What do you expect to see at this API boundary? Perhaps.. and OO API with vtables and everything?
Yup: https://www.kernel.org/doc/html/v4.11/driver-api/infrastruct...
Function pointers in structs as far as the eye can see...
Agreed with the point, though.
Fair point. Though with the explicitness that's required to do this in C (you have to pass the actual struct pointer that would normally be the receiver, along with a general lack of inheritance) results in code that's much easier to understand than the patterns that the traditional OO languages have created for themselves. I suppose that's less of a criticism of OO itself and more of a criticism of the people who build languages that emphasize OO, and the general ecosystems that crop up around them.
> written in a "pre-OO" language
I would be remiss if I didn't point out that the "OO languages" go all the way back to Simula67, which predates C. Though at that point OO was still in its "academic" phase the way functional programming is today.
Why focus on building OOP functionality into the language, when it should be the infrastructure and API frameworks that should be built for OOP?
A2) Because it's about 1,000x to 10,000x more efficient.
An awful lot of the "ills" of modern development practices boil down to the lack of ingrained rules of thumb related to performance. The difference between a local function call -- virtual or not -- and a network call can easily be a factor of a million.
This just isn't in the mental model of most developers. The terms "nanoseconds" or "clocks" are not in their vocabulary.
I grew up and learnt programming in an era where OO was considered extravagantly wasteful because virtual function calls had an extra indirection! Those precious instructions -- and more importantly -- the lost opportunity for inlining or CPU pipelining were considered brutal performance hits.
These days, people throw Python into Docker containers and run them remotely on the network to invoke what amounts to a page of code. They call this "modern".
Then they go on Y Combinator News and complain about how OO is "bad" somehow. Quite a few of these people have probably never written a class hierarchy from scratch themselves.
I literally just spent a day talking to some full-time developers with years of experience, explaining how to implement a simple "storage abstraction" OO hierarchy. You know, you have a base interface or abstract class with a bunch of implementations like "S3BucketStorage", "ZipFileStorage", "LocalFilesStorage", or whatever... and then you have the meta-implementations that combine them, such as "UnionStorage", "CacheStorage", and "RetryStorage", each of which take the abstract interface as input parameters during construction. So you can have local files act as a cache for S3 buckets (with retry) that override a local zip file of static content. Or whatever! Combine implementations to suit your whims.
They looked at me like I had grown a second head that started speaking Greek while the other spoke Latin.
Then they wrote some spaghetti code of functions with hard-coded parameters, checked that garbage in to the repo, and then dutifully sent out an email to management saying "job done".
Is OO bad, or are most developers bad? I suspect the latter...
However, many languages and even C++ with modern compilers can pull tricks to mitigate this. For example, static analysis can often be used to replace virtual calls with direct ones. Similarly, functions can be inlined in many cases, such as a "leaf" class in a hierarchy calling itself.
Languages with "virtual machine" runtimes such as Java and C# can potentially optimise even dynamic scenarios. Java certainly does in some cases.
I think the ideal OO framework would actually be more like what Rust does with traits. Have static dispatch as the default at runtime, but with the traditional OO model of interfaces, classes, derived classes, etc... Dynamic dispatch should be an option, but not the default. Ideally, dynamic dispatch should be used only on the boundary of binary modules such as DLL files or kernel-to-user-mode ABIs.
Note that OO was also designed to reduce compilation times by decoupling implementation from use. So if developer A updates an implementation (class/struct) of an interface/trait, then developer B using that interface can use incremental compilation without having to recompile the usages of the interface! This saves a lot of time for large code bases.
One reason Rust is notoriously slow to compile is because it always recompiles everything -- both implementation and usage of interfaces.
Again, a hybrid approach could work: dynamic dispatch by default for debug builds to enable efficient workflows, and static dispatch by default for release builds for runtime performance at the cost of longer build times.
SAPs ERP system is arguably one of the biggest, if not the biggest software system on the planet. It is written in ABAP and ABAP is a procedural/imperative language. SAP is open source and the code is fairly easy to read. Reusability of functions an procedures provided is fairly easy and groundwork for every developer working with SAP to modifiy, enhance functionality or writing add ons.
Or more specifically it was. ABAP got OOP extensions about 10 years ago and since then the code became more and more inflexible and less maintainable for people other than the original authors.
But maybe the main point about ABAP is that despite its procedural nature it allows data centric programming. You take a line of data, transform this line and return the transformed line. No need for abstractions, inheritances and complex design patterns, concerns with mutability etc. Just plain and simple solving domain problems instead of problems caused from abstraction.
This rise in me the famous joke by Knuth: Beware of bugs in the above code; I have only proved it correct, not tried it.
I wonder which tools he uses to prove correctness, and on which language. I know about Frama-C, Coq, and things like that. But I never had the opportunity to use them out of university. On the same topic, in a recent ACM publication, Ian Joyner compare Frama-C with a lipstick on a Pig.
https://cacm.acm.org/magazines/2021/12/256941-common-ails/fu...
You could argue that hand proof is useless, but by forcing yourself to go through it in tiny steps you can actually sort out a lot of oversights.
These tools are more like putting on X-ray glasses and seeing how your fast-food nuggets are made. You will still eat them if you have to, or if you're in a hurry, but you'll be more inclined to take the time to learn cooking, so you can eat healthier food later.
There may be better ways, but what we have works.
I don't know if the modern world would be possible without OOP. If the only language in the world was Haskell, C, and SQL.... would I have actually learned them, or would I have chosen a different career? Would there be enough devs to have as much software as we do now?
It's hard to tell, because almost everything big is written using OOP. Few things on the scale of LibreOffice or Blink are pure functional.
And Java seems to work exceptionally well for large scale projects.
http://www.smashcompany.com/technology/object-oriented-progr...
> The venerable master Qc Na was walking with his student, Anton. Hoping to prompt the master into a discussion, Anton said "Master, I have heard that objects are a very good thing - is this true?" Qc Na looked pityingly at his student and replied, "Foolish pupil - objects are merely a poor man's closures."
> Chastised, Anton took his leave from his master and returned to his cell, intent on studying closures. He carefully read the entire "Lambda: The Ultimate..." series of papers and its cousins, and implemented a small Scheme interpreter with a closure-based object system. He learned much, and looked forward to informing his master of his progress.
> On his next walk with Qc Na, Anton attempted to impress his master by saying "Master, I have diligently studied the matter, and now understand that objects are truly a poor man's closures." Qc Na responded by hitting Anton with his stick, saying "When will you learn? Closures are a poor man's object." At that moment, Anton became enlightened.
[0] http://people.csail.mit.edu/gregs/ll1-discuss-archive-html/m...
http://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/sha...
Ah, I see, OP has never written nontrivial code.
so its realistically the only programming language you'll ever need and it's the right tool for every job because its point and click and it interfaces with the real world so that people can relate to it better and the learning curve isn't as steep.
1) Most of the time GUIs use OOP, and its the "other designs" which have a hard time being successful.
2) The rejection of multiple dispatch is very "hand wavy", a lot of Julia's success is linked to its multiple dispatch feature.
No mention of Ruby or smalltalk in this post, which i think of as "true" OO languages, down to the runtime. The ruby object model has its merits! Sandi Metz's POODR is a fantastic intro into OO _and_ a compositional approach to design.
FP vs OO is always a false dichotomy for sure. Actors and messaging appear in FP languages. Inheritance surely has nothing to do with OO. Inheritance means nothing for data, and it's almost a bug to extend Record types.
In short, this post seems to rage against inheritance and blind use of design patterns, not the spirit of OO. But the post also qualifies that "that is what an OO advocate would say".
Consider Typescript. The same program can be written with `class`es, or as a module of "loose" types and functions. Really, lets pick a mix that best represents the problem we're solving? I think OO can be a _useful complement_ to FP and other paradigms.
I do wish it were a bit faster ,but hopefully truffle ruby, sorbet, yjit, etc. can put in the work to fix that.
For instance I don’t feel like function are first class citizen still. ( I almost never user higher order function in Java, even if it’s possible )
The book Clean Code [4] helped me a lot to really learn how to write clean Java, and many of these ideas directly translate into writing good functional code. One of the key takeaways was just how small functions should be which incidentally is a great thing to learn when functions are your main unit of composition.
Java isn't a purely functional language, so you obviously will always have some impurity regarding state/mutation. I personally try to do the following: 1. Keep all state in some top-level class and let everything else be immutable 2. Nearly every class I write is immutable 3. Follow common OOP principles like SOLID 4. Write reactive code with heavy use of Optional and Streams
Here's [5] an example repo of board game written using those ideas.
[0] https://www.oreilly.com/library/view/effective-java/97801346... [1] https://www.baeldung.com/java-functional-programming [2] https://projectlombok.org/ [3] https://www.baeldung.com/java-record-keyword [4] https://smile.amazon.com/Clean-Code-Handbook-Software-Crafts... [5] https://github.com/harding-capstone/logic
Those advice ring familiar or interesting. I need to give effective Java a second look. It’s been year and I’m a different dev now.
The solution is simple, there are tons of languages to use today in a single service/application. Just create your own and put it on the market for validation, if OOP or not is really at the center of productivity then it will be addressed.
Otherwise, I really don't understand the point of such rant posts, feels like marketing and personal flexing.
To this day, oop advocates can't even agree on what oop even is or means.
Apparently oop as envisioned by Alan Kay was supposed to work like cells in the body that pass messages between each other and take actions independently.
Why? Who knows! It was never really explained why literally one of the most complex systems imaginable, one that we still really have very little idea how it even works, should be the model for what could and should probably be a lot simpler.
Today's modern oop languages are probably very far from what Kay envisioned (whatever that was), but it's remains unclear why classes and objects are "better" than the alternatives.
And before anyone goes and comments aksully code organization blabla like yes but code organization can be great or shit in oop or fp or procedural codebases, it has nothing to do with the "paradigm".
Let alone that the entrenched, canonical, idiomatic coding styles of most modern oop languages encourage state, mutability, nulls, exceptions and god knows how many trivially preventable entire classes of errors.
Granted, most have now started to come around and are adopting more fp features and ideas every year, but still.
---
EDIT tacking on other old comments:
---
Don't get me wrong, writing programs like cells in the body that pass messages between each other and take actions independently is an interesting idea which deserves pursuing, if nothing else but to satisfy our curiosity and seeing to what if anything it's applicable and suited. (and even if the answer turns out to be "nothing", we've still learned something!)
But going from there to making strong claims about it being a more or less universally superior paradigm for computing and writing code, with little to zero evidence, that's a huge, huge stretch.
To the degree Erlang and Actors work, I think that's kind of a happy coincidence, and not due to any rigorous work on Alan Kay's part.
---
Just look at all the "OOP" languages where both the language developers and the user community are coming around to the facts that
* immutability and absence of state is preferable to mutation and statefulness
* Option/Maybe types (or "nullable types" which are a shoddy implementation of the same thing) are better than null
* Either/Result types are better than exceptions
* making things implement map, filter etc and sending in a function that describes what you want to do is better than manually eg looping through lists etc
etc etc etc
Anyone who doubts how endorsed this is, just read what Brian Goetz and Josh Bloch have to say about how to code in Java.
Just imagine if these languages had been implemented with these ideas in mind from scratch instead of the current situation of trying to adopt and retrofit this style when the core libraries fundamentally don't support it.
The current trend of "OOP" languages is basically inexorably heading towards FP and abandoning the old school "OOP" style. Eventually they will only be nominally "OOP", mostly in order to please people who have irrational attachments to labels like that, but be way more FP in nature and in all but name.
For what it's worth, people shouldn't be irrationally attached to the "FP" label either. Labels aren't important - what matters is the code, how easy or hard it is to reason about it, how well it avoids entire categories of defects from even being possible etc etc.
https://proandroiddev.com/kotlin-avoids-entire-categories-of...
I mean Kay's OO, inheritance, class, polymorphism, abstraction, those are mix of paradigm, design pattern, and language feature.
So does FP with its monads, sumtypes, referential transparency
I guess what can make a better discussion is to take apart the label into these individual items and examine them one by one.
Inheritance - bad - get languages to safely discourage its user from using it
Actor and messages - good - get people to understand the concept and how to implement it in each languages
Immutability - good - let's announce how it helps reduce errors
Functional domain modelling - good - let's make everyone know how to do it
Foreign jargon of FP traits - bad - let's make a more walkable learning curve to those jargons
And so on and so on
>Why? Who knows! It was never really explained why literally one of the most complex systems imaginable, one that we still really have very little idea how it even works, should be the model for what could and should probably be a lot simpler.
If you're curious, I think it'd help to read. In it he illuminates what he means. http://worrydream.com/EarlyHistoryOfSmalltalk/
> I just recently figured out myself the pieces I was missing to writing effortlessly skimmable texts.
No; no, you didn't.
The early (early as in C++ days) benefit of OOP was probably that it made programmers stop and plan before coding.
Having learned FP 20 years after learning OOP, I feel certain that I can do the same things in less and more understandable (and MUCH more easily testable) code in FP.
OOP was awesome for a small set of cases and for academic scenarios. But like REST, it doesn't map well to all needs. Then it becomes awkward and unnecessarily complicated.
FP, or simplified as immutable data transformations with necessary mutations pushed to the edges, works everywhere, all the time. In FP you can still choose to model your data in hierarchies, keeping some of the useful bits of OOP. But it's still data in, results out.
Until you've spent time on real projects in both, you cannot appreciate why FP is superior.
(And to be clear, I'm not even talking about Haskell and type-obsessed FP. Perhaps because I'm not a Haskell guy, I can't appreciate it. But it smells to me like an extreme of a good thing (and therefore not usually the best thing for the situation); but I digress.)
Concretely, I worked recently for a client that needed Ruby on Rails development done on a production product. It was fairly OOPish (in the Ruby way, which is to say much less insane than Java from the past OOP). Even so, the OOP at the business logic level was unnecessary. Testing was complicated and full of code to mock and fake.
When we were given the task to build a completely new solution to the same problem, greenfield, I immediately began building modules with almost entirely pure functions. Because Ruby isn't designed for this approach, it does involve a bit of care to respect memory and object copy time costs. But in many business cases, the volume of data being processed isn't significant or isn't fast-repetative.
The new product had many fewer lines of code, and the test cases were as close to beautiful as maybe is possible for tests. That's a different subject for debate...
The less experienced devs took a bit of time to accept and adjust, but the mid-level engineers became big fans of the approach. The juniors just accepted it (so nice :D). Test coverage went up, and the cost of adding new features went way down.
My FP languages of choice are Clojure and Elixir in that order. Elixir does offer some pretty great features that Clojure doesn't have (extensive pattern matching), but the syntax is imo very noisy compared to the utter simplicity of s-expressions. Either is fine for me. Ruby is fine, with some care. Python too. Vanilla Javascript can be fine, and some libraries can improve this.
Anyone who has read this far and is not convinced, I urge you to follow some Elixir tutorials and reach the point of grokking it. Then if you're a web dev, Phoenix is a fantastic framework. Also, the Erlang (BEAM) VM provides so many useful structures and utilities to allow you to build big distributed things easily compared to other languages.
Be warned though: once you do this, you will forever be frustrated by OOP codebases.
Can't tell if this is deliberately ironic...
Writing something that inherently has a lot of state, like a simulation, game, or user interface? OOP might be a good choice. Working on a backend API that's basically a database wrapper, a data processing pipeline, or something else with less inherent state, FP may be a better tool. Choose the rights tools, design good architectures, and stop blaming the language paradigm.
Strong typing can be a blessing and a curse, and screwing up type signatures through poor foresight can be a problem regardless. But get into elaborate object systems and it can be a different level. When they're done well it's no problem, but when they're done poorly, it's maddening.
"(Anything)-oriented programming" is obviously stupid: the world doesn't conform to your (anything), whichever you choose. QED, an (anything)-oriented programming language will be the wrong thing most of the time.
A general-purpose programming language needs to provide support for solving actual problems, whatever they are. Any program big enough to be interesting addresses sub-problems of different kinds, that each need their own treatment. Most problems benefit from a mix of approaches.
Once in a while objects are exactly the right thing. Then, if your language has stuff for that, lucky you; otherwise you need to cobble something together. Likewise, when any other formalism matches.
Almost always when people complain about OOP, it is because they want their (other-thing)-oriented language to get respect. That does not end well: the same arguments against OOP apply equally well to their thing, given trivial adjustments.
So, nothing to see here.
same goes for all the other language features.
I would say early OO is very different from what is practiced today. I use Kotlin mostly. It's obviously an OO language but it puts some interesting twists on it relative to earlier languages (like Java):
- classes are closed by default and you must define them as open to be even able to create a subclass. When you do, you must explicitly label things in classes that you override. This prevents, un-intential abuse of inheritance that is common in many Java frameworks. E.g. Spring has riduculously deep inheritance hierarchies. When I was still using Java I had a simple rule: any form of class extension is probably something I need to get rid off. Delegation is just preferable in my opinion. I almost always end up regretting class extension to the point where I rarely consider using it.
- Speaking of delegation, the Kotlin language designers obviously agree with this and added interface and property delegation to the language. This is just syntactic sugar but it's awesome. I can take any class and pass myMap: Map<Foo,Bar> into the constructor and then add implements Map<Foo,Bar> by myMap. And just like that you have extended a class but without actually extending it. I can even override some of the methods (because it implements the interface). They basically provided syntactic sugar for a common design pattern: delegation, which you should almost always favor over inheritance IMHO. Property delegation is equally powerful and you can use it to e.g. lazily initialize a property foo: String by lazy { someFunctionThatReturnsAString() }
- it encourages the use of val variables that cannot be reassigned. If you want that, you need to use var. If you define a var and don't reassign it, the compiler will warn you to use a val instead. Immutability by default is encouraged and it helps with e.g. asynchronous code and a few other things.
- it has sealed classes (and interfaces) as of a few versions ago. The advantage of those is that the hierarchy is closed after compilation. So you can't add more sub classes and this benefits the compiler doing some optimizations. Likewise, value classes are now a thing. In the rare cases I do use inheritance, I use sealed classes.
- it actually discourages class extension in favor of using extension functions and properties. This is much cleaner and does not suffer from a lot of the problems associated with inheritance. For example, you can't actually override anything this way. Extension functions are surprisingly useful and I use them a lot. They even work on type aliases or on nullable types. So you can call a function on a null value for e.g. a nullable generic type T and it's not going to trigger a null pointer exception when you do that. More languages should add this. This just removes a lot of use cases where you might have used inheritance or interfaces in the past. This also removes a lot of the need for multiple inheritance; which is problematic in the few languages that still support that.
- having default values on parameters in functions means that you rarely have more than 1 constructor for classes. You can add more constructors, but it's just not something you'd need often and certainly not to support different combinations of properties. Constructors have no body either. All that happens is assigning properties. This enforces the sane rule that constructors must do no work. If you need to do work on class creation, you add an init function.
I'm sure some language designers have plenty of nits to pick with Kotlin. But for me it's a very pragmatic language that mostly manages to nudge people to do the right things while providing them with a lot of convenience. Scala people tend to look down on it for example and that language does have some interesting features. But then I find most Scala code to be utterly unreadable because. Purity has a price, I guess. And of course many Scala coders consider its OO legacy to be somewhat of a mistake apparently.