Why I Don’t Use Classes

Recently, one of my teammates noted that our project’s codebase doesn’t have a lot of classes. The observation wasn’t framed positively or negatively. It was just an observation about a particular code style of the team as a whole.

I didn’t think much about it at the time. But looking back, this observation has lead to some good self-reflection on what I value when writing software. I didn’t write this post to persuade others to follow patterns that I tend to follow. But I’m hoping this post is a catalyst for your own self-reflection.

For context, my projects lately have been in full-stack Typescript, and what I value in writing software is highly influenced by the experiences in the Typescript ecosystem. I’m guessing there are cross-cutting concerns here that are applicable in other ecosystems, but I’m not going to claim that’s the case.

I also should note that I don’t actively avoid writing classes. I just lean toward other solutions, typically functional solutions over object-orientated solutions. Since I spend the majority of my time in the Typescript ecosystem, I have the luxury of choosing either paradigm to solve the current problem at hand.

Abstractions

I’m terrible at making good abstractions with classes. I’ll admit that the results have been mixed. But typically, using inheritance and polymorphism to model a problem space evolves into a mess. I think I know at least two sources of the mess.

Terrible Method Names

First, classes end up having terrible method names if those methods are overriding base class methods. I’ve been fooled by too many method names that end up doing more than what they said they would. A base class will often set the name for a method based on what it should do. 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.

For example, I’ve implemented classes that manage database tables, so it implements an abstract class that needs an “update” method. Well, some tables when updating need to do more than just “update” a record, such as insert a log into a side table. While that might be innocent enough at first, I’ve seen quite a few methods continue to grow in their sets of responsibilities.

Maybe I just have organization problems. But I’ve found much more success creating groups of functions that don’t belong in a class. And I can give them any name I want and split responsibilities into new functions as I please.

Classes Grow

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.

This usually isn’t a huge pain point, but teams I’ve been on have preferred using groups of functions in modules that have state passed to them. It tends to be easier to break large modules into smaller ones if needed.

State Management

I’m not great at managing state in classes. State contained and controlled by a class has been the cause of interesting and undesirably behaviors. While using class-managed state is likely well suited for a suite of problems, I’ve seen a pattern of buggy implementations coming out of class-based solutions that held their own state.

One concrete example of this pattern is class-based React components. Time and time again, whenever I would try to use a class-based component, that component would eventually be the source of a bug if it tried to manage some internal state.

I thought that maybe state management was just inherently complex, and maybe it wasn’t the classes to blame. However, now that I’ve been using React’s hooks to manage state for functional components, I haven’t come across this sort of bugs in my UI code. It seems like removing the class construct and replacing it with functions that can be passed around between components have simplified state management on my projects.

Alternatives to Classes

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.

Maybe functions are more natural than classes in the Typescript ecosystem. There’s a chance that I would admire classes a bit more if I did more .NET development. Maybe I would benefit if I tried to use the object-orientated paradigm rather than the functional paradigm. I just haven’t found a compelling reason to do that on my project as of late.

