10

One of the motivations for separation of concerns is so a change in one place does not affect the other. I am going to make an argument with my limited understanding. Here is a scenario where I fail to see this benefit.

Suppose there is a method with a long switch statement. You want to change what happens inside a case clause. All you have to do is open the class and modify the logic inside the case clause. No other case clauses are affected. If you want to add an additional case, you simply append to the switch statement. I don't see what else is affected.

Now let's talk about separating the application into layers. People say that one benefit of layered architecture is that changing some database code will not affect any other class. However, the place where the class housing the database code is called, is the place where the code would've been, had there not been some abstraction (a method on a specialized class for database operations). You would have had almost the same lines of code, just in a different place. You would have made a very similar change to that code in that scenario, and no code around it would be affected.

EMN
  • 485

11 Answers11

24

I think your issue starts with your first sentence:

One of the motivations for separation of concerns is so a change in one place does not affect the other.

Em, no - that's mixing up cause and effect. Separation of concerns mainly means to split code statements which do already not affect each other into separate code sections. That makes it explicit when a certain code section works isolated from another one, and often adds some safety measures by the programming language, where your compiler or execution environment can check that the isolation holds. This will keep the sections where you expect mutual effects caused by changes a lot smaller.

To explain this with your own example: Let's say you have unlayered class with 2000 lines of code. Now you need to extend the switch statement (inside some private method) and want to reason about the code that the one place you extended is really the one-and-only you need to change. In the worst case, you will have to look through all the 2000 lines of code to make sure this is really the case.

Now imagine you made a clear separation between the database code and application logic beforehand, so you know your database code does not depend directly on any application logic. For the sake of this example, let us assume both sections take half of the original code, each one with 1000 lines of code. When you now have to extend the switch statement in the application logic, you only have to look through 1000 lines of code of the application section, and can exclude the database section.

Of course, this is a very simplified example, but I hope you get the idea. In reality, it is often hard to achieve orthogonality up to the degree where you know for sure that changes of certain types in one place will not affect other code sections. This is especially true in a horizontally layered system where layers often depend on each other or have broad interfaces. Hence, we need tests, ideally automated tests, which are executed after each change. Still, tests are no replacement for having a system structered well.

Doc Brown
  • 218,378
15

All you have to do is open the class and modify the logic inside the case clause.

In a tiny little program, maybe. In real software, however, you would also need, at least, to adapt the tests. And once you do that, you start understanding the major flaws of the large switch.

As soon as you commit your changes to version control, you might have another chance to understanding the flaw, if your colleague happened to work on the same switch and committing his changes a few minutes before you. Say, he reordered the cases of the switch, in order for the most likely ones to appear first, and now you're up for a painful merge conflict to resolve.

And then, twelve months later, you're back to your code. Someone worked on it quite a lot, and you find yourself banging your head, trying to understand what's going on in this 800 LOC method with a switch containing blocks of code that themselves have conditions and loops and even other switches inside, leading to high cyclomatic complexity. Good luck.

In order to mitigate those issues, one applies SoC principle, as well as single responsibility principle.

Say your large switch performs mathematical computations on a set. If a given variable is set to a given value, then it computes the sum. If it's something else, it would determine the mean, or the spread, or... whatever. And it is also capable to do about three dozen other operations as well.

Instead of doing all the computations (some being quite complex) inside the loop, you can create a common interface with one method: it takes the set as a parameter, and returns the result. And a lot of different implementations you can work on separately.

Now, when you work on a complex calculation, and your colleague decides to refactor the switch, no merge conflicts would happen. When you want to fix a bug in a calculation, you have a single place to do it: the class that actually does the calculation (and does host that, nothing else). And when you want to test a part of your app, you test just this part of the app, in isolation.

Sooner or later, you find that you don't even need your huge switch. Maybe a better approach is for each class to declare is capabilities, i.e. what type of calculation it is doing. The location where switch was done would therefore become a hardcoded list of all possible calculations. With the benefit of making it possible to query it, without actually doing the calculations. One possible scenario is to show to the user the list of operations he can do on a set. And the magical thing about SoC is that as the switch is doing just what it is intended to do, such change (i.e. replacing a switch by a list of types of classes doing calculations) would be pretty easy to do, as (1) all calculations would stay unaffected, and (2) regression testing in isolation from the calculations would be very straightforward.

14

If things were restricted to these few bits of code, then yes, there would be no benefit.

But in practice, there is never one method with a long switch statement depending on some value. Nine times out of ten, there are two or more decision points in the code base that depend on the same value, and chances are that whenever you add, remove or change the semantics of a value, you'll have to update all of these places. It's much, much easier to forget one, or to update them in incompatible ways, if the code related to the new value is spread out into many different places than if it's all in one place.

