28

This doubt is about Switch Statements from Chapter 3: Functions of the book named Clean Code

Here we have a function:

public Money calculatePay(Employee e)
throws InvalidEmployeeType {
    switch (e.type) {
      case COMMISSIONED:
        return calculateCommissionedPay(e);
      case HOURLY:
        return calculateHourlyPay(e);
      case SALARIED:
        return calculateSalariedPay(e);
      default: 
        throw new InvalidEmployeeType(e.type);
    }
}

From my inexperienced point of view, I can see that the switch statement inside the function calculatePay is only returning things based on the object Employee. Isn't it doing the "One Thing" mentioned by Uncle Bob?

But Uncle Bob says:

There are several problems with this function. First, it's large, and when new employee types are added, it will grow. Second, it very clear does more than more thing. Third, it violates the Single Responsibility Principle(SRP) because there is more than one reason for it to change. Fourth, it violates the Open Closed Principle(OCP) because it must change whenever new types are added. But possibly the worst problem with this function is that there are an unlimited number of other functions that will have the same structure.

How does the switch statement do more than one thing?

Malady
  • 109
  • 4
The Chinky Sight
  • 529
  • 4
  • 12
  • 18
    The worst part of this code snippet in my opinion is none of those things listed; it's that the concept of the function itself is confused! The units don't even match up -- $/hour, $/year, and $/project are super meaningfully different. – Daniel Wagner Jun 11 '21 at 02:31
  • 2
    > *But possibly the worst problem with this function is that there are an unlimited number of other functions that will have the same structure.* Guilt by future association. How does Bob know this? Maybe this is the last release of an end-of-life codebase, in the middle of a startup that is going under. – Kaz Jun 11 '21 at 22:30
  • 2
    I don't even think this is a large method, even by Uncle Bob's standards. – Helena Jun 12 '21 at 13:09
  • It a) determines the method appropriate to calculate the result for the given employee, and b) it invokes that method? – Hagen von Eitzen Jun 13 '21 at 11:32
  • IMO, with some exceptions, I don't think you can give out *design* advice by talking about 13 line snippets of code. I think for this sort of thing you have to take into account the state of the whole system, where you expect it to be later, and how to not break backwards compatibility. Also... LoC is a very weak metric, and worse, and Uncle Bob takes a million dollar concept ("more than one reason to change") and uses it to solve a 1 penny problem (remove a switch statement); if you can answer the first super hard question, you probably don't need him telling you to remove switch statements. – jrh Jun 13 '21 at 14:12
  • @Kaz In a case like this you want a class Employee, with subtypes Commissioned, Hourly and Salaried. All behavior that varies based on how they are paid goes in the class. – Loren Pechtel Jun 14 '21 at 03:50
  • @DanielWagner: A simple name change could correct that, e.g. if this method was meant to calculate the monthly wage payments. It's not `calculateHourlyPay` but rather `calculateMonthlySalaryFromHourlyPay`, and the more concise variation can be contextually sufficient. – Flater Jun 14 '21 at 12:06

7 Answers7

60

Martin's concept of "do one thing" is overly ambiguous to the point I believe it does more harm than good.

In the passage Martin states that a switch does one thing per case and therefore by definition does N things. This implies that any single method call is "one thing". If you follow this thinking, you will quickly realize a program will never be able to do anything!

Martin has a different definition which is that a method does "one thing" when all operations are on the same abstraction level. But the cases here calculateCommissionedPay(), calculateHourlyPay() do seem to be on the same abstraction level, so this contradicts his general criticism of switch-statments as always doing N-things.

That said, there are reasons to avoid this particular use of a switch. The switch check a type-field and then selects the method to call based on that. The idiomatic way to do this in object oriented programming is to use subclasses and overloading. Employee could have subclasses HourlyEmployee, SalariedEmployee etc., which override a calculatePay() method. That way you could avoid the switch altogether and just call e.calculatePay().

But if the input really is an enum value as in the example, then you need a way to get the appropriate Employee-subclass given this enum value. How do you do that? A switch of course! So you end up with code something like this:

public Employee createEmployee(int employeeType)
throws InvalidEmployeeType {
    switch (employeeType) {
      case COMMISSIONED:
        return new CommissionEmployee(e);
      case HOURLY:
        return new HourlyEmployee(e);
      case SALARIED:
        return new SalariedEmployee(e);
      default: 
        throw new InvalidEmployeeType(e.type);
    }
}

public Money calculatePay(int employeeType)
throws InvalidEmployeeType {
    Employee e = createEmployee(e.type);
    return e.calculatePay();
}

You will notice a few things:

  1. We still have a switch which allegedly does "N-things".
  2. The switch will still have to grow when new employment types are added.
  3. We still have a Open/Closed violation, since adding a new subclass will require us to modify the switch, just as before.

But if there are multiple places in the code were we switch on the employee type as in the first example, this refactoring is a clear improvement since we now only need one single switch and can use overloading in the other places.

If it is not clear from the above, I reject the premise that switch statements are bad and should be avoided in general. Sometimes it is the right tool for the job. But certain uses of switch is an anti-pattern, for example using switch as a substitute for overloading, when overloading would be more appropriate.

JacquesB
  • 57,310
  • 21
  • 127
  • 176
  • 2
    If employees are stored in a database which a field for wage/salary classification, having a function that accepts an employee ID and generates an employee object without regard for the pay classification, and having the payment code use different logic based upon the contents of that field, doesn't seem any worse than requiring that the `getEmployee` function generate objects of different class types based upon the contents of that field. – supercat Jun 10 '21 at 19:33
  • 16
    You would probably want to do composition or interfacing for this, An employee would have a "Payscale" member which would be used to calculate pay. An employee may be subclassed, but not by "Hourly", "Salaried", "Commissionsed", instead many distinct employee types would impliment the "Employee" interface where they would define their own payscales if such a thing were so unstable between categories. Practically though, it really is composition you would use, and it is the payscale itself that would impliment the "Payscale" interface, and each employee would have a payscale as a member. – Krupip Jun 10 '21 at 19:36
  • 1
    @JacquesB Regarding your last paragraph: While that is a valid fix, it also highlights the "second thing" the switch does, namely validating `e.type`. If you replace the switch by the suggested fix, there won't be a default case anymore, so that is something the switch does extra! – hoffmale Jun 10 '21 at 21:13
  • 2
    +1 because indeed, Martin says a coupe of pages away that "*if a function does only those steps that are one level below the stated name of function, then the function is doing one thing.*" (see [here](https://softwareengineering.stackexchange.com/q/429041/209774)) and this function seems ok according to this criteria. The argument about SRP seems also doubtful, especially in view of his own explanations [here](https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html). But ok, for OCP he's right – Christophe Jun 10 '21 at 21:44
  • 1
    To amplify @Krupip's comment, if you can imagine the same employee changing from one payment basis to another, then the payment basis should not be reflected in the concrete class of the employee object, since an object cannot change its class. Use delegation instead, and call it the Strategy pattern if that floats your boat. – Mike Spivey Jun 10 '21 at 21:59
  • 1
    The "switch" is of course still there in the OO example. You just don't have to manually code it. – T.E.D. Jun 10 '21 at 22:34
  • 3
    @T.E.D.: In what way is the switch still there? Overload resolution is not implemented via a switch under the hood, if that is what you mean. – JacquesB Jun 10 '21 at 22:41
  • @JacquesB - It is, if its using dynamic dispatch. If its *not* having to use dynamic dispatch, then you probably didn't need the original switch either. – T.E.D. Jun 10 '21 at 22:44
  • 1
    @T.E.D. What... in what world is a switch required to follow a function pointer? That's literally all that would be happening there. What you said also implies that all classes indirectly know about all other classes, which is not the case. – Krupip Jun 10 '21 at 23:39
  • @Krupip calculatePay would be a virtual function, as in e.calculatePay() – Owen Reynolds Jun 11 '21 at 00:20
  • 1
    @OwenReynolds why would there need to be a switch statement over all possible classes involved in a virtual function call... each class has their own V-table... it should be able to immediately get the function from the table. – Krupip Jun 11 '21 at 00:55
  • 1
    @JacquesB The `switch` is still there because virtual dispatch is logically a form of branching on an object's type. (E.g. if I really wanted to I could replace all booleans with a specially designed class + subclasses and all `if`s with virtual method calls.) The implementation has nothing to do with it (and likely doesn't actually contain such a branch). – HTNW Jun 11 '21 at 01:03
  • 2
    For the OO solution, you're assuming that calculatePay belongs on the employee object. It could, but more likely pay isn't a concept of an Employee, and is a concept of a pay subsystem. In which case that logic in no way belongs on the employee class And it would require the instantiator of the class to know which type of employee to instantiate. Depending on the purpose of the program that may be ok, but I'd say more commonly it isn't. – Gabe Sechan Jun 11 '21 at 03:09
  • As @GabeSechan hinting you'd more likely use the visitor pattern rather than moving "calculate salary" into a "person" class... that also resolves "this switch does multiple things" by making it to do just dispatch rather than dispatch + calculations. – Alexei Levenkov Jun 11 '21 at 03:53
  • 2
    @JacquesB In the book "Clean Code" Uncle Bob used the `Abstract Factory` pattern to hide the implementation of the switch statement, but I really didn't get it. Why he need to hide it? and why it is a bad thing to use switch statement at the first place? – The Chinky Sight Jun 11 '21 at 04:00
  • 1
    @HTNW: Please explain how "the switch is still there". Virtual dispatch is just following a method pointer, there is no branching involved. – JacquesB Jun 11 '21 at 10:29
  • 2
    @JacquesB Didn't he spell out exactly why he thinks so? _"virtual dispatch is logically a form of branching"_, meaning its behaviour is dependent on some variable. _"The implementation has nothing to do with it"_, meaning he's not talking about the implementation. – Passer By Jun 11 '21 at 10:32
  • While I wouldn't go as far as to say using a switch as you show here is always a bad solution, you don't *need* a switch. A map would do nicely as well. With type-safe enums, you could add the construction to the enum definition as well. For this example the difference is negligible but if you were building a truly extensible system, you should avoid enums and use a map of keys->constuctors instead. – JimmyJames Jun 11 '21 at 21:09
  • 1
    Could we interpret the advice against switches as an argument against branching in general - for performance reasons (branch prediction penalties) *and* code cleanliness? Seems like an extreme way to contextualize branching, but I think it makes *some* sense. –  Jun 11 '21 at 23:39
  • @JimmyJames: I don't think there is a coherent argument against `switch` in the first place. There is a argument against `switch` in cases where overloading is more appropriate, but in that case using a map are some other contrived solution would be just as inappropriate. – JacquesB Jun 14 '21 at 11:50
  • 1
    @CtrlAltF2: A switch will be faster than overloading in almost all cases. But I don't think micro-optimization arguments are really valid anyway - I suspect the difference would be almost immeasurable. Unless you are writing an OS kernel, you should leave microoptimizations to the compiler. – JacquesB Jun 14 '21 at 11:56
  • @JacquesB Again, I'm not saying a switch is always inappropriate, it's just that your wording implies that it's inevitable that you will need a switch. If you are creating an extensible solution, a switch is a really poor choice. For example, take something like GSON where you register types to (de)serializers. If that was handled with a switch, the only way to extend it or change the associations would be to override the entire switch and reimplement the built-in type support that you wish to retain. – JimmyJames Jun 14 '21 at 14:13
16

The calculatePay in your question is doing indeed too much:

  • It extracts the type of the employee.
  • It calls calculateCommissionedPay in one case.
  • And it also calls calculateHourlyPay in another case.
  • It also knows about calculateSalariedPay.
  • And finally it handles the default case by constructing and throwing an exception.

The nice thing is that calculatePay doesn't need to do all that. If the application was architected correctly, there wouldn't be any Employee.type. Instead, there would be classes like ComissionedEmployee, HourlyEmployee and so on which implement the interface Employee. The interface would have a calculatePay method, and the different classes will provide the actual implementation of this method.

This is essentially what Replace conditional with polymorphism section in Refactoring by Martin Fowler is all about: relying on polymorphism instead of picking an execution path based on the type of an object.

This change would reduce calculatePay to one line:

public Money calculatePay(Employee e) {
    return e.calculatePay();
}

Then, it can be removed: the caller of this function can call Employee.calculatePay method directly.

This is essentially the example you see in Clean code in Listing 3-5, on the next page.

Arseni Mourzenko
  • 134,780
  • 31
  • 343
  • 513
  • 35
    Clearly that is Bob's preferred solution, but in my view his five complaints about that function are rhetoric hyberbole and all boil down do more or less the same thing. Sure, when you can replace a `switch` with dynamic dispatch, yea! - but that isn't always possible, and in many cases a judicious `switch` can absolutely be the right choice. (Usually when there *aren't* a lot of other places with the same `switch`.) – Kilian Foth Jun 10 '21 at 12:31
  • 2
    @KilianFoth: agreed, `switch` can be a valid option in other *cases* (no pun intended). The question, however, is about the specific piece of code. Do you believe that the function is well-written, and that having `calculateCommissionedPay` and `calculateHourlyPay` and `calculateSalariedPay` is a good choice? – Arseni Mourzenko Jun 10 '21 at 12:53
  • 16
    It almost certainly isn't, since Bob introduced it in the context of refactoring, and using dynamic dispatch instead *is* possible. **But you can't really decide this just from the example.** "Doing too many things" is *not* what makes the code bad. I once wrote a 500-case switch (in the middle of a text-to-TeX converter), and any attempt to hide this behind a nice object-oriented hierarchy would only have turned a plodding-but-okay bit of code into a festering monstrosity. The point is really that the code is using `switch` where an alternative solution is *clearly superior*. – Kilian Foth Jun 10 '21 at 13:17
  • 1
    @KilianFoth: I see. I completely agree with you then. – Arseni Mourzenko Jun 10 '21 at 13:32
  • 1
    Maybe worth adding the link to the free web version for people who don't have a copy of Refactoring to hand: https://refactoring.com/catalog/replaceConditionalWithPolymorphism.html – bdsl Jun 10 '21 at 13:34
  • 1
    Most likely the `Employee` object will be an entity and it will not have the required services to calculate its own pay.. And even if you go this route, your Employee class will now become smelly.. switch statement in a service is better IMHO. – Koray Tugay Jun 10 '21 at 18:36
  • 3
    @KorayTugay: If employees of all type are stored in a common database or other such entity, and retrieved via common function, having a single class of employee and a switch in a payment calculation function seems cleaner than requiring that the code which generates an object based upon information in the database include special-case logic to handle different pay classifications. – supercat Jun 10 '21 at 19:26
  • 2
    @supercat: If you don't want to subclass `Employee`, you could create a separate `PayType` class to handle the special-case logic, and have each `Employee` object contain a reference to the appropriate `PayType`. – dan04 Jun 10 '21 at 23:49
  • @ArseniMourzenko I don't get it, why we used avoid using statement at the first place? and hide it's implementation like you used the Factory Pattern to do so. What is wrong with the switch statement? – The Chinky Sight Jun 11 '21 at 04:13
  • @dan04: And how is each Employee object supposed to magically acquire that reference? If one uses an enumeration type that maps to explicitly listed integers, or else manually defines integer constants for all of the different pay categories, the code that reads and writes the records won't need to know or care about any of the different pay types. – supercat Jun 11 '21 at 04:44
  • 1
    @supercat If one builds appropriately-typed objects out of the records (nothing "magical" about that) then only the code that reads and writes the records needs to know or care how such things are represented in the database. – Aetol Jun 11 '21 at 08:00
  • 1
    @ChinkySight Because elsewhere we might need to perform specific behaviour based on the type of employee. Without a factory, we need to carry the switch\if with us everywhere. – CaTs Jun 11 '21 at 08:12
  • @CaTs Why we should avoid using switch statement ? – The Chinky Sight Jun 11 '21 at 09:17
  • 2
    @ChinkySight If your compiler/language does not warn you about switches with missing cases, then you need to be careful about using switches like this. The main OOP languages (java, c#, etc) did not traditionally have such compiler checks, so the fear is that you have 12 switches on employee type scattered in the code, and then you add a new employee type but only update 11 of the 12, thus the 12th switch might have really unexpected behaviour. Because of this very real danger, OOP purists in those languages regard switches with much suspicion. – Graham Jun 11 '21 at 16:53
  • @CaTs: On the flipside, if there are multiple applications which read data from the database, and most of them wouldn't care about how different pay classifications are handled, the programs that don't care about such things shouldn't need to include the source code responsible for dealing with them. Having a means by which a "pay classification handler" library can add callbacks into a table indicating what special handling is needed for various pay classifications may allow a nicer program organization than using switch statements, but such an approach has some issues of its own. – supercat Jun 11 '21 at 17:45
  • @Aetol: I meant to ping you to the above. – supercat Jun 11 '21 at 17:46
  • 1
    This is just passing the buck! How do you write e.calculatePay? – user253751 Jun 11 '21 at 21:07
  • While I would agree that the `switch` in the example is a good chance to look at the code and say "maybe this should be implemented another way", I wouldn't say it's a "smoking gun". This example is far too small and missing a lot of context. Making a whole new class hierarchy is its own design decision and has its own cost/benefit analysis, I don't think there's enough context to say for sure whether it's worth it right now. E.g., I would say that if this `switch` would be repeated multiple times and conceptually the interface makes sense, your solution might be a good idea. – jrh Jun 13 '21 at 14:05
12

Everyone complains about Robert Martins "one thing". Let me try to demystify it.

calculatePay is one thing. Just the name is enough to set us on the path of expecting one thing. When I look inside this I expect to find things that calculatePay.

These seem like they're focused on our one thing: calculateCommissionedPay(e), calculateHourlyPay(e), and calculateSalariedPay(e) and indeed they help us get there but these are not focused on the whole of our one thing. They are smaller cases of it. They're details. We just want to calculate pay. We don't care why you're payed what you're payed. We just want to calculate it.

So what if e had e.wages() and e.bonus() hanging off of it? Could we add them together here or are those evil details? Well when wouldn't you add them together? This isn't some weird part of calculatePay. This is calculatePay.

One thing isn't something that can't be decomposed. That's what's really wrong with the example Mr. Martin gives. It makes it seem like each function should only be one line. Which is nonsense. One thing means this code should tell me one simple understandable story without dragging me through weird cases and up and down through wildly different abstractions. Do the work it takes to make the function simple enough to look like it's only doing one thing. Even if e.wages() is hiding tons of polymorphism, nasty switch cases, or 100 different levels of object oriented abstraction. At least here it's just e.wages() that you can add to e.bonus(), whatever that really is.

candied_orange
  • 102,279
  • 24
  • 197
  • 315
  • 5
    "One thing isn't something that can't be decomposed", that's on the spot! The issue with "one thing" is that ["thing" is ambiguous and it's rarely "one"](https://softwareengineering.stackexchange.com/a/429044/209774). The accounting point of view counting the operations gets us lost in details when doing functional decomposition, whereas he could simply speak about one goal. – Christophe Jun 10 '21 at 21:54
  • 3
    If you were only allowed to call methods that did the whole of the one thing, you'd never be able to write any useful methods at all! The longest method you could write would be `Money calculatePay(Employee e) {return internalCalculatePay(e);}` but then how would you write internalCalculatePay? - you'd have the same problem! – user253751 Jun 11 '21 at 21:06
  • 1
    @user253751 yes that's exactly what I'm pointing out. But Uncle Bob never said you couldn't decompose. What he did was give us an example that made people think that. Which is why we keep talking about this. – candied_orange Jun 11 '21 at 21:09
  • Martin = Robert C. Martin = [Uncle Bob](https://en.wikipedia.org/wiki/Robert_C._Martin) (*not* [Martin Fowler](https://en.wikipedia.org/wiki/Martin_Fowler_(software_engineer))). – Peter Mortensen Jun 12 '21 at 19:37
9

Uncle Bob's criticisms are mostly valid, but his proposal to make payment calculation a virtual method tied to the employment type makes the code less flexible.

Having the pay calculation depend on a piece of data rather than on type, makes it easier to convert employees. For example, if a commissioned sales person gets promoted to a salaried management position, do you really want to construct a new employee and destroy the old one? Or would you rather change one field in the existing employee record.

Better Option 1 -- Composition

Instead of coupling the payment algorithm to the employee type, tie it to the employee's wage type. This still solves the problem with polymorphism, but it keys off of mutable data field rather than coupling it to an employee type (which is much less malleable).

class WageStrategy {
  public:
    virtual Money CalculatePay(const Employee &e) const = 0;
};

class SalaryWages : public WageStrategy {
  public:
    Money CalculatePay(const Employee &e) const override;
}

class HourlyWages : public WageStrategy {
  public:
    Money CalculatePay(const Employee &e) const override;
}

class CommissionWages : public WageStrategy {
  public:
    Money CalculatePay(const Employee &e) const override;
}

class Employee {
  public:
    Money CalculatePay() const {
      return m_wages.CalculatePay(*this);
    }
  private:
    WageStrategy * m_wages;
};

Better Option 2 -- Parameterization

Is it worth having independent implementations of wage calculations? If the company comes up with a new compensation plan (e.g., small base salary plus commissions), so you really want to create another type? Do you like having to search all over the code to know all the different ways wages can be computed? What if we just parameterized the business logic like this:

Money CalculatePay(const Employee &e) {
  Money pay = 0;

  pay += e.GetBaseSalary();
  for (const auto &sale : e.GetSales()) {
    pay += e.GetCommissionRate() * sale.GetTotal();
  }
  pay += e.GetHoursWorked() * e.GetHourlyRate();

  return pay;
}

No switch statements. No polymorphism. Data-driven so you can effectively create new pay strategies (especially hybrid ones) without code changes--that's even better than open-closed. A single path of execution provides clarity and simple code coverage testing.

Adrian McCarthy
  • 981
  • 5
  • 8
  • Totally agree, option 1 would be my solution of choice. One should favor composition over inheritance and having different approaches to solve the same problem (calculate salary) screams strategy pattern. – Thomas Hilbert Jun 12 '21 at 00:13
  • Is the plus in `pay += e.GetBaseSalary();` just for consistency? Or is there some subtle design-principle involved? Maybe in case a line is added before that line? – Cyclops Jun 13 '21 at 16:32
  • @Cyclops: Yes. It also allows the lines to be re-ordered without having to make subtle changes for correctness. – Adrian McCarthy Jun 13 '21 at 20:18
3

While I generally don't agree with Martin on this point, here's one way of looking at the situation which might be useful:

"This piece of logic acts as a gatekeeper." In an out-of-the-way place where you might never think to look for it.

This piece of logic, "although it is not part of an Employee object," is nonetheless cognizant of the employee-type, such that it determines which one of several "fairly beefy" subroutines are to be called.

So – if you're "chasing a bug," and you think that it must be located in one of these routines, there's one critical piece of information that you might be missing: "under what conditions are they actually called?" This piece of logic ties all of this to the value of e.type, but it's external to the place where such logic properly belongs: "the employee object."

Likewise, these various subroutines really ought to be methods of that same object, so that it's now possible to open "just one source-file ... the one which implements this object ... and to see everything that you need to know to understand it all, and to know that you have actually done so."

Mike Robinson
  • 1,765
  • 4
  • 10
  • Martin = Robert C. Martin = [Uncle Bob](https://en.wikipedia.org/wiki/Robert_C._Martin) (*not* [Martin Fowler](https://en.wikipedia.org/wiki/Martin_Fowler_(software_engineer))). – Peter Mortensen Jun 12 '21 at 19:41
1

I understand where you're coming from, but in this particular case, you're focusing on the wrong thing. The main point he's trying to illustrate in this section is that this should be treated as a code smell (a strong indication of possible unwanted coupling) because there's likely a bunch of other functions in the system that have (literally or conceptually) the same switch statement in them. He lists, as a hypothetical example, isPayday(Employee e, Date date), and deliverPay(Employee e, Money pay), stating that there are other functions like that in the codebase. They all switch on employee.

This kind of coupling is extremely common in the real world.

And it's bad because you have to hunt down and change all of them if, say, you need to introduce a new case (or if you face any kind of change that requires the same kind of behavior to be repeated in all of these places). And nothing guarantees that these changes won't propagate outside of those functions. This is how you end up having to modify 20 different files because of a "simple" change.

The main point is to DRY out this (literal or conceptual) repetition; the book goes on to suggest to solve this by confining the switch statement to a single factory that returns a polymorphic result.

The "does more than one thing" is an offhand remark in this section of the book, probably best interpreted in the context of this statement at the start of the section: "By their nature, switch statements always do N things" - presumably because of their N cases.

So, sometimes it calculates a commissioned pay, sometimes a hourly pay, and sometimes a salaried pay. Building upon our discussion here (about the idea that implementations shouldn't be jumping levels of abstraction), while these are all at the same level of abstraction, they are conceptually distinct things: they aren't a group of functions that together achieve one overall task (they don't form steps of an overall operation), instead they are largely operationally unrelated, operating on a case-by-case basis1. As Daniel Wagner noticed in a comment to your question: "$/hour, $/year, and $/project are super meaningfully different"; these two problems are connected.


1 The concept of single responsibility is a rule of thumb intimately associated with the concept of cohesion. Cohesion is a measure of how closely related things in a module (function, class, component, ...) are. What you want is internally strongly cohesive, but externally loosely coupled modules (functions, classes, components, ...). If elements are not strongly related (and change for different reasons), as the codebase evolves, you (1) get internal coupling that makes changing each thing independently hard, but also (2) it's likely that elements that are related to each thing in that particular module were spread throughout several other modules (so, there's external coupling). The strongest form of cohesion is when there's an operational relationship - a number of functions that combine to accomplish a single overall task. Here you don't have that, you have logical relatedness instead (they all do the same sort of thing), and that's not a strong form of cohesion.

Quote from (a):

Another wording for the Single Responsibility Principle is:
Gather together the things that change for the same reasons. Separate those things that change for different reasons.

Sources:
(a) The Clean Code Blog: The Single Responsibility Principle,
(b) SRP: The Single Responsibility Principle (incidentally, this is the broken link from the book)


Depending on the language, you can't always fix all of the code smells, but sticking a switch statement into a factory turns this from a design that causes a proliferation of the same switch statement to something that converts a key into a specialized object focused only on one particular case, an object that you can pass around your application for later use by more self-contained components.

P.S. "Second, it very clearly does more than one thing." - I guess another takeaway from this is, if you're trying to explain something to other people (or if you're writing a book), never assume that what's clear to you is also obvious to everyone else :D

Filip Milovanović
  • 8,428
  • 1
  • 13
  • 20
0

I agree whith those answers who point out that this specific case would be handled better by using inheritance.

But on the other hand you can't rely always on inheritance. I disagree with the idea that the switch statement violates the SRP. The switch statement acts as a dispatcher to the proper action when the options are not just black and white. I don't know what was written in the book, if it was a warning against placing a lot of code within the case block instead of a single call that would be understandable. But criticising the switch only because it has more than one case seems overly fanatic, that would rule out a lot of common practices that provided a lot of clean code for many years. What would the propoents of this interpretation think of the Scala style pattern matching?

FluidCode
  • 709
  • 3
  • 10