Great point... but doesn't it continue to prove the author's point that this is needlessly complex?
It strikes me as bad design if something as simple as an action (hit) now needs to become it's own class that is instantiated merely to execute a single function and then have to delete itself. That just feels like insane overhead/boilerplate, no?
The argument here is that it's far better to have player and monster be simple data structures, and a single function hit(player, monster, weapon).
The Hit class may be a Singleton and only instantiated once. Creating a class is like 3 actual lines. Also at least in Java the Hit class can be an inner class of the Person class. It just feels like name spacing in that case.
It is needless complex if the code is simple.
This solution is more flexible that just a pure function. If you have different players who use different hit strategies how do you handle that with just a pure function? Lots of if statements in the function? Passing in lambdas in every case you call the function? Then you could curry the function and call it something different too and that works I guess. OOP shines when you have lots of different ways things can work in different combinations. OOP is a tool like other programming paradigms, you use it when it solves your problem better than the other ways.
> If you have different players who use different hit strategies how do you handle that with just a pure function? Lots of if statements in the function? Passing in lambdas in every case you call the function?
I'll list a few strategies I've seen in different contexts:
1) Clojure has "protocols", which is a strategy of doing polymorphism where the verb is charge instead of the noun.
2) Rust has trait objects, where you have to implement the polymorphic behavior you want for a separate player type but the data is separate from the behavior and not interlinked or owned by the class. You can implement the behavior in the scope of the module where it is used.
3) You already mentioned this, but lambdas in Javascript or functors in C++ satisfy a lot of specialization requirements.
All 3 of these approaches are different ways of approaching the problem of different behavior on the same data. The key thing with them is that it allows domain specific behavior to be decoupled from an owning object. There may be reasons that OO is more suited to a problem (as you mention), but I tend to agree with the author's post around the dogma of using OO everywhere.
In the article, the author also mentions a quote around encapsulation
> Encapsulation is an object-oriented programming concept that binds together the data and functions that manipulate the data, and that keeps both safe from outside interference and misuse.
In my opinion, this view of binding the data and behavior together stems from an idea that data is scary and can be changed at any time. In an immutable, pass-by-value, functional world, those getters and setters and indirection is just another thing in my way from composing the data together with collections tools I am very familiar with. I think it was Neal Ford who said that "OO encapsulates moving parts, whereas functional programming removes moving parts." I think that's very appropriate in this context. In a data-first view, you model what properties and data shapes you care about in your records, and then you create little functions that connect those things together; in OO, you create the links first through methods (even if those have interfaces), and you're stuck with those connections anywhere you want to pass the object.
Ultimately the answer is to use a language in which passing a function reference is as easy as writing a singleton that implements an interface and passing a reference to that. (Or easier—in any language with decent function-passing support, writing a function and referencing it is generally going to be easier because fewer things are happening.)
OO makes sense if the different types of behavior map to object identities and inheritance trees, because it generates a chain of implicit if statements that capture this logic. If they don't—e.g., a hit changes behavior depending on whether the attacker is proficient in the weapon, rather than depending on the weapon class or attacker class itself—you're likely better off writing the if statements yourself.
> It strikes me as bad design if something as simple as an action (hit) now needs to become it's own class
Why, it's a noun in the problem domain. Why shouldn't it be a class?
> that is instantiated merely to execute a single function and then have to delete itself.
This may be inefficient in some language implementations because of limits of optimization, but there is no fundamental reason that, if that's all you do with it, the compiled code or runtime behavior needs to be much different than if it were just a procedure call.
On the other hand, lots of times you will want to do more than that with actions occurring in the domain then executing them (queueing, logging/serializing, etc.) and having a datatype for the event (a class in class-oriented OOP) supports that.
> The argument here is that it's far better to have player and monster be simple data structures, and a single function hit(player, monster, weapon).
Naturally it's better to be simple if the logic involved actually is simple, but surely the point of the example is that game logic typically isn't. Hits in games don't just change a hit-point value, they tend to play animations, trigger events, work differently depending on this or that piece of global state, etc.
It isn’t uncommon, nor is it bad design to define value classes that represent simple actions. This enables the action/command/event value to be reified, serialized, transmitted over the wire, deserialized, and potentially processed remotely, asynchronously, or by multiple arbitrary consumers.
Hit is an event that occurs, so not sure why it should escape OO even though in this example even though it's only decreasing ints. I routinely code event objects rather than modifying another class directly by the delta that should occur. OO isn't good for continuous time and events but it does add ergonomic convenience for the developer in this case (there could be other information on the hit, a historical list of Hits looks better as List<Hit> rather than List<int>, it's easier to inspect a hit object during debugging rather than setting watches on player/monster/weapon values).
In the OO version of this pathology, these arguments become the instance variables of a class introduced to do nothing more than hold them.
Poor design has a way of cutting across programming paradigms, or even (in the case of inappropriate inheritance, for example) leveraging their features.
It has nothing to do with efficiency. Insofar as Bassman9000's example indicates a real problem (and there are a couple of ways it might do so, ultimately leading back to the poor use of abstraction and separation-of-concerns), stuffing the arguments (or any other weakly-related collection of variables) into an ad-hoc struct (or object) having no real cohesion is simply sweeping the dust under the rug, and is just as indicative of a likely design problem as are long argument lists of nominally unrelated data.
Why is no cohesion, in, let's say a HitEvent object? This knows the target, source, method (weapon, magic, physics like falling down), etc. You can make handy helper methods and subtypes depending on important concerns.
It strikes me as bad design if something as simple as an action (hit) now needs to become it's own class that is instantiated merely to execute a single function and then have to delete itself. That just feels like insane overhead/boilerplate, no?
The argument here is that it's far better to have player and monster be simple data structures, and a single function hit(player, monster, weapon).