3

During code reviews a couple devs have recommended I break up my methods into smaller methods.

Their justification was (1) increased readability and (2) the back trace that comes back from production showing the method name is more specific to the line of code that failed. There may have also been some colorful words about functional programming.

Additionally I think I may have failed an interview a while back because I didn't give an acceptable answer about when to break things up.

My inclination is that when I see a bunch of methods in a class or across a bunch of files, it isn't clear to me how they flow together, and how many times each one gets called. I don't really have a good feel for the linearity of it as quickly just by eye-balling it.

The other thing is a lot of people seem to place a premium of organization over content (e.g. 'Look at how organized my sock drawer is!' Me: 'Overall, I think I can get to my socks faster if you count the time it took to organize them').

Our business requirements are not very stable. I'm afraid that if the classes/methods are very granular it will take longer to refactor to requirement changes. I'm not sure how much of a factor this should be.

Anyway, computer science is part art / part science, but I'm not sure how much this applies to this issue.

EDIT:

Check out: What is the ideal length of a method for you?

  • I am not sure whether I understand your question. What are you asking here? When to split up an code entity into smaller ones? Or how to deal with the perceived loss of "linearity" (more complex execution paths)? Or whether you are right in expecting that refactoring would be more of an effort? Or whether the whole suggestion of splitting up is grounded in science or just simple experience of a craft? – stakx Jun 11 '14 at 22:04
  • (That being said, I like the topic of your question. I would really look forward to a little more focus to it, and the answers it will receive.) – stakx Jun 11 '14 at 22:07
  • stakx: Basically how to navigate when to break up code and addressing the trade-offs going down the smaller or larger paths. I'm a little disappointed that no one has really tried to addressing the volatility thing or how many times a method gets called. Probably my fault for explaining it poorly. – micahhoover Jun 12 '14 at 20:32
4

It's closely tied with the idea behind unit testing ... where "unit" is a hotly contested and poorly defined term. Basically, if you start chunking your code up, at what point do you hit nice little bundles that behave in unsurprising ways? It's nice to exercise your conceptual units before they get blended into the whole.

In OO, it's also guided by the Single Responsibility Principle, which bascially says that each class should do one thing and one thing well. That is, you should be able to think about each thing in your system easily without having to resort to using your fingers and toes to count.

At the core, I look at my code and try to identify distinct ideas. Usually these ideas have some properties, and a set of operations that make sense on them. If the idea really is just an idea, then the odds of it being useful in more than one place go up.

When you've defined code around the data and operations related to core ideas, you can start putting your clean, sharply-defined ideas together in a way that turns into a big messy program.

Let's say you're writing a Sock Drawer app. Some of the ideas that you'll need to model are: a dresser, a drawer, a sock, a pair of socks. Each of those is pretty easy to define in terms of their attributes and operations you can perform on them. Once you have those things defined, written, and tested, then you can start putting them together to make a whole system.

Let's say that version 2.0 of your Sock Drawer requires a rocket-assisted conveyer belt and a black-box AI module which selects socks for you based on the weather, from all over the world. If you have your core ideas in place, then you can start wrapping them in AI logic and extended functionality. Your system can grow by adding richness in the interaction of core ideas, without fussing with the core ideas themselves too much.

...

A couple of practical exercises that will help break things up are: 1) limit the size of your coding units, 2) limit the amount of indentation you allow yourself (no more than 3 nested levels!!!!!! but try no more than 2 as an exercise) 3) follow "don't repeat yourself" religiously.

