How do you think when you're coding?
Talk presentation
Software engineers spend a lot of time "in a code environment" for various reasons (along with time writing design documents etc, of course). How much attention do you pay do how you spend this time?
Do you think and act the same way when diagnosing a problem as when fixing it? How about when you're writing a prototype vs when you're writing a production feature? Does hobby coding feel like professional coding? In this talk, Jon will reflect on how he thinks when coding – not as a directive for you to do the same thing, but as an example of how you might think about coding, encouraging you to reflect on your own practices. What works really well? Which practices are "nice to have" and which ones are crucial, leading to real problems if you ignore them? Expect to have more questions than answers by the end of the session - but questions that you'll need to ask yourself and your team.
- Jon Skeet is a Staff Developer Platform Engineer at Google, working on Google Cloud Platform client libraries for .NET, working from home (or rather, his shed) in Reading
- He's best known for contributions to Stack Overflow as well as his book, C# in Depth. Additionally he is the primary maintainer of the Noda Time date/time library for .NET
- Outside of software, Jon is a committed Christian, and enjoys theatre (particularly musical theatre), playing board games, and spending time with his amazing family
Talk transcription
Thank you for having me today. As an introduction to this talk and to provide some context for our discussion, the introductory remarks touched on my Christian faith and interest in theater. These aspects have seamlessly merged with my work in software over the past few years. Specifically, I've been responsible for managing the audio-video system at my local church, immersing myself in the intricacies of AV technology. While I continue my regular work, I've dedicated my free time to reflecting on my thought processes during coding. Today, I will delve into how I approach coding and thinking, hoping that my insights may resonate with you. It is important to note that I don't intend to impose my practices on you; rather, I encourage you to contemplate your own coding mindset.
The objective is for each of you to consider how you approach coding, drawing connections between the ideas I present and your own experiences. Our differences in thinking are natural, and I want to emphasize the importance of being aware of and respecting these distinctions. Collaboration becomes more effective when you understand how your colleagues work. Reflecting on successful and challenging experiences is crucial. Having delivered this talk multiple times, I've become more conscious when my actions deviate from my best practices. This awareness allows me to make adjustments, ultimately enhancing productivity.
It's essential to recognize that everyone has unique preferences and methods of working. Some may thrive in focused, uninterrupted tasks throughout the day, while others prefer switching between different activities. Understanding your colleagues' diverse thinking styles can foster a more collaborative and productive work environment. Personally, I struggle with genuine multitasking. Attempting to engage in conversation, review a document, and write code simultaneously is counterproductive for me. Even listening to a podcast while performing a seemingly straightforward task proves challenging, as I often find myself unable to absorb the podcast's content. Therefore, I've come to acknowledge my limitations in true multitasking.
But I'm very adept at quick context switching, a skill that some find challenging. I tend to work on numerous tasks simultaneously, both in my professional and personal life. To manage this, I maintain a task list comprising various small tasks. The idea is to complete each task in a single session, allowing for potential context switches to entirely different activities. However, it's crucial to ensure that I fully understand the task at hand and what it entails. I operate in different modes, though not in a formal, documented way. These modes include learning, exploring, and understanding, where the focus is on enriching knowledge rather than producing source control-worthy content. On the other hand, fix, enhance, and clean up modes lead to pull requests and contribute to the codebase. The rest of this talk will delve into these six modes, discussing their goals, best practices, thought processes, and expected concrete artifacts.
The question might arise: why bother thinking about these modes? From my experience, working in this manner results in a code base with a clean commit history, which is significant to me. I avoid merge commits, opting to rebase instead, as I value a commit history that provides useful information. Commit messages like "WIP" (work in progress) or commits with multiple changes are considered unhelpful. Organized and disciplined work allows me to maintain a clean commit history by focusing on one task at a time.
To facilitate this approach, I keep a detailed task list. The attached example illustrates my current personal life task list, with a more extensive notebook dedicated to my professional tasks. If I encounter a bug while working on something, I add it to the task list instead of addressing it immediately. Similarly, if I come across an intriguing link while learning, I note it down for future exploration. Personally, I find satisfaction in ticking off completed tasks, and it provides a sense of achievement. At the end of the week, I compile snippets summarizing my weekly accomplishments for work.
This makes it much easier for colleagues to see what I've been up to, especially with a task list. I typically begin the week with a fairly extensive task list that grows as the week progresses. This allows me to inform my colleagues about my expected activities for the week. Now, let's delve into the specifics of these modes. The first mode is the learn mode. This occurs when I'm exploring a new language feature in C# or something in ASP.NET Core, such as a framework feature. It could involve learning an entirely new framework or investigating a new protocol. For instance, my work with digital audio mixers for the church AV system has exposed me to both well-documented and less-documented protocols. In this mode, I rely on existing documentation or other resources.
The best practices I follow, and I recommend you discover your own best practices, generally involve starting simple and gradually building a solid core of knowledge. It's common for individuals to attempt to progress quickly, such as trying to create a mobile game without a foundational understanding of the programming language being used, be it Java or C#. While the goal of creating a mobile game is admirable, it's not the most productive approach when you're still learning the basics of a language. There's a significant amount of setup involved, and trying to incorporate advanced concepts like polymorphism into a mobile game can be overwhelming. Therefore, starting with a simpler project, like a console app, is beneficial, allowing you to focus on the fundamentals before venturing into more complex applications.
As much as possible, try to immerse yourself in a distraction-free environment when learning. If you're exploring a specific GUI framework or learning how to develop for mobile, that's perfectly fine. However, avoid the pitfall of attempting to learn multiple language features simultaneously while tackling a broad topic. It's easy to get overwhelmed, so focus on one aspect at a time. Separate and organize your learning goals. For instance, if you're learning mobile app development, don't simultaneously tackle a dozen new language features. Take notes using a platform like Google Docs or another online note-taking tool. These notes are for your reference, and they don't need to be overly formal. They should make sense to you, especially when revisiting them in the future. I highly recommend finding resources that suit your learning style. If you prefer written tutorials, opt for web pages or physical books. If you learn better through visual aids, consider video tutorials. Choose resources that provide clear explanations and code examples. Be cautious of bad resources, such as code that doesn't work or tutorials with numerous warnings.
When in learn mode, understand the purpose of the resource and ensure it aligns with your learning goals. If you're learning a language feature, break it down into manageable sections. For example, if you have bookmarks for different aspects of learning a language feature, understand each bookmark's specific focus before moving on to the next one. It's crucial to have a clear understanding of what you're trying to learn from each resource. Official resources for frameworks or languages are often more reliable than experimental ones. They provide a solid foundation and are less likely to introduce confusing or incorrect information. Ultimately, adapt your learning approach to what works best for you, and don't hesitate to invest time in finding quality resources that align with your learning preferences.
Learning is an iterative process, and it's essential to reinforce what you've learned rather than contradicting it. While skimming over similar content, ensure that it aligns with your previous knowledge and provides a deeper understanding. Fundamentals are crucial, but it can be challenging to determine which ones are relevant until you have a better grasp of the overall topic. Be cautious of going down rabbit holes where you invest excessive time in niche topics that may not be immediately applicable. On the other hand, skipping over fundamentals can lead to confusion later. Striking the right balance involves covering essential fundamentals while identifying areas you can temporarily skip.
To check your understanding, move beyond copying and pasting examples. Experiment with variations, such as using your own data or altering the user interface. This approach allows you to encounter and resolve challenges in a controlled learning environment, preparing you for real-world scenarios where shortcuts may not be viable. In learn mode, focus on comprehension rather than completing a task. If something doesn't work as expected, use it as an opportunity to explore why and discover better alternatives. Unlike production mode, where completing a feature may take precedence, learning mode encourages in-depth understanding and problem-solving.
The outcome of learn mode may include throwaway code—simple examples that illustrate the most basic functionality of a concept. Later on, I'll discuss the approach to keeping code, but for now, consider these throwaway examples as a means to understand and experiment with concepts, such as creating the simplest WPF application or implementing basic data binding without involving a database. When you're investigating MVVM as a pattern or exploring other concepts, it's crucial to go beyond mere copy-pasting of examples. Write code yourself, take notes on initially unclear aspects, and document your improved understanding. Don't lose valuable insights—anything you've worked hard to comprehend should be noted down for future reinforcement. Additionally, maintain bookmarks of useful resources and books you've found beneficial. This compilation of resources will be valuable for further exploration and investigation.
Now, let's transition to the second mode: explore mode. While it may share similarities with learning, explore mode is distinct. It involves delving into uncharted territories where resources are scarce or nonexistent. This could mean reverse engineering a protocol or figuring out how to integrate two frameworks that haven't been combined before. In explore mode, you're essentially navigating in the dark, hoping to illuminate the path. During exploration, it's essential to validate your findings as accurately and early as possible. Keep a record of everything you try, as an initial attempt that doesn't work might lead to valuable insights later. Using a source control system like Git liberally is highly recommended. Make frequent commits, even if they are not perfect, and include notes about the attempts in commit messages or a separate notebook. This practice helps you track your progress and easily revisit specific points if needed.
While you don't need to worry about production code quality during exploration, consider aspects like naming conventions and add comments. If you encounter something that works but isn't fully understood, document it with comments, indicating that it requires further investigation in the future. This approach ensures that your exploration efforts are not lost and can serve as a foundation for future work. Regarding unit tests, it's essential to consider whether they're genuinely helpful for your exploration. If writing tests aids your understanding or if you're following a Test-Driven Development (TDD) approach, that's fantastic. However, in exploration mode, especially when you might discard an approach quickly, writing tests may not be the most efficient way to explore.
While exploring, the focus is on understanding what you're learning and identifying working approaches. Additionally, you're trying to get a sense of what the production code might look like—understanding abstractions, architecture, and how to organize things. The goal is to create something that feels useful, even if it's initially clunky. Visioning the desired abstraction for future useful code is crucial. In exploration mode, you may end up with code that you consider horrible, but it's essential to keep it along with notes. This code can serve as a valuable resource, especially if you need to revisit the project after some time.
Moving on to understand mode, this is distinct from explore mode because the system already exists, and you are trying to trace a path through it. This mode is often triggered when you need to fix a bug or implement a new feature in a codebase you don't fully understand. In this mode, you're not necessarily writing a lot of new code, but you're carefully stepping through existing code, possibly adding logging, debugging, and writing tests to understand the system better. When in understand mode, it's crucial to avoid assumptions, especially assuming that the problem lies in a particular place when fixing a bug. Take time to go through the code systematically, make careful notes, and consider transposing subsystems into simpler contexts, like a console application, to speed up the learning process. This approach allows you to work on the subsystem with a quicker development cycle and gain a better understanding of its intricacies.
In understand mode, it's crucial to avoid making assumptions about the source of issues. Stack Overflow questions often reflect a belief in external bugs, but the reality is that most problems lie within the code. By eliminating assumptions and approaching the problem objectively, you increase your chances of identifying the root cause accurately. When debugging, write accurate and detailed notes, avoiding assumptions about the code's correctness. Focus on reproducing issues reliably to gather useful information. Isolating factors that trigger or avoid bugs can be insightful for resolving problems. Experiment with changing variables and configurations to narrow down the scope of the issue.
Try to reduce the system's complexity by isolating components, and don't assume perfection in any part of the system. This process involves making changes to the code for investigative purposes, but not necessarily for production deployment. At the end of the investigation, you should have comprehensive notes, potentially a new bug report, or a simplified reproduction of the problem. This streamlined reproduction can be crucial for further analysis and resolution.
Moving on to fix mode, the goal is to address a specific bug. It's important to note that fixing a bug doesn't always involve changing just one line of code. A single conceptual bug may manifest in multiple places within the codebase. Consider the example of leaving UDP ports open for too long in a digital audio mixer, where fixing the issue requires addressing cleanup in various parts of the system.
In the next stages—enhance and clean up—you'll aim to improve and refine the codebase. These modes involve making changes beyond bug fixes, such as implementing new features (enhance) and refining the existing code for better maintainability and readability (clean up). Each mode serves a distinct purpose in the ongoing development and improvement of the software. Continuing with the process, after fixing a bug, it's important to focus on that specific issue without introducing new problems. If the fix requires significant changes, consider cleanup or refactoring before addressing the bug. Look for patterns across the codebase, and if there are other similar issues, make a note to address them separately.
While fixing a bug, change as little as necessary, and if applicable, write a unit test. The goal is to keep the changes focused on resolving the identified problem. By doing this, code reviews become more straightforward, as each pull request can be dedicated to fixing one specific bug. The next mode is "enhance," which encompasses everything that makes the code more useful. This involves implementing a single feature. If the implementation requires refactoring, consider refactoring the code first and then adding the new feature. It's essential to be aware of when to refactor, especially to avoid complications while in the middle of implementing a feature.
A practical approach is to commit the work in progress, return to the main branch, and then perform the refactoring in a separate commit. This separation allows for a cleaner workflow and a more organized history. After the refactoring is complete, return to the feature implementation with a clearer context. In summary, the stages include fixing specific bugs, enhancing the code by implementing new features, and considering refactoring when necessary. By following a systematic approach and maintaining a clear focus on each task, the development process becomes more manageable and efficient.
And improving comments that can be non-controversial and straightforward. However, if you're making more substantial changes, especially if they affect the structure or functionality of the code, it's crucial to consider the implications and potential risks. In conclusion, when in cleanup mode, focus on refining the code without altering its capabilities. This can include refactoring, removing unused code, improving documentation, and enhancing overall readability. It's important to maintain a balance, ensuring that the cleanup efforts contribute to the code's long-term maintainability and readability.
By following these distinct modes—fix, enhance, and clean up—you establish a systematic approach to software development. Each mode has its unique goals, and transitioning between them helps manage complex projects more efficiently. Additionally, it supports collaboration by providing clear boundaries for tasks, making code reviews smoother, and ensuring that the end result is high-quality, maintainable code. Thank you for sharing your insights on the different modes of development and code review. Your systematic approach to managing tasks and transitioning between modes provides a valuable framework for developers. If there are any questions from the audience, feel free to ask.