Why A Unicorn Dies Every Time You Test A Private Method
And other thoughts
This weekend has been awesome in that I've eben able to spend a lot more time thinking about coding than coding.
I've been watching a lot of the Uncle Bob videos lately, and learned a ton. It's so neat to me to learn to be a better rubyist from watching videos about writing Java code. And really, it makes me understand that I'm neat trying to be a better rubyist, I'm trying to learn to become a better software engineer. I guess it's like how, as a chemist, reading things about physics or biology can make you a better scientist, and able to think more effectively about chemistry and the questions to be asked there.
In any case, I've been thinking a lot, lately, about this idea of "Functional Core, Imperative Shell", and about functional code in general. And I think that I've at once resolved (at least for myself) the "tension" between writing, say OO and functional code, and simultaneously come to understand the assertion:
Well-factored OO code tends to read like a bunch of buck-passing. Drilling down a stack trace can be a bit maddening - I asked this controller to do this, but instead it punts out to this model, which hands it off to a collaborator, which hands it off to a service object, that finally does a bit of work.
It's great to have "thin controllers and fat models", and you want your objects to "Do one thing and do it well", so that makes sense. You need to find the proper place for that responsibility to live. But finally, that object is going to DO SOMETHING. Those other objects were sending messages back and forth to each other, and that's "good" object-oriented code. But then one of them will finally INVOKE A FUNCTION and do some work. Functions do work, objects send messages to each other. Duh.
But in ruby, both of those things look the the same:
def message_received_from_a_collaborator do_some_work end def do_some_work score += 1 end
And a lot of times, I've struggled to decide on a technique for organizing these in my files. I've seen the question asked in ruby mailing lists, and the only responses I remember have been, "scoff you should never have so many methods in a class that you would need to THINK about organizing them," and "I don't know, I've struggled with that myself, and have settled on alphabetically". Holymoly. And then, I have taken that and gone and organized my classes so that the method definitions were alphabetical. Ack!
But, finally learning about interface segregation, and Uncle Bob's thoughts on form, I have a better strategy for that. And I know that first of all, anything that is not part of that object's public API should be private, and that functions should appear basically sorted by layers of abstraction - so as you drill down into gory details, you get lower and lower in the file. Awesome! Much better than, "Keep splitting your classes until you don't even know where you started" or alphabetical.
And then this realization is tightly coupled with TDD and Sandi Metz's talks and writings about how you should test your objects especially that you should look at each object as a capsule with well-defined boundaries and you should only test as things cross those boundaries. She argues that of course you should not test private methods as a general practice, but that sometimes they will remain, and a handy practice is to mark the tests as, "delete this test as soon as it fails."
And this made some sense to me, but that hadn't really coalesced until today when I was watching the Uncle Bob video about TDD where he is literally turning an awesome tri-colored hat as he goes from red, to green, to blue (refactor) and it was while thinking of that that I realized that the reason you should never have a test of a private method (thus causing the unfortunate death of a noble unicorn) is that private methods are never born during the "red", or "green" phase of development. Private methods are born during refactoring. You wrote failing tests that all interact with the public interface of that object, and then got them green (possibly while working in that external-facing method) and then once you were green, you refactored well and segregated your interface.
I realize that a lot of the times that I've wanted to start testing a private method because testing the public methods that call that method is TOO HARD. Why is it too hard? Because I have a system that's too tightly coupled. This system is too tightly coupled and hard to test precisely because it was not test-driven to begin with. And despite the fact that over the years that this codebase before me where the developers ran rcov on every test pass and made sure that they had "good" test coverage, I've got a tightly-coupled, non-segregated mess.
I've encountered this a few times now (either walking into a new codebase or revisiting an old one) and tried "THE BIG REWRITE" enough times (though on thankfully small scales) to be convinced of Uncle Bob and Micheal Feathers (and probably loads of other smart people) positions that it's really a great idea most of the time and it seems to be that the only way to get from here to an excellent codebase that I'm very proud of is to start today, make no excuses, and be discpilined about every line of code I write from here on out.
That, to me, is Super Pumpup.