All of these are fairly objective criteria that will force you to think harder about how to find patterns and consolidate like code into cohesive bundles.

  • +1 for mentioning the way dealing with things on a higher level can help the code adapt in a faster, more understandable way. Your guidelines are objective, certainly, and perhaps a good rule of thumb. I completely agree with (3). (2) doesn't seem to work as well for cases where you have to do a lot of null checks (I consume a lot of XML, so that's my world :(). – micahhoover Jun 12 '14 at 17:51
  • Question about the v2 case you brought up: what if the new requirements involve reworking how all the internal modules relate to each other and a new dev is at the helm. Are very granular methods/classes advantageous or not in that situation? – micahhoover Jun 12 '14 at 20:35
  • If your code units are clear and have well-defined semantics (i.e. they mean something coherent), then odds are they won't change much in v2. A Sock is still a Sock. The trickiness in extending code is where different code units interact with each other. So if the logic for Sock leaks into the code for Drawer, then extending functionality will get tricky, because there's all this interaction logic that's scattered around. So, ideally, you'd want to boil things down into independent chunks that you don't have to change much. – Rob Jun 12 '14 at 20:49
  • Um, that felt long-winded. Basically, if your boundaries are well defined, and you have good separation of concerns, it makes it easier to reuse code, whether it's between systems, or between a v1 and a v2. So that argues for more granular code units. – Rob Jun 12 '14 at 20:51
  • The counter-argument to that is the OO equivalent to "Spaghetti Code," which has been called "Ravioli Code". If you make your code really small, tightly encapsulated, and granular, then your application starts to look like a big plate of ravioli, where all the delicious little chucks are sort of laying around with no clear relation to each other. ...Then again, I read one OO expert's reply to that, "But isn't that what an OO application is supposed to look like?" – Rob Jun 12 '14 at 20:54
4

When I first started coding (and still a little now) I thought much the same way as you:

Why break things apart, when it's so much easier to find everything in this one place?

Until I started getting in bigger and bigger and bigger applications. Now, writing my own content from scratch, I am seeing more and more opportunity to segment, decouple and reuse.

Mentally you have to wrap your head around the procedural (unless threadding) nature of how the computer will run your application. It's going to init, fetch, cache and output in the way you told it to, even when using objects. So if it helps, as it does for me, arrange your files visually in the way you expect them to flow logically.

For example I might have 3 files open at a time: Model, View, Template.

The tabs will be in my IDE in that order. My brain reads that as "get data", "do something with data", "display data".

Then, within each of those files the classes, and their methods, are organized in a similar way. While it's not specifically necessary to have __init__ or main() in a specific order in most languages it certainly makes sense to put that thing first when you are writing. Now extrapolate that same thought process out against the entire project.

As you are writing and find yourself saying, "Hm, I need to do xzy here." check whether xyz was already written. It if was pull that thing out of wherever it was, make it into a reusable function. Now both methods do the exact same thing and you saved yourself time. Both now and in the future when you need to do xyz again.

3

In any good language it's easy to compose two small functions into a bigger function, but there's no way to split a big function into two smaller functions. Likewise, it's easier to understand a big function in terms of what smaller functions do than it is to keep track of all the state in a big monolithic function. Human memory is at a premium; if we could reliably track large amounts of data we'd just code in assembly.

  • To me, the text of this answer is hard to understand and seems to be the opposite of reality or I'm completely reading this wrong. – stephenbayer Jun 11 '14 at 22:04
  • @stephenbayer Which part is hard to understand? – Doval Jun 11 '14 at 22:57
2

You are missing three big points. First, using well-named and well-designed smaller functions means you usually don't have to understand how all the functions flow together and how often they are called. You look at the level of abstraction you need, and don't try to hold the entire call stack in your head at once.

For example, if I'm reading an algorithm and come across number.isPrime, I have a pretty good idea what that does. I can debug perfectly fine at that layer of abstraction and only have to delve a layer deeper if I discover it is returning incorrect results. Admittedly, it's a little extra work if I do have to go there, but usually I don't.

On the other hand, if the original author didn't break out isPrime into its own function, I have no choice but to slog through that extra layer of abstraction every single time.

Second, smaller functions are easier to test and debug, because they only do one thing, and therefore are more likely to be correct. isPrime is pretty easy to test with some sample data at boundary conditions, then you can trust it after that point. If it's buried inline deep inside another function, it's difficult to isolate where any problems are.

Finally, smaller functions are more likely to be reusable. When you use smaller functions, you will naturally find places to avoid duplication, and it will be easy to consolidate. Also, the reused functions are already tested and debugged, saving you time.

It's not so much the difference between an organized or unorganized sock drawer as between a sock drawer and a knitting machine. Even if a knitting machine can spit out 10 socks per minute, that's still not the abstraction level you want when you're getting dressed in the morning.

1

Besides the other good answers above, I'd suggest you check into several studies of software development that demonstrate that highly cohesive functions are easier to understand and reason about. The more ideas that are jammed into one function/class/module the lower the cohesion. Lower cohesion (among other things) leads to lower rates of comprehension.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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