Hacker News new | past | comments | ask | show | jobs | submit login
I Don't Use Classes (atomicobject.com)
213 points by philk10 on March 12, 2020 | hide | past | favorite | 288 comments



Hm. Not sold. Sure, you can write terrible code with classes. But that also works great without classes.

> I’m terrible at making good abstractions with classes.

Then don’t. Or only do it after everything is done and you have explored the domain. Don’t try to be clever from the start. Unless you have understood everything, it will backfire.

> But as other classes extend and override those methods to address their version of the problem space, they usually end up having some other responsibilities in addition to the original task.

Inheritance gone wrong. If a derived type behaves differently, it should not be a derived type. See Liskov substitution principle. Just because a hash map is somewhat like a list of key/value pairs doesn’t mean it should derive from one.

It could also be a problem of missing separation of concerns/responsibilities, which is entirely unrelated to OOP.

> Secondly, I’ve noticed that classes have a tendency to grow large.

Yes, I did see many big classes. They were always the result of... missing separation of concerns/responsibilities. The fabled god objects.

The section about state management appears to be very much React-specific, too. So, not about classes.


> Then don’t [use them, it's that simple!]

The problem with classes in languages that use them as a main abstraction (Java, Ruby, etc) is that people reach for them for all abstraction and software design as their Day 1 tool.

All program state and behavior is now immediately fragmented from Day 1 in arbitrary buckets and my experience some moss quickly grows on them and things are never changed for the life of the program.

Just seems your post doesn't offer much insight beyond the classic OOP advice I've heard for years when people realize it's not that great: oh no, you're doing it wrong, don't do the bad things, just do the good things and ensure everyone else is doing them too. I don't see how that's actionable nor useful.

Limiting class use as much as possible and using functional module pattern as TFA does, on the hand hand, is actionable advice for these common issues with classes.


I just don't see much of a practical difference between these two approaches. From a functional perspective, objects are fundamentally a set of namespaced functions that implicitly take the same state/map/JSobj as an argument bound to "this". You can pass that state in explicitly and it just becomes what the author is talking about. And you can have the same state-management nightmares with that approach, because nothing is fundamentally different. Instead of obj.method(arg1) you have namespace::function(obj, arg1). I've worked in a very large frontend codebase using this technique and it's just as much of a pain as implicitly managing state with objects, if not more of a pain b/c it's no longer idiomatic.

State management is hard. Changing how you refer to the state isn't going to make it any easier, just different.


That implicitness is part of the problem.

In an object, all the functions implicitly take that state (or explicitly don't if you mark them static).

So instead of namespace::function(arg1), you now have namespace::function(obj, arg1), even when your function has nothing to do with obj. From the outside, you then have to wonder which members of obj might be being read or written inside the function. If lots of those methods call other methods, which may or may not modify the state of the object, you can end up in a situation where it's hard to reason about the functions because you have to read a lot of code to work out what the inputs and outputs really are.

This is the problem in the Java-style "everything must be a member of a class" approach. People have a tendency to make everything into members of objects.

A function that only takes the values it needs is much easier to reason about.

I think that you are right in that there isn't much practical difference between the two approaches when well-written (in particular when staticness/mutability/constness are clear). However, I find poorly written OO (e.g all methods are instance methods, local variables stored as members) to be really tricky to reason about, and all too common. I haven't had that problem with poorly-written non-OO code.


I think this can reasonably be described as a footgun rather than a bad design. Instance methods should relate to the state of the instance. If they don't that's a poor design choice. This is a pretty easy code smell to reject in code review.

Perhaps Java would be cleaner if it had free functions. But this isn't so different than a big class that has a pile of static methods.

C++ also addresses the "you have to wonder what members might be read or written inside the function" by encouraging the use of "const" as much as possible. You get really nice invariants preventing surprise state changes in receiver objects and other parameters through judicious use of "const", even if everything is a class or object.


> This is a pretty easy code smell to reject in code review.

It is if the code was reviewed. If, OTOH, it just grew until something needs to be done (like the object with 60 public methods that I'm currently trying to refactor), you can have a devil of a time unpicking it.

The same could be true where free functions are preferred, but it seems that most programmers seem to have grasped the idea that global variables are hard.

> by encouraging the use of "const" as much as possible.

I'm a big fan of const-correctness. I haven't written C++ in years, but I always used to love putting 'const' everywhere.


If you aren't doing code review then no design principles will save you.


But that's the point I'm trying to make - Over my career, I have inherited bad code in both styles - "prefer objects" and "prefer functions". In my experience, bad objects tend to be a nightmare. Bad free functions tend to just be a bit annoying.

There are only two ways in which code review can keep your codebase clean in this situation: Stop doing any other development until it's all nice, or invent time travel.


The difference with this approach is that any function can take obj as an argument. If it's pass by reference, any function can mutate obj. If obj winds up in an inconsistent state, any function in the entire codebase could have done it.

With a class, however, you normally make the data members private; that is, only the member functions of the class can change them. If the data gets mangled, you now have a very small set of places to look. That difference matters when debug time comes around.


If you use a functional approach, you’re probably not mutating anything ever. That usually makes it trivial to find the source of a problem, because you can quickly isolate it to a specific call stack.

> only the member functions of the class can change them. If the data gets mangled, you now have a very small set of places to look.

But probably anything could have called those mutating methods, so you still need to consider the entire codebase.


Anything could have called those mutating methods. But only those mutating methods could have put the object in an inconsistent state. I therefore have to fix the method that did that, and then let anything in the entire codebase freely call those methods.


Those limits only apply to certain languages. In typescript you can keep functions private to a module, and in functional languages you would likely return a new object instead of mutating the existing ones. Heck even in C you can enforce privacy without needing a class -- only expose certain functions in the header and pass around handles instead of the data structure directly


If classes were just that then they'd not be so bad. They also tend to bring inheritance along with them, though, and that's a messy beast.


It's classical inheritance in particular that's fragile and rigid and problematic.


Prototypical inheritance (which, I assume, is the other side of what you're referring to) is also weird. Prototypes eliminate classes as a special thing (relegating them to mere constructors or factories, which is fair) but they don't help with typing (compiler-enforced or logical).

If A has B in its prototype chain, are all values of A able to be transparently treated as values of B? Plausibly, this works, but there's nothing that really enforces it. Prototype inheritance is just a way of not needing to repeat code.

I agree it's a bit better than class inheritance, but most of the time all you need and want is composition.


We're agreed that prototypal inheritance (at least in Javascript) is orthogonal to typing. Your question about the values of A and B seems to imply a parent:child relationship -- which is the kind of hierarchy that we're looking to avoid, right? It's straightfwd to flatten the prototype chain using a single delegate prototype.

Yes, favor composition over inheritance.

FWIW, I really like Eric Elliott's take on this stuff: https://medium.com/javascript-scene/common-misconceptions-ab...


"Don’t use classes" is not what I meant. It’s "don’t create abstractions”. Create the literal thing you want to create, in any way (though preferably somewhat organized), then maybe think about making it cooler.

It also wasn’t supposed to be advice. (Not sure what you mean by TFA.)


What do you mean by 'abstraction' here?

By the definitions I'm familiar with, functions would be abstractions of program segments, and variables would be abstractions of program data.

With that understanding, the closest you could get to writing a program without creating abstractions would be to write in assembly—but even if you opt to use as little abstraction as possible, you may just get further from your original recommendation to "Create the literal thing you want to create":

I can see a couple ways of interpreting that statement.

1) Imagine what you want to create already existed: what it would literally be is a block of machine code, so when you write your program you should make it as close to that hypothetical literal existence as possible.

2) What you want to create doesn't exist materially, so its only literal existence is a set of high-level domain-specific concepts (i.e. its existence is a spec), in which case the most literal way to represent it would be through a set of matching high-level concepts defined through a programming language, i.e. a set of matching abstractions.

Under the second interpretation at least, it requires heavy use of abstraction in order to implement the spec as literally as possible.


I think they mean something like abstract base classes.

It's tempting to design a class hierarchy from first principles: everything in the game is an entity, from which players and NPCs are derived. The NPC base class has several derived versions (allies, monsters); the monster class is specialized into animals and dragons, which in turn....

You can often paint yourself into a corner this way, where you suddenly don't want the dragon class to reuse part of the MonsterBase. They're suggesting making the classes stand-alone with duplicated code, at least until you figure out what these classes actually have in common and what is incidental overlap.


That's why designing the actual program is important. Also, static and strong typing for powerful refactoring plus extensive tests, and you can refact that Dragon class into something new. And while I do agree with you, I think it is not OOP that's bad but the programmer doesn't understand when to do inheritence vs composition etc


I surmise that "TFA" means "The Fine Article" referring to the source article


> Inheritance gone wrong. If a derived type behaves differently, it should not be a derived type. See Liskov substitution principle. Just because a hash map is somewhat like a list of key/value pairs doesn’t mean it should derive from one.

The trouble is that "extends" conflates three different things, and many languages don't offer an easy way to do those things separately:

1. Ad-hoc polymorphism: this thing offers the same interface as this other thing, but with a different implementation

2. Composition: this thing contains this other thing, and uses it to implement its own functionality

3. Delegation: this thing implements this interface by forwarding it to this thing that it contains (possibly overriding some parts)

So you end up with things that need to share implementation accidentally exposing a common interface, or vice versa. Thankfully we're seeing more and more languages offering the option to do delegation more explicitly (Kotlin/Rust-style delegation, Haskell/Rust-style derivation), and to do ad-hoc polymorphism without needing to share implementation (Java-style interfaces, Haskell typeclasses / Rust traits).


In Raku those things are also separate.

1. Polymorphism

    class Example {
      method be-nice () {
        'be nice'
      }
    }

    class Bill-and-Ted is Example {
      method be-nice () {
        'be excellent to each other'
      }
    }
2. Composition

    role Salaried {
      has $.salary;
    }

    role Employee {
      has $.title;
    }

    class Manager does Employee does Salaried {}
3. Delegation

    class Price {
      has $.cost;
    }

    class Product {
      has Price $.inner handles( 'price' => 'cost' );
    }
Any call to the `.price()` method on `Product` will delegate to the `.cost()` method on the inner object.


Big classes or no, I've found the key is to have good tests around your public facing contract. IME most unit tests are poor and too fragile because they treat methods as the "unit", even helper methods (or mock out calls to their own internal helper methods, if they're the public-facing methods). So if you want to move stuff around, you find your tests aren't testing for the true behavior/side effects, but just that your previous method heirarchy is in-place.

But when your public facing methods are big blobs, they tend to have lots of params that cause fairly different thing to happen, and it gets much harder to have testing confidence around that than if you had a bigger set of smaller public-facing operations.


Your thoughts on this reminds me of: https://martinfowler.com/articles/mocksArentStubs.html#Class...

It's a basic philosophical difference in what a unit test should be doing. It goes back over a decade.


Thanks for the link, I hadn't seen those terms before, but yeah, I have been debating it for a while with coworkers.

I'm a bit in between the way described there, though, since I'll often happily leave other objects mocked as long as I know that my contract successfully called that object. I don't need to test all the way through their own implementation - there should be sufficient testing on that object to drive a refactor of it. I just want to support a refactor of me.

My view is basically: "we're gonna have to restructure some of this code as future requirements come in, so let's make sure our tests ensure that the things we really want to happen do happen" (like an update being sent to the database or other backend service layer).

I have similar hesitancy about applying a lot of design patterns early. Most of the places I've worked have been in the experimentation and rapid feature development stages of the business, with all development being internal development (rather than providing an SDK to others), so being able to change the code is more critical than being able to treat the code as more of a framework.


Large classes are not always a bad thing. It's better to have a large class with 1K lines which you can break up at a later date than having lots of small classes which have unclear or overlapping responsibilities and which will be difficult to reorganize later. It's much easier to split up and categorize parts of a large blob of code than to re-categorize and merge bits and pieces of logic that are scattered all around.


What kind of coding do you do where a 1K class is considered large?


What do you consider large?

I think classes without internal state, can be large and still simple to understand. But if many mutable fields and internal constraints, then things can get complicated. Better keep such classes smaller


Yes, key point is to separate classes if they have state or not. I have a simple convention in my code base, a repository does not have state, a service depends on repositories & can have state, thus a repository can have lots of methods (different ways of retrieving data) & a service is specialized & only have small number of methods.

If an existing services does not match exactly what you need to do, you write a new service and reuse the same repositories.


I would never argue smaller isn't better. But the code I work on every day tends to have much larger classes than that.


> Then don’t. Or only do it after everything is done and you have explored the domain.

I think this really hits the nail on the head. Classes are the ultimate DRY tool but it seems like much of our industry is realizing that it's rare for any project outside of small self-contained libraries to reach the point where you know enough about your problem domain to ever start meaningfully factoring code out like this.

Instead having "here are the different shapes of state you might see floating around" and "here are some functions that can do things with certain shapes" becomes really powerful when you need to be nimble with features/requirements. Encapsulation is beautiful when the scope is specified completely up front but it I've found it leads to more complicated code and mental overhead because you create a world where the least cost path to doing what you want does a run around with types.


I came here to talk about Liskov, but since you beat me to it I'll mention SOLID principles of class design instead, which it really sounds like the OP isn't aware of or just isn't following and it feels like is the source of a lot of their class based woes.


Every "problem" that the post brings up is a consequence of incorrectly using good programming practices. Solution? Not use the good programming practices at all if we can't get it right.

Seriously, just hire a good senior software engineer


You can write terrible code with nulls and without nulls, but when all else is equal, code without nulls is less terrible. Same with code without classes.


But if I'm the kind of programmer who's going to write terrible software, why would you expect me to be able to choose a good paradigm? For that matter, why would you expect me to be able to be good enough to avoid nulls?


I used to be an oo programmer. Now I'm fully FP, and I'm never going back. When you say "not sold", I guess my question is, have you tried it?


I am and have done both. I tend to prefer a functional style, but even then I'm going to be using classes (or a similar construct) for quite a lot of problems.


porque no los dos?

I find some problems are solved better in an OO way such as interacting with a relational database.

Others like data processing, are better done in a functional pattern.

I look for the right tool for the job rather than some idealistic purity


> I find some problems are solved better in an OO way such as interacting with a relational database.

I've switched to functional interactions with a database, (Elixir/Ecto) and really, it's much better. In particular the explicit and declarative nature of the database interactions combines the convenience of an ORM with "not hiding important gotchas from you". I'm not arguing for pure functional systems (which I can see going very poorly with relational databases).

There are some things that objects are good at like caching state as a model. But even here the FP systems typically do it better via FP actors and (again, impure) message passing, which enforces no-shared memory, and at least in erlang virtual machine systems, couples segregated state with limiting blast radius for your systems failure domains, which is 150% the "right thing to do". Of course if we're getting pedantic, this is even close to OO the way that Alan Kay imagined it, than what the OP and GP talk about, and I don't consider that to be OO in the contemporary, colloquial sense of "everything that happened after Bjarne Soustroup invented C++"

I highly recommend this video for a more philosophical take on how to do "impure FP" without OO that is informed by a decade of industry experience: https://www.youtube.com/watch?v=yTkzNHF6rMs which is by Gary Bernhardt (of WAT fame)

> I look for the right tool for the job rather than some idealistic purity

This is all about right tool for the job. IMO, FP code is easier to read, easier to reason about, easier to maintain. I think it's a shame that FP gets this reputation for being 'about ideological purity'. I like using "working programmer's FP", and the FP that I use are built on designs that are driven by real, customer-use-case driven concerns and real-world constraints, and how to make life easier for programmers and operators.


I largely use the OO patterns to collect related functionality based on the domain.

Many of the methods on those organizational classes are implemented in a functional manner.

I rely on the class/instance pattern for working with a database. static class methods for actions that work on multiple rows, instance methods for single row interactions.

This works especially well in typescript where I can set private/protected/public

That said, I rarely use inheritance except when it's obvious - no nested class hierarchies or other such traps.

Thanks for the video, I'll give it a watch.


>I'm not arguing for pure functional systems (which I can see going very poorly with relational databases).

Why? The very best "OO" interactions I have with a database are via Linq which is a monad [1]. What it actually does is simply build the plan for what to do in the database and actually do the query at the last possible opportunity. Exactly how it would work in a purely functional setting.

[1 ]https://github.com/louthy/language-ext/wiki/Thinking-Functio...


Yes, ecto is basically a monad too (it's based on linq), but at the edges it will violate functional purity.

Linq is functional, but functional-impure.


Best thing about 20 years of freelancing has been that I have NEVER been asked about, ridiculed for, or heard complaining about my code. Nobody even sees it, or cares. It's efficient and responsible code in my opinion, but I don't have to care about this strange problem where programmers actually have opinions about how code is put together. I don't know how you guys put up with it, I'd go nuts if someone told me I had to use more classes, or a certain language, or tabs over spaces, or whatever.


I think we put up with it because personal development requires outside feedback. I couldn't imagine getting better at the pace I am without both having to justify and explain my decisions to others.

I don't understand your viewpoint at all.

The best part about coding for me is getting a review with a lot of interesting comments about how it could be done differently. It jogs my brain down paths I would never have seen by myself.


As a consultant (not the OP) one of the best parts of writing code is having it solve real business problems (and getting paid well for it). It's a point of personal pride to write reasonably good code, but the structure doesn't matter to the person paying the bills - the outcome does.

It's like living in a house: as long as it doesn't fall down and doesn't cost unreasonable amounts to maintain, I don't really care about how well-formed the mortar is, or that the drywall is hung perfectly according to company standards. I just want to live in the house and not worry about it.


Well, what I'd say to that is:

* Somebody maintains that code. If it is only you you have a bus-factor of one if none of it is actually reviewed. If it is not you you are imposing a single persons idea of what the business problem is and how it should be solved on the maintainer, which they might not agree with

* Personal development depends on realizing when you are wrong or have used the wrong tool. If nobody challenges your decisions you will probably not realize that as often (or at all) and therefore not improve as much.

* Solving business problems means solving problems for a business. Usually businesses evolve over time, so the business problems might change. Who will understand the code in that case? How do you know the code is understandable to other people if nobody else reviewed it?

And as for the house example: before buying that house I would definitely hire somebody else to check it, and that is legally required in some countries. I don't trust anyone to rate their own work.

In this case the person who hired your consultancy bought the house, and they should definitely have an expert that did not build it check it.

---

Just a FYI: I'm also a consultant, but work on a project where we have discussions about how to structure the code and do code reviews and so on.


Yes yes, I agree with all that. I much prefer to discuss approach with someone senior before tackling hairy problems and have others look at my approach after the fact to improve. But since I often work with clients where I am the dev team, I rely on my professional network to discuss approach.

Agree that getting a technical review from a third party isn't a bad idea.

FWIW this doesn't change my main point: the business owner cares about outcomes, and not code structure (a good outcome is dependent on the code being good enough to maintain e.g. the house not falling down). There's a simple test for this BTW: write lited, beautiful code that doesn't solve the business problem you were hired to solve and see how popular you are with your client.


The structure does have an impact on its maintainability though. You said you don't care about well-formed the mortar is or how the drywall is hung, but would you care if it was made in a way that would make it much harder later on to run new wires inside the wall? I would care personnaly.

> writing code is having it solve real business problems

Sure, but writing good code is about solving future problem the business is going to have too. A problem has more than a single dimension.

If your code will never evolve, if it's a definitive solution to their problems, the structure doesn't matter that's true, but sadly, that's almost never the case.


It's like building someone a house where the design is flawed and someday it'll fall down and kill everyone. By the time it happens, you'll be long gone. Doesn't mean it's the right thing to do.


> the structure doesn't matter to the person paying the bills - the outcome does.

The structure matters to the outcome; but the outcome problems of bad structure tend to manifest after paid-by-engagement contractors have left.


For me, as a developer who maintains open source software, the best feedback has nothing to do with the code. Honestly, nobody cares. The application does what it claims or it doesn't. People who spend all their energy on code style have clearly, in my opinion, never had the experience of directly interfacing with their users. Here is my checklist of competence:

* defects - The application has certain defects or it doesn't and you can prove it with tests. If there is a defect I will prioritize it, or solve it immediately, or mark it as won't fix.

* performance - Does the application execute fast enough in all stages and interactions? This is a simple yes or no, but performance testing is often volatile so this needs to be tested and measured as well so that it can be observed over time keeping slippage in mind.

* ease of use - Does the application work out of the box? If not its broken. If it requires a bunch of manual configuration then nobody will use unless its forced on them. For this I take all my frustrations with corporate software from my past jobs and I intentionally design against that when I write software and encourage my users to complain to me.

* documentation - Does the documentation stand on its own? Is it complete, well organized, and simple enough that a high school freshman can read it? If you cannot write you aren't as great a developer as you think you are.


If your code has 20 if else in a row I don’t care if it’s performant and has no bugs, you’ll get a change request because I don’t want to maintain that pile of garbage.

There are minor code style preferences and others that affect maintainability. Just because it works now it doesn’t mean the code is done.


Yes, there are conventions that are generally harmful to code maintenance. For these things the preferred approach is screening via some sort of automated gatekeeper whether its unit tests, code beautification, better custom types, code validation, or something else. If a developer isn't willing to do the work to automate these subjective concerns then their complaints really need to be something measurable. Everything else isn't worth the energy when instead you could be productively writing the next hot feature.


Some bad code is hard to lint automatically (e.g. if there’s a single function that all it does is arg.slice(1), no linter will catch that).

I’ve found all kind of bugs just refactoring code because poor code obfuscates them.

Your way of building things will lead to debt and bug reports


How that code example a style violation? I am not even sure how it's bad code. In JavaScript that would just return a value without the first index. What code style rule would solve for that in a meaningful way that you are willing to enforce uniformly on all concerns?


It is because if a file is made of a hundred 1-line functions that’s not good code. The separation of concerns is likely not meaningful and it’s detrimental to the reading/understanding of the file.

Why create a function that shortens/clarifies nothing and is used once? If you want to explain some code, use a comment, don’t make spaghetti


> Honestly, nobody cares

Other programmers care. A one/two man workshop doesn't need order or processes to get things done. A ten man workshop cannot function without it. The same is true with coding - it becomes a tool for programmers to communicate intent, it's becomes important to be ordered, clean and understandable by every other dev.


The end users of my open source software are all developers and they never complained about the code style. Not once. My largest application lived for 10 years and was used by hundreds of thousands of people.

> it's becomes important to be ordered, clean and understandable by every other dev

Code style is the wrong tool for that job. Better are code reviews and written documentation.


Yeah, I don't really need that. I like to learn to do new things, and my passion is coding and tinkering with projects and learning new tech, so I have no problem finding my way down new avenues of doing things. It's the petty stuff that I hate.

I did work somewhere during the dot-com years and we had an entire meeting about double spacing. Whether we press enter TWICE after an if-statement bracket, or once. I hated every second of it.


> personal development requires outside feedback

Is it true? If I read book or blog, I improve my skills. Sure, feedback helps but I don't think that it's a hard requirement.


I don't think anything is quite as effective as having a conversation with others. It challenges you in a deep, possibly mind-changing way that a book cannot.

I didn't quite realize this until, after 5 years of building things on my own, I had a co-founder. I feel like I gained 10 years of experience just from working with him and having to explain my solutions or why I didn't consider this other one, or having to realize that I was in fact wrong. That's a hard pill to swallow when you've only worked with books.

Craftsmanship is more than just learning new things. Sometimes you need to have your entire model of what's best swapped out for new ideas, like society realizing a republic is better than feudalism, than never evolving past your original position.


The steel man of that argument is that yes, with no feedback you have nothing to evaluate your decisions against, and all feedback is outside feedback.

That said, I think the person you're responding to meant outside feedback as in opinions from peers, which I agree is very powerful, but not strictly necessary.


In my personal experience, reading alone is less efficient by at least 1 order of magnitude.


"because personal development "

2) standard practices, idioms, consistency.

You can't use tabs if the entire company uses spaces or use Python if it's a Java shop.

3) Because in most cases, an extra set of eyes can help provide real, material constructive feedback.

