Contrary to what the others have said, I would argue that a long public method is a design smell that is not rectified by decomposing into private methods.
But sometimes I have a large public method that could be broken up into smaller steps
If this is the case then I would argue each step should be its own first class citizen with a single responsibility each. In an object-oriented paradigm, I would suggest creating an interface and an implementation for each step in such a way that each has a single, easily-identifiable responsibility and can be named in a way that the responsiblity is clear. This allows you to unit-test the (formerly) large public method, as well as each individual step independently of each other. They should all be documented as well.
Why not decompose into private methods? Here are a few reasons:
- Tight coupling and testability. By reducing your public method's size, you have improved its readability, but all the code is still tightly coupled together. You can unit test the individual private methods (using advanced features of a testing framework), but you cannot easily test the public method independently of the private methods. This goes against the principles of unit testing.
- Class size and complexity. You've reduced the complexity of a functionmethod, but you've increased the complexity of the class. The public method is easier to read, but the class is now more difficult to read because it has more functions that define its behavior. My preference is for small single-responsibility classes, so a long method is a sign that the class is doing too much.
- Cannot be reused easily. It's often the case that as a body of code matures reusability is useful. If your steps are in private methods, they cannot be reused anywhere else without first extracting them somehow. Additionally it may encourage copy-paste when a step is needed elsewhere.
- Splitting in this way is likely to be arbitrary. I would argue that splitting a long public method doesn't take as much thought or design consideration as if you were to break up responsibilties into classes. Each class must be justified with a proper name, documentation, and tests, whereas a private method doesn't get as much consideration.
- Hides the problem. So you've decided to split your public method into small private methods. Now there is no problem! You can keep adding more and more steps by adding more and more private methods! On the contrary, I think this is a major problem. It establishes a pattern for adding complexity to a class that will be followed by subsequent bug fixes and feature implementations. Soon your private methods will grow and they will have to be split.
but I'm worried that forcing anyone who reads the method to jump around to different private methods will damage readability
This is an argument I've had with one of my colleagues recently. He argues that having the entire behavior of a module in the same file/method improves readability. I agree that the code is easier to follow when all together, but the code is less easy to reason about as complexity grows. As a system grows, it become intractable to reason about the entire module as a whole. When you decompose complex logic into several classes each with a single responsiblity, then it becomes much easier to reason about each part.