DEV Community

loading...

You don't need classes

lukeshiru profile image ▲ LUKE知る Updated on ・3 min read

A few years back, JavaScript added a long awaited feature to the language: Classes. Developers coming from other languages with the classic object oriented paradigm where happy to find their old friend in JS, even if that old friend is just syntax sugar. So first, let's take a look at how classes actually work, and then we can tackle why you don't need them.

Sugar, oh honey honey

Let's just write the classic Shape class:

class Shape {
  constructor({ name = "shape", x, y }) {
    this.name = name;
    this.x = x;
    this.y = y;
  }
  move({ x, y }) {
    this.x = x;
    this.y = y;
  }
}

class Circle extends Shape {
  constructor({ name = "circle", x, y, radius }) {
    super({ name, x, y });
    this.radius = radius;
  }
}

const circle = new Circle({ x: 10, y: 10, radius: 5 });
circle.move({ x: 20, y: 20 });
Enter fullscreen mode Exit fullscreen mode

This is just syntax sugar for functions and prototypes:

function Shape({ name = "shape", x, y }) {
  this.name = name;
  this.x = x;
  this.y = y;
}

Shape.prototype.move = function ({ x, y }) {
  this.x = x;
  this.y = y;
};

function Circle({ name = "circle", x, y, radius }) {
  Shape.call(this, { name, x, y });
  this.radius = radius;
}

Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;

const circle = new Circle({ x: 10, y: 10, radius: 5 });
circle.move({ x: 20, y: 20 });
Enter fullscreen mode Exit fullscreen mode

Basically, when you're using classes, you're already using functions, just with extra complexity on top. This might sound great for someone coming from an object oriented language, but if you think about it, now you need to deal with this, you need to use new when creating a new instance of that class, and so on.

Clean instead of classy

Instead of trying to think everything as a class first, try to think about stuff as just values and process. So the same Shape example can be something like this:

const createShape = ({ name = "shape", x, y }) => ({ name, x, y });
const moveShape = ({ x, y }) => shape => ({ ...shape, x, y });
const createCircle = ({ name = "circle", x, y, radius }) => ({
  ...createShape({ name, x, y }),
  radius
});

const circle = createCircle({ x: 10, y: 10, radius: 5 });
moveShape({ x: 20, y: 20 })(circle);
Enter fullscreen mode Exit fullscreen mode

With this approach we have a few advantages over the classes one:

  1. We don't have to think about this, because we don't use it at all.
  2. We don't need to use new, we just call functions that return values.
  3. We don't worry about mutations, because we never change values, we just take a value and return a new one. This is quite important when testing because state becomes predictable.

Do you actually need classes?

Now think about it for a minute: Do you really need classes at all, or are you just used to them? Before working in WebDev, I was a fan of C++, so naturally I loved classes, but as time went by I realize that every problem that I used to solve with a class, has a cleaner and simpler solution just using functions.

Take a look at your code in the places you used classes, and try to think how you'll do that with just functions, and tell me if that solution isn't just better.

Either way, thanks for reading this and if you don't agree with something said in here, just leave a comment and we can discuss it further.

See you in the next post of this series!

Discussion (67)

pic
Editor guide
Collapse
michi profile image
Michael Z

It's more about code organisation more than anything else for me. I rather have one class be responsible for something than 10 loose functions all transforming a piece of data slightly differently.

One use case I like to use classes for is ORMs like in Adonisjs:

const user = await User.find(userId)
user.name = 'new name'
await user.save() // user.delete(), etc.

// Or

const user = new User
user.name = 'new name'
await user.save()

// Or

await user.projects().create({ name: 'new project' })
Enter fullscreen mode Exit fullscreen mode

All the above just reads very natural to me, the API is very pleasing and simple.

But I agree that they are not needed in many cases.

Collapse
lukeshiru profile image
▲ LUKE知る Author

That logic could look something like this in FP:

const findUser = find("user");

findUser({ id: userId })
    .then(update({ name: "new name" }))
    .then(save);

// or

create("user")({ name: "new name" }).then(save);

// or

findUser({ id: userId })
    .then(append("projects")({ name: "new project" }))
    .then(save);
Enter fullscreen mode Exit fullscreen mode

Or if you prefer the more verbose async/await sugar:

const user = findUser({ id: userId });
const updatedUser = await update({ name: "new name" })(user);
await save(updatedUser);