I have little trust that any standalone developer is going to just write 'solid code'. Who has that level of self-awareness to recognize they are 'that good', let alone actually be good without help?

It's a basic matter of professionalism to work with others and to be good team members, without it, we're not going to get much done.

For smaller projects with very clear requirements, it matters less.


Not to mention you need teams to solve larger problems and/or support more customers. So OP's comment basically amounts to a cruel boast about the BS he doesn't have to put up with.

It'd be like telling a construction worker taking a water break: "That's the nice thing about the office jobs. No need to worry about hydration because I'm not out in the hot sun all day." I have a feeling this kind of observation would not be well received.


Maybe this person is just against nitpicking..?


This sounds like a dangerous mindset. "I don't ever want to even hear about it if what I'm doing is ridiculous". I am not attacking you or saying that what you're doing is wrong, I'm just curious/cautious about the motives behind not wanting to know if there are better/more productive ways of doing things that you just might not know about? Collaboration is usually a very good thing, in spite of the small % of terrible teammates you occasionally get.


It depends on the person. Not everyone needs code review to strive for better and know when you're doing poorly. Collaboration is an easy way to get that, but it's not required. The same people that have the personality to learn things on their own often are the same that will improve their code over time without someone else.


I was in this boat for 8 years or so before starting a company and hiring a second developer. I lucked out and got a highly research driven one to learn from, and I gotta say I've learned a ton in 2 years. Even if it's harder in some ways than being solo, I think it's been worth it to have that pressure to improve code and processes. I was kind of coasting before if I'm honest, even though I felt like I was really good at my job.


