4

I recently watched this presentation of Robert C. Martin about the Single Responsibility Principle: https://www.youtube.com/watch?v=Gt0M_OHKhQE

He presents an Employee class with multiple responsibilities and suggests splitting the functionality into classes like EmployeeRepository and EmployeeReporter. I can very much relate to these refactorings, but I wonder if this is only possible, if we introduce lots of getters to the Employee class. How else is it possible for the Reporter class, to generate the Report? So for me it seems this breaks OO encapsulation and leads to classes, which pull data out of objects to perform operations with this data instead of the object operating on its data itself.

There are many discussions around the web, about getter being evil and breaking encapsulation, but for me it seems, the Single Responsibility Principle needs getters (at least to some degree), to work out.

Martin Fowler suggests, that Getters might be a sign for procedural code:

Although the defense of encapsulation is the common rallying cry for getter eradicators, I think the real motivation for them is rather more reasonable and pragmatic. There's a hell of a lot of code out there in OO languages that is procedural in design. The OO community may have 'won' in the sense that modern languages are dominated by objects, but they are still yet to win in that OO programming is still not widely used. As a result it's still common to see procedures that pull data out of an object to do something, when that behavior would better fit in the object itself - a violation of the pragmatic programmers principle of "Tell Don't Ask". You can only do this kind of procedural programming if you have getters, so telling people to get rid of getters helps push them to move behavior into the right place.

http://martinfowler.com/bliki/GetterEradicator.html

Any suggestions about this topic? Do you need Getters when following the SRP?

2
  • You're quoting the wrong principle. The correct one is "Tell, don't ask." Oh, wait, it's in the passage you quoted. SRP doesn't have much to do with this... getters merely expose internal state, they don't represent new responsibilities unless you're using them wrong. Commented Aug 4, 2015 at 15:04
  • Did you read the whole thing? "I have a lot of sympathy with this motivation, but I fear that just telling people to avoid getters is a rather blunt tool. There are too many times when objects do need to collaborate by exchanging data, which leads to genuine needs for getters."
    – Andy
    Commented Oct 3, 2015 at 13:34

2 Answers 2

3

Programmers are mostly divided into two groups:

  • those who think accessors and mutators are okay
  • those who think accessors and mutators are evil

Neither of the two groups is correct. Just as anything else in programming, accessors and mutators may be used correctly or incorrectly. But does it mean, they are necessarily evil? Not really. To truly understand when is the right time to use an accessor, you must know what Single responsibility principle is really about.

The problem begins, when people start creating Anemic Domain Models as a part of their procedure to SOLIDify their code.

Many programmers I have worked with misunderstand the concept of Single Responsibility Principle and are afraid to put any logic inside their models, because they are afraid it would break the SRP. This means they end up with models which are really stupid, contain little to no business logic and have to be passed around to thirty different classes to be able to accomplish the simplest data operations.

I believe SRP is the most difficult SOLID principle to get right, what's worse, it's the first of them. Programmers focus so hard on it they usually forget about other important things.


The Single Responsibility principle

Where does it begin and end?

A question, which is so hard to answer, that it is the reason why so many people don't understand the SRP definition.

Let's take a look at a very simple class Human.

public class Human
{
    public readonly DateTime    DateTimeOfBirth;
    public          DateTime?   DateTimeOfDeath;
    public          string      Name { get; private set; }
    public          float       Height { get; private set; }
    public          float       Weight { get; private set; }

    public Human
        (
        DateTime    dateTimeOfBirth,
        string      name,
        float       height,
        float       weight,
        DateTime?   dateTimeOfDeath = null
        )
    {
        DateTimeOfBirth = dateTimeOfBirth;
        DateTimeOfDeath = dateTimeOfDeath;
        Name            = name;
        Height          = height;
        Weight          = weight;
    }
}

As every domain model, the responsibility of this class is to take care of its data and do it well. Does it take care of its data? It does. Does it do it well? Not so much. There are literally no rules for data manipulation of this class, you could set a DateTimeOfDeath before the DateTimeOfBirth, that is an invalid operation, which should never occur.

After tweaking the class you may end up with something like this:

public class InvalidInputException : Exception
{
    public InvalidInputException(string message) : base(message) { }
}

public class Human
{
    public readonly DateTime    DateTimeOfBirth;
    public          DateTime?   DateTimeOfDeath { get; private set; }
    public          string      Name { get; private set; }
    public          float       Height { get; private set; }
    public          float       Weight { get; private set; }

