During nearly every presentation in which I explain my view of object-oriented programming, there is someone who shares a comment like this: "If we follow your advice, we will have so many small classes." And my answer is always the same: "Of course we will, and that's great!" I honestly believe that even if you can't consider having "a lot of classes" a virtue, you can't call it a drawback of any truly object-oriented code either. However, there may come a point when classes become a problem; let's see when, how, and what to do about that.
There were a number of "rules" previously mentioned that, if applied, would obviously lead to a large number of classes, including: a) all public methods must be declared in interfaces; b) objects must not have more than four attributes (Section 2.1 of Elegant Objects); c) static methods are not allowed; d) constructors must be code-free; e) objects must expose fewer than five public methods (Section 3.1 of Elegant Objects).
The biggest concern, of course, is maintainability: "If, instead of 50 longer classes, we had 300 shorter ones, then the code would be way less readable." This will most certainly happen if you design them wrong.
Types (or classes) in OOP constitute your vocabulary, which explains the world around your code—the world your code lives in. The richer the vocabulary, the more powerful your code. The more types you have, the better you can understand and explain the world.
If your vocabulary is big enough, you will say something like:
Read the book that is on the table.
With a much smaller vocabulary, the same phrase would sound like:
Do it with the thing that is on that thing.
Obviously, it's easier to read and understand the first phrase. The same occurs with types in OOP: the more of them you have at your disposal, the more expressive, bright, and readable your code is.
Unfortunately, Java and many other languages are not designed with this concept in mind. Packages, modules, and namespaces don't really help, and we usually end up with names like
AbstractCookieValueMethodArgumentResolver (Spring) or
CombineFileRecordReaderWrapper (Hadoop). We're trying to pack as many semantics into class names as possible so their users won't doubt for a second. Then we're trying to put as many methods into one class as possible to make life easier for users; they will use their IDE hints to find the right one.
This is anything but OOP.
If your code is object-oriented, your classes must be small, their names must be nouns, and their method names must be just one word. Here is what I do in my code to make that happen:
Classes are prefixed. My classes always implement interfaces. Thanks to that, I can say they always are requests, directives, or domains. And I always want their users to remember that. Prefixes help. For example,
RqBuffered is a buffered request,
RqSimple is a simple request,
RqLive is a request that represents a "live" HTTP connection, and
RqWithHeader is a request with an extra header.
An alternative approach is to use the type name as the central part of the class name and add a prefix that explains implementation details. For example,
DyDomain is a domain that persists its data in DynamoDB. Once you know what that
Dy prefix is for, you can easily understand what
DyBase are about.
In a medium-sized application or a library, there will be as many as 10 to 15 prefixes you will have to remember, no more. For example, in the Takes Framework, there are 24,000 lines of code, 410 Java files, and 10 prefixes:
Xe. Not so difficult to remember what they mean, right?
I find this approach to class naming rather convenient. I used it in these open source projects (in GitHub): yegor256/takes (10 prefixes), yegor256/jare (5 prefixes), yegor256/rultor (6 prefixes), and yegor256/wring (5 prefixes).