I recently attended RubyConf 2012 in Denver, CO. I had a great time, and I learned a lot. While getting a clearer picture of the Ruby development community was interesting — particularly the tension among MRI, JRuby, Rubinius, etc. — the talks I keep thinking about were essentially language-independent.
The Insufficiency of Good Design
Sarah Mei of Pivotal Labs gave the morning address on the last day of the conference, a talk entitled “The Insufficiency of Good Design.” Her speech was all about how communication impacts code. The central premise was that the structure of code reflects the (largely invisible) communication dynamics of the team — or teams — working on a piece of software. To give an example, if you assign three teams to work on a piece of software, you will get a program with three subsystems. If you had assigned it to five teams, there would be five subsystems. And wouldn’t you know it, the areas of your code with smells will be right where those subsystems interact. I know I’ve seen this first hand. I once worked on an enterprise software product, and in many ways each sub-product was silo’ed during development, which let to a lot of redundant code and configuration options — not to mention inconsistent interfaces.
Sarah also mentioned how the same problem exists on the micro level of individuals on a team, especially if the environment does not encourage frequent, informal communication. In particular, she talked about how while having your own office is nice in some regards, it can be all too easy to disappear for days, assuming email correspondence with coworkers is sufficient, but then wind up with everyone building pieces of a jigsaw puzzle that simply don’t fit together.
I think her most insightful advice was that, when you find these smells in the code, don’t just fix them — use them to identify problems in the communication patterns on your team. She cited one example where her advice of “skinny controllers, fat models” was taken to the extreme, and after seeing some of the programmers on her team put too much logic in a model, she realized that her team was now ready for a more nuanced version of the maxim. I was really impressed by the way she treated bad code as a symptom rather than the problem in and of itself.
The other RubyConf session I keep coming back to in my mind was Gary Bernhardt‘s talk, called “Boundaries.” Rather than a discussion of how teams communicate with each other, his talk was a discussion of how different segments of the code communicate with each other, specifically with the interplay between “pristine” business logic algorithms and the difficulty of integrating that logic with the complexities of external libraries, I/O, database access, and the like. He prescribed a methodology he called “Imperative Shell, Functional Core.” His talk refined and crystalized some ideas that have been floating around in my head for a while.
He began by describing the debate in the programming world over the pros and cons of using mocks. In short, while mocks allow for isolated, fast tests and for top-down TDD, they obfuscate the boundaries between different parts of the program, and allow for programmers to trick themselves into thinking the code is working, only to have it fail in practice. For many programmers, the danger of using mocks outweighs the good.
Gary advocated putting your business logic into functional units (or functional-style code, if you are using an OO language like Ruby), and then interfacing between those units with an imperative shell. He sagely pointed out that the vast majority of interesting conditions occur in business logic, and that when done right your business logic should have very few dependencies on the outside world. Code with many conditions (i.e., many test cases) and few dependencies is ideal for unit / isolated tests. Meanwhile, the imperative shell, with its many dependencies and few conditionals, is ideal for integrated tests, where you want to make sure everything “fits” together, but where running many tests takes a prohibitively long time. I am very curious to try out this method on a future project.
That said, I can already think of several challenges to overcome. Object Relational Models (ORMs), in particular, practically beg you to use them directly for imperative purposes at any point in the code. For example, if you have an ActiveRecord model for user information, and you want to change the user’s email, generally you don’t want to create a new ActiveRecord object. But in a functional paradigm, you really should create a new user with the changed information. For me, this is yet another argument for not using your ORM directly in your business logic, but instead using an abstraction layer built on top of it.
Another potential issue is that while the functional core is intended to prevent you from having to use mocks at all, you will inevitably want to pass down a function (or an object instance of a single-responsibility class) — we are trying to write functional style code, after all. You would still have to mock in that case. You could, in theory, pass Proc’s along everywhere you need a function, and create a Proc in your test cases which returns a simple value, but that is essentially a mock. That said, not having a mock library included in your project would keep you free from the temptation to do things like
which I think both mock fans and mock haters will agree is a pretty smelly thing to do.
I look forward to putting ideas from both of those talks into practice in the months ahead. What new ideas have you encountered recently that changed the way you go about your work?