Depends on how you view OOP. For instance "The Repeated Deaths of OOP" (2015) [1] notes how the definition of OOP has been morphing over time to match current realities.
My personal opinion is the problem people are solving moved from what used to be called "desktop applications", for which OOP is a good solution, to other problems. Also, the advantages of HTML/JS (cross-platform, ease of distributing updates) is so great that native apps have fallen out of favor. On the server side and for things like ML/AI, OOP is less helpful, hence the rise of FP. Unfortunately, the people developing web frameworks apparently did not have much experience with native GUI libraries, which all work more or less the same, and they decided that a functional programming style with React was the way to go. So now you have a soup of React hooks and JS callbacks that makes code as readable as something with goto everywhere. But each individual page is rarely complex enough for this to make maintenance more than slower / more tedious than necessary. OOP is old-fashioned now. React is the way.
Last pop quiz: what makes Python an object-oriented language?
Ah, hm. It can’t be classes, or I’d tell you you’re wrong. So what is it? [...]
self? No, that’s not a keyword or anything; it’s just the de facto standard name
for the first argument. So what makes self work?
That’s close enough, really. The answer is descriptors, which are basically “the
things that make self work”.
From another blog post on the same site [1]:
A descriptor is just an object; there’s nothing inherently special about it.
Like many powerful Python features, they’re surprisingly simple. To get the
descriptor behavior, only three conditions need to be met:
1. You have a new-style class. [...]
It therefore follows logically that Python code using old-style classes is not object-oriented, and that Python was not an object-oriented language prior to the introduction of descriptors.
In the first case, she's mentioning descriptors to make the same point as for "this" with javascript: you can bind a set of related data to a function.
Exposing "descriptors" is a latter thing, but their default implementation was already a thing before they were visible to python users.
You can write object-oriented code in C, but no matter what tricks you do with storing function pointers in structs, you still have to pass the struct itself as an explicit argument.
A sprinkle of clang blocks and Block_copy on top of your structs and you’re on your way!
It is interesting that Java is mentioned there as a language without first-class functions. Maybe, but from programmer‘s perspective it kinda does and they are encapsulated in «namespaces»: they are static methods of classes and with ˋimport staticˋ they do look like first-class functions.
Well... you can define an interface, and have a class that implements the interface, and pass an instance of that class as a variable. That's not quite "passing a function as a variable", but it's kind of close if you squint.
What do you mean? Since Java 8 you can assign a function to a variable, pass it as a parameter etc. Lambdas in Java are not pure, since they are converted to objects by compiler, but on user level they have all attributes of first class functions.
>Object-oriented programming is about objects: bundles of state and behavior.
That means hiding state, and having it spread throughout the app, in places that are hard to find, which is error prone and leads to complexity. And monstrosities like FactoryFactoryFactory [0] and Kingdom of nouns.
Golang has first class support for bundling state and behavior: Receiver-Functions, often called methods. This means exactly 2 things; a) That for a type `Foo` I can do this:
f := Foo{}
f.funcThatUsesFoo()
instead of this:
f := Foo()
funcThatTakesFooAsFirstArgument(f)
and b) that Foo implements all interfaces whos method signatures it's receiver functions satisfy.
It doesn't mean that I now have to suddenly drown my code in over-architectured patterns just to satisfy some OOP sense of code-aesthetic. It doesn't mean that I have to try and hide state from code in the same module. It doesn't mean that I have to bundle things that could be free-standing functions into some type just because. It doesn't mean I have to make any of the mistakes OOP popularized over the years.
This ten year old essay is still strong. It makes some points that are worth repeating
> I have a hypothesis: this pattern is so common for the simple reason that Java doesn’t have first-class functions.
She’s picking on Java deservedly because Gosling made what in retrospect was a mistake to go so all-in on classes, and because it became such an important pedagogical and deployment language (and nobody will shoot at you if you’re unsuccessful).
Say what you will about C++ but it took a different path, not only changing its name from “C with Classes” but making classes a tool for manipulating the type system (and not the only one) and supporting the true benefit of a class system, generics.
Edit: I changed an incorrect "He" pronoun to "She". Thanks to quickthrower2 for pointing out this embarassing blunder.
Here's my hot take. Most patterns are incredibly awful and abused because people prefer to solve architectural related problems in code and produce "elegant" solutions over delivering features.
Here's my pattern that I use instead of a MVC/MVVM/MVP/etc/etc: I have a handler function to choose what url and routine I need and it calls some functions and methods and then renders some json / templates out some html.
I think a good rule of OOP is to try to write everything out as functionally pure as possible and refactor into classes when you see data AND functionality blocked together that you can bottle up into an object. To start ranting, I think a lot of the OOP insanity you see in .net/java/etc is from dogmatic approaches to unit testing over there and the common examples of how to do it involving the mocking and interfacing that end up obfuscating the actual production code.
You can kinda argue whatever you want when it comes to this stuff. It doesn't really matter. Is the code readable? Does it not make me angry to look at? Yes? Then its good enough. I don't care if the object has one method that probably should be a function. I can still read it fine.
I agree. The problem these OO patterns tend to introduce is unneeded levels of abstraction that all boil down to making the code difficult to read without three wide screen monitors.
Well Simula was developed specifically to model real world problems. And when they had invented it they found it much more useful than that which had preceded it. OO is not supposed to model nature. It's not string theory or whatever other incomplete-yet-closer-to-reality-than-oo model that physisicts use.
But what about how humans think? We make use of universals in our language all the time. And sometimes that fits a problem domain well, like making a GUI or running a simulation of things.
we don't really think in terms of OOP, OOP is an abstraction and systematization of inheritance, which we do use to think sometimes, but as its abstracted it becomes untennable to use ...
there are relations between things that can be mapped well with inheritance, but thats a problem, we don't really want code to represent the thing we're coding, code needs to be more dynamic easier to read, change and move around, etc. representation should be between the end result of the code and the thing we're trying to represent
It's a good thing the greatest minds of programming have worked hard to solve this problem by giving us the even more platonic and rigid functional programming.
> The app is the object, and the various URL handlers are its state.
There were a lot of assertions in this article that rubbed me wrong, but this one was particularly egregious. Handlers are behavior. They handle something.
This feels like the kind of article I would've written early in my career when I was getting really clever with stuff and starting to be able to question the way I understood the world of programming.
I think it's saying that the state of the app object itself is the configuration of the linkage between paths, verbs and handler functions, and that's why it's worth making an app an object.
The Handlers implement behavior, sure. But from the perspective of the `app` object, the handler function objects are state. You can tell, because `app` is an instance of the Flask class and the Handlers are added to the instance by way of a function call.
So, Handlers are functions that "handle something", but they are also part of the app object's state, not its behavior.
Handlers are functions that are part of the app object's state, which alters the app's behavior.
In the majority (possibly overwhelming majority) of the cases this is true. The purpose of Handlers and similar Publisher/Subscriber patterns is effectively to change application behavior when they are configured.
> There were a lot of assertions in this article that rubbed me wrong, but this one was particularly egregious. Handlers are behavior. They handle something.
class App {
public IHandlerFoo Foo; // routed as /app/Foo
public IHandlerBar Bar; // routed as /app/Bar
...
}
...
App.Foo = new HandlerFooInstance(...);
...
Foo, Bar, etc. can evolve under an app's state changes, so the handlers to invoke are states of the app. They implement behaviour too, yes, but the original wording isn't wrong either.
I wonder if at some point, we could train an LLM to churn out hot takes about why this one design pattern is awesome but that other design pattern is horrible and why you need to die in a fire if you use MVC but everyone should be using MVVCP instead but do stay away from MVPVC or your life will be wasted. Then we could train a second LLM to read the first LLM's articles and write flaming rebuttals and have the first LLM write rebuttals to the rebuttals. And if everything works, we could finally automate the entire debate away and go on writing code.
We could train our vacuums to utter snarky comments and eat popcorn while they watch these LLMs. Then we can go back to do something productive like gardening work.
Current LLMs are trained on internet data which is mostly hot takes including the ones you mentioned. I'm not sure there is much room for improvement and what you suggested will probably work well with existing LLMs without any specialized training.
The many "you're wrong if you believe that" made me remember the anecdote of Alan Kay attending some conference about OOP and saying that it was wrong, that he invented OOP and it was not like that.
IIRC, Kay's vision is that OOP is about messages.
I have my own pet theory, of course it must be wrong too, but if I may, only as food for thought: The core usefulness of OOP is usability. A language is a matter of connecting thoughts through a mental model. Subject, verb, predicate. Object, method, parameters. That's mostly it. The rest are implementation details and lots of bikeshedding.
combine a type system with dispatch logic, using abstractions .. it is very clear for some engineering applications and/or tinkertoys. Many people can get the basic ideas with thirty minutes of introduction.
I haven't used it, so not sure if it fits the pattern, but the question for me is: is it useful, time-saving, more usable? does it make the task more clear for the programmer that uses it?
It depends. It is a fundamentally different branch of the OOP family tree than what most people are used to seeing. Enough so that I've seen people declare it to *not be OOP*. So if you stumble across the model having only seen the style of OOP popularized by C++ and later Java, no. You will probably *not* find it to be liberating.
The idea is that you have classes which model state. And then you have generics that model functionality. And you define methods which provide an implementation of the generic for a class. But it's more flexible than what you see in Java because such a method can be easily added after the fact as it's not intrinsically part of the class' definition.
If you're familiar at all with Typeclasses in Haskell & Scala, personally I find those to be similar enough to get the gist. Likewise Dylan and R's S4 objects are modeled after CLOS' structure.
Runtime, multi-method dispatch is probably the singular thing that stands out as "missing" from other systems. I don't miss it a lot, but there have certainly been times when I have.
I also enjoyed the ability to not be locked into the rigor of a class structure when it comes to methods. Since CLOS is function based, it's trivial to add functions to a class that you don't even have the source code to.
There have been many times working with a 3rd party or system library where I've had that "if only I had this little method" moment that would make my life easier. I'd rather have that capability and fight, say, namespace issues for the "Well what if everyone added their own 'upshiftFirstLetter' method to the String class" problems on an ad hoc basis.
Part of this, of course, stems from the locked down nature of the scope of classes. Not having access to internal structures and state. I'd rather take those risks of leveraging internal state knowledge not supported by the original designer vs the alternatives of sometimes having to throw out the baby with the bathwater.
This is the reason I find it useful. To me, OOP is as much about your organization as it is about the best way to load, transform, present, edit, and store data. I think the culture of some companies lends itself to various kinds of programming, but it's the cultureless companies where OOP is most useful. The places where nobody is trying to change the world, where people work to pay their mortgages, where an executive may only work for two years and a programmer may only work for six months.
It's in an environment like that where a self-documenting, self-configuring code base with custom classes and exceptions that guide the next developer is essential.
Every developer should have two users in mind. The person using the software, and the next developer who maintains the software after you're gone. OOP is a great way to empower the second user when the only thing that will reliably outlive the developer is the code base.
I want to second the idea that the primary benefit of OOP is logical organization.
For smaller projects I don't care one way or another about OOP practices, but once you start getting into hundreds of thousands of lines of code IMO it becomes an absolute necessity.
But we don't need classes, vector tables, or other runtime features to achieve organisation. We just need our compiler to recognise namespaces. "module Foo where"
If I create a REST API no one complains they don't have access to the inner-workings, local variables, etc. But if I give a similar experience and call it a "class" suddenly it's ugly and mean.
I find that's often because classes come with a lot of stuff that is less desirable - mainly inheritance and its assorted complexities.
The other side is that classes aren't the only way to get this sort of encapsulation. The classic example is closures - data inside the closure acts as the encapsulated data, and the returned type of the closure is its public API. ML languages typically use modules in place of classes - the module signature defines the public API, and rather than calling methods, you instead call functions with arguments (not `list.length()` but `length(list)`). But again, that's just syntax - we're keeping the same encapsulation because the module-defined functions are the only ones capable of fiddling with the value's internals. You also see this in Rust, which does use method syntax, but has traits and types that act more like modules than typical classes.
All in all, I don't think anyone's complaining about encapsulation, but rather it's a question of whether typical OOP (with all that that typically includes) is the best form of it.
Objects open up some advantages in how you snap together code that namespaces alone do not.
Code is much easier to deal with when you define abstractions around small sets of functionality and then allow the caller to pass in various objects that provide those functions to the code that needs it.
You can have an application that accepts a 'data_backend', and then provide a data_backend that just stores information into a dict for testing and getting the app initially written, one that tracks all of the changes made or exposes them for tests to check, or another that stores it to sqlite for running a local instance, and another that stores it to some real database for a larger one.
The calling code doesn't need to know what the data_backend does or how it works, it just tells it "store this", "read that" and the data_backend does whatever it does and data goes in and out of it.
You build up all your code that way and you'll be able to easily stub chunks and replace them with functional implementations, and then swap those out when needs change or by options given at runtime.
It's a lot easier to read and write than code that's littered with a million if statements trying to keep track of too much complexity in too many ways all at once.
OOP is just syntax sugar and compiler constraint enforcement for the same kinds of things you see the linux VFS do. There are many filesystems for linux, but each just provides a handful of functions in a structure that should be called against the structures representing the various types of filesystems. In Linux's C you have to slap it all through a (void *), but in languages with objects, you can use those as the medium of abstraction instead of doing it manually.
Some make you do a bunch of inheritance garbage with stapling objects together, others will let you build to interface definitions, or just check the structure of the object to see if it matches the needs of the caller, or be a dynamic language that just checks for the members at runtime.
I think that OOP is the best way to accomplish the goals you list only if you stick to languages with mainstream appeal - but think it's sad that's the state of the world. Objects (as they are used in e.g. Java) default to stateful and complect data with the methods on that data. I find that most of the time what I want is closer to OCaml's Modules[0] which give me many of the tools of code organization without the complexity with state. (note that OCaml allows objects, so you get a real sense for how often you want an object over a module, 95% of the time I wanted a module).
This is one thing I like about Scala. While its classes can be used in the exact same manner as Java, that's not how its creators pitch them. I've seen them advocate using classes/objects akin to ML's modules, but there's enough flexibility to pivot if for some reason that does not make sense.
Everything is wrong or illfitting. I mean we are making tiny blocks of silicon do extreme amounts of human work, while being entertained with music and videos. OOP is like five abstractions deeper than what it intend to model. It's a tool, I wish people stopped trying to make the extreme case of engineering that is software development some sort of axioms around physical law. Our expectations would be way better especially when our bosses require we implement a nosql db that stores rss feeds and use machine learning to sort them in order of "coolness".
There's very few actual rules despite opinionations derived from arbitrary paradigms and what should be done to work cohesively as a team. I wish there were a smidge less ivory towers and a smidge more common language.
> The core usefulness of OOP is usability. A language is a matter of connecting thoughts through a mental model.
Incidentally, this is exactly how I came to "reinvent" OOP as a newb programmer. I was working in a codebase where I didn't know what tools I had at my disposal, but we had a few modules where I could just type `module.` and see a list of all the methods, right there in front of me (in VSCode's intellisense). I asked, "Why don't we do this for our helper functions?" and we ended up with a handful of major objects to import with easily discoverable methods.
now of course I could have crawled the codebase to get the same information, but for someone new to programming and/or someone brand new to the codebase, that isn't necessarily a good use of a time. it can be a lot of slow unraveling of "what goes where", whereas organizing things in "OOP" (I still don't know if I'm actually using this term right because it was just part of how I learned, without being named) teaches that information more quickly through use and experimentation. basically what I was looking for was "namespacing" I guess, but I would also say it helped organize our code in a more useful way.
But Smalltalk also has inheritance and metaclasses, so it wasn't just objects passing messages. It was also designed as a visual live programming environment which was basically the entire computer system. Kay had his vision, but Smalltalk was implemented as more than just message passing.
That and Simula proceeded Smalltalk, which C++ was inspired by, even if Kay coined OOP.
Meanwhile, in busytown…millions of developers will wake up on Monday morning, sit down to their work and make something using the best tools they know.
Some of us would prefer spend our days with WhatsApp server's elegance rather than than the Facebook server's ugliness. When you have to write a compiler for a poorly designed dynamic language just to get decent performance because there isn't a feasible plan to migrate to something that truly solves the problem you have, you can't help but think that maybe things could be simpler.
> It’s nothing. There’s no way to describe it without sounding like a blowhard. It’s not “a controller for some URL space”, because that’s what the class is. An instance of it is utterly meaningless!
IMO, this is being almost willfully obtuse and pointlessly pedantic.
For one thing, the instance, not the class, is the thing most clearly described as the controller in this case. It's true that there's no need to have instances for this static logic, but it's also true that some routing behaviors have their own dynamic data (like caches or database connections, etc.) So a point of just always using instances is that you don't need to know or care externally or ahead of time whether or not such data exists (and to provide something better than singletons or worse things to manage it when it does.)
I'm not a big fan of this form, BTW, though it's not on OO grounds. It's a code and logic organization thing. When I've seen this in real life it turns into spaghetti code of little objects connected in a tangle. I think it goes down like this: people start by encapsulating little steps of processing, I guess because they image they are independent. At this point it's OK -- a sequence of objects connected in a chain of references matching the processing steps. Then they realize they aren't independent, but instead of unwinding the chain, they add more objects -- to hold bits of common context -- and connect them here and there. The spaghetti is starting to tangle. (Mistakes are always made at this point -- e.g. there's ends up no coherent way to update this common data as things change.) The thing grows 10x and these kinds of things are repeated ten times and you have a big ball of spaghetti. Oh, and someone introduced a DI framework to solve these, which helps a little, but also introduces its own complications.
Is the problem here not simply that Python does not have a convenient way to namespace functions except a class declaration? The `module` keyword in e.g., Julia or Rust seems like what's desired to avoid this phenomenon of "a class which is just a bundle of static methods".
You don't have to use __init__.py -- you can just create any python file and it will be a module.
And I'm not sure why you think that's unergonomic. If you want a bunch of functions to share the same namespace, just put them in the same file. The name of the file becomes your module name.
I actually like "dumb" service classes with 1 method. Usually that method may require tens of dependencies to function (db connection, authorization logic, etc.), which in turn have their own dependencies recursively, and your choices are: 1) use global variables/functions for dependencies, 2) pass all dependencies as arguments to your function, 3) inject dependencies in the constructor of a service class/functor. Global state is a no-no in multithreaded code and is poorly manageable (side effects and all), while forcing a client of your function to pass all required dependencies manually as arguments is cumbersome and basically leaks implementation details (hard to refactor). So for me, service classes are the best option. It's kind of equivalent to a closure (object fields are basically captured variables), just written using the familiar OOP syntax (OOP syntax != OOP semantics). In the context of web, I tend to prefer stateless architectures (because they are easily scalable and less error-prone), and coupling behavior with state often goes in the way, in my experience. So my favorite architecture is a network of service classes whose dependencies are injected in their constructors and which are basically "smart" functions (not "objects"), they have no state, and they operate on domain objects which have no logic other than basic self-validation during mutation (to uphold invariants). I know it's a heated topic (rich model vs. anemic) but I've seen projects naturally, through evolution, end up being more anemic than rich. Pure OOP, where state and behavior are entangled, feels very cumbersome to me, it's harder to refactor when requirements change every day, and having too much state is pretty error-prone. YMMV.
"A closure is just a method where the instance members are implicit. A method is just a closure where the closed over state is explicit."
I agree with you about service classes. I think one of the things that service classes do well that regular functional code does not is indicate to the consuming developer which parameters are meant to be provided by the environment and are largely static, and which are meant to vary per invocation.
Lisp does this instead with dynamic scope. But dynamically scoped dependencies felt too much like global variables to me. The service class instance has its scope bound to it and it won't change. But dynamically scoped functions always felt like the ground could get ripped out from under me at any time.
Exactly! I find myself writing out the same code with classes and functions to show this to folks. I still prefer using classes as it’s easier for me to see at a glance that constructor = DI/curry vs a function returning an object of functions or something. So a class is just a way to communicate a pattern.
I have definitely had the whole "what is the difference between this class <class with constructor and one method> and this function that receives the same as the class constructor returns a function implementing the method" conversation with my teammates before!
I do this and advocate for this in the teams I work in. In my experience, it works well.
Service classes are "configurable functions", as I tell my team. With the advantage that you don't have to monkey-patch an import in another module to do a unitary test of the function, but rather you can inject mocks!
One thing I do insist on is that a service class either does something (calculate some value, retrieve some information, etc.) xor it coordinates service classes (parse value, then call the validator and, finally, persist value). This aims to prevent deep call chains where every function in between does a little bit and calls someone else, which can be hard to reason about.
One nitpick from the article, PHP does have first class support for functions in the language. Functions are available in a few different ways:
- Normal function declaration in global and "module"/namespace scope.
- Anonymous functions that can also be assigned to variables (except in a few cases, like object properties)
- Anything extending the Closure class
The last one is obviously a nod at what is going on under the hood, but as far as the developer writing the code is concerned, it doesn't matter.
A library even exists to explicitly provide some functional programming functions under a namespace for easy inclusion with other projects. There is no requirement that all functions must be defined in a namespace, as evidenced by the first 10 years of crappy PHP code littered with functions in the global namespace!
OOP is a tool like any other, and it has real costs and benefits. The original idea of being able to easily add new common APIs via inheritance is very powerful and widely applicable. In fact both Haskell and Rust wound up with a similar mechanism to inheritance (the 'deriving' annotation) to reduce the pain of problems like "I don't want to have to write code to define equality for every struct" which are hard for the purely-functional approach to solve.
Neither Haskell or Rust use inheritance for this though. They use Typeclasses or Traits respectively. Those have the nice property that they solve the specific problem of satisfying an interface for something without the accompanying problems of inheritance. Derive as an example doesn't use inheritance it uses code generation precisely because Rust wants to avoid inheritance.
The key insight of eevee's article is that OO has succeeded mostly because bundling state and behavior together is a really useful way to structure your code and is the thing that you should leverage the most out of object oriented languages.
`derive` macros would not be possible to imitate with inheritance. `derive(PartialEq)`, for instance, examines all the fields of a struct definition and generates a function which compares them all. Using inheritance, you could inherit from a parent `PartialEquivalence` class, but if you added any new fields, you'd have to write your new implementation of the `==` operation by hand to accommodate them.
Macros have a much longer history in purely functional languages (in particular, lisps) than they do in procedural or OO languages. So you could argue that this problem is actually much easier to solve from a functional approach.
I don't see this mentioned, but on python a file can be considered a singleton class. If you come from java, this may help you write better python code. No need to encapsulate things on a class! The file is the singleton itself!
It's how the majority of the stdlib is written. Construction of objects with the stdlib is pretty limited to things that allocate os resources (files, sockets, etc with obvious state full ness and destruction behavior) unless you're already operating on objects (dict views, iterators).
The exceptions are those which were ported (unittest was a clone of java 's framework incl method names).
She seems quite deeply knowledgeable about a lot of different languages which is impressive. Or much research was done. I just don’t have that much stuff stick especially for languages I don’t use every day.
In my opinion you're not really doing object-oriented programming unless each object is also a separate process. This is why the best object-oriented programming uses functional programming languages and the actor model (i.e., Erlang). Just having state and methods, but without having an associated isolated process means you end up in situations in which multiple processes may fiddling with the same objects and all your encapsulation of data just gets you in a twist. It's only when you commit fully to functional programming and have immutable state that you truly unlock the power of object-oriented programming.
> without having an associated isolated process means you end up in situations in which multiple processes may fiddling with the same objects and all your encapsulation of data just gets you in a twist.
1. Don't do that. Don't have multiple threads messing with the same data.
2. If you have to do that, use a mutex/semaphore/guarded region.
So far, so conventional. There's nothing about OOP in that.
The difference with OOP is that only member functions can access that data, so you have a strictly limited number of places you have to think about protecting. OOP makes that aspect easier. Each class just protects its own data.
Of course, you still have to worry about "deadly embrace", and by putting the semaphores in the class, it can make that aspect harder to reason about.
> It's only when you commit fully to functional programming and have immutable state that you truly unlock the power of object-oriented programming.
Um, no. Almost all real programs have mutable state, and the state has to live somewhere.
I work in embedded systems. There's often a lot of persistent state, and, worse, shared mutable persistent state. FP isn't going to make that go away; it's just going to distribute it differently. But if I have to reason about the shared mutable state, then a distribution that pretends it isn't there doesn't make that easier. (Pure functions, on the other hand, do help.)
I mean, look, FP is absolutely right that shared mutable state is evil. Avoid it... if you can. (And think harder about whether you can.) But sometimes the very nature of the problem is shared mutable state, and when that happens, you need tools that help you deal with it, not tools that say "don't do that".
> Um, no. Almost all real programs have mutable state, and the state has to live somewhere.
The state lives in the process and you can have millions of processes if you'd like. If you want to get some bit of state then just send a message to the process that manages that state and it will give it to you. It's like traditional object oriented programming except the objects are alive.
There's a reason Erlang works the way it does, but not every problem will make sense to solve with a million object processes communicating with each other.
For when the main thrust of the program is to simulate interactions between objects. I'm not the biggest general fan of the style, but that is where it shines.
Maybe because there are a lot of times where it really is more convenient to just update a variable or print a string instead of jumping through a million hoops to show off how pure everything is? Or we just want to implement quicksort without having a seizure?
Ok, you prefer to deal with entire classes of bugs in order to keep using objects because you incorrectly think that objects model the world better. I used to have the same perspective, and all I can say is that you'll either grow out of it or you won't.
Objects aren't an ideal tool for modeling the world, and immutable objects and variables even less so. Objects and classes often come in handy for modeling something obviously fake, like a video game.
I've never thought, 'Gee, this game would be so much easier to program if I had to copy every data structure when I wanted to update it, and have programs be impossible to step through because I'd rather write a pure program with no good debugging tools just to sound cool.'
It is interesting that you think functional programming is somehow more difficult or less powerful than object-oriented programming. I have found the opposite to be the case. Functional programming makes it easier for me to debug programs, frankly. That's why I like it. Once I got the hang of it it honestly felt like a huge weight off my shoulders.
I recently escaped an organization that had a couple of apps built using this approach everywhere but for the opossite reason stated in the article. The team wanted to write code in a functional way in Java because _FP is better than OO_.
Most (if not all) classes had a single method and implemented BiFunction. The app was wired with Spring... if you had the bad luck of using Spring you can get the idea of how ugly this was. All method invocations were `apply`s and navigating the code was as easy as finding a needle in a haystack in the middle of the night. A good amount of the wonderful features of a tool like IntelliJ couldn't be used.
My rule of thumb is to follow the grain of the language / framework / library as to produce as little surprise to other devs.
The author of the article also writes about “good classes.” She criticizes two specific mistakes one can make in Python classes (and she is right), but not the OOP itself.
200 comments
[ 4.0 ms ] story [ 237 ms ] thread[1] https://loup-vaillant.fr/articles/deaths-of-oop
[1]: https://eev.ee/blog/2012/05/23/python-faq-descriptors/
Exposing "descriptors" is a latter thing, but their default implementation was already a thing before they were visible to python users.
A sprinkle of clang blocks and Block_copy on top of your structs and you’re on your way!
That means hiding state, and having it spread throughout the app, in places that are hard to find, which is error prone and leads to complexity. And monstrosities like FactoryFactoryFactory [0] and Kingdom of nouns.
[0] https://factoryfactoryfactory.net/
[1] http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom...
Golang has first class support for bundling state and behavior: Receiver-Functions, often called methods. This means exactly 2 things; a) That for a type `Foo` I can do this:
instead of this: and b) that Foo implements all interfaces whos method signatures it's receiver functions satisfy.It doesn't mean that I now have to suddenly drown my code in over-architectured patterns just to satisfy some OOP sense of code-aesthetic. It doesn't mean that I have to try and hide state from code in the same module. It doesn't mean that I have to bundle things that could be free-standing functions into some type just because. It doesn't mean I have to make any of the mistakes OOP popularized over the years.
> I have a hypothesis: this pattern is so common for the simple reason that Java doesn’t have first-class functions.
She’s picking on Java deservedly because Gosling made what in retrospect was a mistake to go so all-in on classes, and because it became such an important pedagogical and deployment language (and nobody will shoot at you if you’re unsuccessful).
Say what you will about C++ but it took a different path, not only changing its name from “C with Classes” but making classes a tool for manipulating the type system (and not the only one) and supporting the true benefit of a class system, generics.
Edit: I changed an incorrect "He" pronoun to "She". Thanks to quickthrower2 for pointing out this embarassing blunder.
Here's my pattern that I use instead of a MVC/MVVM/MVP/etc/etc: I have a handler function to choose what url and routine I need and it calls some functions and methods and then renders some json / templates out some html.
I think a good rule of OOP is to try to write everything out as functionally pure as possible and refactor into classes when you see data AND functionality blocked together that you can bottle up into an object. To start ranting, I think a lot of the OOP insanity you see in .net/java/etc is from dogmatic approaches to unit testing over there and the common examples of how to do it involving the mocking and interfacing that end up obfuscating the actual production code.
there are relations between things that can be mapped well with inheritance, but thats a problem, we don't really want code to represent the thing we're coding, code needs to be more dynamic easier to read, change and move around, etc. representation should be between the end result of the code and the thing we're trying to represent
There were a lot of assertions in this article that rubbed me wrong, but this one was particularly egregious. Handlers are behavior. They handle something.
This feels like the kind of article I would've written early in my career when I was getting really clever with stuff and starting to be able to question the way I understood the world of programming.
So, Handlers are functions that "handle something", but they are also part of the app object's state, not its behavior.
An alternate view might be:
Handlers are functions that are part of the app object's state, which alters the app's behavior.
In the majority (possibly overwhelming majority) of the cases this is true. The purpose of Handlers and similar Publisher/Subscriber patterns is effectively to change application behavior when they are configured.
I find that phrasing very fair.
IIRC, Kay's vision is that OOP is about messages.
I have my own pet theory, of course it must be wrong too, but if I may, only as food for thought: The core usefulness of OOP is usability. A language is a matter of connecting thoughts through a mental model. Subject, verb, predicate. Object, method, parameters. That's mostly it. The rest are implementation details and lots of bikeshedding.
The idea is that you have classes which model state. And then you have generics that model functionality. And you define methods which provide an implementation of the generic for a class. But it's more flexible than what you see in Java because such a method can be easily added after the fact as it's not intrinsically part of the class' definition.
If you're familiar at all with Typeclasses in Haskell & Scala, personally I find those to be similar enough to get the gist. Likewise Dylan and R's S4 objects are modeled after CLOS' structure.
I also enjoyed the ability to not be locked into the rigor of a class structure when it comes to methods. Since CLOS is function based, it's trivial to add functions to a class that you don't even have the source code to.
There have been many times working with a 3rd party or system library where I've had that "if only I had this little method" moment that would make my life easier. I'd rather have that capability and fight, say, namespace issues for the "Well what if everyone added their own 'upshiftFirstLetter' method to the String class" problems on an ad hoc basis.
Part of this, of course, stems from the locked down nature of the scope of classes. Not having access to internal structures and state. I'd rather take those risks of leveraging internal state knowledge not supported by the original designer vs the alternatives of sometimes having to throw out the baby with the bathwater.
Yes. The actor model is closer to what Kay proposed than the C++/Java style of OOP.
This is the reason I find it useful. To me, OOP is as much about your organization as it is about the best way to load, transform, present, edit, and store data. I think the culture of some companies lends itself to various kinds of programming, but it's the cultureless companies where OOP is most useful. The places where nobody is trying to change the world, where people work to pay their mortgages, where an executive may only work for two years and a programmer may only work for six months.
It's in an environment like that where a self-documenting, self-configuring code base with custom classes and exceptions that guide the next developer is essential.
Every developer should have two users in mind. The person using the software, and the next developer who maintains the software after you're gone. OOP is a great way to empower the second user when the only thing that will reliably outlive the developer is the code base.
For smaller projects I don't care one way or another about OOP practices, but once you start getting into hundreds of thousands of lines of code IMO it becomes an absolute necessity.
If I create a REST API no one complains they don't have access to the inner-workings, local variables, etc. But if I give a similar experience and call it a "class" suddenly it's ugly and mean.
The other side is that classes aren't the only way to get this sort of encapsulation. The classic example is closures - data inside the closure acts as the encapsulated data, and the returned type of the closure is its public API. ML languages typically use modules in place of classes - the module signature defines the public API, and rather than calling methods, you instead call functions with arguments (not `list.length()` but `length(list)`). But again, that's just syntax - we're keeping the same encapsulation because the module-defined functions are the only ones capable of fiddling with the value's internals. You also see this in Rust, which does use method syntax, but has traits and types that act more like modules than typical classes.
All in all, I don't think anyone's complaining about encapsulation, but rather it's a question of whether typical OOP (with all that that typically includes) is the best form of it.
Code is much easier to deal with when you define abstractions around small sets of functionality and then allow the caller to pass in various objects that provide those functions to the code that needs it.
You can have an application that accepts a 'data_backend', and then provide a data_backend that just stores information into a dict for testing and getting the app initially written, one that tracks all of the changes made or exposes them for tests to check, or another that stores it to sqlite for running a local instance, and another that stores it to some real database for a larger one.
The calling code doesn't need to know what the data_backend does or how it works, it just tells it "store this", "read that" and the data_backend does whatever it does and data goes in and out of it.
You build up all your code that way and you'll be able to easily stub chunks and replace them with functional implementations, and then swap those out when needs change or by options given at runtime.
It's a lot easier to read and write than code that's littered with a million if statements trying to keep track of too much complexity in too many ways all at once.
OOP is just syntax sugar and compiler constraint enforcement for the same kinds of things you see the linux VFS do. There are many filesystems for linux, but each just provides a handful of functions in a structure that should be called against the structures representing the various types of filesystems. In Linux's C you have to slap it all through a (void *), but in languages with objects, you can use those as the medium of abstraction instead of doing it manually.
Some make you do a bunch of inheritance garbage with stapling objects together, others will let you build to interface definitions, or just check the structure of the object to see if it matches the needs of the caller, or be a dynamic language that just checks for the members at runtime.
Maybe one day modules will hit the mainstream.
[0] https://ocaml.org/docs/modules
There's very few actual rules despite opinionations derived from arbitrary paradigms and what should be done to work cohesively as a team. I wish there were a smidge less ivory towers and a smidge more common language.
Incidentally, this is exactly how I came to "reinvent" OOP as a newb programmer. I was working in a codebase where I didn't know what tools I had at my disposal, but we had a few modules where I could just type `module.` and see a list of all the methods, right there in front of me (in VSCode's intellisense). I asked, "Why don't we do this for our helper functions?" and we ended up with a handful of major objects to import with easily discoverable methods.
now of course I could have crawled the codebase to get the same information, but for someone new to programming and/or someone brand new to the codebase, that isn't necessarily a good use of a time. it can be a lot of slow unraveling of "what goes where", whereas organizing things in "OOP" (I still don't know if I'm actually using this term right because it was just part of how I learned, without being named) teaches that information more quickly through use and experimentation. basically what I was looking for was "namespacing" I guess, but I would also say it helped organize our code in a more useful way.
That and Simula proceeded Smalltalk, which C++ was inspired by, even if Kay coined OOP.
If it works for you and your team, it works.
IMO, this is being almost willfully obtuse and pointlessly pedantic.
For one thing, the instance, not the class, is the thing most clearly described as the controller in this case. It's true that there's no need to have instances for this static logic, but it's also true that some routing behaviors have their own dynamic data (like caches or database connections, etc.) So a point of just always using instances is that you don't need to know or care externally or ahead of time whether or not such data exists (and to provide something better than singletons or worse things to manage it when it does.)
I'm not a big fan of this form, BTW, though it's not on OO grounds. It's a code and logic organization thing. When I've seen this in real life it turns into spaghetti code of little objects connected in a tangle. I think it goes down like this: people start by encapsulating little steps of processing, I guess because they image they are independent. At this point it's OK -- a sequence of objects connected in a chain of references matching the processing steps. Then they realize they aren't independent, but instead of unwinding the chain, they add more objects -- to hold bits of common context -- and connect them here and there. The spaghetti is starting to tangle. (Mistakes are always made at this point -- e.g. there's ends up no coherent way to update this common data as things change.) The thing grows 10x and these kinds of things are repeated ten times and you have a big ball of spaghetti. Oh, and someone introduced a DI framework to solve these, which helps a little, but also introduces its own complications.
And I'm not sure why you think that's unergonomic. If you want a bunch of functions to share the same namespace, just put them in the same file. The name of the file becomes your module name.
I agree with you about service classes. I think one of the things that service classes do well that regular functional code does not is indicate to the consuming developer which parameters are meant to be provided by the environment and are largely static, and which are meant to vary per invocation.
Lisp does this instead with dynamic scope. But dynamically scoped dependencies felt too much like global variables to me. The service class instance has its scope bound to it and it won't change. But dynamically scoped functions always felt like the ground could get ripped out from under me at any time.
Working in functional langs I just do DI with higher order functions.
Service classes are "configurable functions", as I tell my team. With the advantage that you don't have to monkey-patch an import in another module to do a unitary test of the function, but rather you can inject mocks!
One thing I do insist on is that a service class either does something (calculate some value, retrieve some information, etc.) xor it coordinates service classes (parse value, then call the validator and, finally, persist value). This aims to prevent deep call chains where every function in between does a little bit and calls someone else, which can be hard to reason about.
- Normal function declaration in global and "module"/namespace scope.
- Anonymous functions that can also be assigned to variables (except in a few cases, like object properties)
- Anything extending the Closure class
The last one is obviously a nod at what is going on under the hood, but as far as the developer writing the code is concerned, it doesn't matter.
A library even exists to explicitly provide some functional programming functions under a namespace for easy inclusion with other projects. There is no requirement that all functions must be defined in a namespace, as evidenced by the first 10 years of crappy PHP code littered with functions in the global namespace!
https://github.com/lstrojny/functional-php
Edit: formatting
The key insight of eevee's article is that OO has succeeded mostly because bundling state and behavior together is a really useful way to structure your code and is the thing that you should leverage the most out of object oriented languages.
Macros have a much longer history in purely functional languages (in particular, lisps) than they do in procedural or OO languages. So you could argue that this problem is actually much easier to solve from a functional approach.
The exceptions are those which were ported (unittest was a clone of java 's framework incl method names).
I would put C++ and PHP in second and third place, and they have the same flaw.
PHP supports first-class functions.
1. Don't do that. Don't have multiple threads messing with the same data.
2. If you have to do that, use a mutex/semaphore/guarded region.
So far, so conventional. There's nothing about OOP in that.
The difference with OOP is that only member functions can access that data, so you have a strictly limited number of places you have to think about protecting. OOP makes that aspect easier. Each class just protects its own data.
Of course, you still have to worry about "deadly embrace", and by putting the semaphores in the class, it can make that aspect harder to reason about.
> It's only when you commit fully to functional programming and have immutable state that you truly unlock the power of object-oriented programming.
Um, no. Almost all real programs have mutable state, and the state has to live somewhere.
I work in embedded systems. There's often a lot of persistent state, and, worse, shared mutable persistent state. FP isn't going to make that go away; it's just going to distribute it differently. But if I have to reason about the shared mutable state, then a distribution that pretends it isn't there doesn't make that easier. (Pure functions, on the other hand, do help.)
I mean, look, FP is absolutely right that shared mutable state is evil. Avoid it... if you can. (And think harder about whether you can.) But sometimes the very nature of the problem is shared mutable state, and when that happens, you need tools that help you deal with it, not tools that say "don't do that".
The state lives in the process and you can have millions of processes if you'd like. If you want to get some bit of state then just send a message to the process that manages that state and it will give it to you. It's like traditional object oriented programming except the objects are alive.
I've never thought, 'Gee, this game would be so much easier to program if I had to copy every data structure when I wanted to update it, and have programs be impossible to step through because I'd rather write a pure program with no good debugging tools just to sound cool.'
Most (if not all) classes had a single method and implemented BiFunction. The app was wired with Spring... if you had the bad luck of using Spring you can get the idea of how ugly this was. All method invocations were `apply`s and navigating the code was as easy as finding a needle in a haystack in the middle of the night. A good amount of the wonderful features of a tool like IntelliJ couldn't be used.
My rule of thumb is to follow the grain of the language / framework / library as to produce as little surprise to other devs.