Conversation
  • meowme says:

    Noob. Learn Classes and get better in abstraction ;)
    No, serious: I often wonder why/how to use classes in order to make my code better but I think the point is maintainability and working with different people at different parts of the software.
    Maybe the truth is in between.
    Thank you for the article!
    :)

  • Ido says:

    You’d still need structs to logically make sense of hundreds of variables the represent together something, like a “User” or other entity.
    Then you’d have a module that contains that struct and some helper functions that manipulate it.
    You can finish it there, but this is in fact, at least logically, a class.

    Above that, the idea of polymorphism and inheritance can really just dumb down to the idea of “interface” – just an explicit statement that two function have the same signature, and then they can be used / act upon interchangeably.

    • Teixeira says:

      Well, a class is just a struct with methods applied to it

  • Kyle says:

    This is so classless.

  • Douglas Muth says:

    “But typically, using inheritance and polymorphism to model a problem space evolves into a mess.”

    Then don’t do that. :-)

    I’ve been writing OO code for years and don’t use inheritance. I instead opt for “has-a” relationships (classes using other classes) than “is-a” relationships (classes extending other classes).

    You may want to look into the “dependency injection” technique as well, it’s quite useful for when building classes that use other classes.

    Good luck!

    • Marc says:

      ….ahh dependency injection, the functional style of object oriented software :D

  • Florian says:

    As somebody above already said, don’t ditch classes because inheritance and polymorphism are so terrible. Consider classes modules around structs, and you’re in pretty safe territory. The rest of OO is overrated.

  • Anthony Rogan says:

    If you are not good at abstractions, you won’t be good at classes – irrespective of anything else.

  • I’ve been working with OOP for my entire life and I love it. What you face is the topical wall you find after working with it for enough time.

    If you continue investigating about how to solve these valid issues you mention staying into the OOP realm you will find yourself evolving to the next level. Implementing strong SOLID practices, having very small clases where private methods almost disappeared, using composition instead of inheritance and several other solutions already identified.

    Having said that if you found another way to deal with your issues that’s fine if these solutions do not have another suite of side effects.

  • Jesper says:

    Classes are great when we model physical objects. It is very intuitive to model classes of objects you can see and touch.
    When you model stuff you cannot see or touch, you must imagine how the “World” you are modelling makes sense. And this is where things can get messy. Your initial set of requirements, and knowledge of the domain, lets you come up with a model that is simplified in all the right places. But a couple of years of changes to requirements will be lots of small changes, till it suddenly hits you, that the model was wrong. And now all your methods could be tied to the wrong entities.
    Your initial model can now be stuck in your brain as the only obviouos way to model that domain, since brains tend to be very happy with sticking to known patterns.

  • Greg says:

    In my experience, inheritance is only really useful in classes that get reused across many projects, like a LinkedList that everybody in the world needs. But it is generally not that useful in your own custom code, mainly because while a LinkedList will get tested to death and every bug will get solved, your custom code simply does not and winds up having more bugs.

    The more bugs problem is exacerbated by years of developer turnover, and changes in the latest fashion trends of programming.

    Functions with state avoid this problem, and are also easier to test – just call the function with appropriate combinations of arguments. No need to figure out how to modify or assert private field values, or update a bunch of tests because a new field and/or logic was added that breaks a number of tests, etc

    Devs are like everyone else – if something is too difficult they’ll find way to not do it. If maintaining the custom inheritance code unit tests is s pain, they’ll just stop adding any more tests.

    I find objects with state are a nuisance over the long term – developers have different levels of skill dealing with them, which translates to different classes of bugs, and different ranges of how much unit testing they get. You can say what you want about what devs should or should not learn, what they should or should not do, but reality has a way of just not caring what you or I think.

    So generally I would agree that using functions that accept state as args is generally better, as it is easier to reason about with fewer bugs, and unit tests that are easier to write. This means devs are more likely in practice to actually have and maintain unit tests, and actually have not too many bugs, and less likely to have really insidious bugs.

  • Nikunj Bhatt says:

    I create classes only for common code which can be used multiple projects, for example database class for insert, update, delete, etc. operations applicable to all tables and entities. Creating classes according to database tables or entities arises problem when there are multiple tables to update or when there are only few fields of a table to update; and such requirements are in most of projects. Either we will require to create lots of functions in classes for different requirements or mix OOP and non-OOP code. Changing such big classes is also big pain when requirements/database structure change.

  • zach says:

    Literally every argument you made against classes is an argument against the module pattern.

    Your argument as completely orthogonal to classes vs. modules.

    Not saying I agree or disagree, just pointing out there is nothing unique about your arguments between modules and classes (with the exception of the mentioned inheritance/polymorphism which can be 100% avoided in all cases when using classes and can also be implemented using modules).

  • skrymsli says:

    Yeah, you are describing my early experience with C programming. It totally works! It is often much simpler to understand. C++ and classes were introduced way back in the day to solve certain problems that are painful in C. Some major ones: Encapsulation. Resource lifetime/cleanup. Polymorphism/Interfaces (possible in C, but awkward). And then of course that introduced other issues like modeling problems using objects when that doesn’t really fit and crazy shit like multiple inheritance. The nice thing about C++ and Typescript is that you can choose the tool that works best. The standard template library in C++ is functional AND object-oriented. I prefer languages that can work both ways. Free the function.

  • I have mostly used function but only recently using classes. I use classes for some basic stuff, to group a certain module. So that it appear good and separated from rest of the code. I never reached to the point of extending classes and you made some good points to consider when extending classes.

    In addition I found out that using classes for utility libraries are not recommended as classes are not tree shake-able. May be that’s the reason people are shifting from momentjs to date-fns as one used OOP over functional programming.

  • Kevin says:

    You wrote: “For example, I’ve implemented classes that manage database tables, so it implements an abstract class that needs an “update” method. Well, some tables when updating need to do more than just “update” a record, such as insert a log into a side table. While that might be innocent enough at first, I’ve seen quite a few methods continue to grow in their sets of responsibilities.”

    I think the underlying problem here is that you haven’t well defined what the class actually does. A class that manages database tables should update a record, and nothing else. But your derived class no longer manages a table, it implements business logic.

  • Philip Oakley says:

    This is likely to be a viewpoint / direction of view problem.

    If one is looking outward to real world issues (i.e. awkward and unmanaged) then it’s almost impossible to prejudge what will come later, so the design becomes a bit of a shanty town.

    If it’s conceptually an inward looking design (i.e. most of the problems are decided, as are the predominant paradigms of the space) then it will all flow nicely with well planned boulevards for the big city.

    This is a broad sociological problem, rather than a technical problem. One useful analysis approach is the Competing Values Framework (CVF) of Quinn et al which allows one to place these various design methods onto particular stylistic quadrants, which have two acceptable (tolerated) neighbours, and then that fourth quadrant is just anathema, a failing style. The CVF is a useful technique because it’s problem oriented, rather than solution oriented.

    Classes are prejudiced, obviously. They don’t work that well if you can’t pre judge the (majority of) future needs.

  • ChadF says:

    Don’t blame observed problems with code solely on its particular paradigm. The fact is, there are a lot of bad programmers out there writing bad (if not awful) code. And since OO has been the mainstream choice in recent decades, many of those bad programmers have written bad code in OO. But don’t kid yourself, after being being mainstream for 20 or 30 years, I’m sure functional code bases will have just as much bad code as OO did after being mainstream that long.

    Prior to the OO takeover, there was a lot of bad C code out there too.

  • RandyB says:

    People often talk about OO and Classes almost as if they are the same thing. Many of the problems I have seen in Class design is that people are not using OOAD techniques, but more database techniques. If you start your analysis/design by thinking about the properties of a class you have missed the mark. In the “old days” OODA took a more functional approach – objects were designed as “collections” of methods. Exposing public data was frowned on, especially when just writing a data value. Many of the state related problems you mention may have their root in state being publicly writable. State should only be set from a business method. Start by designing your objects (not classes) and only define public business methods. Your classes will fall out of your object design. Classes should be about behavior, structs are about data. If your class design looks a lot like your database design you are not doing OO, only using OO technologies to build DB or Client/Server apps.

  • Comments are closed.