    public Human
        (
        DateTime    dateTimeOfBirth,
        string      name,
        float       height,
        float       weight,
        DateTime?   dateTimeOfDeath = null
        )
    {
        if (dateTimeOfBirth > DateTime.Now) {
            throw new InvalidInputException("The date and time of birth must not be higher than current date and time.");
        }

        if (dateTimeOfDeath != null && dateTimeOfDeath < dateTimeOfBirth) {
            throw new InvalidInputException("The date and time of death must not be before the date and time of birth.");
        }

        if (name == "") {
            throw new InvalidInputException("The name must not be an empty string.");
        }

        if (height <= 0) {
            throw new InvalidInputException("The height must be a positive number");
        }

        if (weight <= 0) {
            throw new InvalidInputException("The weight must be a positive number");
        }

        DateTimeOfBirth = dateTimeOfBirth;
        DateTimeOfDeath = dateTimeOfDeath;
        Name            = name;
        Height          = height;
        Weight          = weight;
    }

    public void gainWeight(float gainedWeight)
    {
        if (gainedWeight < 0) {
            throw new InvalidInputException("The gained weight must be a positive number.");
        }

        Weight += gainedWeight;
    }

    public void loseWeight(float lostWeight)
    {
        if (lostWeight < 0) {
            throw new InvalidInputException("The lost weight must be a positive number.");
        }

        if (Weight - lostWeight < 0) {
            throw new InvalidInputException("The amount of lost weight must not be greater than the current weight.");
        }

        Weight -= lostWeight;
    }

    public void changeName(string newName)
    {
        if (newName == "") {
            throw new InvalidInputException("The name must not be an empty string.");
        }

        Name = newName;
    }

    public void markAsDead()
    {
        DateTimeOfDeath = DateTime.Now;
    }

    public bool isAlive()
    {
        return DateTimeOfDeath == null ? true : false;
    }
}

While remaining simplicity, you now have a class which does something. It encapsulates the logic for the data manipulation. You no longer need to worry about validation of passed data, because the class will tell you what exactly is wrong, should something be.

But the class now has multiple responsibilites! It contains data as well as performs its validation and even creates a DateTime object.

Based on Single Responsibility responsibility, a class or module should have one, and only one, reason to change, but if I, in the future, decide, I want to allow a Human to have an empty name or that DateTimeOfDeath can actually occur before the DateTimeOfBirth, I will have to perform two changes to my class, but I am only allowed to perform one.

No, it does not have multiple responsibilities. This class is responsible for its data, you can pass any data to the class and the class itself will make sure the data is correct. That's all part of its logic.

If you decide to change the rules for integral data validation, even if you changed all of them, you are not violating the one reason to change rule.

The class actually has only one reason to change, that is, if you decided the integral validation should be performed in a different way, and it applies to all the data inside your class.


The connection of SRP and Getters

Following the previous example, even though a class should always contain valid data, you still decide you would like to validate the data so the object can be safely passed to a MySQL database.

You create a Validation interface containing a bool validate method and make your Human class implement it, which will ensure the class' data is MySQL valid.

Doing it that way violates the SRP.

  • 1st reason to change is, if you decided to set different validation rules of integral data.
  • 2nd reason to change is, if you decided, the validate method should perform its operation in a different way, because you decided to switch from MySQL to PostgreSQL and it somehow requires a totally different code to perform the validating operation.

On top of that, all the previous code for MySQL will be either left commented out or completely removed, so if you at any time in the future again decide to validate for MySQL, you will probably have to write all the code again. That's a huge red flag, that something is wrong.

You are forced to create Validators for your class, which somehow have to get access to its data. That's where the accessors come into play.

Accessors (getters) provide a simple public interface to class' internal implementation. If the Validator may not access some attribute, because the accessor for it does not exist, then the Validator should not. It should trust the object its validating, that the internal components will always be valid.

The accessors are there, so you can retrieve the data from the structure and when you retrieve it, unless you change the data, you may treat is as a part of the object.

The class should also perform operations for you, such as finding out whether a Human can vote or not, using a bool canVote() method.

public bool canVote()
{
    const int allowedVotingAge = 18;

    return (DateTime.Now.Subtract(DateTimeOfBirth).TotalDays/365 >= allowedVotingAge);
}

That looks much better than doing the following operation:

var human = new Human(/* */);

const int allowedVotingAge = 18;
var canVote = DateTime.Now.Subtract(human.DateTimeOfBirth).TotalHours/365 <= allowedVotingAge;

Once again, the class itself knows the rules to finding out, whether a Human can vote or not. A user of the Human class should not be forced to come up with the logic.

Even though very simple, canVote method is still a method which encapsulates the internal implementation nobody should know about. Furthermore, it gives you access to a variable, which does not even exist in the class as a member, because the variable is made during the runtime as a return type. So you cannot even access the CanVote variable directly.


