Thanks @anuragvohraec for your point of view :)
While I myself tend to lean my code in a more functional slant (though it's very far from the purist ideology of it), I don't have anything against OOP or classes. Yes, I'm attacking one of the four pillars of OOP, but I'm not really attacking OOP itself - OOP often stands on just three pillars in some newer OOP languages that don't provide facilities to do inheritance.
So, let me see if I can discuss some of the points you brought up.
- Real world objects can be easily mapped to objects and categorized with help of Inheritance.
Ah, categorization. I'm going to talk about the whole "is-a" vs "has-a" relationship idea here, as that's a common metric used to decide how to categorize something (if it's is-a, use inheritance, otherwise don't). Let's put this concept under a stress test with a commonly cited classical inheritance problem. You're building a payroll application. Salaried employees will be paid very differently from hourly employees, so you ask yourself, Is a salaried employee an employee? Is an hourly employee an employee? Why yes! Inheritance it is! But wait ... they also receive different wages depending on their job title. Is a back-end developer an employee? Is a front-end dev an employee? Why yes they are! Multiple inheritance it is!
Alright, so we'll end up with a nice inheritance hierarchy like this (the mixin pattern is being used to simulate multiple inheritance, because Javascript doesn't natively support it):
class FrontEndSalariedEmployee extends mixin(FrontEndEmployee, SalariedEmployee) { ... }
class BackEndSalariedEmployee extends mixin(BackEndEmployee, SalariedEmployee) { ... }
class DevOpsSalariedEmployee extends mixin(BackEndEmployee, SalariedEmployee) { ... }
// ...
class FrontEndHourlyEmployee extends mixin(FrontEndEmployee, HourlyEmployee) { ... }
class BackEndHourlyEmployee extends mixin(BackEndEmployee, HourlyEmployee) { ... }
class DevOpsHourlyEmployee extends mixin(BackEndEmployee, HourlyEmployee) { ... }
// ...
new FrontEndEmployee()
Hopefully, this company doesn't have too many job titles.
But... perhaps there's another way to formulate this problem.
Does an employee have a salary, or an hourly payment type? Does the employee have a front-end or back-end job title? Why... yes they do! So, we could also model this problem with composition instead (in this case, I'm specifically using the strategy pattern).
const hourlyPaymentType = { ... }
const salaryPaymentType = { ... }
// ...
const frontEndPosition = { ... }
const backEndPosition = { ... }
const DevOpsPosition = { ... }
// ...
new Employee({
paymentType: hourlyPaymentType,
position: frontEndPosition,
})
Wow that's simpler, and much more flexible as well!
The thing is, you can take any is-a relationship and rephrase it so it sounds more like a has-a relationship. Perhaps there are times when inheritance is the right choice, and using it make the overall structure of your codebase nicer, but I wouldn't automatically choose it simply because at first glance, it falls into an "is-a" category, because, perhaps placing it into the has-a category instead can lead to a nicer-looking codebase. Perhaps this is the reason why the phrase "favor composition over inheritance" gets thrown around, because the is-a vs has-a thing doesn't do a great job of letting you know when inheritance should be used.
Anyways, I guess all this means is, if we're going to use inheritance, we've got to understand what problem it's trying to solve and why the codebase needs it. We can't just reach for it because "it's the thing to do", because we have an is-a relationship.
(aside: I assume you probably wouldn't have actually modeled this problem using inheritance for each employee position, because it makes for multiple inheritance. The point is, it could be done, the only reason we don't is because we look for reasons outside of is-a vs has-a to make these decisions. Which is exactly my point - is-a vs has-a doesn't do a great job at making these decisions for us.)
OK, what's next...
- To achieve modularity and Inversion of control.
Perhaps this point was only meant to help explain why classes are useful? If so, I wholeheartedly agree that classes can be used to achieve modularity and inversion of control. If you intended to use this point to explain a pro of inheritance, feel free to expound and show why inheritance is useful for achieving modularity and/or inversion of control. I can guess at how inheritance would fit into it, but I'm scared I'd end up rambling about something we both agree on anyways.
- Easy testing : I can create dummy objects and push them for testing, without touching my original code. There are libs which can automate that for me.
Honestly, a lot of "good practices" get changed when you go into testing mode. For example, you're no longer supposed to keep your code DRY, instead, it's good to repeat yourself all over the place. If you want to use inheritance in a testing environment, I'm good with that.
However, I don't think I've ever seen inheritance get used to create mocks. The popular mocking libraries I've seen, like Sinon, usually just monkey-patch your objects, then restore them when testing is over.
- A way of combining related functions and variable under one roof : Classes and their objects. All functions and states belonging to a Class Boy are encapsulated in One object. There is no way he is going to behave other wise ( a better control in other words). A splattered states and function modfiying them across different files make it very hard for me to understand whats and where and how things are happening in my code. I can very conveniently convert real-world problems into Classes and Objects. And then create different versions of them with inheritance.
This sounds like it's mostly an argument for classes, which again, I have no problem with.
I will point out that there are ways to achieve the end goal of having "different versions of a class" without using inheritance. For example, you can create a single class and customize its behaviors using the strategy pattern. Or, you can create an interface (if you're using typescript) and create independent classes that all implement that interface. In fact, there's very few scenarios I'm aware of where inheritance (or Go lang's embedding feature) really becomes the only good way to achieve a desired effect.
- Inheritance help me precisely change my object behaviour and help me trace who has changed its properties. In a FP programming any one can attach properties to an object and it will be really hard to conclude which portion of the code did it. While in inheritance a particular behaviour is attached to an object just because a particular type of instance was passed in.
I think my feedback for this one is pretty similar to the previous one. Inheritance can be used to solve this problem, but so can other techniques.
Overall, I think my point is, in general it's considered good practice to favor composition over inheritance, and if you look around, there's actually a lot of ways to achieve a desired effect using various composition techniques. If we want, we can take a look at specific concrete examples where inheritance is usually cited as a "good thing", then try to model it without inheritance and see if the non-inheritance version is just as good if not better. I've already deconstructed the classic salary vs hourly employee example, which is normally cited as a shining example of where inheritance should be used, and showed that the same effect can be done in a simpler way via the strategy pattern (granted, I did purposely overcomplicate it with multiple inheritance for the purposes of talking about is-a vs has-a, but even with single inheritance, I would still prefer the strategy pattern there).
It sounds like your view of FP is simply "OOP, but without classes". Honestly, that's probably anyone's view of a different paradigm before they get comfortable with it. While you're trying to use a new paradigm, all you'll ever think about is "But how do you do X"? Unfortunately, it's not until after you've used it for a while that you begin to understand its strengths, and then, going back to your original paradigm you'll realize that it too lacks some pretty important features.
And, actually, if you look closely at FP, you'd find many of these "missing features" in there too - modularity, testability, encapsalation, polymorhpism, etc. They just come in different forms and flavors that you might not recognize at first.