The SRP states, in no uncertain terms, that a class should only ever have one reason to change.
Deconstructing the "report" class in the question, it has three methods:
Ignoring the redundant
Report being used in every method, it is easy to see why this violates the SRP:
The term "print" implies some kind of UI, or an actual printer. This class therefore contains some amount of UI or presentation logic. A change to the UI requirements will necessitate a change to the
The term "data" implies a data structure of some kind, but doesn't really specify what (XML? JSON? CSV?). Regardless, if the "contents" of the report ever change, then so will this method. There is coupling to either a database or a domain.
formatReport is just a terrible name for a method in general, but I'd assume by looking at it that it once again has something to do with the UI, and probably a different aspect of the UI than
printReport. So, another, unrelated reason to change.
So this one class is possibly coupled with a database, a screen/printer device, and some internal formatting logic for logs or file output or whatnot. By having all three functions in one class, you're multiplying the number of dependencies and tripling the probability that any dependency or requirement change will break this class (or something else that depends on it).
Part of the problem here is that you've picked a particularly thorny example. You should probably not have a class called
Report, even if it only does one thing, because... what report? Aren't all "reports" completely different beasts, based on different data and different requirements? And isn't a report something that's already been formatted, either for screen or for print?
But, looking past that, and making up a hypothetical concrete name - let's call it
IncomeStatement (one very common report) - a proper "SRPed" architecture would have three types:
IncomeStatement - the domain and/or model class that contains and/or computes the information that appears on formatted reports.
IncomeStatementPrinter, which would probably implement some standard interface like
IPrintable<T>. Has one key method,
Print(IncomeStatement), and maybe some other methods or properties for configuring print-specific settings.
IncomeStatementRenderer, which handles screen rendering and is very similar to the printer class.
You could also eventually add more feature-specific classes like
This is made significantly easier in modern languages with the introduction of generics and IoC containers. Most of your application code does not need to rely on the specific
IncomeStatementPrinter class, it can use
IPrintable<T> and thus operate on any kind of printable report, which gives you all of the perceived benefits of a
Report base class with a
print method and none of the usual SRP violations. The actual implementation need only be declared once, in the IoC container registration.
Some people, when confronted with the above design, respond with something like: "but this looks like procedural code, and the whole point of OOP was to get us -away- from the separation of data and behavior!" To which I say: wrong.
IncomeStatement is not just "data", and the aforementioned mistake is what causes a lot of OOP folks to feel they are doing something wrong by creating such a "transparent" class and subsequently start jamming all kinds of unrelated functionality into the
IncomeStatement (well, that and general laziness). This class may start out as just data but, over time, guaranteed, it will end up as more of a model.
For example, a real income statement has total revenues, total expenses, and net income lines. A properly-designed financial system will most likely not store these because they are not transactional data - in fact, they change based on the addition of new transactional data. However, the calculation of these lines is always going to be exactly the same, no matter whether you are printing, rendering, or exporting the report. So your
IncomeStatement class is going to have a fair amount of behaviour to it in the form of
getNetIncome() methods, and probably several others. It is a genuine OOP-style object with its own behaviour, even if it doesn't really seem to "do" much.
print methods, they have nothing to do with the information itself. In fact, it's not too unlikely that you'll want to have several implementations of these methods, e.g. a detailed statement for management and a not-so-detailed statement for the shareholders. Separating these independent functions out into different classes gives you the ability to choose different implementations at runtime without the burden of a one-size-fits all
print(bool includeDetails, bool includeSubtotals, bool includeTotals, int columnWidth, CompanyLetterhead letterhead, ...) method. Yuck!
Hopefully you can see where the above, massively-parameterized method goes wrong, and where the separate implementations go right; in the single-object case, every time you add a new wrinkle to the printing logic, you have to change your domain model (Tim in finance wants page numbers, but only on the internal report, can you add that?) as opposed to just adding a configuration property to one or two satellite classes instead.
Implementing the SRP properly is about managing dependencies. In a nutshell, if a class already does something useful, and you are considering adding another method that would introduce a new dependency (such as a UI, a printer, a network, a file, whatever), don't. Think about how you could add this functionality in a new class instead, and how you could make this new class fit into your overall architecture (it's pretty easy when you design around dependency injection). That is the general principle/process.
Side note: Like Robert, I patently reject the notion that an SRP-compliant class should have only one or two state variables. Such a thin wrapper could rarely be expected to do anything truly useful. So don't go overboard with this.