// or

const newUser = await create("user")({ name: "new name" });
await save(newUser);

// or

const user = await findUser({ id: userId });
const updatedUser = await append("projects")({ name: "new project" })(user);
await save(updatedUser);
Enter fullscreen mode Exit fullscreen mode

No need for new, no mutations, and lots of reuse and composition.

I understand your point: If you're using a framework that has a class based API, it makes sense. But the main point of this article is that you don't need them for your code.

Collapse
milabron profile image
milabron

I use classes, and I like it better "currently ",
I have never had a problem with mutations.
That superior abtrastion is what I like the most since I'm embracing a shape class with everything it has to do inside, with functional programming it's a bit more messy.

What is the problem with the new and this operator?, does it slow down the software?

Thread Thread
milabron profile image
milabron

Any examples with the problem of mutation please?

Thread Thread
milabron profile image
milabron

I can also tell you that all the projects I have seen with functional programming are chaotic, instead object-oriented projects are better ordered and more extensible thanks to the interfaces and the inheritance. Why? I don't know, I hope you can answer me. Although I could tell you with object-oriented programming it almost forces you to have an order, while with FP it doesn't.

Thread Thread
lukeshiru profile image
▲ LUKE知る Author

I'll go one by one, so bare with me:

I have never had a problem with mutations.

You're super lucky, then. One of the main sources of errors in JS is mutations, which make state unpredictable. If you never change anything, you just create new stuff, then you never end up in scenarios in which the state has a value you don't expect.

That superior abstraction is what I like the most [...]

What's more "abstract" than functions? Classes are already "classifying" stuff, while functions are just transformations of data. Functions can be used directly, or used inside other functions, or used as methods of objects, and so on. The only way of achieving something similar with classes is by using static methods or extending a class with other class (which can be only one), so way less flexible/abstract.

What is the problem with the new and this operator?

