Section 7.3 Roles, responsibilities and reasons for change
Subsection 7.3.1 The Single Responsibility Principle
The philosophy behind the tell-don’t-ask principle is simple: do the thing you know how to do, then ask other objects to help you out with what they can do best. This is a general principle to follow:
Each object should have one set of responsibilities, tasks which it knows how to do, and is expected to do. This set of responsibilities represents the role that the object plays in the overall design of the application.
This principle is often stated slightly differently, with reference to the reasons for which our object might have to change:
Single Responsibility Principle a class should have only one reason to change.
In this context "change" means some new behavior that our clients want. We would like it to be the case that given a specific new behavior that we would like to introduce, it is clear which classes need to change in order to accommodate that behavior. And the same class should not be changing for completely different reasons.
As an example of this principle, consider the current state of our grade-reporting program, with its
processGrades
method. Let’s consider the following kinds of "change":- We want to use the system with a completely different grading scheme, that uses different letters rather than the common A/B/C/D-plus/minus system.
- We want to enable reading from other data formats, for example with comma-separators between the various entries in each row, rather than spaces.
- We want to print extra information in the report, print the gpa with more precision, etc.
These three requirements are quite different. Yet right now trying to implement any one of these requires changes to the same file. This one class currently has too many unrelated responsibilities. Ideally I would have three classes instead, each responsible for one of the above:
- Some sort of
GradeSystem
class that knows what symbols are valid and what they mean. - Some sort of
GradeReader
class that knows how to read the grade out of a file. - Some sort of
Summary
class that knows how to summarize and print that grade information.
These objects would then work together to achieve the common goal of reporting; but we could change each of them independent of the others.
Another important side-effect is also this: We could test each of these classes independent of each other. And that’s really useful. We can write very localized tests that are testing precisely one thing, without conflating what they are testing with other behaviors. Looking back at the test in the grade-reporting file, you will find that not to be the case at all: If one of those tests fail, do you know the reason for it? Was it that you are not reading the data right, you are not computing grade points right, or not printing things right? Any one of those would have caused your tests to fail. That’s not helpful; we want our tests to be able to immediately point us to our problem. We’ll have more to say about tests in the next section.
You might wonder, how do we achieve this? how do we take the current grade-reporter class, with its jumble of responsibilities, and break it into three classes? What are the steps that allow us to do that in a disciplined way? What are the code smells that suggest us that we need to do it? We’ll answer these questions later in this chapter, when we discuss another group of refactoring steps, this time related to the creation of new classes and other transformations involving multiple classes. But first, we’ll discuss a few more concepts.
Subsection 7.3.2 Functions have responsibilities too
The single responsibility principle applies to individual functions as well: Each function should have a well-defined reason to be, a clear set of expected behaviors. It is responsible for getting something done. Then it calls on other functions to help it along the way. We have another maxim for that:
A function should do one thing. It should do it well. It should do it only.
The "extract method" refactoring was the main tool we had to ensure that is the case; any part of the big method that was doing something clearly identifiable as "a thing" was extracted into its own method. And we ended up with lots of small methods, each responsible for doing one thing, and calling on other functions to fill in the details.
The idea of the single responsibility for classes is an extension of that; classes group together those smaller responsibilities into one big area of responsibility for the class. But the overall idea stays the same.