There are two extremes on a spectrum. On one end are sets of things that are merely informational and manipulated as text, numbers, or references to enumerated instances. For example, a dog salon application might record your pet’s name, color, and breed, merely as text for identification purposes. On the other end of the spectrum are sets of things that need to have different properties, operations, or methods. For example, a video game might want to emit a different sound for each breed of dog when it barks.
The decision one has to make as a designer is how accurately to reflect the “domain of discourse” vs. when to take shortcuts for a particular application. In the domain of discourse, each breed has characteristics that make it unique, but for the purposes of a dog salon application, you just don’t care. In that case, you could design away the detail and hope you never need it in the future.
The problem lies in the middle of that spectrum, where someone makes the wrong decision. I have worked on large enterprise systems, where someone squashed an inheritance hierarchy into types explicitly encoded as values in one or more database columns. This was horrific because it became the programmers’ problem to know which other columns were valid for every encoded type. The system had switch statements all over the place to implement business rules that evolved over time into something very complex. The system had lots of bugs.
For that reason, in the middle of the spectrum, where things are not so clear cut, I would err on the side of many classes. The reasoning is that accurately reflecting the domain makes it easier to tell whether or not requirements are met, and can make the code more intuitive to maintain (assuming it’s DDD). The key to reflecting the domain is accurately representing sets of things. For example, “Fido” is a member of the Dog set and of the Animal superset; “Sylvester” is a member of the Cat set and of the Animal superset. This is easy for people to understand, and each class can have different implementations hidden from the outside. Once you start interpreting types explicitly encoded as strings, integers, or enumeration-literal references, you start needing switch statements all over the place, and you have an unmaintainable, hard to understand mess. Many OO languages can save you from all that, but you have to create a factory with a complete list of types. The runtime overhead is negligible (especially if you implement the factory to run in constant time), and the types can be mapped to values in a relational database so you can query and report on them. Besides, as you point out, you may be able to take advantage of generics.
If you know your classes will always be empty, as is the case in a dog salon application, don’t bother with classes. If you know they will have different properties, operations, and methods, or if you’re not sure, use classes.