-3

Supposing I live in a country where every car brand has a different tax rate, and I have a base class called Car

public class Car{
    public string CarType { get; set;}
    public int Year {get; set;}
    public abstract double calculateSalesTax();
}

Would it be OK to add an abstract method to calculate the sales tax? Or does that already breaks the SOLID principle? A usual guideline for S in SOLID is "If you have to use the word AND when describing your class, it's most likely breaking the principle". Here, the implementation of a method to calculate sales tax seems to be requiring that AND. This class sets the attributes of the car AND calculates its sales tax.

So If I implement the derived class

public class Volkswagen : Car{
    
    public override double calculateSalesTax(){
         //Something to calculate a VWs tax
    }
}

is it already breaking the S in SOLID?

4
  • 2
    When I see the example of "Volkswagen" as a class, my eyebrows narrow. Independent of the S in SOLID, inheritance is not the only tool in the toolbox. Commented Dec 8, 2020 at 5:02
  • 3
    You have a simplistic and wrong definition of S.
    – user949300
    Commented Dec 8, 2020 at 5:06
  • 1
    Does this answer your question? How to determine if a class meets the single responsibility principle?
    – gnat
    Commented Dec 8, 2020 at 5:11
  • @gnat That answer is not very useful: "use your (and your coworkers') best judgement."
    – user949300
    Commented Dec 8, 2020 at 6:19

2 Answers 2

6

A usual guideline for S in SOLID is "If you have to use the word AND when describing your class, it's most likely breaking the principle"

That guideline is not a strict definition, it's an approximation. And as with all approximations, they are imprecise.

The way you are thinking about the granularity of responsibility would lead to madness. Because in Car.Year, you are storing that integer value's first bit AND its second bit AND its third bit AND ... Should we now write classes that each store a single bit value?

The definition of a responsibility is contextual. Which leads to the quote that so aptly encapsulates many (if not all) medior/senior-level programming problems:

enter image description here

Just because something can be broken down further, doesn't mean it should be broken down further.

For example, let's say I want to calculate the hypotenuse of a right triangle based on the length of the other two sides (i.e. the Pythagorean theorem). Now this calculation actually consists of multiple mathematical operations:

  • Squaring a number
  • Adding two numbers
  • Taking the square root of a number

Are these three things separate responsibilities? Yes or no? The answer is that it's contextual.

If I were making a calculator app or something that calculates many different kinds of geometric properties, then the odds are that I'm going to want access to these individual operations (square, add, square root), and I should consider them as separate responsibilities.

However, if the Pythagorean theorem is the only mathematical operation in my entire application, then it doesn't matter to further subdivide the Pythagorean theorem calculator into separate classes. Doing so would be a waste of time and effort.
In this case, "calculates the hypotenuse" is the correct description of the responsibility and should not be broken down further.

To summarize, responsibilities need to be scoped appropriately to their context. There is no one-size-fits-all definition of precisely how finegrained a responsibility needs to be.


Would it be OK to add an abstract method to calculate the sales tax?

Well, that depends on context. What's the responsibility of your car?

For example, if you were writing a sales tax calculating application which calculates the sales tax for cars, houses, appliances, ... and the application does nothing but calculating sales tax, then it would make sense that each product class (Car, House, Appliance) would house its own tax calculation logic.

However, if you were writing a car management application, where you store a bunch of cars and can manage them in various ways (calculating sales tax, maintenance, repair, performance grading, suggesting this car to certain kinds of customer), then calculating the sales tax is no longer the primary purpose of Car.

Therefore, in this second example it's not as justifiable to house the tax calculation logic in Car, because Car would then also have to house the maintenance logic, the repair logic, the performance grading logic, the suggestion logic, ... and now I hope you see why this massively violates SRP.

In the first example, cars would only ever be used to calculate their sales tax, so there was no reason to ever separate the car logic from the car sales tax logic. They were functionally equivalent, and they represent the exact same responsibility (calculating car sales tax), so there was no point in subdividing the same responsibility.

This class sets the attributes of the car AND calculates its sales tax.

Those are not (inherently) two distinct responsibilities. For example, if the CarType and Year properties are used in the sales tax calculation, then even if you're in the context of the first example (sales tax calculation app), you inevitably need those values to calculate the car sales tax.

However, if those values are being stored in Car but they do not belong to the car sales tax logic, that does indirectly suggest that your Car has a secondary responsibility and does not solely exist for the purpose of sales tax calculation, so it looks more like the second example now.

0

This seems like a contrived example; unfortunately such examples don't work for the SRP because SRP can only be usefully applied to problems which have real, tangible requirements and involve or affect real people. (Whether those people are other developers, users, product owners, business owners, customers, testers, etc.)

There's a good summary about identifying 'reasons to change' (responsibilities) from Robert C Martin here: https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html

Keep in mind that "reason for change" is a "reason for change.. in the future" (some date in the future, possibly long after the code has been released into the wild and a developer is revisiting it for the purposes of maintenance or enhancement).

To that end, the main prerequisite for being able to apply SRP is having a concrete understanding of the business requirement(s) and expectations of your stakeholders for the software or module you're creating. Only someone with that knowledge/understanding can usefully project the future and conclude the best way of dividing different responsibilities or reasons to change.

There's no guarantee you'll always get it exactly right, but without that understanding you really have no hope of projecting how some code may need to change later on. For really contrived, abstract code examples, "future changes" are probably Not-Applicable.

The real importance of SRP is in the phase before you attempt to write any code or think about its design or structure; your focus should be on fully understanding the rationale behind the requirements you're trying to implement, the needs/motivations of those who will use or be affected by the solution, and also consideration toward exactly how you're going to test and verify the requirements are fulfilled and expectations are met. (preferably with as much automated testing as you can realistically afford) SRP encourages this line of thinking as that's the best way to be able to identify where different responsibilities are and how they divide up by projecting the future.

Otherwise, in regard to contrived, abstract examples; The best you might find are answers based on something that will never come to pass (i.e. contrived code doesn't have any real stakeholders nor a future, and won't make it into any real software; it will never change since it exists only for the purpose of illustration, so to answer someone will have to invent some arbitrary reasons to change). This leaves complete finger-in-air guesswork about what a contrived example might possibly represent without any context, and such guesswork won't help when you're faced with a real problem with real requirements and stakeholders.

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.