A few guidlines on how to become a better developer

  • Domain Driven Design: Build your app around your business-logic, that is your domain, not around your data (building around your DL usually makes your classes include both business and persistence logic).

    Pass domain models around your applications, if you do, you will never be able to pass an object in an invalid state.

  • Make rich domain models: Your classes have to include some logic. They are responsible for your business, let them take care of handling their data.
  • SRP is a recommendation: Do not treat it as an idol and do not follow it blindly.

    For example, even though it seems as a different responsibility, implementing bool Equals() method on an object is a completely valid operation, the method should however be used to compare only two objects of the same type and return false every time a different type is passed to it.

    Because if you compare an Apple and a Banana, no matter how much effort will the Banana put into looking like an Apple, it will never be an Apple.


Recommended reading

  • Domain-driven Design by Eric Evans: An excelent book on what it says in the title.
  • Patterns of Enterprise Application Architecture by Martin Fowler: An excelent book to read before diving into agile development. Includes many amazing and modern design patterns, which in my opinion should be enforced, but sadly are ignored in favor of patterns which are easier to implement but don't provide as much space for code reuse.
  • Design Patterns: Elements of Reusable Object-Oriented Software by The "Gang of Four" (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides): A no. 2 book about design patterns to read. Great for providing general outlook.
4
  • So Human::canVote is an example of encapsulating business logic? Seriously? All elections have the same voting requirements across the planet? Your answer is a classic illustration of why attempting to encapsulate behavior seldom works in practice.
    – Cerad
    Commented Oct 3, 2015 at 17:17
  • @Cerad Voting was not the best illustration, but I believe, it will give a person an idea what encapsulation really means. canVote is a simple enough class interface, that a user of said class can use and will not have to worry about checking himself. I used it because it is an operation which can easily be checked outside the class, but should remain inside it.
    – Andy
    Commented Oct 3, 2015 at 21:49
  • Not trying to pick on you personally. I feel I have done a great deal of research on this topic, read innumerable articles and viewed dozens of videos. In every case the encapsulated method described is just an example and not something that would actually be used. It's frustrating. I know I am missing something basic but I can't help but wonder why all the real production code is being kept secret.
    – Cerad
    Commented Oct 5, 2015 at 14:15
  • @Cerad The main reason is production code costs money and in most cases companies have their employees sign a NDA, meaning they can't publish the code even if they wanted. In the end, design patterns are a field so broad and general, you shouldn't be limited to concrete implementation details. DPs provide a guidance on programming as a whole, rather than methods how to implement specific parts. They exists to make tackling common problems easier and faster.
    – Andy
    Commented Oct 5, 2015 at 14:47
-1

The problem of getters is that they encourage to move logic from where it belongs—that is the class itself—to a different location, increasing both the complexity of the code and the coupling between classes.

Let's consider the following example. A product has a price. So naturally, you add a getter and a setter which makes it possible to get the price as well as change it.

Now, you need somewhere in your code to change the price of the article, because there is a rebate being applied or because a tax such as VAT changed. So you draft the code which uses the getter to find the current price, and the setter to set the new price.

Let's now imagine that you can't have getters or setters, and that getPrice and setPrice qualify as getters and setters as well. You can't trick anyone either by using different names, such as findPrice or changePrice. What do you do?

Well, you move your logic. And you start creating methods such as applyRebate or changeTaxes. See, now you have Product class which has responsibility over the logic which is related to products, and the logic related to products is entirely encapsulated by Product class instead of being scattered around.

4
  • 2
    I don't downvote, but imho it borders on a non-answer. For most part it just redescribes the problem, and concludes with a general call to be pragmatic (you can't go wrong with that guideline). Commented Aug 4, 2015 at 9:27
  • @KonradMorawski: Thank you for your feedback. I thought that providing a concrete example will help the OP understanding the relation between SRP and getters/setters, thus the focus on the example itself. It would be much better to reuse the actual example referenced in the question, but I can't do that, because I haven't seen the linked presentation. Given those indications, do you feel that it still doesn't bring anything useful and should be deleted? As for the last paragraph, it was indeed not constructive; I removed it. Commented Aug 4, 2015 at 9:42
  • I certainly don't think it should be deleted : ) Although I believe the question remains open, and the gist of it is: how to tell whether behavior X belongs to class A or not; what's the litmus test for that Commented Aug 4, 2015 at 10:11
  • This example is more of why Product is the wrong model to use in the cart. You OrderLine, created from a Product, would not have a publicly settable Price, but you'd need a getter to show the user the price. Products settable Price would be used in the admin side of things for the business to change the price.
    – Andy
    Commented Oct 3, 2015 at 13:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.