> I'd go nuts if someone told me I had to use more classes, or a certain language, or tabs over spaces, or whatever.

It really depends on the industry your work with, code base size and number of contributors.


Different perspectives help you grow. I can't imagine not getting feedback. We aren't ridiculed about code structure but given feedback that you can take, or not, with a discussion. It seems pretty problematic that getting feedback would make you "go nuts". Obviously this depends on the organization but, most of the time, I'm not talking about superficial stuff, like tabs over space, but legitimate changes that can improve my code. No one writes nothing but perfect code and having someone double check you can be immensely valuable.


Working a similar number of years in various corporate jobs, I haven't seen too much of that, but it does happen. I'm probably my harshest critic when I'm looking at code I wrote years ago. Or maybe someone is cursing me after I've moved on.

I wonder, you've never been critiqued, but how many times have you gotten a project and had to unravel badly written code?

I think the macro-style issues people debate are mostly about code that is being maintained and reworked over and over again in a product with a long life.

> I'd go nuts if someone told me I had to use more classes, or a certain language, or tabs over spaces, or whatever.

Those issues all have root causes you must have dealt with.

Tabs vs. spaces type stuff is mostly about keeping diffs clean in a repo.

Language choice is just a matter of components being able to work together, and being able to hire people who can work on it.

And choices like "more classes" are usually revolving around testability of code.

> I don't know how you guys put up with it

Absolutely, maintaining a product over a long period of time can be exhausting.

I think there are benefits to it, though. It's extremely gratifying when you've written something that someone else builds on and they actually get the structure and their new code fits in very closely to how you intended.

The whole thing kind of becomes your baby, albeit a baby that you're thinking, "one of these days I'm going to cut her head off and stitch a new one on and she'll be way cuter."


