top | item 22555938

I Don't Use Classes

213 points| philk10 | 6 years ago |spin.atomicobject.com | reply

288 comments

order
[+] fuzzy2|6 years ago|reply
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.

[+] dpcan|6 years ago|reply
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.
[+] INTPnerd|6 years ago|reply
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.
[+] chmod775|6 years ago|reply
> 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.

[+] xamuel|6 years ago|reply
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.
[+] ken|6 years ago|reply
> 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.

[+] mumblemumble|6 years ago|reply
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.
[+] Koshkin|6 years ago|reply
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.)
[+] tomstuart|6 years ago|reply
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

[+] CuriousSkeptic|6 years ago|reply
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?

[+] bcrosby95|6 years ago|reply
There are languages that solve the expression problem, such as Clojure. But they aren't very popular.
[+] Scarbutt|6 years ago|reply
In some languages runtime polymorphism is not tied to types.
[+] ledauphin|6 years ago|reply
OTOH, classes are just one way of doing polymorphism/dynamic dispatch. see Clojure's multimethods.
[+] sreque|6 years ago|reply
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.

[+] threatofrain|6 years ago|reply
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.
[+] mirekrusin|6 years ago|reply
Sparkles of inheritance can be good. I use them sometimes to aid functional combinators.
[+] skocznymroczny|6 years ago|reply
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(...)

[+] djsumdog|6 years ago|reply
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.
[+] bfung|6 years ago|reply
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.

[+] nickthemagicman|6 years ago|reply
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.

[+] agumonkey|6 years ago|reply
> 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

[+] LessDmesg|6 years ago|reply
By "I separated things out into classes", do you mean that you used the whole triad of inheritance + polymorphism + encapsulation?
[+] cryptonector|6 years ago|reply
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.

[+] smabie|6 years ago|reply
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.

[+] BiteCode_dev|6 years ago|reply
"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.

[+] jvanderbot|6 years ago|reply
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).

[+] rclayton|6 years ago|reply
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.
[+] bernawil|6 years ago|reply

  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.
[+] klyrs|6 years ago|reply
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

[+] randomdata|6 years ago|reply
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.

[+] chrisseaton|6 years ago|reply
> 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.

[+] remmargorp64|6 years ago|reply
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!

[+] crimsonalucard|6 years ago|reply
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.

[+] babypuncher|6 years ago|reply
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...
[+] arcosdev|6 years ago|reply
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.
[+] Vinnl|6 years ago|reply
Aren't classes in Javascript mostly syntactic sugar to make inheritance easier?
[+] cryptica|6 years ago|reply
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.

[+] peter_d_sherman|6 years ago|reply
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.

[+] lalaithion|6 years ago|reply
But Classes and Objects are not the only way to create boundaries, and may not be the best way.
[+] Vinnl|6 years ago|reply
I feel like you could replace the words classes and objects with functions, without affecting its validity.
[+] asdfman123|6 years ago|reply
Finally, a man who truly strives to live in a classless society.
[+] codr7|6 years ago|reply
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

[+] frou_dh|6 years ago|reply
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".

[+] lmilcin|6 years ago|reply
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.

[+] ch_123|6 years ago|reply
> 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?