4

If a class can only perform one task, could it be considered to be "too specialized"?

My intuition is that classes should be abstractions that are capable of representing themselves in multiple ways. I'm reviewing some class definitions that are so specific that these objects cannot be used except for a single use case.

Example: If I wanted to have a class called Dog, I may also want a subclass like Dog::Collie. If we want an instance of Lassie the dog, Dog::Collie should be abstract enough to describe her. If we need to specialize a new class Dog::Collie::Lassie, I'd be inclined to refactor Collie to support this - not super specialize a new derivation.

Edit: for clarification, Lassie is not an attribute of Collie or Dog, it's a concrete class definition that has functions like getHelp() and smileForCamera() that are not in Dog or Collie.

  • 4
    This depends entirely on your application's specific requirements. The general rule of thumb is that a class should have a *single responsibility.* In your particular example, the name of a dog is probably an *attribute* of `Dog`, not a specialization. – Robert Harvey Jun 09 '20 at 16:30
  • Not all classes should be abstractions, and IMO, classes should be smaller then how many people write them. But, if I'm interpreting your question correctly, it seems like what you have here is a hierarchy that's too deep, and that's not good either. I'm guessing your example is just for illustrative purposes and that the actual code goes beyond that. Now, 3 levels is not bad in itself, but you have to make a judgement call here. A one-off subclass can be OK, especially if it makes things easier to maintain/understand. But sometimes classes can be too specialized without being all that useful. – Filip Milovanović Jun 09 '20 at 16:59
  • @RobertHarvey Lassie is name of a specialized class, not an attribute of Dog. – user3645592 Jun 09 '20 at 17:04
  • @FilipMilovanović You're correct, the example is for illustrative purposes. In the code base I'm working with **every** instantiated class is a specialization that has been hard coded for one purpose only. This is done through five levels of inheritance, which feels too deep to me, but I know that "too deep" can mean different things in different contexts. – user3645592 Jun 09 '20 at 17:09
  • 1
    *"Lassie is name of a specialized class, not an attribute of Dog"* -- OK. How does `Lassie` specialize `Collie`, other than to specify what the dog's name is? How does a `Lassie` differ from a `Collie`, other than in name? – Robert Harvey Jun 09 '20 at 17:43
  • 1
    As to the depth of inheritance hierarchies, the only time I've ever seen hierarchies five levels deep is in UI frameworks. – Robert Harvey Jun 09 '20 at 17:47
  • @RobertHarvey Regarding your questions: 1) Lassie may have a method such as getHelp(), which doesn't exist in Dog or Collie, and 2) You're correct, this is for a UI framework :) – user3645592 Jun 09 '20 at 18:15
  • [On the other hand](https://softwareengineering.stackexchange.com/questions/202477/can-too-much-abstraction-be-bad) – Laiv Jun 10 '20 at 10:56
  • 2
    Animals give horrible and often incorrect examples for OOP. I'd never make a "Lassie" a subclass of "Dog", instead I'd create a class "ActingAnimal" which _contains_ a reference to some animal. – gnasher729 Jun 11 '20 at 08:40

3 Answers3

4

If we need to specialize a new class Dog::Collie::Lassie, I'd be inclined to refactor Collie to support this - not super specialize a new derivation.

Why? Apparently, there have been nine different Lassies over the years, and they presumably each had their own birth date, weight, height, medical history, list of film and television appearances, etc. Depending on what your program is meant to do, it might make sense to have Lassie as a subclass because not all Collie instances need to track their media appearances, fan club connections, etc.

I think the point you were trying to make is that if a class represents one specific thing rather than a category of things, it's too specific. My counterargument is that when you create a class you often can't know how many instances of a thing you'll eventually have, and you shouldn't worry about that. If generalizing Collie to include the behaviors needed for Lassie makes you write a lot of code that looks like:

if (dangerPresent) {
    if (name is Lassie) {
        runForHelp
        barkBarkBark
    else {
        runAway
    }
}

then it's probably a lot better to create a Lassie subclass even if you know for certain that it'll only ever represent a single dog.

If a class can only perform one task, could it be considered to be "too specialized"?

Some would argue the opposite, i.e. that according to the Single Responsibility Principle a class should only do one thing. Exactly what counts as a "task" or "responsibility" depends a lot on where the class fits into the design of the system. Your example above helps to clarify that... if the job of the Collie class is to represent any collie, then making it also model characteristics that apply only to some subset of collies (even if that subset has only one member) violates SRP.

I think a class can be too specialized, but mainly because it purports to be more abstract than it really is. Again, see the SRP. If a class's implementation is doing things to support only one or more subsets of the category that it represents, then the class is too specialized and should be refactored.

Caleb
  • 38,959
  • 8
  • 94
  • 152
  • A *Method* should only do one thing. Useful classes almost always have more than one method, unless they're Data Transfer Objects, in which case they *store* more than one thing. – Robert Harvey Jun 09 '20 at 18:09
  • I am afraid this is not a helpful answer. "they presumably each had their own birth date, weight, height, medical history, list of film and television appearances, etc." This is **all** instance data, hence not a good reason for sub-classing. And what Robert says. It is all over the place without addressing the question. – Martin Maat Jun 09 '20 at 18:25
  • @MartinMaat I agree up to the point of the code example where there's a specialized behavior which I think is a valid reason to subclass. – JimmyJames Jun 09 '20 at 18:40
  • @RobertHarvey It's all about granularity. SRP as stated in the WP article and by Bob Martin talks about *modules*, not necessarily methods. My point here is that a Collie class's responsibility is to represent "things that are collie", not "things that are collie and also sometimes something more specific." – Caleb Jun 09 '20 at 21:44
1

Collie can be regarded a special kind of dog, Lassie cannot be regarded a special kind of Collie.

You seem to have trouble making the distinction between classes on the one hand and individual variations on the other hand. Lassie is a Collie incarnation, an individual or in OO-speak: an instance. You can have an infinite number of instances of Collie and they may all be different in some way, yet they would not be different in "collieness".

It is only for fundamental differences that apply to all instances of a kind of dog that you would consider introducing a new class. Collies have long hair, they all have a pointy snout, they all share the patched coloring and the fluffy tail, so... maybe we should have a sub-class of dog to model this kind of dog. And then you would only do this if you care about these traits, if they make a difference in your problem domain. I am more of a cat man so I would be happy with just Dog (dogs are dogs, I don't care about their furs or sizes, I just want them to keep their distance).

The bottom line is: a class should make your software easier to understand, to write and to maintain. If a new class defeats that purpose, if it only adds complexity period, then you are over-specializing.

Martin Maat
  • 18,218
  • 3
  • 30
  • 57
  • I must have been unclear in my OP. Lassie is a specialized class that inherits from Collie. I'm saying that this should be avoided, and that Collie should be able to describe all Collies. – user3645592 Jun 09 '20 at 19:25
  • _"Lassie cannot be regarded a special kind of Collie"_ That very much depends. If not all collies are able to reticulate a spline, but all collies that _can_ reticulate a spline are a Lassie, then Lassie _is_ a special kind of Collie. The proof is in the pudding: "Collie" is a designation just as much as "Lassie" is, and the only difference between them is our contextual understanding of it, where we assume Lassie can only do what any collie can do. If Lassie can do more than your average (or should I say base) collie, then Lassie is a special kind of collie (both semantically and logically). – Flater Jun 11 '20 at 11:33
1

Classes are not exactly abstractions, they are concretions (potentially of an abstraction). Generally, whatever you can create an instance of typically:

  • Is concrete.
  • May have specific state.
  • May have specific behavior.

It might be that you are confusing the class definition and the class instances. Of course class definitions are an abstraction that represents the range of all possible instances (defined as the set of all instances occurring by cover the entire domain of the internal state parameters).

Your Dog::Collie:Lassie example showcases a minor problem. Dog and Collie are class definitions, but Lassie should be a specific class instance. It is a just matter of taxonomy, really. Collie is a Type, but Lassie is a Name. So, unless you really want to represent Lassie as a Type (in which case you may have multiple Lassie instances).

To make this clearer, let me introduce you to my two friends. I have 3 instances of Michael and another 3 instances of Jordan:

Michael morningMichael = new Michael();
Michael afternoonMichael = new Michael();
Michael nightMichael = new Michael();

Jordan morningJordan = new Jordan();
Jordan afternoonJordan = new Jordan();
Jordan nightJordan = new Jordan();

Now, this is my mental model. When I see Michael in the afternoon, it is a different Michael, who happens to bear terrific resemblance to the morning version of Michael. He is a little more tired than morning Michael, his weight is slightly different, and stuff, but he still has two hands, two legs, one head, same hair color, same complexion, etc. There are two obvious problems with this mental model, and one that is not so obvious.

  • There is gross definition duplication. Michael and Jordan share very similar traits so I will have to code definitions multiple times. I can overcome this by using base definitions and inheriting, but I will reach a point where, after inheriting, there will be virtually nothing more to define.
  • There is no resemblance of this mental model to reality, which is what I have been trained in for my entire life, so reality is a model I can perfectly, easily and fluently relate to, and whatever I can translate to or from reality is, practically, easier for me to understand, grasp and develop around. When I see a friend after a day goes by, I don't ever feel like it is a copy of my friend...
  • The not so obvious problem is that this is perfectly allowed using available coding tools and definition/prototyping capabilities.

In solving the third problem, at some point, Types must end, and State must come to be. Therefore, I choose to stop my definitions (types, really) at Human and let state manifest, in the form of instances. Then, whenever I read my code, it really takes me less than a few milliseconds to understand that whenever I come across instance micheal, it is really the same Human I came across earlier, when talking about michael. He may have changed internally, but it is the same guy (viz. the same byte-group in memory).

Human michael = new Human();

Human jordan = new Human();

So, like I said earlier, it is a matter of taxonomy, really. It is more like philosophy, but also a lot about what makes you more productive. Object-oriented programming borrows from the real world, because understanding the real world is something we all have a lot of training in, and we can express our ideas much easier in that model.

All that aside, the extent of detail one reaches is always dependent on the case. For example, while birds are, technically, dinosaurs, you will not come across many real-world code-bases with a hierarchy such as Dinosaur::Bird.... So, as usual, more often than not, your very own definitions in your code generally reflect what you are trying to achieve, and this is what you should use to balance your choices.

Vector Zita
  • 2,372
  • 8
  • 19
  • In my example, Lassie is a subclass of Collie, which is a subclass of Dog. Lassie's class definition (not instance) may have methods like GetHelp() and SmileForCamera(). The problem is that this specialization is only ever used for one dog, and there are billions of possible Dog class instances. We should strive not to make concrete definitions for all Dog instances. Instead our base classes like Collie should be able to describe all Collies in our use case. In this case, Lassie might be able to inherit from Collie and some kind of Actor class. – user3645592 Jun 09 '20 at 19:23