There is nothing wrong directly with them (if we don't take into account that "what's this?" is actually a super common problem). But not having to use them, or any of the complexities they bring is just great to make code more simple.

Any examples with the problem of mutation please?

Again, this is kinda common, but basically is accessing something that maybe changed. Without mutations maintenance, testing, reuse, composition and so on is far easier. Instead of modifying state, you pass it around and return something new.

I can also tell you that all the projects I have seen with functional programming are chaotic, instead object-oriented projects are better ordered and more extensible thanks to the interfaces and the inheritance. Why?

Basically you have the answer in your own question. It isn't related to the actual paradigm being used, but to the fact that one project was organized and the other wasn't. A class based codebase can go bad quite easily without organization, there's is a reason behind the classic line about classic inheritance:

"You wanted a banana but what you got was a gorilla holding the banana and the entire jungle"

That line basically means that every time you extend something, it comes with lots of methods that you might no need, which introduces extra complexity you wouldn't had with just a function. This happens more often than not in OOP.

Collapse
squidbe profile image
squidbe

After reading your article, I came to the comment section to make a comment about FP, then saw that you referenced it here, so I'll just mention here that it's odd to me that your article makes no reference to functional programming. I don't think the #functional tag is enough. It'd add clarity if you at least made one overt reference to the fact that you're advocating for an FP approach (which I generally agree with).

Thread Thread
lukeshiru profile image
▲ LUKE知る Author

I want to approach this line of articles as just an "optimization" to make code simpler, not a paradigm switch. Still, this optimizations are based on the FP paradigm, so next 2 articles will be about getting rid of if/for/while, and getting rid of variables (immutability).

Thread Thread
squidbe profile image
squidbe

I understand your desire, but this is definitely more than just an optimization. Just trying to help you communicate with your audience better.

The title could use a tweak, too, because I initially thought the article was gonna be about avoiding taking online classes for JavaScript . 😀

Collapse
jwhenry3 profile image
Justin Henry

Classes are useful when you have interfaces, models, and services that can be swapped around using a dependency injector, but in a system where you just call functions, classes don’t have a place. Once I started diving into React, classes started disappearing from my day-to-day, even in my Angular work. If you work in both states of mind, you start to see their benefits and when to use them, rather than just using them because they are there.

Collapse
lukeshiru profile image
▲ LUKE知る Author

100% agree! Had a similar experience myself. When you're working with a framework that makes use of decorators and classes like Angular, it kinda makes sense to use classes (not always, tho). But when using more "close to vanilla" libraries, classes are an overkill.

Collapse
thewix profile image
TheWix

Dependency Injection and polymorphism aren't restricted to classes. You can do the same with functions. A constructor that you inject a dependency into is just a function after all.

Collapse
jwhenry3 profile image
Justin Henry

Very true. Typically the project will determine what gets used the most and how it is utilized. Angular is heavily Class driven whereas React is heavily Function driven. It's just all about style and convention and what makes the most sense at the time.

Thread Thread
thewix profile image
TheWix

Agreed, if you are in a codebase that is using OO then it makes the most sense to keep that style.

Collapse
josef37 profile image
Josef Wittmann

There was a talk by Robert C. Martin (Uncle Bob) once about SOLID, where he started to explain, what advantages OOP has over the "old way".

And his main point was that classes make polymorphism so much simpler that it enables you to use dependency injection - which in turn does not make your top-level code depend on all details (aka inversion of control).

But most JS code I write and read does not use that at all, so classes are not that useful in these cases.

Collapse
jcubic profile image
Jakub T. Jankiewicz

Sometimes classes or prototype based code is cleaner and easier to understand. Also with your function based approach you loose identity, you no longer can check what type the object is using instanceof. But it doesn't mean that you always should use classes, as Okham Razor says you should not create entities if you don't need to.

There is general rule that everyone should follow, do whatever make your code easier to understand. Classes like everything can help with that or do the opposite of you overuse them.

Collapse
michaelcurrin profile image
Michael

As the post author has said, functions are more reusable because you pass data structures to them. A function is not bound to a class.

To solve the issue of knowing if it safe to pass a data structure to a function, you can use types or interfaces in TypeScript. Vanilla JavaScript will let you call a function with too many arguments or not enough arguments (which become undefined) and to won't get an error. TypeScript let's you define the structure of inputs, say a string, a number and an array of strings and a tuple with 3 elements and they are required unless you mark them as optional with ?. You might define that once as a type or interface which you reuse across all your functions which must follow that input signature. You can even create interfaces from interfaces

Some functional programs like Haskell need types while adding it with TypeScript is optional but useful.

Collapse
michaelcurrin profile image
Michael

Here is an example of what I mean. Enforcing that parameter bar must have a key bazz using a reusable interface.

interface Bar {
  bazz: string
}

function foo(bar: Bar) {
  console.log(bar.bazz)
}
Enter fullscreen mode Exit fullscreen mode

I have snippets of interfaces and type aliases here for interest, based on the TS docs and my experience

michaelcurrin.github.io/dev-cheats...

Thread Thread
lukeshiru profile image
▲ LUKE知る Author

Yup, I use that quite a lot. I prefer type instead of interface, but just because I like more that syntax. Either way is the same :D

type Bar = {
  bazz: string
};

const foo = ({ bazz }: Bar) => console.log(bazz);
Enter fullscreen mode Exit fullscreen mode

Also, in the topic of JSDocs, you can use them to define interfaces as well, and they work with TS inferance:

/**
 * @typedef Bar
 * @property {string} bazz
 */

/**
 * @param {Bar} bar
 */
const foo = (bar: Bar) => console.log(bar.bazz);
Enter fullscreen mode Exit fullscreen mode
Collapse
lukeshiru profile image
▲ LUKE知る Author

I indeed love TS. Even if you don't use it directly and just type trough JSDocs, TS is great to take care of the typing side of JS <3

Thread Thread
michaelcurrin profile image
Michael

Are you referring to something like this? Does it give hints in the IDE?

/*
 * @param foo string
 */
Enter fullscreen mode Exit fullscreen mode
Thread Thread
lukeshiru profile image
▲ LUKE知る Author

You can do this:

/**
 * @param {string} user
 */
const hello = user => `Hello, ${user}!`;
Enter fullscreen mode Exit fullscreen mode

And TS infers the types as you were doing this:

const hello = (user: string) => `Hello, ${user}!`;
Enter fullscreen mode Exit fullscreen mode

So even if you don't like TS, you can benefit from it :D

Collapse
lukeshiru profile image
▲ LUKE知る Author

You're right, you loose instanceof, but you also no longer need it if you are using the functional approach correctly. The lack of "identity" makes your code far more flexible (reusable and composable), which is preferable to the alternative of having everything "trapped" inside classes.

Collapse
jcubic profile image
Jakub T. Jankiewicz

I agree that functional programming is create you make code composible with functions. But this is not the way you should write code in JavaScript. If you came from Haskell you will think that everything should be pure function and if you came from Java you think that everything should be a class. I've seen examples on twitter then someone wrote single class in JS just to define a function without any state, it was pure function.

Writing everything with functions or everything with classes is bad idea. Also your example of Clean is not every easy to understand, so I prefer classes for objects and using functions when I can benefit of them, not using function everywhere just because I want to have everything in functional style.

Thread Thread
lukeshiru profile image
▲ LUKE知る Author

I understand your point, but the thing is: When do you really need classes? In which scenarios are classes more useful/clear than functions?

I mean I know that people say "use composition when you have a has-a relationship, and inheritance when you have an is-a relationship", but one could argue that when something has an is-a relationship, also has a has-a relationship, so the inheritance is not even needed.

Taking the same example in the post: Circle is-a Shape, so we use inheritance, right? Not quite. We could read that as Circle has a position and a name, so we don't need inheritance at all. This same principle can be applied to pretty much any class based architecture.

The main point of this article is that you can achieve the same stuff you can do with classes, without them. So if we forget about them we have a cleaner code, and one less thing to worry about.

The next posts in this series will be about how we can forget about other stuff, like flow control and even variables :D

Thread Thread
jcubic profile image
Jakub T. Jankiewicz

Yes you also don't need named functions and numbers, you can write everything with just anonymous functions (like lambda calculus). Check this article: Programming with Nothing, here is the code in JavaScript.

The reason why the code is simple is not because you use less of language features, but because you use features that are known by other person that read the code that also know that language.

This is not good way to go, your code will be less and less understandable by other programmers, but I will probably never convince you.

Thread Thread
lukeshiru profile image
▲ LUKE知る Author

But you the idea with a good discussion is not to convince the other person, is to share knowledge so both parts end up more knowledgeable. If you provide information based on logic, and that logic is good enough, my point of view could shift in your direction.

You now added that "that is not good because is harder for other programmers", and I agree... but if you really think about it, is hard because they need to "unlearn" stuff that was taught to them as indispensable, and if they use less features of the language to achieve the same thing, they are basically using just english to express themselves.

You also mentioned "lambda calculus" and not needing named functions, and I don't agree with that. You need to have good names for your functions, constants and attributes, because you're using those to express the intent of your code, and without them then the code is unreadable. Haskell is a classic FP language, and I despise how they have super short names for functions, and 1 letter names for attributes. Is like they really don't want people to understand:

foldr :: (a -> b -> b) -> b -> [a] -> b
Enter fullscreen mode Exit fullscreen mode

vs

type reduceRight = <Item, Output = readonly Item[]>(
    reducer: Reducer<Item, Output>
) => (initialValue: Output) => (array: readonly Item[]) => Output;
Enter fullscreen mode Exit fullscreen mode

A Haskell fanboy would be like: "but .... but ... you wrote more there!" ... and that's true, but I did that once, and then when I use that function I have great documentation in the IDE itself while using it, instead of getting a, b, and so on.

TL;DR: The idea with this posts and the ones that will follow is not to suggest we should move from a programmer model to a mathematical one as Haskell folks would suggest, but to move from a programmer exclusive model, to one that is closer to plain english (easier to learn, easier to teach and easier to think about).

Collapse
alexgwartney profile image
Alex Gwartney

I feel like classes in es6 makes things a lot easier to deal with. As while you can use proto for me using oop syntax just makes it easier to structure your code. I think it just comes down to preference, because I was actually discussing something similar with people that use typescript to add that addition to the language.

Collapse
lukeshiru profile image
▲ LUKE知る Author

I use TypeScript daily and I love it, is my go to language nowadays. The point with the comparison show in the post is that classes are still functions internally, so no added benefit besides expressing stuff in a cleaner way. Now compare the first example (Classes) with the last one (functions+composition). That last one is far better and it doesn't use classes nor prototype, just functions and values.

Collapse
alfredosalzillo profile image
Alfredo Salzillo

I agree with you. I think classes only make things more complicated. JavaScript now can, and should be used without classes.

Collapse
lukeshiru profile image
▲ LUKE知る Author

The best feature ES6 added from my point of view was arrow functions. It made expressing functions a breeze, and curried functions are glorious. We went from this:

var add = function (value1) {
  return function (value2) {
    return value1 + value2;
  }
}
Enter fullscreen mode Exit fullscreen mode

To this:

const add = value1 => value2 => value1 + value2;
Enter fullscreen mode Exit fullscreen mode
Collapse
russellabraham profile image
Russell

Some good points, I usually opt for writing functions and implementing a prototype chain to emulate class behavior. It's just more compatible, however, I think you could have improved your explanation by demonstrating what is going on with the javascript engine behind the scenes and how to take care of the constructor as well as __proto__.

The javascript engine, when you invoke a method on a particular object or even any property it could be a name or a function, it could be a data property or a function property. It will first check if that property exists on that object directly, if it doesn't exist it will check if that exists on the object's prototype.

If it doesn't exist on that object's prototype it can check if it exists on its parent and prototype and so on it will go on until the parent object which is the in-built object type and the inbuilt object type prototype property is set to null.

That is where it will stop searching and it will return undefined if it can't find the property through the chain.

So prototypes are a core feature of the language.

With a more complex code base it could become easy to lose track of these things.
Luckily you can apply arguments in your chain to simulate the constructor for classes, and also change enumerability, configurability and writability.

function Doctor(name){
    this.name = name;
}

Doctor.prototype.treat = function(){
    return "treated";
};

Doctor.prototype.toString = function(){
    return "[Doctor "+this.name +"]";
};

function Surgeon(name,type){
    Doctor.apply(this,arguments);
    this.name = name;
    this.type = type;
    this.initialize.apply(this, arguments);
}

Surgeon.prototype = Object.create(Doctor.prototype,{
    constructor: {
        configurable : true,
        enumerable: true,
        value:Surgeon,
        writable:true
    }
});

Surgeon.prototype.treat = function(){
    return Doctor.prototype.treat.call(this)+ " operated";
};

Surgeon.prototype.toString = function(){
    return "[Surgeon "+this.name +" type "+this.type +"]";
};

Surgeon.prototype.initialize = function(){
    console.log(this);  
};

var doctor = new Doctor("John");
var surgeon = new Surgeon("Bob","Dental");

console.log(doctor.treat());
console.log(surgeon.treat());

console.log(doctor.toString());
console.log(surgeon.toString());

console.log(doctor instanceof Doctor);
console.log(doctor instanceof Object);

console.log(surgeon instanceof Doctor);
console.log(surgeon instanceof Surgeon);
console.log(surgeon instanceof Object);
Enter fullscreen mode Exit fullscreen mode

The class syntax takes care of the properties that take a lot of code to setup right, but I agree they are not totally necessary.

Collapse
lukeshiru profile image
▲ LUKE知る Author

The main idea with the post is that when we use classes, we are actually using functions with extra complexity (like the prototype chain, this, new and so on), so we can just avoid using classes and prototype altogether, and just use functions.

The code you wrote as an example is just a class at heart, using prototypes. Imagine your code without the complexities of classes in it.

Collapse
johnkazer profile image
John Kazer

I think classes might have a role once your code is mature and the structure and relationships between functions is clear. Maybe they can more clearly then highlight these relationships. But don't seem essential.

Collapse
lukeshiru profile image
▲ LUKE知る Author

Everything that can be expressed with classes, can be expressed without them in a cleaner way. You just use composition instead of inheritance, so if you want to have stuff together, you can just group them in an object, but you never run into a pretty common scenario in OOP in which you wanted a banana, and you ended up with a gorilla holding a banana and the entire jungle.

Collapse
eecolor profile image
EECOLOR

In my opinion there is only a few reasons to use a 'class like' construction:

  • when different functions receive the same arguments
  • when you have moving parts and want to hide them

If you find you are calling a set of functions and are passing them the same arguments you will find that there is some form of concept hiding. And that this concept can probably be given a name. You can see this as a convenience to 'partially apply' multiple functions at once.

function MyConcept(sharedArguments) {
  ...
  return {
    function1(inputA) {
      ...
    },
    function2(inputB) {
      ...
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

If you are tracking some user behavior:

function MeaureUserSpeed() {
  let previousPosition = ...
  let previousMeasureTim = ...

  ...

  return {
    get speed() { ... }
  }
}
Enter fullscreen mode Exit fullscreen mode

Note that I am not using a class here, calling the method (MyIdea(...)) and using new (new MyIdea(...)) has the same effect: return a bunch of function that are 'partially' applied and can share state that can actually be hidden.

In my mind there is one valid reason to use actual classes (implemented with prototype): performance. This however is only a valid reason when you have 1000s of objects that share something. I have not yet seen code that requires this and can only imagine situations with some form of scene rendering that would qualify.

Collapse
abnt713 profile image
Alison Bento

Thank you very much for your post! Here is what I think: Classes are made for "humans", more specifically for domain modelling. Using an OOP approach, you can give data structures meaning and, if you will, identity. You can express metaphors and behaviors that are somewhat "natural" to humans, as it is easier to understand the relationship between two living beings than two mathematical sets. The FP approach, on the other hand, is more barebones and skips these human level abstractions. Handling data purely as FP does is a huge win in terms of avoiding side effects and simplifying state transformations, since everything is just surface level - gotta love the purity of the functional approach, I certainly do.

I don't think OOP and FP are mutually exclusives. Using classes to express high level abstractions and domain related entities can help programmers to understand what the previous programmer meant with their code back then. Also, talking to non-tech stakeholders should be easier in terms of entities and domain model. Now, since classes abstracts their implementations... What about using FP? Since functions are way more composable and context free than classes, calling and composing functions inside a class is as easy as doing it somewhere else.

I agree, using classes is not necessary. You can pretty much describe everything a class does using only functions, maybe even more. But I find it harder describing or explaining a domain model using only functions.

Collapse
lukeshiru profile image
▲ LUKE知る Author

Thanks for your comment, @abnt713 . I just want to clarify something that might not be super clear from the post using part of your comment as a base:

It is easier to understand the relationship between two living beings than two mathematical sets.

I completely agree. I just want to make clear that, as I said in another comment, I don't like the "haskell mindset" of thinking everything mathematically. The point with this post (and the ones coming in this series) is to show that we can express the same things we could do with classes, without them, in a cleaner, composable, more reusable and more testable way.

So my position is like a "middle ground" between FP and OOP, in which we do everything using functions, and avoiding mutations and flow control, but without sacrificing the legibility of our code. So I don't want something like this:

foldr :: (a -> b -> b) -> b -> [a] -> b
Enter fullscreen mode Exit fullscreen mode

I prefer something like this:

const reduceRight = reducer => initialValue => array =>
    array.reduceRight(
        (accumulator, item) => reducer(item)(accumulator),
        initialValue
    );
Enter fullscreen mode Exit fullscreen mode

Which is longer, yes, but provides an excellent developer experience in our IDEs when paired with TS, is readable and easier to understand than that Haskell code and is still functional. My first post in dev.to was about the importance of naming, which basically pointed out that we need to use good names for our functions, atributtes, constants and so on.

TL;DR: We can get rid of classes, and still have readable code without changing our mindset to a mathematical one.

Collapse
ezio1404 profile image
EJ Anton Sabal Potot

This is a good read Mr.Luke. TLDR; use functional programming if you want to mess up your code.

Collapse
thewix profile image
TheWix

Oof, couldn't disagree more.

Collapse
lukeshiru profile image
▲ LUKE知る Author

Care to elaborate why having composable pieces of code are a mess from your point of view?

Collapse
valeriavg profile image
Valeria

Actually that's a super valid point, thank you for the article. I've been struggling with pet SQL query builder as with classes it didn't feel right. I was either forced to use static functions or assign values to the object.
And functions are so much more test friendly!
Thanks again!

Collapse
simplifycomplexity profile image
Kiran Randhawa

I think the problem here is that you'll possibly lose type safety. How can you be 100% if you're interacting with a shape later down the line?

Type safety offers additional runtime checking but but not using classes you're losing that right?

Collapse
lukeshiru profile image
▲ LUKE知る Author

Type safety can be achieved trough JSDocs, TypeScript or Flow. You shouldn't need to check types at runtime, and even if you do, there are other ways to check types that don't rely on stuff like instanceof.

Getting rid of classes doesn't mean you're getting rid of types (I love TS, I would never suggest that), it just means you can achieve the same things with just functions. You can't do that the other way around without using stuff like decorators, meaning adding even more complexity in top of all the ones coming from classic inheritance.

Collapse
simplifycomplexity profile image
Kiran Randhawa

Sorry did I say runtime doh?! I meant compile time. But still doesn't typescript promote classes? Ooooo now I think about it you could type an object using the "as" keyword. Is that what you're referring to?

Thread Thread
lukeshiru profile image
▲ LUKE知る Author

You don't need classes to type. You can use the interface or type keyword to create types in TS, or @typedef in JSDocs, and then used them without ever using classes. You generally don't need to use the as keyword to achieve type safety, just when the type inference is not enough.

Collapse
klvenky profile image
venkatesh k

Nice article Luke. That's a pretty neat explanation of how functions can be used instead of classes.
I kind of have a slightly different perspective over fp vs oop. I would say what makes your team better is the correct approach. Because, as you rightly mentioned I am someone who came from java, so I like classes better.

Also, I would say class might be much better suited when we are trying to ensure consistent code structure if the team is not very experienced & has little knowledge prototypes in javascript. I do agree to the point that people need to have that knowledge. However the approach that allows my team to be faster, better & write maintainable code is my choice. It's purely relative to the experience, level of comfort of the team.

Just shelling out my perspective.
Cheers

Collapse
rsvp profile image
Аqua 🜉іtæ ℠

You don't need static, do you?

Collapse
lukeshiru profile image
▲ LUKE知る Author

When you don't use classes, you can forget about complexities like static/protected/private/public, classic inheritance, and so on. So yup, I don't need static.

Collapse
rsvp profile image
Аqua 🜉іtæ ℠

but.. nowadays JS, especially that one on the dark side of a Node's backend do need. You are welcome to have variable-but-constant mix, as well as mix-ins, getters and setters and so on! BTH, nice to meet reasonable people for free.

Collapse
zoedreams profile image
☮️✝️☪️🕉☸️✡️☯️

classes are a good way to encapsulate a set of functions within your heal memory. Under the hood when you use class its like using .prototype however its shorthand and a bit cleaner.

By using classes within an object oriented language, you are able to maintain the code easier and better. It does the mostly by making the code much easier to read. Everything in JS is a function, including objects, symbols, maps, and classes for example.

Back in the days of <ES4 language the style of coding was very similar to your .prototype example code. for a simple one file module i would probably not use classes, I would use all functions and prototype to wrap the stuff up. Another key point to note here is that .prototype stuff works more like struct's and processes fast for things like three js or games that need to run at a very high frequency.

Collapse
lukeshiru profile image
▲ LUKE知る Author

The example using prototype is just there to illustrate that the classes are still just functions in JavaScript. My argument in the post is against classes in JS, mainly because if internally you're still using functions, you can just use them directly enabling better reuse, composition, testing and so on.
You can use modules to encapsulate, if you really want to have an object with methods you can create one putting several functions together, but you can't go the other way around (reuse or compose classes). You'll end up with the classic inheritance problem of wanting a banana and getting a gorilla holding that banana and the entire jungle.

Collapse
gdledsan profile image
Mundo

Well, I agree you don't need them for anytjing front end, which is where javascriot should have stayed.

Since JS began being a "general purpose" language, you do need eays to organize your code, and prototypes are just ugly.

Collapse
lukeshiru profile image
▲ LUKE知る Author

I agree, prototypes are disgusting. If you read the article, you'll notice that I don't suggest we should use prototypes, what I said is that we shouldn't be using classes or prototypes, but actually just use functions.

Any language that has first-class functions can be used without classes anywhere (no matter if it's backend or frontend). Node (and Deno) enable JS in the back-end, so you can have both a classless frontend and a classless backend and even reuse utils in both with ease 😀

Collapse
nitsanavni profile image
Nitsan Avni

in this function:

const moveShape = ({ x, y }) => shape => ({ ...shape, x, y });
Enter fullscreen mode Exit fullscreen mode

why do we provide { x, y } first and then shape, and not the other way around?

Collapse
rommguy profile image
Guy Romm

Because the common use of such functions is to use the curried result as an argument to another function

So you would do this -
const moveByFive = moveShape({x: 5, y: 5})

Then you would do some operations on a shape or a collection of shapes

flow(
filter(shape => shape.x < 10),
moveByFive,
map(transformShape)
)(shapes)

Collapse
lukeshiru profile image
▲ LUKE知る Author

Thanks a lot @rommguy for taking the time to explain that. Great explanation btw! :D

Collapse
samuelfaure profile image
Samuel FAURE

Proposition to rename this article: "You don't need classes in Javascript"

Collapse
lukeshiru profile image
▲ LUKE知る Author

It is in the #javascript topic 😅 ... and if I had to rename it to be more specific, I would say: "You don't need classes in languages that have first-class functions" 🤣

Collapse
aleaallee profile image
Alejandro esquivel

Classes are easier to understand compared to your example. I know es6 syntax but it took me a while to try and understand what did your example do.

Collapse
lukeshiru profile image
▲ LUKE知る Author

You say this because you already have experience with classes, but think about it: You can create more composable and reusable code, that's easier to test, by getting rid of classes. This basically means that when thinking about a problem, the "cognitive load" (the amount of things you need to know) is smaller and more powerful. Take a look at the examples on the post again, and think about them:

class Shape {
  constructor ({ name = "shape", x, y }) {
    // We need to know about `this` (and sometimes use stuff like bind/apply)
    this.name = name;
    this.x = x;
    this.y = y;
  }
  // If we have an object that also has x and y, but we don't want to have
  // a name with it, we can't reuse `Shape.prototype.move`, because is
  // "trapped" inside this class
  move({ x, y }) {
    this.x = x;
    this.y = y;
  }
}

const shape = new Shape({ x: 10, y: 10 }); // Don't forget about `new`!
Enter fullscreen mode Exit fullscreen mode

Now with a functional approach:

// No `this`, just a function that returns an object. It even has the same
// signature that the Shape constructor has)
const createShape = ({ name = "shape", x, y }) = ({ name, x, y });

// Move just takes an x and a y, and then takes an object and returns a copy of
// that object with the new x and y. No mutations. No limitations. And now no longer
// trapped in the Shape class.
const move = ({ x, y }) => source => ({ ...source, x, y });

// We can start creating new stuff from that ^
const moveToX5Y5 = move({ x: 5, y: 5 });

// And then we can use it with shapes, or anything that has x and y:
const shape = createShape({ x: 10, y: 10 });
const notAShape = { x: 20, y: 20, foo: "bar" };

moveToX5Y5(shape); // { x: 5, y: 5, name: "shape" }
moveToX5Y5(notAShape); // { x: 5, y: 5, foo: "bar" }
Enter fullscreen mode Exit fullscreen mode

Think about it for a second:

  • With the functional approach you can achieve all the same things you can with classes, but with less complexity involved (no this, no new, just functions and values).
  • You can't have it the other way around. Meaning you can't achieve all the things you can with functions, using classes.
Collapse
aleaallee profile image
Alejandro esquivel

I prefer OOP programming, your functional programming example is a mess. Functional programming is more complex than object oriented programming. There is no complexity in classes, how is "this" or "new" complex? All you have to know is the code's context.

There is no point in making the code fancy if everyone else struggles trying to understand it.

Thread Thread
lukeshiru profile image
▲ LUKE知る Author

Alejandro, you say people struggle understanding something with this shape:

const something = value => otherValue;
Enter fullscreen mode Exit fullscreen mode

More than with this:

class Something {
  constructor(value) {
    this.value = value;
  }
}
Enter fullscreen mode Exit fullscreen mode

As I previously said, your point of view based on the fact that you already know classes. I also was a OOP dev back in the day (C++ has a special place in my heart), but in languages that have first-class functions like JS, it makes more sense to just use functions. A simple example like the one in the post might seem "messy" because everything is in the same "place" to mimic OOP, but generally you have every function in their own files (yet another thing you can't achieve with a class method).

You might have to read a little bit more about context, mutations and all the problems they bring to the table compared to the functional approach (or maybe experience those problems yourself in a project). Let's not even talk about the classic class problem of wanting a banana, and ending up with a gorilla, holding a banana, and the entire jungle...

I worked with class heavy frameworks like Angular, and functional heavy libraries like React ... trust me, over time the functional approach is always better. You might also be biased by the fact that you "hate" React (I kinda understand you because I used to hate JSX until I understood what React was really about, and is not JSX).

Don't get me wrong, in both scenarios is better to work with types (with amazing tools like TypeScript), but having an entire class when you can have a plain object or a function instead, is just nonsense.

Some comments have been hidden by the post's author - find out more