You might think that this is nothing but a crutch for people with bad memories or flagging attention, and that's exactly right. We all have bad memory and flagging attention compared to computers. Even though the changes still have to be made, it's a large benefit to be able to make them in a way that's less conducive to programmer error. Half of all software engineering is really about human factors and not about clever technical tricks.

Kilian Foth
  • 110,899
11

Suppose there is a method with a long switch statement. You want to change what happens inside a case clause. All you have to do is open the class and modify the logic inside the case clause. No other case clauses are affected. If you want to add an additional case, you simply append to the switch statement. I don't see what else is affected.

It's likely unintentional but you're cherrypicking your problem to fit with your argument.

Consider the scenario where every case logs a certain message (different messages for different cases), but because you haven't separated your concerns, that logging action explicitly appends it to a file on disk. In other words, every switch case looks like this:

case "anothercase":
    DoThisSpecificThing();
    DoTheOtherSpecificThing();
    File.AppendLine("Doing the specific and other specific things");

And you have many of these cases. Now, you decide that as part of your logging, you want every log line to start with a timestamp. Or, alternatively, you want to change your logging from being file-based to a console output, or storing it in a database.

You will have to change every case in order for that change to take effect, and if you forget one case, not just across your switch but across your codebase; you will have fragmented your logging approach and it won't be consistent.

If instead you had created a logger class which separated the logging concern into a separate class on its own; then this would not have been an issue. You only would have needed to change the logger's logic. Every consumer (e.g. the cases in your switch) would only be doing something along the lines of:

case "anothercase":
    DoThisSpecificThing();
    DoTheOtherSpecificThing();
    myLogger.Log("Doing the specific and other specific things");

Changing the internal logic of myLogger.Log (e.g. having it prepend a timestamp, or store the message elsewhere) does not require you to make any changes to the code that calls myLogger.Log. Regardless how many switch cases you have (in this file, or in your codebase), you are able to alter the logging behavior by only needing to alter the logger.

That benefit is one you can only achieve if you pre-emptively identified that logging is a separate concern and therefore should be separated into its own class. If you build it without separation in mind, then you will struggle once you realize that you have to make changes to this concern that is now littered across your codebase.


If you're on board with the above, then your subsequent question will likely be where we draw the line of what should and what shouldn't be a separate concern and if we should apply this process infinitely precisely.

Yes, there is such a thing as too much separation where it creates effort/complexity instead of resolving it.

No, this can't be easily answered. There is no universal deterministic measure of precisely how granular a responsibility/concern should be. This is a subjective and contextual assessment made by an experienced developer, not a rule that you can write down and expect any developer (even if a newcomer) to blindly follow and execute perfectly.

No, the lack of a universal rule is not an argument that we shouldn't bother with this separation of concerns. The value of separating your concerns is clear. Even if reasonable people can disagree on exactly where to draw that line, the overwhelming consensus is that there is such a line that we should be mindful of.

Flater
  • 58,824
7

Other folks answered about testability, maintainability, developer co-authorship, and other concepts which are all totally valid. I think another thing to consider, based on the gist of what you're asking is being able to debug a problem. Separating activities into classes and methods that do "one thing" gives you a stack trace that is usable and highly focused (in languages that support that kind of thing).

Imagine your program is built in a release mode, and doesn't have debug symbols, so now there are no hints about line number in the source code where the fault or exception occurred. You have a 2000 line long method with a massive switch statement and basically nothing to go on but a vague error message and maybe a user's testimony of what they were doing. If your switch was broken down at a minimum, into separate methods per case, your stack trace is going to start you in a better place.

K0D4
  • 423
  • 1
  • 6
3

I think the confusion stems from the fact that you're thinking about this in terms of files/folder or some material (well, almost) quantity.

A design pattern or idiom or concept mainly exists to help the human brain organise a behemoth of complexity into groups that make sense to you and other humans.

For example: separating the logging system and the domain model helps you think clearly about the flow of business data without mental noise from the secondary functions of the logging system.

Imagine trying to decipher the execution flow of a complicated mathematical function and having to mentally dodge logfile rotation code, database reconnection logic etc. Ew.

Excessive/Premature separation

The strictness of this separation is drastically different across contexts. It has to be applied at the appropriate time and to the appropriate degree.

An example: a garden-variety one-off database migration script. You open it up and on the top you see a dependency to another "logging-system.blah" - ok why? that sounds silly. Instead of reducing complexity, it simply added to it. There wasn't anything complex to begin with, so you pay a basic flat tax of a separate logging thingy for gaining nothing in return.

In this case, Separation of Concerns is at most just a log function in the same file. It strips ANSI color codes and that's just about it. No need to get fancy.