I'm in a similar boat. When I started programming, I always was the smartest guy in the company so I never had a chance to improve from feedback. Last years I'm alone as a programmer and I love it. If someone would criticize my code, that probably would make be very unhappy and I probably will fight that, because every critique I can imagine will be based on subjective factors (of course bugs are objective, but I don't usually write buggy code).

I guess, some people just better left alone as long as they can find someone ready to pay them.

The downside is that I can't really write big projects. My biggest projects are few dozens of thousands of LoC and typical project is few thousands of LoC. They are pretty useful to end-users, though, but participating in some huge project with millions LoC probably would be an interesting experience that I won't encounter.


It sounds like you have only ever worked alone or with incompetent teams. I strongly recommend at least trying to work in a team of smart people with more experience than you. It can be an enlightening experience.

There's a chance you will learn

- how to handle criticism professionally;

- that you actually write more buggy code than you think;

- that there are a lot of critiques that are not subjective that you simply currently cannot imagine (because if you could, you wouldn't get it wrong in the first place, right?); and

- there are some subjective opinions which you didn't know about but agree with once you do.


Do you not see value in more experienced people giving you feedback on how to tackle a problem better?


Mainly because the code that is written in organizations is meant to be easily read and traversed through by other developers. Every place I've worked at had different standards on how to to something not because it was superior but because it is predictable.


> I'd go nuts if someone told me I had to use more classes, or a certain language, or tabs over spaces, or whatever

Yeah, but they usually have loaded guns to our head so we tend to comply.

Jeez.


The main benefit of classes is as a way to define custom types. Otherwise you are stuck with the types built into the language, which are not designed to help ensure correct program state, logic, and behavior. When you use classes in this way, I call it type oriented programming (TOP). Why would I compare classes to types? Just pass in the initial value at construction time, define the "operations" on that type by creating methods for your class, and define which other types those operations work with by setting the types of parameters those methods work with. Make the class immutable, always returning a new instance with the new updated value passed into the constructor. Why did I mention these types helping the program be more correct? This should be used as the primary form of contracts. But these contracts are very simple to use, you just specify the relevant type in the parameters and return values, and you are done defining the contract for any given method. For example, let's say a method should only work with integers larger than zero, instead of either accepting an Int or an UnsignedInt, which would allow 0, you could define your own class called PositiveInt. It would be designed to throw an exception if you pass anything smaller than 0 into the constructor. Then instead of writing code inside the method that makes sure the user of the method is following the contract, you just specify PositiveInt as the parameter type. If the contract is violated, the exception will be thrown as early as possible, before the method is even called, helping programmers catch the original source of the problem. This also makes your code more readable, because you can see exactly what each method accepts and returns just by looking at the method signature. When you start thinking this way, you will notice many core types are missing from the language, that should have been there from the beginning. Fortunately you now know how to build them yourself.


> The main benefit of classes is as a way to define custom types. Otherwise you are stuck with the types built into the language

I'm going to stop you right there, because plenty of languages (especially functional ones) have ways of declaring custom complex types without using classes.


A simple TypeScript example:

    interface DuckTypedObject {
      quack: true
      bark?: false
      eyes: 'beady'
    }
This will require that any object used as a DuckTypedObject must have the `quack` and `eyes` properties and may optionally have a `bark` of `false`, but doesn't otherwise prescribe what the object actually has to be.


This is the best part of TS IMO (structural typing).


agreed 100%. structural, statically-checkable typing of objects is the best thing to happen for reusable and testable code in my programming lifetime.


Sorry to be pedantic but that isn't quite right. I do use the same technique myself quite a lot with the options pattern.

However it is worth keeping the following in mind. The compiler will check that in your code that these properties are present and assigned correctly. However at runtime nothing is guaranteed especially when dealing with the DOM.

One of the things that I don't like about TypeScript (I have written a fair bit of it) is that you believe you have type safety when it is really type hinting.


Validation functions are pretty easy to write, as long as your data is being handled in a predictable way.


Yes. However you can for example do the following in typescript:

    function someFunction(obj?:DuckType) {
        //Snip other logic
        someOtherFunction(obj);
    }

    function someOtherFunction(obj:DuckType) {

    }
The type checker catch that. So you still have to do:

    function someFunction(obj?:DuckType) {
        obj = obj || { /* set some object properties */
        //Snip other logic
        someOtherFunction(obj);
    }

    function someOtherFunction(obj:DuckType) {

    }
I have found plenty of examples where people haven't set a default value because the compiler hasn't flagged anything wrong with the code and you get an uncaught reference error.


Huh, shouldn't the things on the right hand side of the colons be types not values?


Which languages would you say are really good for defining your own types? Would they be a good fit for the example I provided where a function needs to accept integers larger than zero? Do they also allow you to define your own operations on those types? That is the part where classes seem to be a good fit, methods are basically operations supported by a type.


> Which languages would you say are really good for defining your own types? Would they be a good fit for the example I provided where a function needs to accept integers larger than zero?

I'm not an Ada expert, but it has excellent support for range-restricted integer types.[0]

Ada's 'discriminated types' are also fun. They let you create members which only exist when they're applicable. [1]

> Do they also allow you to define your own operations on those types

Looks like Ada supports operator overloading, yes. [2]

[0] https://www.adaic.org/resources/add_content/standards/05rm/h...

[1] https://www.adaic.org/resources/add_content/standards/05rat/...

[2] https://www.adaic.org/resources/add_content/standards/05aarm...


Ada is also great at separating the various aspects of OOP into separate language concepts, instead of going "everything is done with classes!!" as many popular languages do, leading to a lot of confusion in this thread.


A typical way to handle this in a functional language would be to create a datatype with a name like "PositiveInt", which just contains an Int inside it. However, in a language like OCaml, you can make it so that users of this type cannot directly create it, and instead must use a function like "makePosInt", which would check that its argument is positive, then give you back a value of type PositiveInt containing your data.

I'm not too experienced with this though, so this is pretty much the extent of my knowledge on this topic.


Rust is excellent for this. You can define:

    struct PositiveInteger(u32);
Then give it a constructor (which in rust is just a regular static function) that checks the non-zero variant. You can define method on this type, and make it implement traits (which are kind of like interfaces).

The best bit: there is zero runtime cost to this, the memory-representation of this type is identical to that of the underlying u32.

Rust-style enums which can contain data, and also have method implemented on them are even better. Doesn't mean classes (structs in Rust) aren't useful, but once you use a language that allows you to define other kinds of custom types, they seem very restrictive when they're the only available tool.


That sounds like a class.


Yep. This whole discussion just seems like a C vs C++ styleguide slapfight.

Having the functions that operate on the struct attached directly to the struct declaration, vs having some functions that the first parameter is the struct on which the function operates, doesn't seem like a particularly meaningful distinction to me. OK, you like C-style programming in favor of C++-style programming, congrats. It's still a class either way.


The distinction you describe is not meaningful, but the key feature that separates classes from other forms of code organization/polymorphism, like typeclasses as in Haskell/Rust, is not that. It is inheritance.


I guess it's kind close to a C++ class. It's pretty different to a class in a language like Java, because all classes in Java are heap allocated and behind references.

Enums are are the better example of non-class types. For example, you can have:

    enum StringOrInt {
        String(String),
        Int(u32),
    }
And you can go ahead and implement methods on that type. Classes have "AND-state", not "OR-state". But a Type in general can have either kind of state.


That's equivalent to wrapping an enum in a class. Emulations of type hierarchies without OO often fail like this, having a A-or-B be literally the same type so losing out on type safety/forcing constant rechecking of the discriminant.


That can be done in Rust by having two different types implement the same interface


It's a product type[0], containing a single inner type of `u32`. Sometimes classes look like product types.

[0] https://en.wikipedia.org/wiki/Product_type


Data types are not classes.


Sum types and typeclasses in Haskell (or enums and traits in Rust) are a much better fit than classes and the mess that is operator overloading.


Thanks! These definitely sound like what I should explore next in my journey.


I believe you're looking for dependent types: https://en.wikipedia.org/wiki/Dependent_type


You actually don't need types or classes to do this. You could use design by contract, which is what I've done in Python and Perl, neither of which have very fancy type systems compared to something like Haskell.

With design by contract you can put in whatever fancy constraints you want on function parameters and return values, and those will be enforced.

As far as objects go, they're much more useful for me as just a means of passing state. Rather than using a bunch of global variables or having to pass in a ton of function arguments, I can just use an object which contains all the state I need.

Of course, having lots of state can usher in its own set of problems, and there's something to be said for trying to make your code as stateless as possible. But sometimes you need state, and maybe even a lot of it.


I use C structs in the same way. I think it's fine to use classes for this purpose. The problem with classes is that certain people go crazy with inheritance and polymorphism, things which sound smart on paper but almost always lead to horrific unmaintainable code in practice.


Yeah, basically the lesson of the last 20 years of Java and C# and so on is that inheritance sucks and what you mostly are after is composition.


This is a good point. Right now Kotlin is my favorite language. By default classes and methods are closed for inheritance, and therefore for polymorphism. I like the way this is the default, but you can override it when/if necessary. It also has data classes, which in my opinion is what most classes should be. I think when a class is not be a good fit for data classes, this is a code smell.


An excellent article on type-driven design and development was posted on HN [1][2], as darkkindness put it "Encode invariants in your data, don't enforce invariants on your data"

[1] https://news.ycombinator.com/item?id=21476261 [2] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...


> For example, let's say a method should only work with integers larger than zero

I dislike this example. The numeric systems of every programming language I've ever used has been (more or less) terrible, precisely because there are extremely common and simple arithmetic types, just like this, which it's terrible at representing. Half of the "modern" languages I've seen just provide the thinnest possible wrapper around the hardware registers ("Int32"!).

(What if I need to accept an integer in the range 5 < x < 10 instead? Am I supposed to define a new class for every range?)

Instead of saying we need a system of user-definable classes so every user can fix the numerics on their own, for each program they write, I'd say we should fix our numeric systems to support these common cases, and then re-evaluate whether we really need classes.

Are there non-numeric analogues to this type of value restriction? Maybe. It doesn't seem like a very common one, but it is an interesting one. Perhaps what we really want is "define type B = type A + restriction { invariant X }". I can't think of any examples offhand in my own work, but that could be because I haven't had that language feature so I wasn't looking for places to apply it.


I guess it comes down to an unresolved duality.

One being the idea of types as describing shape of data. In the best of cases perhaps some semantics tied to the data (how the bits are to be interpreted)

Than there is the the other view, the Curry-Howard one. Where types describes proofs and invariants of the program it self, and how interesting properties of program compositions can be ensured.

It seems much time is wasted when people holding one perspective debates with people holding the other.

Perhaps we should have separate words for these concepts.


Not only were custom types invented decades before classes, but the languages with the strongest emphasis on type safety tend to either lack them or consider them to be unidiomatic.


Which languages would you say are really good for defining your own types? Would they be a good fit for the example I provided where a function needs to accept integers larger than zero? Do they also allow you to define your own operations on those types? That is the part where classes seem to be a good fit, methods are basically operations supported by a type.


For the integers larger than zero example, I'd say the earliest archetype for a good design (that I know of) is Ada, which lets you declare a type with a limited numeric range like so:

  type MyType is range min .. max
There's already a built-in for positive integers, which is defined as

  subtype Positive is Integer range 1 .. Integer'Last;

Note the subtype there. Ada recognizes that a positive integer is a type of integer, but not the other way around. And it enforces that in the type checking: You can pass any Positive into a function that accepts Integer, but you can't just pass an Integer into a function that accepts Positive. This happens even though they're not classes and this isn't OOP. Ada does have object-oriented constructs, but they are a later addition to the language. I have never used Ada professionally, but my understanding (based on book learning) is that it tends to be used conservatively.

It's similar in OCaml. Despite the O standing for "object-oriented", creating classes isn't necessarily considered idiomatic. The other tools in the chest tend to be conceptually simpler, and therefore to be preferred when they will get the job done.

"define your own operations" is a requirement I'm having a hard time making sense of. To me, that is just another way of saying, "define functions", which is a feature of every language I've used except for one really ancient dialect of BASIC.


If I recall correctly, Pascal would also let you define a ranged integer, long before Ada.


Thanks, looks like you recall correctly.

Every so often, I wonder if I should spend some time with Pascal.


You can go with more of a Haskell/Rust style approach, where you declare a type, and then define operations using either plain functions, or using something kinda similar an interface (typeclasses in Haskell, traits in Rust).


> When you use classes in this way, I call it type oriented programming (TOP).

Watch out, you might make topmind mad! https://wiki.c2.com/?TopMind


In my experience, custom types often turn out either being so restrictive as to be less than useful or being leaky abstractions. (This might be an example of just where a healthy engineering compromise is unavoidable.)


have you heard of structs?


You’re always going to be on one side of the expression problem [0]: either your operations live with your datatype (classes), which makes it inconvenient to add a new operation, or they live somewhere else (“functional modules”), which makes it inconvenient to add a new case to the datatype. This choice is probably informed by your expectations of how often you intend to do either of those things, but there’s going to be inconvenience regardless.

The big benefit of objects, and therefore classes (or something like them), is polymorphism via dynamic dispatch; it seems a shame to throw the baby out with the bathwater because you don’t like inheritance or mutable state, both of which are independent of the choice to use classes.

[0] https://en.wikipedia.org/wiki/Expression_problem


I’m not sure dynamic dispatch is that much of a killer feature. Take C# it default to static dispatch of method and you tend to get very far without explicitly making things virtual.

Event when using interfaces many times it’s seems possible to arrange things such that parametric polymorphism would be able to use a static type. There are some boiling proposals for future versions that will help with exactly this, and indeed some of the latest changes to the languages have included allowing more static dispatching in polymorphic constructs like disposal and enumeration.

I’m beginning to suspect that dynamic dispatch could be entirely dropped without losing to much expressiveness.

Have no experience with Rust but isn’t that kind of the conclusion from that community?


What's more, languages like Smalltalk which only have "virtual" dispatch achieve their performance by recognising that most dispatch is just to one type. And of the remaining cases, most of those are just to 2 types, etc. Pushing this further resulting in Strongtalk which lead to the Hotspot JVM [1].

[1] http://strongtalk.org/index.html


Not really anything of an expert with Rust, but I'd say that the language tries to dispatch function calls statically whenever possible, though it certainly has the capacity to provide polymorphism when needed, through the use of trait objects [0].

From using it personally, I've found that I can usually get by with generics and the static dispatch that they provide, but having the option to use trait bounds can be a real boon when you need the flexibility to call on shared pieces of functionality between separate complicated systems.

[0] https://doc.rust-lang.org/1.30.0/book/2018-edition/ch17-02-t...


rust has `dyn FooTrait`, which lets you do dynamic diapatch, similarly to `FooInterface x = foo();` in e.g. java. even Haskell has a way of doing this via existential types like `(forall a. FooTypeclass a => a)`.

both of these predominantly statically-dispatched langs added ways to do dynamic dispatch later on, which seems to go against "dynamic dispatch could be entirely dropped without losing to much expressiveness". i prefer static-by-default as well, but it looks like it's hard to do without sometimes.

(of course you can always roll your own vtable-ish thing, so technically you could drop DD from the language without loss of expressiveness, but then you'd likely get multiple incompatible libraries for that...)

and i'd guess that some sort of dynamic dispatch is unavoidable if you want to do some kind of plugin architecture


Are you sure that Haskell thing is dynamic? I was under the impression that the type you have there is resolved statically by finding a type class for a


existential types allow you to make it dynamic. see https://wiki.haskell.org/Existential_type, there's a section comparing it to OOP-style dynamic dispatch

simple example copied from the linked article:

  data Obj = forall a. (Show a) => Obj a

  xs :: [Obj]
  xs = [Obj 1, Obj "foo", Obj 'c']

  doShow :: [Obj] -> String
  doShow [] = ""
  doShow ((Obj x):xs) = show x ++ doShow xs
we can have a "heterogenous" list of `Obj`s, like a java `ArrayList<IPrintable>`. each `Obj` is basically a pointer to the actual value of some type T and a pointer to T's Show dictionary (vtable). Which is afaik analogous to how OOP languages do it


> I’m not sure dynamic dispatch is that much of a killer feature.

It's a feature that, when it fits your problem, can be extremely useful. I have rarely had it fit a problem. The ones it fit, though, it was nearly magic.


I think that's broadly true but it should at least be pointed out that static dispatch tends to end up inflating binary size and compilation time. Rust also supports dynamic dispatch so you can make a context dependent choice.


Static dispatch or monomorphization?


Isn’t using parametric polymorphism to get static dispatch reliant on monomorphization? That’s at least the common case in Rust.


There are languages that solve the expression problem, such as Clojure. But they aren't very popular.


In some languages runtime polymorphism is not tied to types.



OTOH, classes are just one way of doing polymorphism/dynamic dispatch. see Clojure's multimethods.


Classes are a means to an end: objects. Objects give us the three core components of OOP:

* encapsulation

* message passing

* late binding

Things like inheritance tend to muddy the waters. If you aren't benefiting from the above 3 things, then you are probably either using objects wrong or you are applying them to a problem for which encapsulated, late-bound message passing isn't a good solution.

As a corollary, state management and OOP are in my opinion orthogonal. You can write and program with immutable objects just as you can with mutable ones.

I do agree with some other comments that OOP in college isn't taught very well. Most people leave their first OOP class not really knowing anything about OOP or why it's useful. My hunch is that OOP is something that should be taught later in a program, not earlier.


IMO, when people talk about OOP they aren't talking about bags of static functions -- that's just a namespace. The interesting objects are those which hold state.


You can have an encapsulated object that is immutable and is not a bag of static functions anymore than any object is a bag of static functions. Look at Scala's interfaces for immutable collections.

* they are encapsulated. You don't know how the data is stored internally.

* There is message passing involved (at the JVM level). The methods are virtual and you don't know exactly which method is being called. There can also be multiple implementations using different data structures.

* Late binding is again as defined by the JVM (interface table method lookup)

In contrast, a static function is no different than a global variable. When you call it, you know exactly (sans JVM linking) which function you are calling.


Objects whose methods are closures are particularly interesting. They hold state, but it can be immutable state.


Sparkles of inheritance can be good. I use them sometimes to aid functional combinators.


Right. I'm not saying inheritance should never be used; just that it can be a distraction for learning what OOP is really about. If I were to teach OOP in Java to new students, I might start out just focusing on interfaces.


Using classes doesn't necessitate using inheritance and polymorphism. Whenever I'm programming in a non-OOP language, I still gravitate towards class-like code. From my perspective, there isn't much different between:

doStuff(&fooStruct, ...) vs. fooClass.doStuff(...)


The core difference is in testing. With the later, you need to get your class into the right state to preform the test. You might even need a mock or two. With the non-OOP approach, you set the entire state of the function and see the results.


Is that even true? In the end it’s all the same code. You can write classes that need a lot of context but the same can happen with functions.


If the method needs just as much state in a struct as you would on a class then its equivalent.


You are wrong. You need to create just as much state in fooStruct as fooClass in the example GP gives.


> you set the entire state

And how would you easily create that state?


You create a mock :)



One of my favorites:

https://wiki.c2.com/?ClosuresAndObjectsAreEquivalent

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.


In school they taught classes and inheritance like it was the biggest thing since sliced bread.

I feel like they're but one tool, and necessary in limited cases.

For example I recently encountered an API that had to intake 100+ key/value pairs and manipulate these values, and send them to various places.

I tried to do the mappings and data manipulations and everything in one long piece of code functionally but it led to some serious spaghetti. When I separated things out into classes, it was much cleaner code, with a more well defined domains. It was definitely not perfect but ended up being much cleaner. This is a simple example but classes can be useful. It's arguable that the API could have been better written and that's true but sometimes you have to work with what you're given.

Otherwise I would venture to say most code doesn't require classes.


> In school they taught classes and inheritance like it was the biggest thing since sliced bread.

they even had to dance around how to have a main entry point in class only languages like java

the early 2k were really religion blinded around a class god, without teaching the real values and uses of classes in the context of problem solving


Not even 2k, it was a thing in 1990s already. I remember a student coming in to the company I worked with around 1993/4. His thesis was about O/R binding frameworks and approaches for relational DBs. Generations of developers have drank the "impedance mismatch" and other OOP pseudo science koolaid since.


By "I separated things out into classes", do you mean that you used the whole triad of inheritance + polymorphism + encapsulation?


Do you need all 3 of them to have a class? In my experience classes are data+ methods that manipulate that data. Like a Vector class would have a rotate,normalize etc methods that return a new Vector.


That's a question of definition. There are many who will passionately insist that yes, you absolutely need all of them. I hypothesize but can't prove that these are people simply regurgitating what they were taught in a classroom and who have not yet left Java/C++ to see what else is out there and what else can work. It is perfectly feasible to be educated that way and then spend your entire career in Java or C++ and come to conceptualize everything else as "that crazy minority stuff that isn't like Real Programming", and, in their defense, they are at least correct about the "minority" part. It's easy to forget if you read HN a lot, but inheritance + polymorphism + encapsulation remain the massively dominant paradigm, especially if you assume most C# is still basically written that way despite supporting other paradigms.

Personally I take the loosest possibly view, mostly just "associating data and methods to operate on that data fairly tightly together, where they all get passed around as a unit". Some ability to have "x.y()" result in multiple possible implementations of "y" is necessary, or you just have a weird way of spelling function calls. That's about all I'll require to call something "OO" nowadays.


I think the number of developers that when starting a project start creating some inheritance tree is large. Some concepts work fit inheritance very well like GUI widgets or game entities, you would make a disservice not to tell students about the concepts and have them re-invent them.

Not sure how other languages do it, but in my daily work the only time I use inheritance is with an ORM , to get the magic to work you need to extend the base Model class , add a few lines of configuration code and you are done, for more advanced uses I think you have to override some methods.

But except the ORM code I don't think I used inheritance in a long time, though I used interfaces in some other cases.


Game entities really don't fit inheritance very well though! Even if using an OO approach it's far more flexible to compose. Otherwise it suffers from the classic large inheritance tree issues of lots of state and functionality leaking up the tree to common base classes. As an example the Unreal Engine used to have a heinously enormous base class called Actor. Another is that all the game entities in EVE Online actually bring the AI code with them. You can make it work but it's in spite of rather than because of inheritance. Anyone teaching students it's useful is doing them a great disservice.


I don't have experience with game engines, but I worked with Qt, WinForms, Flex4, Swing GUI libraries, and it made sense. All components inherit from a base widget, This base widget has everything you need, layout, positioning, mouse and keyboard events, painting. If you want to make a custom button that say is rotating when you click it you extend the Button and not start from scratch like you do in html. In html you see many custom components created from nested DIVs, this custom widgets are missing many basic features like accessibility and keyboard shortcuts and are broken for edge cases. As an example PayPal has a money amount text input but the Delete key does not work on it.

Could there be some insane case where the OOP of the GUIO widget is problematic, maybe , like when you want your app to have a window in the shape of a circle ... but I think is fair you get easy to use library for 99% of the cases and for 1% of the cases you might have to get your hands dirty and go outside the standard ways and maybe look under the hood.


Yeah GUIs seem to end up fitting reasonably well. They tend to be fairly modelled as specialisations. The problem with games is that it feels like they should but aren’t.


Maybe the issue is that the engine game evolves, new requirements are added and maybe a developer has a bad day and some ugly code is created.

If we consider a game like Minecraft, I would use something like a base class for blocks and a different base class for mobs, then if you add a new feature you put it in base class and all the objects will get it for free. I am not sure how Minecraft is architected but I loved how all entities including the player are the same, if you can do something to a player(push, drawn, burn, catapult, activate circuits , etc) all mobs would work the same.

I think GUIs work well because is a very well known and studied problem where with game engines you have a generic problem and then each game is bending the engine to try to use it for different type of games that the engine author did not consider at the beginning.

My conclusion is that some OOP is a tool you can use, you should not avoid it because a dude in a blog said so,the same with GOTO - I think I only used GOTO once , it was more efficient and more readable to use goto there then trying to workaround using it by creating variables and then adding checks (the problem was to efficiently break from inside 2 or more nested loops efficiently)


I think we need to separate inheritance trees (arbitrary depth of inheritance) from mixin/trait inheritance which has height = 2. The latter is a lot more useful in eg. games where objects are assembled from mixins like Legos. The former leads to fragility, class bloat and other problems (remember MFC...).

Basically, GUI frameworks are the only area I know where OOP - real, authentic long inheritance chains with encapsulation and extensibility - has worked well.


Exactly. Clusters of parameters and functions that manipulate those parameters all specific to a 'domian' is really what a class is for. Inheritance/ Polymorphism is just more machinery to do that with.

Functionality like mapping the keys to new names or uppercasing certain specific values or sending the data to certain places.

You could have the functions and parameters all in one long piece of code, or you could put them into domains which makes dealing with it, reading it, understanding it, and working with it easier, in my opinion.


Nope, that's not a class, that's a struct and a module with functions. Any method called as obj.foo(5) is really just a function foo with a hidden argument foo(obj, 5). Rust makes this architecture explicit with its structs and trait impls separated - and no OOP in sight.

Classes, on the other hand, are not structs with functions. More like black boxes whose contents can be swapped out at any moment. OOP is about hiding implementation and infinite extensibility.


I do not know Rust but this seems an implementation detail, like if I would make the Vector.normalize function static and then pass the this as an argument. Sometimes this pattern with static function works well too. My classes also have private methods and sometimes even private static pure functions. Anyway you Rust people do whatever you want and call them whatever you want , I will call a struct with methods a class.


Nope. This was just an extremely simple case of domain separated parameters with domain specific functionality pertaining to those parameters.

When you have 100+ parameters to deal with it helps in multiple ways to separate those out into domains and classes are a useful tool for that.


Ok, good to know. I was afraid that actual full-scale OOP was what saved the day, but it was unnecessary yet again.


haha very true. I probably could have used it if I got super hard core about separating my domains up but it just seems to over complicate things.


This is like so many "OOP is bad" posts. Yes, OOP is bad.

OOP is bad because inheritance is not as good as interfaces.

OOP is bad because typically it puts state changes inside mutable, shared objects, which makes concurrency difficult.

Java has interfaces and generics -- use those instead of inheritance.

C++ has templates -- use those instead of inheritance.

Design and implement interfaces that operate on immutable, preferably linear data types, and concurrency will be easier.


My prefered style is a class that's ideally no more than one level deep. Mix in some traits if you need them. Also, as such as possible, make every class/object immutable and the methods side-effect free. I find this leads to a very natural style in which functions are logically grouped together (also, helps with intellisense) but never modify any state. If there are classes/functions that modify state, make it explicit through naming (I append '_!' for Scala).

Essentially, what I'm describing, is just functional programming where f(a, x) becomes a.f(x). I find the latter easier to read and lends itself to a fluent, lightweight style.

In general, I don't really think the discussion of OOP vs FP or whatever is that meaningful. What is meaningful is mutable vs immutable. Every great developer I've met has agreed with me that mutation should be avoided as much as humanly possible: really the only valid reason to introduce mutation in your program is for performance reasons. And yet, we as an industry still are by and large writing mutation heavy code for no reason at all. Things have gotten a lot better in the last 10 years, but we still have a long way to go.

Edit: I'm mostly talking about Scala in this post, btw. I also want to note that I do think deep class hierarchies are a good solution to some problems. The two examples that immediately come to mind are UI APIs and collection libraries. Both these problems are very naturally solved by OO, the essence of which is the deep (as much as necessary) class hierarchies. Very few problems I've encountered besides the above actually require these subtyping.

An great alternative approach is row polymorphism, ala OCaml. I've not had any professional experience with OCaml or row polymorphism, so I'm not sure how well it actually works in practice.


> I find the latter easier to read and lends itself to a fluent, lightweight style.

Maybe I'm bikeshedding, but I think this is a nontrivial part of why people like OOP languages. Being able to read right to left, without nested parens, makes code comprehension easier.

I think the mathematical notation for function application has held programming languages back. I realize that there's a huge benefit to uniformity of syntax across languages, and that syntax is ultimately much less important than semantics, but I wish we could settle on something better than f(x). I really appreciate languages that are willing to do something different (e.g. haskell and lisp).


There's the pipe operator in a lot of languages (|>) to try and solve this problem, but it has always felt clunky to me. Compare:

f(g(h(x)))

x.h.g.f

x |> f |> g |> h

I think most people prefer the second one to all the rest.


I think the third example should be:

x |> h |> g |> f

Which is identical to the second, except for a more eloquent choice of the pipeline symbol?


That is the version of the |> operator I'm familiar with from Julia.


"As I’ve mentioned above, I like to use modules that expose groups of functions. These functions accept state and other dependencies. These modules tend to look like what my colleague Drew describes as the functional module pattern."

I dislike that.

When I see a class, I expect it exists because there is a central state around which the methods revolve, and are meant to expose or change it.

When I see a function, I expect it to be side effect free.

Sure you can use any paradigm to make any program, but I use paradigms, just like I use design patterns, to communicate intent. Solving the problem is half the battle. In a way, coding style is documentation. I think we should build boring API, and only use cleverness to find solutions, or make the API easier to use and more elegant.

I worked once on a code base that broke a lot of implicit expectations. Like, it could add objects in a registry when you instantiated some classes, get_stuff could set other stuff, do_stuff would not do stuff but actually_do_stuff would, etc.

It was a nightmare to work with, because anything could happen anywhere. You had to look at the code every single time and keep the whole system in your head, which had 900k lines of highly technical code.


You misunderstand, I think. Or maybe I do.

If done properly as described, the functions accept state. This means they adjust state by adjusting input/output parameters, not by affecting some internal state. This is basically Dependency Injection extended to state. This is also how classes are implemented (see python's self parameter to all class functions).


Yes, but looking at the function, and banning some project specific naming convention, there is no way to know its role is to mutate a state.

While in Python, when you see a class, you just know the methods are mostly about "self".

It helps making sense of the code immensely.


Correct, but the effectiveness of the design is separate from its correctness. Latter of which is what I commented on.

For effectiveness, I think it reduces ancillary class members, and encourages (necessitates!) dependency injection.

For example, changing "delta t" in a game object update is trivial: update the one delta-t you have, and pass it into everything that needs it. With classes, there may be a temptation to have a delta-t member variable, which costs many updates. Stupid example, but a mistake which is not possible with Dependency Injection.

But really, proper scoping in typed languages (and naming in dynamic ones) can allude to input vs output parameters. In C/C++ this is passing by ref vs value. (In Google C++ style guide, they recommend use of pointers for "adjusted" or "output" parameters.) In MS C++ from back in the day, I think they had ways to explicitly specify parameters as input, output, or input+output. Does someone have an example of this?


plus favoring mutability over immutability opens up a whole other can of bugs



It would be nice to group all the functions that operate on a piece of state together in the same place.

It would also be nice to prevent functions that are not in this group to make changes to that part of the state.

Then you could prefix all those functions with a name, so it's clear what's their scope.

Finally, in some cases it would be great if you could create multiple independent "instances" of the state, so you could manage more than one at a time without risking conflicts between the various copies.


Well, they can still be grouped by module, header, and/or namespacing


This is a great reply. If you don’t use classes to define behavior for objects that manage their own state, there’s no point in using OOP.


I don't think you actually do state management in OOP, only manage a single level of abstractions.

In java, inheiretance is directional in a single way, but if you instantiate a singleton component, you can end up affecting the state of every other class that uses that same component. This is not wrong per se, it is a property of OOP in itself.

If this rubs you the wrong way, possibly you should be working in purely, monadic functions.


In think singleton has been labelled as anti-pattern in OO for a long time.

Having a single reference is useful, but using instantiation as a way to get it has proven a bad design decision.


That is really my point. Isn't it better to just use a tool that limits what bad decisions you can make? Or at least make these bad decisions so hard that once you know how to implement them, you know better ways to write your code?


OOP has come to mean different things at different times. For a time, OOP definitely meant tightly coupling state and behavior. Same as inheritance, this practice seems to have been left behind some time ago.


  These functions accept state and other dependencies
That "dependencies" term is very loaded. You need to define if you call "dependencies" what the functions in your module take as argument or you're talking about those other modules your module calls.

  When I see a function, I expect it to be side effect free.
Why? though I think it's a commendable goal it's not a reasonable expectation from most languages.


When I read "these functions accept state and other dependencies," I took it to mean that "state and other dependencies" would be an ever-growing list of arguments to the function -- not that they'd necessarily be modifying external state.

I work on a project where I framed up both patterns, and ultimately decided that the hodgepodge^ pattern was preferable to repeating long lists of arguments.

^ kinda like the mixin pattern but I think it's been taken too far


JavaScript, and thus Typescript by extension, lived for many years without classes. The addition of classes is relatively recent. It stands to reason that classes are not heavily adopted since since a lot of the foundational work happened when classes were not even an option.

In contrast, I cannot image being able to get far in Java without using classes. The concept of using classes is central to that language.


> I cannot image being able to get far in Java without using classes

Well you literally can't write a Java program without writing a class.


The #1 complaint I always see with classes is due to inheritance. When classes inherit from classes that inherit from other classes, you can very easily end up with a tangled web of methods over-riding and extending other methods, and run into all sorts of unexpected behaviors and difficulties wrapping your head around what is going on.

This is why (at least in the Ruby world), it's generally considered best practice to avoid using inheritance and just use mixins instead (whenever possible).

Completely avoiding classes because you are using them incorrectly is kind of like throwing the baby out with the dirty bathwater. Don't throw away your tools just because you are using them incorrectly!


Inheritance is a debate that already ended long ago with OOP practitioners vehemently against it.

The argument now is different. There is another flaw with classes and that is mutation of internal variables with getters and setters. Classes typically allow you to do this and promote this behavior, structs do not typically allow you to do this and are not used this way, typically.

It makes it so that a class is more than just a data type, it is a mini program with it's own internal state and api that you are passing around in your program. It ups the complexity of your program by 10 fold.


This is a great post. "A mini program with its own internal state and API" is a great description of what goes wrong with overdesigned classes. Encapsulating state into classes originally made sense with early OOP languages, compared to all of it being global which is even worse, but nowadays we've got better ways of managing complexity.

Any time something threatens to be a mutable class-level variable, I refactor that out of the design before proceeding. Most often this refactoring is to separate the data into a struct-like class and the behavioral methods into a static class, with what would have been that mutable class-level variable instead passed in to each behavioral function that needs it.


I feel like you missed a bit of the explanation out there. Where does the struct like class end up being instantiated and how is it linked to the class that presumably would have previously contained it? Does the struct like class get put into some kind of data store and the class that needed mutable state then holds a handle or similar?


If I had a nickel for every blog post I've seen highlighting an incorrect usage of a pattern as a reason not to use the pattern at all...


I'm not sure how using mixins is better. Now you don't know half the time what is overriding what. Composition seems to be the better tool IMHO.


Aren't mixins a form of composition?


I would argue no they are not. I am thinking of their implementation in python specifically, so my understanding is likely skewed.

My notion of composition with classes would be:

  class Dog(object):
    def __init__():
      self.wagger = Wagger(); #composing the Wagger class
in python mixins look like this:

  class Dog(object, WaggerMixin):
     ...etc...


Aren't classes in Javascript mostly syntactic sugar to make inheritance easier?


I think the problem is that most developers did not learn OOP correctly. I noticed a lot of developers these days don't understand basic concepts like encapsulation and class dependencies.

When I was studying OOP at university, there was an entire course on UML diagrams and that taught people how to structure OOP software. Good OOP design tends to produce elegant-looking UML diagrams with relatively few interconnections between components.

I don't always physically draw UML diagrams, but when I visualize a project in my head, it looks very much like a UML class diagram. I can't imagine how it's possible to reason about complex software in any other way TBH.

Sadly, if I had to draw a UML class diagram for most real projects which exist today, the relationship lines between UML classes would be crossing all over the place and it would literally look like a tangled mess (definitely more like a graph than a tree). The people who designed the software had no vision for it, they just kept adding stuff without ever actually understanding how it all fits together or thinking about how to minimize complexity and make it more elegant by minimizing the number of components and the interconnections between them.

I think that, in a way, functional programming is more forgiving when it comes to allowing people to keep adding stuff on top without having to visualize the system as a whole. It doesn't mean that FP adds any net value over OOP. I would argue that being able to clearly visualize the whole product and the interaction of data and logic is essential regardless of what paradigm you follow.


Classes are the design-time version of instanciated Objects, which are active at run-time.

Objects, are like little minature software programs, inside of a larger programs.

Think fractals, that is, smaller things inside of larger ones...

So why have a minature software program inside of a bigger one?

The short answer is boundaries.

If the larger program is permitted to modify the internal state of an object, then that's really no different than having one big program where all of the variables are global, and any line of code can make any change to any variable at any time.

Is that OK? For small programs it is, but the larger the program, and the more programmers that work on it, the more it must be segmented into boundaries, and these boundaries enforced (aka, encapsulation) in order to prevent bugs.

That, and the ability to divide code into logical groups which can be individually tested will yield a great deal of clarity and information about a codebase, when done by a competent programmer or programming team.

At 100,000+ lines of code (and sometimes a lot less), you will trip over your own code unless you segregate it into seperately manageable sub-systems with clearly defined boundaries. "Divide and conquer" as the old saying goes.

On the other hand, programmers inexperienced with Object-Oriented development can make code more obscure, unreadable, and harder to work with...

It's a double-edged sword...

Use wisely.


But Classes and Objects are not the only way to create boundaries, and may not be the best way.


Give an example of when they might not be the best way.


There are many well known problems with classic OOP:

http://okmij.org/ftp/Computation/Subtyping/


"Classes are the best way" is a much stronger statement than "Classes may not be the best way".

Why do you think classes are the best way?


I feel like you could replace the words classes and objects with functions, without affecting its validity.


Finally, a man who truly strives to live in a classless society.


And as always, first thing a classless society does is to find a way to emulate classes.


You win the internet.


Classless society (or programming) is like cell-less biology.


While I like classes, the first 4[1] programming languages I used did not have classes. The first dialect of the first language I learned didn't even have functions!

1: Basic, Logo, C, Pascal. Logo and Basic I learned 2 fairly different dialects of even (Atari/Apple and GW/Quick respectively).


Indeed there was life before cells, too :)


Eh, I think classes are closer to Eukaryotes having mitochondria than something as fundamental as cells. Cells seem to be a prerequisite for complex life while classes are definitely not a prerequisite for complex programs.


Not sure about that. All cells exhibit encapsulation, inheritance, and polymorphism...


It was a miss opportunity not to call the article "I'm Classless" or "Class is cancelled"


Neither do I if I can avoid it, I use type hierarchies and generic methods.

Unfortunately it's not very popular; the only supporting languages I know of are Common Lisp, Julia & Perl 6.

I've designed and implemented several scripting languages [0] over the years, but they all have this pattern in common, I've never felt tempted to implement classes.

[0] https://github.com/codr7/gfoo


Dylan also supports it and it can be done in Python with a library [1]. I don't know if you'd count it but it's also straight forward in Haskell [2] at the type class level.

[1] https://en.wikipedia.org/wiki/Multiple_dispatch#Python

[2] https://stackoverflow.com/questions/26303353/can-multiple-di...


Please note that Perl 6 has been renamed to Raku (https://raku.org using the #rakulang tag on social media). If you want to stay up-to-date with events in the Raku world, check out the Rakudo Weekly News: https://rakudoweekly.blog


Surely the ultimate example of this phenomenon is OCaml.

It's wearing OOP right on its sleeve in the name of the language, but its users inevitably say "Oh, we never (or hardly ever) use that part".


Absolutely. Objects in OCaml is useful for structural typing, but I reach for classes way less often than I initially thought I would (read: never).

It turns out modules are indeed a better solution for, well, modularization.

OCaml is multi-paradigm in the most broad sense of the word I've experienced, just with functional and immutable as the default rather than the other way around. And it has a pretty great OOP implementation too.

Suggested reading for those wondering why the community deemed the O in OCaml mostly unnecessary and how rest of the language helps achieve the same goals in a different/better way: https://discuss.ocaml.org/t/objects-use-cases-in-ocaml/2282


I think the lesson from OCaml is that 90% of the value of OOP comes from things like packaging datatypes with functions that use them and separating implementations from interfaces which can be implemented with non-OOP things like modules. From an OCaml perspective, the main thing that you can only get from OOP and not elsewhere is open recursion, which is useful but only in certain niches.


There are people who think every carpentering problem can be solved with chisel and the only book they ever red is one that says how to solve every problem with a chisel.

The response to this is not to throw out all chisels.

The response is to learn to use various tools and once you know how to do this, how to apply different tools to different types of problems.

There is no shame in overusing a tool for a period of time. The novice carpenter must be able to play to learn limitations of tools but also to learn which inventive ways to use tools are actually helpful.

Master carpenter will understand that playtime is necessary step in getting to mastery and will not call out novices for their overuse of the chisel but instead will encourage play with the intent to speed up education.


> Secondly, I’ve noticed that classes have a tendency to grow large. They will collect pieces of functionality that need to live in the context of the class, usually to access the internal state of that class. This dependency on the internal state makes it difficult to break the methods up into logical chunks.

Sounds like they need to pay more attention to the Open-Closed principle :)

It's not really clear to me how a random grab-bag of functions is better than a random grab-bag of methods. If the functions in the too-large module can be broken up into small units without damaging cohesion, why can't the same be done for the class with two many methods?


I've been all over the board on this throughout my career. Currently, we are looking at a substantial reduction in the number of types in our codebase. This would imply fewer & larger classes. Which is totally fine, as long as you know how to manage that complexity.

Our approach for managing this complexity is to simply say that each class in the domain model aligns to the bounded context of a particular business unit, process, or activity. So, instead of modeling a bunch of different types within some namespace, the type itself represents the entire extent of that business concern, including any business facts as instance properties. Complex types are modeled as nested classes within each context. Nothing is shared between contexts except for trivial domain-wide concerns in the outer scope. This allows for very easy serialization of business state in and out of a database (among many other things). Consider the power that a method override might have in this realm if you wanted to construct a hierarchy of such contexts.

An example of one of these concrete types might be SelfCheckoutContext (if you were working in a retail business domain). This type could be derived from a wider CheckoutContext which could be simultaneously shared other concrete contexts like HumanCheckoutContext.

In our approach, there is also a lot of stuff that exists outside of each context (Views, Services, Repositories, etc). We basically treat these items as a platform layer for the contexts to operate on top of.


I hear some echoes of domain driven design / development in your comment. Have you looked into that?


Yes absolutely. We took what we felt was the bare essential idea of DDD (bounded contexts) and ran with it. The fewer patterns the better. Allows you to work with fewer constraints, assuming you are responsible with the added power.

Bounded contexts are IMO the most powerful abstraction out there for accurately constructing a very complex business system. They directly get at the root of the problem, which is aligning the implementation with the same grain as the business. They also permit complete isolation of various business units which might have unique perspectives into what would otherwise be shared (and increasingly-convoluted) models. In our latest implementations, models are never shared between contexts. There might be a few models defined in the outer domain scope for things we believe apply universally to the application. Any cross-context communication is achieved via simple mappers.


Typescript works well without classes because you have great type system, you can implement an interface on the fly by just passing an object with the correct properties to fulfill it.

But the class based architecture doesn't necessary have to be as bad as described in the article.

Inheritance is somewhat of the old school of OOP, modern OOP rarely use inheritance, it uses composition, just like a functional code base. In my projects my classes are always marked as final. Can't even remember the last time I inherited a another class, maybe if I interacted with some legacy code.

It is important to separate your classes, what classes can have state & what classes can not have state. Classes with state should be small & classes without can be large, because stateless classes can easily be reused.

Classes have the main advantage that you easily inject dependencies in the constructor (composition), e.g. a repository depending on the database driver.

You can do that with functions as well, e.g currying, but I find it a bit more messy if your language is not specialized against that use case & with a class you "curry" all class methods at once. If you don't inject your depencies in your functions you will have a much harder time to reuse that function & test it.

I agree with that class based GUI code rarely works, this is because GUI (if it is HTML or OpenGL) does not translate well to a class with hierarchies. GUI code have tendency to more be procedural.


I've noticed that in large Java hierarchies, where I spend a lot of my time, people often use interfaces where they really should be using modules. Like, there's this vague concept of a "service" and methods of this service are bound together via an interface. Which ends up coupling all kinds of weird dependencies in the class definition.

My guess is that this is a similar case in Typescript that the author is describing.

So, this approach is, instead of having IFooService with two methods, IFooService.doOneThing and IFooService.doAnotherThing you just have a package "fooservice" with functional interfaces DoOneThing and DoAnotherThing. This seems to still play nice with most of the tooling, and often, keeps the code simpler and makes it easier to refactor. DoOneThingImpl and DoAnotherThingImpl can easily share code, but that aspect shouldn't matter at all to the caller.

Another way of thinking about it: don't worry too much about a perfect interface for layering your business logic initially. Start with just some functions, then as you spot patterns you may recognize there's a state pattern that does things better. I don't think this is really specific to Typescript.


Good advice. Don't use classes. At least resist using them as much as possible.

Consider this simple aspect: one of the key differences between a class method and a separate funcion is implicit (and sometimes explicit, though that's not much better) self/this parameter that allows access to all object fields. Essentially you're opting in to grow the input of every method call by all the data held by an object. That's a mistake on many levels:

- it is one of the most important "best practice" advice to only pass the data that function needs to do the job, with classes you're breaking that advice immediately

- ease of access to additional data doesn't leave room to think if it's actually good idea to do so, factor in laziness and inexperienced contributors and you get methods that grow in complexity much more than they should

- testing anything becomes torture, as unlike with simple function class methods now depend not only on incoming arguments but also on state of the object itself, i've seen tests with dozens of lines of setup for single line of test

Prefer simple pure functions, prefer builtin data types, prefer immutability, only pass arguments that function needs to do the job.


> only pass arguments that function needs to do the job.

Why would users of that function bother to know what data it needs to do its job? It seems you'll end up polluting your whole code with the implementation details of one of its components. What happens of you want to change those implementation details?


Because the two alternatives we’re talking about here are ‘foo(bar, baz)’ where foo, bar and baz convey meaning and ‘object.doWork()’ which means nothing and explains nothing.

Yes, it’s a spectrum and there’s a lot of space in between those alternatives, I simply believe being closer to the first option is better than to the second.

Pure functions are conductive to transparency, objects, methods and class hierarchies are conductive to obscurity.


This reminds me of a podcast I listened to recently about creativity. Maybe its not about the idiomatic question of whether classes are good or bad but more about the act of placing the limitation that helps drive the masterpiece?

https://www.npr.org/transcripts/719557642


In Kotlin classes and methods are closed by default, making avoiding inheritance and polymorphism the default. It also makes you mark each variable as mutable or immutable, by declaring it with a var or val, making you think more about (and enforce) state being mutable or immutable, thus avoiding a lot of state problems. It also has a concept called data classes, which works really well for most well designed classes. I now consider the case where a class is not a good candidate for data classes to be a code smell. Most classes should be a good candidate for data classes or else you are usually using classes in a way that causes more problems than it solves.

The thing is, it doesn't prevent you from creating classes or methods that are open for inheritance, nor does it prevent you from allowing mutable state, but it does use the defaults and the language design to encourage limitations that are usually good.

Perhaps they could take these concepts a step further by making classes data classes by default, having a keyword for it to not be a data class.


I have always associated classes with inheritance. Perhaps there are other scenarios where classes are unrelated to inheritance. I don't know as I intentionally avoid classes all together.

I avoid inheritance because inheritance increases complexity. By complexity I simply mean it in terms of complicate, or to make many. I am not speaking to anything related to challenges, easiness, or hardships. Inheritance is a practice of a concept called polyinstantiation, which allows easy dynamic replication of an object in memory so that many child or cloned objects are independently extensible.

* https://en.wikipedia.org/wiki/Polyinstantiation

* https://en.wikipedia.org/wiki/Complexity


> I have always associated classes with inheritance.

This is a common pattern I've noticed in discussions about OOP. There is a tendency to reduce it to "inheritance," which turns the conversation into a straw man: "here is why inheritance is bad, therefore OOP is bad".

Same goes for identifying classes with inheritance. Their primary feature is grouping of methods and fields into (potentially) encapsulated units, and being a mechanism for abstraction: classes are more general than their instances and serve as a general pattern; upon construction they are 'configured' through parameters to their constructors and particular forms are arrived at.


It's better to associate classes with encapsulation.

I generally don't do inheritance apart from a few weird cases because composition over inheritance works way better.


I really like programming in C++ and don't write a lot of classes. They are a useful tool in the toolbox but hardly the only one.

Well, looking again, I do use a lot of structs (or their moral equivalent, using 'class' to define a struct with some protected state) but don't use inheritance much.


Most useful programs have state. As the program scales you need to break it up into parts (or 'modules') with clear interfaces. Usually these parts own state.

The problem with OOP is that it assumes you need lots of these parts. Whereas really, you might have a few or possibly a handful. Use, class-like constructs to represent these, and use normal functions within these, with ideally some kind of polymorphism that also doesn't require classes.

For small programs, don't even bother with this. Just use pure functions for key logic, and unit test those, and delay testing anything with state for as long as possible. When you do need to, leverage interfaces and stubs/fakes (not mocks) to test them.

That's what I do now anyway.


My pet theory about the abundance of classes is that they only gained popularity because of their inherent complexity. There's no one, authoritative way to use them, so a bunch of people can went to conferences and wrote papers about SOLID or encapsulation, or whatever. So people thought there must be value in them.

It takes a long time for a lot of developers to realize anything you can do with classes you can do without. And if you structure your code without classes, it tends to be more refactorable because moving chunks of code in and out of different contexts is a breeze.

The only things I find potentially useful about classes are:

- portability (but you can get that with modules, caveat that not every language does modules well)

- auto-complete in a noun->verb context


Question:

The author writes (as an example of his problems relying on classes):

Well, some tables when updating need to do more than just “update” a record, such as insert a log into a side table.

I do not understand exactly if there is a problem here, or anyway I do not understand what the Functional alternative would be.

Assuming this is a problem, I suppose we have three choices:

a) You have to provide implementation for two distinct methods: - Update(...) and LoggedUpdate(...) (the latter calls the former and then logs the result, most probably).

b) You decide that update() implicitly logs maybe based on a global configuration or similar mechanism.

c) You provide a boolean flag as parte of the method signature update(log).

What would be the proper Functional approach here?

(Caveat - I have no experience with Functional programming)


Ever since I discovered Julia a few years ago, I’ve found I can’t stand using classes any more (particularly in Python). Multiple dispatch just feels so much more elegant than trying to dispatch everything on the type of an implicit first argument.


My first programming teacher emphasized that this at-the-time-newfangled "object-oriented" programming was only a way of organizing the code, you still had the same data and functions.

Use classes, don't use classes. It doesn't matter.


I feel like dynamically-dispatched open recursion gives you a level of flexibility that you can't get in, say, Pascal — you need first-class function pointers if you don't have virtual functions.

BTW, I finally responded to your comment from the other day; sorry about the delay!


Well, Oberon had language-level OOP-- records with methods --which is what my teacher was talking about as just a way to arrange the same data and functions, but the OS also had runtime dynamic message dispatch OOP built on top of but not intrinsically requiring the language-level OOP. I suspect you could implement that in Pascal and make all of Hewitt's Actors model with it.

> BTW, I finally responded to your comment from the other day; sorry about the delay!

Cheers! I'll go check that out :-)


Yeah, the Oberon language added function pointers (I don't remember if those were in Modula-2), and the Oberon OS used that form of language-level OOP to implement its runtime dynamic message dispatch OOP. You can't implement that in Pascal because Pascal doesn't allow you to store function pointers in records (because they might be nested functions, with all that implies). So in Pascal the best you can do is write a switch function for each function pointer type (signature) and use an enumerated type or equivalent to dispatch calls.


> So in Pascal the best you can do is write a switch function for each function pointer type (signature) and use an enumerated type or equivalent to dispatch calls.

Yeah, you would have to spell it out and you couldn't do compile-time optimizations, but you could get all the dynamic flexibility you need?

Really, what I was saying above is just that if your problem is easier to describe as a bunch of interacting "objects" (like a game or a factory sim) then use OOP, but if it's easier to describe as a set of functions and data structures then don't use OOP. :-)


Well, sort of; I mean clearly Pascal is a Turing-complete programming language, so it provides you all the flexibility you need. But if you write a switch function for each function pointer type, it means that local changes in an OO language, or in C, map to global changes in the Pascal translation of it, so I'd argue that the OO language, or C, is more expressive in a fundamentally meaningful way.

(Actually existing implementations of Pascal usually added a function pointer type of some kind, but I'm talking about the language standard.)

I think the crucial question is whether the objects have different behavior. If all the objects implement a particular operation in the same way, or in one of two or three ways, there's no advantage to OOP. If their behavior varies in an open, expandable way — like, you might have an arbitrarily large number of monsters whose movement strategy varies arbitrarily — then OOP, or function pointers, can be helpful. If their behavior needs to vary in orthogonal and arbitrarily combinable ways, OOP may not be expressive enough, and maybe you should look at an entity-component-system architecture.


That's basically what the Oberon OS messaging system did, it was a little clunky but it worked. The Gadgets widgets system is still my favorite after all these years. It was like OLE but good.

I agree, if your code doesn't need objects, there's no advantage to OOP. and ECS is like extra-OOP. :-) (I replied to your reply in the other thread, BTW.)


At least the version of the Oberon OS described in the book used the built-in Oberon support for function pointers for dynamic dispatch; were you looking at an earlier or a later version than the one in the book?


I'm talking about the Viewer message system. There's a "coarse-grained view" on pg. 46 of https://inf.ethz.ch/personal/wirth/ProjectOberon/PO.System.p...


Right, that's what I was talking about too; although the version of the book I read was the 1992 edition, this part of the system hasn't changed. It works by using the Oberon language's support for embedding function pointers ("procedure variables") within records, which is precisely what was prohibited in Pascal. You give different windows ("viewers") different behavior by giving them different Handle procedures ("WndProcs", you might say, or "overridden methods") so that, when the system ("controller") sends a message to the viewer, it can behave in a dynamically determined way.


Yes, the Handle procedures are associated with their objects by means of function pointers (for e.g. C++-style OOP), but also inside themselves they implement big ol' IF,THEN,ELSE chains to do (Smalltalk/Actor-style OOP) messaging handling.


Oh, I see what you mean — instead of having a function pointer per message type, say, stored in a vtable, the handlers for each message type are dispatched by a function, which can do things like delegation (Smalltalk doesNotUnderstand:). Piumarta's "COLA" approach is a sort of hybrid, where you dispatch with a function, but the dispatch function returns a function pointer you can then cache at the call site.

What I was saying about Pascal is that instead you'd have to have a function that dispatched on the receiver type, not the message type. I thought you were saying that the Oberon OS was doing that, even though the Oberon language supported function pointers.


Yeah, cheers! ^_^ (It took me years to notice that.)


You may still have the same data and functions, but you don't have the same number of case/switch statements. ;)


I came across a random answer on quora as to why classes are bad (as opposed to structs). It gets into the very nature of program design and organization.

https://www.quora.com/Is-senior-full-stack-engineer-Ilya-Suz...

Be sure to read the comments, it's pretty interesting.


The only classes that really make sense are ones that model something in the real world to some extent. These classes are incompatible with the single responsibility principle however and are considered/lead to bad designs with a lot of coupling. Most of my classes get instantiated very few times as well, so the blueprint concept is only sometimes applicable. Inheritance, one of the three pillars, is a trap.

The whole OOP framework is a bit muddled and to make it work you end up in this web of contradictions. You suspect there is a better way but until you find it, you use Java or C# because that’s what your company/tools use


...so when do classes make sense then?


Polyymorphic implementations are nice - Eg, the State pattern, or using interfaces to abstract out a server during development. Command pattern. Things you have a lot of. It’s not the entire app though, just subsets of it


The biggest upside of programming with objects/classes is dependency injection.

And no, I don't mean DI frameworks like Spring.

I mean simply the dynamic (i.e. runtime) composition of program behavior, typically by passing collaborating objects into the constructor. (Functional programming languages have something similar with higher-order functions.)

Contrary to prior claims, objects aren't great at representing real world entities: to do that you would also need to add a notion of time and concurrency to those objects (e.g. actors, processes, services). However, they are a great way to represent the components and capabilities of such entities.


The fugly state in objects is tricky. One way to look at a class is to treat it as a kind of coroutine. Consider some pseudocode:

    class Stats
       sum Num = 0
       sum_squared Num = 0
       count Num = 0

       acceptNum(x)
          sum += x
          sum_squared += x ^ 2
          count += 1

       mean()
          return sum / count

       variance()
          # I think this is right...
          return (sum_squared / count) - (sum / count) ^ 2
But that could just as well be:

    coro Stats
        sum = sum_squared = count = 0
        result = nothing
        loop
            msg = yield result
            switch msg.name
            case acceptNum
                sum += msg.x
                ...
            case mean
                result = sum / count
            ... 
We don't do objects like this. We really want a file object, for instance, to be something like:

    coro FileReader
        # Not opened yet
        raw_handle = popen(...)
        # Reading
        ...
        if msg.name == read
            yield read(raw_handle)
        ...
        # Data exhausted
        ...
        if msg.name == read
            yield eof
        ...
        # Handle closure
        ...
        if msg.name == read
            throw ReadOnClosedFile
        ...
(I think you could handle inheritance with a scheme like this, presumably a "subclass" would simply yield to the "superclass".)

What's nice about that is the state is explicit in the structure of the coroutine. Ideally, we'd have some sort of protocol types that reflect that we're accepting different kinds of messages at different stages in an object lifecycle. In this example, after a file closes, it's a type error to try and read from it.

Instead, we generally munge it with properties, depending on lots of checks instead of structure.

We can solve state through more classes, of course. The Builder pattern handles a specific case of this quite nicely, you have one mutable construct that then returns an immutable construct.

But to use that more universally would lead to a profusion of classes.


I like classes. But you must understand your code, even before you write it. If the syntax that you use (the author is using TS, and JS/TS classes are syntactic sugar) matches your mental model of the program then I would say that you are choosing the correct syntax.

If it’s your library and your responsibility then I will try to read it your way. Comprehensive testing and consistency in style are more important than any specific syntax or abstraction.


I think I understand what the author means.

When writing Java code I prefer to use public members in my data classes and only use getters/setters sparingly. Also, I prefer to have the logic in until classes if I can. I suppose I'm not a huge OOP fan.

Of course I have to follow whatever patterns my teammates follow and hardly anyone writes Java like that... But my personal projects I think have much cleaner and easier to understand code with this approach.


Classes are everywhere, but are used in different ways or hidden.

There are types of classes that are hidden and cannot be re-instantiated.

To me, those are modules and the main execution environment.

Whenever i've been caught out when coding its always one or both of these things, so i always wrap these in a class otherwise when it comes to reusing something multiple times you're caught short.

So, i always use classes for base functionality.

But i don't throw them around like sugar candy.


I also tend to avoid writing many classes (in Python). Jack Diederich has a great video called Stop Writing Classes:

https://www.youtube.com/watch?v=o9pEzgHorH0

It's about Python, but the observation can apply to several languages. Pithy quote:

"I hate code and I want as little of it as possible in our product."


(Apologies to the Flaming Lips, and I think the pronunciation shift you have to do at the end makes it funnier)

I know a girl who

wrote some code

she'll make a linked list

she'll add some nodes

But she don't use classes

and she don't use C

she don't use Haskell

or any of these

she uses assembly

assembly

assembly


The first object oriented programming language I learned (LambdaMOO) didn't have classes. It's entirely conceivable to do nice clean object oriented code using a system which has only prototypes. In fact it encourages one to think about the important parts: encapsulation and delegation without becoming obsessed with classification.



They’re using TypeScript. Ts classes are horrible: you have to explicitly use “this.” to access anything in the class - it’s as good as it was possible to make it with the JS legacy, but it just doesn’t work very well.

Using namespaces with state passed to the relevant function ends up being basically just as easy, and sometimes easier as you end up using more closures and can access enclosed variables directly.

Classes in languages designed for OO work a lot better.


I don't either. Almost always an arrow function (or just regular old function) does what I need. Plus, React Hooks!


For a small to medium sized codebase, functions and structs are simple, clear and don't obscure state.


I use classes a lot in typescript, but not for OOP. Just glorified objects with a constructor and getters.


I once tried to drive a nail with a socket wrench and it went poorly. I have since cleared my garage of all socket wrenches because they are clearly useless.


It's 2020. You're preaching to the choir on this.

I myself use classes just fine. I love CLOS and I'm having a lot of fun with OOP on Forth as of late.


Interfaces are good though.


classes are OOP. FP doesn't need them. The end.


A whole article and no code examples. Fail.


Even if he did present code examples I doubt it would change your mind.

I think OOP people should be aware that there is significant backlash against OOP among smart people who are very familiar with the paradigm and they should take the time to understand why.

The division is real and given the invention of languages like GO and RUST that do not have classes I would think really hard about the topic.


GO is simple decomposition of OOP, class was split into structure (with composition) and interface(with multiple inheritance without implicit definition what interfaces can can coop with structure). Go only restricted that you cannot define virtual methods.


Rob Pike designed Go with the philosophy for people not to use structs like classes. This is intentional and deliberate.

If you are using GO like you use OO, you've missed the entire point.

https://twitter.com/rob_pike/status/942528032887029760


Please read carefully what I've written. I've never said that struct == class. Secondly Rop Pike wrote under tweet: `But the more important idea is the separation of concept: data and behavior are two distinct concepts in Go, not conflated into a single notion of "class".` which only proves what I presented in first comment.


Please carefully read what I wrote. I never claimed you said struct == class.

You literally said go removed virtual methods. This is not what pike is talking about. Pike is talking about putting a function onto your struct. This is legal in go as functions are first class but rob is saying it should be done sparingly because data and behavior need to be distinct concepts.

It is not me who failed to parse your writing, but you who failed to understand pike and my comment.


I was thinking the same. His arguments seem thin at best and suggest a misunderstanding of what OOP really means.


I would argue that a heavy practitioner of OOP, and one who does not see the extreme downsides of using that paradigm is actually the person who misunderstands Programming in general and what OOP is in that context.

Say if you practiced procedural programming in a very heavy way and typed functional programming in a major way along with OOP then you would have a better picture.

If you practiced OOP exclusively for the majority of your career I would argue your view point is flawed and biased. Over the years, as a programmer with a lot of heavy experience in all of the paradigms mentioned above, I have found that OOP is in fact not an apples and oranges comparison to other programming styles but is in fact a highly flawed paradigm.


> I Don't Use Classes

OK, cool.


classes only encapsulate context. nothing else. you don't have to use them if you don't want to.


Yes, I never use classes, only data and pipes.

[The Pure Function Pipeline Data Flow v3.0 with Warehouse / Workshop Model](https://github.com/linpengcheng/PurefunctionPipelineDataflow)

1. Perfectly defeat other messy and complex software engineering methodologies in a simple and unified way.

2. Realize the unification of software and hardware on the logical model.

3. Achieve a leap in software production theory from the era of manual workshops to the era of standardized production in large industries.

4. It's the basics and the only way to `Software Design/Develop Automation (SDA)`, SDA is an innovative and revolutionary approach to develop large-scale software, just like `Electronic Design Automation (EDA)`, because it systematically simulates an integrated circuit system.


Sounds like an idiot with opinions.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: