I’ve wanted to work in a pair programming environment, but it was always difficult to justify the lost initial productivity of two programmers working on the same task. When the tasks are numerous and trivial it makes sense to break them up so they can be solved in parallel. Even when an individual task is difficult, I’ve found that it can be even more challenging for two people to see the same solution to a problem. Sometimes the solution to a problem is straightforward once the problem itself is well defined, explaining the problem to another person or rubber ducky can be helpful in fleshing out those undefined details. Brainstorming sessions, sanity checks, and intermittent reviews can provide some feedback on the solution and an understanding of the problem. These are great cooperative progress checks to ensure the quality of the final solution without absorbing all of the time of the reviewers in the process.
Senior-junior pairing on the other hand isn’t so much about the constant review of the code and the solution as much as the knowledge transfer and relationship building. The mentoring aspect is a much bigger value add than any increased quality of the solution.
For all of my forumulative learning experiences, I never had a close mentor, a senior engineer willing to sit down with me and show me all the details involved in solving more than just an example problem for more than an hour or so. From my perspective, this lack of involvement wasn’t a bad thing, I got all of opportunities to try the wrong paths and discover a decent solution the hard way. I had to learn how to learn on my own, and it was tough going. I didn’t know what to think of the senior engineers, I had no idea how they did their job better than I did mine. Everyone just bashes their head against this wall of impenetrable code until the code breaks or their head does, right? There were group discussions on tool usage and engineering ‘best practices’, but not seeing a problem taken from concept stage to production by a senior engineer put a big design block in my way when understanding how problems get solved. The thought process itself was the key to getting things done. While my peers and I would discuss it at length when working on school projects and the like, translating that into the business world was slightly different, as none of us had the experiencing of working at that scope or scale. There’s plenty of good advice out there to make the jump, but nothing beats real-world examples.
I’ve had the chance to coach a few junior engineers so far, and I took the same hands-off approach that was taken with me. I monitored their progress, provided good breakdowns of problems that were easily digestible, and helped when they were stuck. We had some design discussions and some fun bug hunts, but the day to day interaction was generally focused on the high level concepts. The closest I would get to the implementation process would be to provide a some sample code as a starting point or as a test case. I helped proctor a few training sessions that sometimes required intensive interactions, but they were synthetic problems with a solution I already knew. The students never got to see my process of making a solution; I’d watch over their shoulder and ask the how/why questions when I saw something problematic in the code. The philosophy was ‘ask, don’t tell’, everyone was encouraged to have their own creative process. There were technical requirements on the final solution, but not the tools to get there. The debugger was taught, but no one cared if you used printf to do inspection. Any advise given was far too broad to be applicable to a beginner.
I’ve recently taken a more involved approach, spending a whole week, 8 hours a day, going through each and every step in a new project with an intern. This approach was not taken lightly, it was long build to where I thought this approach was necessary. I found the intern wasn’t comfortable at all with learning Python or SQL, the two major tools for this data analysis project. So the pairing was a ‘on-the-job training program’ as well as an important progression for a auxiliary project. At first I took the same back-seat approach as the training classes, only offering suggestions and not taking control of the problem. Once I saw that the stumbling blocks were all rooted in explaining the problem, I focused on getting a great mutual understanding up front of the general problem and in the specific solution we were after. This was great for the high level project goals, but it quickly became tedious to go back and forth from the whiteboard to the keyboard for the specific tasks, taking a line or block of pseudo-code into something that could be tested. I found that most problems are best explained with concrete examples, something we can both observe and adapt. Because you can’t run an algorithm’s pseduo-code, writing it out didn’t lead to any better understanding of the system if the code itself was an obstacle to understanding. Eventually I found myself in a cycle of explaining, instructing, and exhibiting. We would ask questions back and forth about the problem and what the solution was trying to accomplish. Just explaining the solution to someone helped us both understand all of the assumptions about the problem statement. It exposed how difficult it can be to be critical of how one comes to a solution, not just that the solution works. I often found myself explaining implicit assumptions I had about how the code would work, or what to keep in mind when designing very foundational logic. How would someone who didn’t know SQL know how to avoid full table scans when writing queries? You have to be able to justify each and every decision. It stopped me from going down some premature optimizations and it helped the intern understand the requirements and internals of the system. The intern learned equally as many short-cuts about how to avoid unnecessary deeply nested loops with more declarative code, or how important scoping can be to organize thoughts.
I often found that I avoided writing code when mentoring that used advanced features or relied on complex ideas only so that I wouldn’t have to explain it. Just because that’s how I envisaged the problem doesn’t mean that my first instinct is the most understandable solution. This was a big hurdle when explaining my solution to someone who didn’t have the same mastery of tools. To them it probably looked like I was skipping steps and prematurely optimizing, instead of starting with something they understood I’d jump right to what I thought was the juicy parts of the problem, leaving them confused by a solution that wasn’t described by terms they understood. My wider vocabulary of solutions doesn’t mean that I can’t use the simpler pieces, only that I now naturally think with these larger patterns. Instead of thinking about using a loop and an if statement to do something, I’m thinking about how best to structure the inputs such that I can reduce the complexity of my edge cases. There’s a big gap between a developer who thinks ‘in code’ and someone who thinks in implementation ideas. I honestly don’t think at all if something should be a for loop, a list generator or a map operation. I’ll do some of it out of habit and other parts just feel obvious based on the feel of the solution I’m pursuing.
When starting from the more basic building blocks, the tendency is to optimize the steps for performance, but leaving a suboptimal solution in terms of performance often opened up additional flexibility when revisiting the algorithm for changes. Enhancements that would have required a large restructuring if the implementation were more efficient were much easier to implement when fewer assumptions were made. I often notice similar productivity gains when comparing my functional and imperative code. Thinking in bigger pieces is tougher up front, each bite of the problem has to be a little bigger in scope than “Ok, now we need an if statement to catch this case, and now another function because this is doing something different”. This kind of insight and implementation design is super-tough to teach. It isn’t a pattern like GoF, it’s a not a technique like TDD, DDD, or AOP, it’s a higher level understanding of the problem to see the lines for more than just the bits. The highest level concepts of programming really require a set of experience that can see how to best leverage all of the tools available to find a solution that best models the problem’s domain.
To return to where this started, what I’ve come to realize is that good mentoring should be tough. Senior developers don’t mentor as much as they should (from my experience) and as a senior developer mentoring can be very frustrating because it imposes a big overhead of communication where you’ve already completely streamlined your thought process. Having to break down that thought process again appears to be very inefficient. Pairing makes it much, much easier to step through the lines of thinking that are crucial to developing a better understanding of how to solve problems. I used to think that pairing was about the code, it’s quantity, quality, and documentation. But pairing is more about building an understanding of the problem that the code is trying to solve. Bringing two views on the problem can shake out some of the most harmful assumptions that wouldn’t have surfaced if a single developer’s vision put something together that ‘made sense at the time’.
It’s clear that for pairing to be effective and efficient, there does have to be a good shared vocabulary of techniques and terms. If one developer has a very different take on solutions (even within the same language!) it could be very tough to find a solution that accommodates both. What one programmer thinks is an obvious solution could unintelligible by the other.
Pairing has led to some of my greatest introspective experiences into what I actually do every day. Working in a team doesn’t offer the same frequency or types of feedback as working one on one. It’s the feedback that’s what really helps grow skills! I’m looking to do more of this in the future!