Now on the other extreme; the Space Shuttle's flaps control system. This probably needs a fairly sophisticated telemetry system (multiply redundant, fail-safe logic, this, that etc). You expect that to live in not just a separate file but a separate machine. It has so many complex requirements that you accept that it's going to feel unfamiliar interacting with it and that's perfectly fine. You don't want to anyway.

So there's a spectrum.

There is an essay by Carson Gross where he describes this concept as Locality of Behaviour; the other end of the spectrum being Separation of Concerns.

Separation of Concerns alone isn't a quality characteristic. The quality characteristic is the appropriate degree that it's applied to, bearing the context in which it's used.


Read More:

closely parallels the cohesion vs. coupling spectrum.

Also, ideally, you shouldn't need to edit a class to extend it:

nicholaswmin
  • 2,019
1

All you have to do is open the class and modify the logic inside the case clause. No other case clauses are affected.

Sometimes this is good reasoning. Separation of concerns is not always the best way to go. Abstraction can make it harder to think about the code sometimes.

But in many cases, if you separate concerns you happen to get some other advantages automatically with it.

  • Understandable Architecture: When there are many components that each do one thing, naming them is easier and gaining a rough overview only requires you to read these names.

  • Code reuse is easier: When every function does exactly one thing, you can use it in multiple places. When your function is a huge switch statement, you could still use it just for that one case, but that calling code will usually be unclear to read, because the function name is so generic.

  • Maintenance is easier: When you have a function, you can use tooling to find out where it is called from. When you modify that function, you check all places where it is used because you might have to modify the way it is called, for example. If your function handles many cases, the tooling will show many call-sites that do not matter to you at all in that moment, making it harder to modify the code.

  • Reading code is easier: A new developer comes upon a place that calls your function. They know the whole code-base is old, buggy, and unreliable. Nothing can be trusted. If you separated the concerns, then they only need to verify the case that it is handling, and not also the big switch statement in itself.

lucidbrot
  • 662
0

Look at any old BASIC program, say 300-600 lines or so in length. Run it, figure out what it does, then imagine some major-ish change that would add a new feature. See how many different areas of code you have to touch to achieve it. Early BASIC programs usually didn't have good separation of concerns in the code, so changing just about anything could potentially "disrupt" the whole program.

Sometimes the change doesn't have to be major. Say some aspect of the user dialog has to be uniformly changed. With good factoring and separation of concerns, the UI code would have a couple of layers, so that changes would apply to just one or two layers, spread across one or few dozen lines. Instead, you may end up changing single lines spread throughout the program. That'll be because one concern (UI) is not well separated from the other concern of "business logic".

0

Just touching a function has a risk of breaking things. Even when adding case 9: to the end of a switch it's possible there were existing items with a 9 which were intended to fall through and now won't. Or it could be that before the switch there was a range-check for 8 or less. Fixing to include 9 seems totally safe, except maybe it wasn't a range-check, or it was but was more complicated than it seemed.

That was actually one argument for subclasses. Pre-subclassing we were happily faking it with member functions like if animalType==1 sound="meio" else if animalType=2 sound="ruff"... . It was pointed out that the completely trivial job of adding one more animal type would sometimes break the old ones. Formal subclasses -- where we write separate functions for the new subtype, and have them automatically dispatched instead of adding if's -- make it so you never touch old code, removing a source of error.

0

When every branch of the switch statement opens a connection to an external resource to read/write changing the type of the resource implies changing each branch of the statement. That is another type of the approach of changing something then apply stress on all the involved sides until getting the intended result.

Separating the concern of peripheral communication and calling it from each branch it is going to require changes when the business needs change, presuming the business is different than implementing the peripheral communication that is an underlying mean supporting business communication.

Peripheral communication is a scenario chosen to describe the benefits of separation of concerns that could be replaced with any other concept that is a concept supporting the business. In business vocabulary there are the concepts of business verticals (the actual business) and business horizontals (the business support).

-2

Separation of Concerns and Single Responsility principles are empty buzzwords. They suggest to isolate domain areas and group your code in accordance to those. You can not. Domain would only be clear and decomposed once the implementation is done, complete. This makes both principles just a bloated and foggy application of DRY.

Let me explain how you determine "concerns" and "responsibilities" in practical terms.

  • write some code
  • notice repeating patterns, concepts or associated changes
  • once they repeat more than three times, extract them to a separate component
  • make some more changes, split the component again as needed
  • look at the components that survived

Surprise, you now isolated a concern and responsibility. With experience you can predict when code would be duplicated and can cut corners, but you can't identify "concerns" and "responsibilities" without implementing them multiple times.

Separation of Concerns and Single Responsility make sense for precogs. For everyone else, there is DRY and foresight.

Basilevs
  • 3,896