David Scott Bernstein's Blog, page 8
November 13, 2019
Use Examples
One of my favorite books on acceptance test-driven development is by Gojko Adzic called Specification by Example. This book talks about how you can specify the features of a system through examples cleanly and clearly so that they are straightforward to work through.
Examples allow us to quickly flesh out details and drive out inconsistencies in our thinking so that we can all get on the same page.
The problem with features specifications or system requirements or use cases is that they talk about system capabilities in the abstract, which makes it fairly hard for us to comprehend. It leaves room for ambiguity and very often when we talk about features in the abstract, everyone gets a different impression on exactly what the feature is.
Conversely, when we work through an example of a feature and how it should operate, even if we’re not clear on the user interface yet, we can immediately get on the same page in terms of what the future should do and why. When we do this it greatly reduces the number of errors and inconsistencies in a system.
Working through examples is really at the heart of both acceptance test-driven development and test-driven development or test-first development, as I like to call it. In all cases, we are asserting a particular behavior in a system before we create that behavior and then we implement that behavior by making our assertions pass. This makes software development kind of like a dialogue between the programmer in the system where we’re constantly asserting a new behavior and then teaching the system how to implement that behavior. It’s a fun way to develop.
I’m a huge advocate of test-first development and I find that many of the things that I like most about doing TDD, I also find when I do ATDD or acceptance test-driven development. I find that I can more quickly and easily flesh out error conditions and inconsistencies in a feature description by working through an example quickly with my Product Owner and thus get my questions answered while they are available rather than waiting until I’m knee-deep in development and they’re off doing something else.
Working through just a few examples with my Product Owner oftentimes gives me enough clarity to let me dive right into development and build out the feature that they want. It gives us all clarity on exactly what they want so they’re getting more of what their users need.
I highly recommend Gojko’s book. It’s well written and full of great information. You don’t need to do acceptance test-driven development formally with a framework to be able to benefit from the ideas in his book. Just thinking through examples gives us a great deal of insight into a system that we are about to build and that clarity translates into better features that built more rapidly.
Note: This blog post is based on a section in my book, Beyond Legacy Code: Nine Practices to Extend the Life (and Value) of Your Software called Seven Strategies for Seven Strategies for Measuring Software Development.
November 6, 2019
Specify Edge Cases
Another huge benefit of Acceptance Test-Driven Development or ATDD is that it helps us flesh out edge cases, exceptions, and alternate paths for a story to follow. I far prefer to specify edge cases and exceptional paths through acceptance tests rather than with use cases because acceptance tests are more articulate and they are executable.
Acceptance tests also give me a way to call out edge cases upfront so we know to be aware of them. I simply write an acceptance test for each edge case or exception I think of and that helps me keep track of it as I build it out in the system.
Actually, I find that acceptance test-driven development is a great way of driving my to-do list because I can clearly see what features need to be done and what features are already complete.
Stories generally take the happy path. For example, if my story is about withdrawing funds from a bank account then I might have a story that says something like, “As a customer using the ATM, I want to withdraw one hundred dollars from my savings account so that I have the cash.” But what happens if there is less than $100 in the account? The story doesn’t cover that. It isn’t meant to. A story is a high-level conception of a feature in a system and therefore it doesn’t go into a lot of details.
For me, a story is exactly what Alistair Cockburn says, “a promise for a conversation.” A story is simply a one-sentence description of what a feature is, why it is wanted, and who it’s for. That’s not enough to code the feature up. We get the details we need to code by having a conversation with our Product Owner and fleshing out examples that lead us to write the correct set of acceptance tests.
My stories don’t have exceptional paths or error conditions in them typically but my acceptance tests do. It may not have all of the edge cases I end up implementing but it will typically have the major ones.
Now we get into a bit of a gray area because, as I said before, the real difference between acceptance test-driven development and test-driven development is a matter of the level of granularity we are working in. Acceptance test-driven development is about fulfilling acceptance criteria whereas test-driven development is about fulfilling the itty-bitty tiny steps involved in order to fulfill the acceptance criteria.
I have seen this work in different ways. Some teams like to call out a lot of edge cases in their acceptance tests and that ripples down into their code. Other teams simply think about the happy path in their acceptance tests and then they implement the edge cases and exceptions through writing unit tests. I have seen it work both ways.
And just like we specify our happy path we can also specify unhappy paths or exceptional conditions. In both cases, we typically use the “given, when, then” syntax. Given some set of preconditions, when a trigger occurs, then some set of post-conditions will occur.
For example, given Michael’s account has $99 in it, when Michael tries to withdraw $100, then the account should reject the request and generate an “insufficient funds” exception.
Notice how using the “given, when, then” syntax, we are immediately able to describe an example of the behavior that we want to create without specifying how to create that behavior? And this is yet another benefit of acceptance test-driven development.
Note: This blog post is based on a section in my book, Beyond Legacy Code: Nine Practices to Extend the Life (and Value) of Your Software.
October 30, 2019
Automate Acceptance Criteria
You can do acceptance test-driven development (ATDD) manually without the aid of a tool by just keeping track of the acceptance criteria for a story and noting when it meets the criteria. Automating the process is helpful but the real value comes in articulating the acceptance criteria ahead of time.
It is estimated that more than half of the defects and half of the cost in software development is due to poorly articulated requirements. Written requirements for software is ineffective because spoken language isn’t precise enough to articulate effectively the behavior of software. However, we found that by articulating examples of the behavior that we want to create we can more accurately and easily describe the behavior in a way that allows us to implement it naturally. Development becomes a process of working through an example, which has many advantages over trying to code in the abstract.
One of the first acceptance test-driven development frameworks were called FIT, which stood for the Framework for Integrated Testing. It was basically an interpreter that allows you to specify acceptance tests by entering information in HTML tables, which were then parsed and used to generate automated acceptance tests.
The premise was that if acceptance tests were expressed in HTML tables then customers would be more apt to maintain these tables. In my experience that doesn’t always happen but in the rare cases that it does happen then it can be great. Often I find that the responsibility for maintaining acceptance tests goes to the software development team but often the value of these tests is so great that the team has no problem maintaining them.
The main burden and effort in ATDD are in the process of creating acceptance tests in the first place. This is done in partnership with the Product Owner and it allows the team and the Product Owner to gain immediate consensus on what features should do and therefore when the developers can move on to work on the next feature.
Today we have a range of tools to help us write good acceptance tests. Tools like SpecFlow for .net and Cucumber for Java are examples of acceptance test-driven frameworks that use the Gherkin language to allow us to express acceptance tests in a human-readable form that can be easily translated into something that the computer can run against and validate.
Notice that acceptance tests aren’t tests in the traditional sense. Rather, they are assertions of behaviors for features in the system. This helps us define the feature but it is often defined at too high a level to help us actually build out the feature. For that, we use test-first development. Acceptance test-driven development is at the right level to help us define the feature from the Product Owner’s perspective so that we build it to have the intended value.
Right next to continuous integration I find that automating acceptance criteria through acceptance test-driven development to be one of the highest value activities teams can engage in. It’s especially valuable for helping the relationship between developers and the Product Owner so the teams build more of what the Product Owner wants and the Product Owner is able to more accurately articulate what’s needed.
Note: This blog post is based on a section in my book, Beyond Legacy Code: Nine Practices to Extend the Life (and Value) of Your Software called Seven Strategies for Seven Strategies for Measuring Software Development.
October 23, 2019
Know Who it’s for and Why They Want it
Another huge benefit of acceptance test-driven development or ATDD is that it helps us get clear on defining features for a specific type of user. And it also helps us get clear on why they want that future.
Why is really the key question. Why do we want to implement or build a feature in the first place? That really is the question and when we answer it we can often discover better ways of fulfilling what they want than how they originally thought.
What we define as a feature is one way for a user to get what they want. Understanding why a user wants what they want helps us find better ways of getting the user what they want. This is why a central part of the user story is why a user wants the feature in the first place. Just having this conversation often leads us to build better features.
Why often sets the context for a feature but there is something else that sets the context for why a feature is wanted, which is the who. No, I don’t mean the rock band although I think they’re awesome. I mean who the feature is for.
“Put yourself in their shoes” as the saying goes. When thinking about a user story and creating acceptance tests around user stories we want to always think about who it’s for and build stories for a particular user type. A user type could be any kind of user that has a specific set of requirements or needs from the system.
Understanding the different kinds of users of a system and why they need what they need from it helps us build a complete feature set for them. Many enterprise systems have multiple different user types that have different needs of the system and so understanding the feature trains for each user type, which is something that Jeff Patton talks about with story mapping, can be really valuable in creating full feature-sets that allow a particular type of user to do everything that they need.
We don’t build stories in isolation and we don’t write acceptance tests in isolation. True, we want our acceptance tests to be insulated from each other and not dependent upon each other for their execution but we want each acceptance criteria to build on itself so that we can express the full range of a feature.
Every feature in the system as a reason for being. It has users who are the ones who derive value and it has a definition or acceptance criteria. When we use user stories in conjunction with acceptance tests we are essentially automating the requirements process and by doing so we are saving enormous amounts of ambiguity while gaining tremendous clarity in what we are building. I’m a huge advocate for acceptance test-driven development, especially for helping teams work with their Product Owners more effectively and efficiently.
Note: This blog post is based on a section in my book, Beyond Legacy Code: Nine Practices to Extend the Life (and Value) of Your Software called Seven Strategies for Seven Strategies for Measuring Software Development.
October 16, 2019
Get Clear on the Benefits of What You are Building
I have heard it said from a number of sources that the largest source of defects and the ones that are the most expensive to fix are our misunderstandings of requirements.
Requirements are essential. If we build the wrong stuff, something the customer doesn’t want or need, then it doesn’t matter how beautiful our code is, we will still have missed the mark. For me, software development has been a major awakening at how ambiguous language is.
We talk in vagaries but you can’t do that in a computer program, you have to be precise and specific. Because of this, there is something often lost in translation between the written requirements and their implementation. It turns out that this is the biggest gap in technology between figuring out what we want and then implementing it.
Fortunately, there is a technique in Agile software development that addresses and largely mitigates this issue. It is called acceptance test-driven development (ATDD) and when we do it properly it helps us not only get clearer on the benefits of what we want to build but also define it in such a way that it is understood among all stakeholders and crystal clear when the system has been updated with this new capability.
All of this doesn’t come for free. There is an extra effort involved in setting up yet another system and then maintaining it, keeping it up-to-date so that it becomes a dashboard that keeps everyone informed as to the status of the system and what features are implemented as well as the ones that are yet to be done.
I find this does great things for a team’s morale because we all can see very clearly where we stand. When all of our acceptance tests pass, it means that the feature is complete because we have fulfilled all of the acceptance criteria. We can see at a glance what parts of the system have been implemented and what parts are still yet to be done.
One of the things I find most valuable about doing acceptance test-driven development is that it helps me get really clear on the benefits of what I’m building and reach consensus with my Product Owner and/or stakeholders on what a feature should do. Often times, just the process of getting people on the same page with the feature will not only ferret out misunderstandings ahead of time but will also help us refine the feature so that we’re getting the maximum value from our efforts.
One of the main reasons that acceptance test-driven development has so many advantages is that it frames the building of features in terms of a test or an assertion. It does this by letting you give a series of examples to the system and asserting that it behaves in certain ways.
This really brings us to the fundamental advantage of both TDD and ATDD, which is that it helps us build systems and features by example. This is actually how the brain works, we think in terms of examples so building out a feature by asserting an example of that behavior turns out to be a good way of creating software, a more natural way, and more in alignment with how the brain actually works. That has been my experience, anyway.
I find that it also shifts the conversation about what we’re building from the abstract to the specific and there are many advantages to that. When we speak in terms of the abstract, really anything is possible and anything could and might happen so it’s very hard to generalize but if we talk in terms of our past experience or in terms of specific examples then the conversation is much more grounded and easier to find logical flaws and easier to make the conversation more logically consistent. This also helps us build behaviors that are more consistent and more straightforward to build.
Clarity is of the utmost importance when we are building software. It seems like there’s an endless barrage of questions and trade-offs we need to address when implementing even the simplest feature so therefore we have to get really clear on exactly what we’re building and why we’re building it. I find that both ATDD and TDD are invaluable tools for helping me do this.
Note: This blog post is based on a section in my book, Beyond Legacy Code: Nine Practices to Extend the Life (and Value) of Your Software called Seven Strategies for Seven Strategies for Measuring Software Development.
October 9, 2019
Why Practice 6: Write the Test First
I wrote Beyond Legacy Code to talk about the value of technical practices in software development. One of the most valuable technical practices for me has been test-first development and I really wanted to discuss it in ways that I hadn’t seen it discussed before that I found to be productive. Having exposed thousands of professional software developers to test-first software development, and Extreme Programming practice, I’ve learned firsthand how people respond to these ideas.
I noticed that often people’s first impression of test-driven development is as some form of testing and so they think about what can go wrong in a system as they write their tests. But this is the wrong way to do test-first development. TDD is not a form of testing but rather a form of specifying behavior before you build it. We call it a test in test-first development but really it’s an assertion of behavior, it’s a specification for a feature in the system, and when we think of it that way it makes a lot of sense to do it first before we actually implement that behavior.
When we start synchronizing the idea of the test and the behavior as one and the same we start to derive many benefits. A good well-written unit test articulates a behavior in the system. It not only specifies the behavior the way our requirements or specification document would but it also is guaranteed to be up to date because you can run it at any time and prove that it still passes. This gives us not only requirements traceability but also requirements verifiability in our system. That’s pretty good.
I know that as a developer it feels strange at first to write the test for a behavior before you implement the behavior but that’s just because we’ve all been taught to do things backward. When we think about it, it makes most sense not to just randomly dive into some part of the implementation and build out from there, it makes more sense to start by defining the behavior that we want to create and then defining it. This is exactly what we do in test-first software development, we write a test for the behavior that we want to create and then we implement the behavior by making the test pass. When we’re doing test-first development we say that no code is added to the system unless it is to make a failing test pass. When we follow that rule we find that we generally get 100% code coverage and our code coverage is not only high, it’s also meaningful because we’re asserting against behaviors.
TDD or test first-development is about writing unit tests for small pieces of behavior in a system whereas acceptance test-driven development or ATDD is about creating a test for a high-level behavior or feature in a system. Conceptually, TDD and ATDD are the same things except that they are at a different level of granularity.
In theory, TDD and ATDD are the same, and in practice they are different. We tend to use different tools with each and for different purposes. Because ATDD is at a high level we use it as a communication tool to define acceptance criteria with our stakeholders so we know when we’re done implementing a feature.
TDD (and I also use the term test-first development), on the other hand, is a developer practice used to define, build-out, and verify software as it is being created.
Still, ATDD and TDD have many things in common and many common benefits. The following seven blog posts are inspired by a section in my book, Beyond Legacy Code, called seven strategies for great acceptance tests. Enjoy these next seven blog posts.
Note: This blog post is based on a section in my book, Beyond Legacy Code: Nine Practices to Extend the Life (and Value) of Your Software called Seven Strategies for Seven Strategies for Measuring Software Development.
October 2, 2019
Keep Code Testable
I’d like to wrap up these Seven Strategies for Increasing Code Quality with a strategy that kinda sums up all the rest of them—keep code testable.
Untested software carries a great deal of risk. If a developer who implements a feature test it manually only once then that feature may be affected by other changes in the system later. The prudent thing to do is to test and retest the system as it’s being built. This is entirely impractical for all but the most trivial systems if testing is a manual process.
One of the biggest values that I see coming from the DevOps movement is that automating software verification can significantly drop the cost of building and extending software. In many ways, this aspect of DevOps really fulfills the promise of Agile.
Automating software verification is the only way that I can see to achieve the first principle of the Agile manifesto which states, “Our highest priority is a satisfied customer through early and continuous delivery of valuable software.”
But even more important to my mind than having reliable regression tests is having testable code. Testable code is the code, not the tests and it’s possible to write testable code without writing any tests at all by simply answering the question in your mind, “if I were to test this code how would I do it?” And if the answer is simple then you know you have testable code, if the answer is not simple then try again.
Not coincidentally, testable code is code that has the CLEAN code qualities that I’ve been discussing in this series of blog posts.
Classes that are cohesive are more straightforward to test because there aren’t multiple issues that can interact and cause side effects. Likewise, correctly coupled code is also straightforward to test because we can easily replace external dependencies for test doubles. Encapsulated code also means that we’re not going to be suffering side effects between other parts of our system and assertive code means that the behavior and the data for the behavior are in the same class, making them more straightforward to test. Finally, nonredundant code means that we aren’t repeating tests throughout the system, which greatly simplifies testing.
So CLEAN code is testable code and testable code is CLEAN code. I find almost universally that as I improve the testability of my code I also improve my code quality. This makes software more straightforward to work with and extend in the future.
Software quality doesn’t just happen nor can you force it to happen using heavyweight processes like Waterfall. Software quality comes from understanding a problem and modeling it accurately. When we can do this we build systems whose cost of ownership is lower and therefore more cost-effective.
Note: This blog post is based on a section in my book, Beyond Legacy Code: Nine Practices to Extend the Life (and Value) of Your Software.
September 25, 2019
Name Things Well
Shakespeare said, “a rose by any other name…” To him, a name was just a label for something deeply intrinsic within the thing itself.
But in software, it’s the opposite. There is no intrinsic-ness to any software objects except what they do and so our names become more than labels, they become expressions of the objects we create and therefore it’s important for us to use names that reveal the intention of what we’re trying to do.
We call this “intention revealing names” and I often find that the more straightforward a name I can use for something the better. I like verbose names, names that clearly say what they do. They can even be little phrases or sentences. I use camelCase.
When naming things I start with what it is I’m trying to accomplish. I’m always implementing some kind of feature in the system and so I focus on what is my goal for that feature is. Then I think about the steps involved in creating that behavior or feature. Again, each step in that behavior probably has sub-steps, and again I start breaking them down based upon what they want to accomplish.
How far do I take this? Generally, as far as possible. I want to name every little bit of behavior in my system because it gives me an opportunity to document what I’m doing and I far prefer using the names of methods to document what they do rather than creating a separate document to describe this or even adding comments near the code to describe what the code does.
We want our code to be expressive. In order to do this, we want to make our objects singular and purposeful around the task or feature that they are implementing. There are many guidelines for naming as well as standards and conventions that I won’t go into here.
In my last blog post, I talked about the importance of hiding implementation details and that’s especially important in how we name things. We want to name behaviors based upon what they do not say or imply how they do it. This can be a bit tricky because we normally don’t make this distinction when we are speaking to each other.
But, at some point the pedal has to hit the metal, we have to do the thing that we really want to do so how do we make that happen?
For example, if I want to sort a document in multiple places in my program I may simply call sort generically but my sort() method may implement a particular way of sorting like a quickSort. If I find out later that based on my usage, a different sort would be better then I can simply change sort() to use that different implementation and all of my clients or callers will benefit from using the improved implementation without needing to be changed at all.
If instead, when I wanted to implement sort in my code I called it what it really was, quickSort() then, if I realized I wanted to extend it later, I have to change those calls from “quickSort()” to “sort()” in multiple places.
Situations like these drive us to creating generic behaviors often implemented through interfaces that then resolve to specific implementations. This also opens opportunities for polymorphism in code.
I often find that when I scope methods locally I prefer to use more generic terms for their behaviors. When I scope methods more globally, in other words calling external methods, I prefer to name those methods to be more specific and their names tend to be longer. Again, these are just generalizations and I can think of exceptions to these rules.
Good names for data and behavior are our first line of defense for understanding a program. Perhaps this is the most important skill that a developer has, to be able to come up with good, understandable names for the components that they are building. It is certainly a quality that is highly appreciated by anyone who is charged with maintaining that code.
Note: This blog post is based on a section in my book, Beyond Legacy Code: Nine Practices to Extend the Life (and Value) of Your Software.
September 18, 2019
Hide How with What
As I was writing my book, Beyond Legacy Code, I started to see a pattern emerge. Practice 1 is “say what, why, and for whom before how.” One of the anti-patterns in software development, and therefore something that we want to avoid, is leaking implementation details in our code by saying how we do something in software. In software, especially object-oriented programming, we have this idea of an inside and outside. The ultimate outside of a computer program is its user interface but more often we like to subdivide within the program so that there are insides and outsides all over the place.
We conceive of our programs as a collection of objects and each object has public parts and private parts. This allows us to partition behaviors and hide implementation details. This is another way of seeing object-oriented programming that is quite valid, it’s all about hiding implementation details. Why? Because when we can hide something we can change it later without affecting other parts of the system.
A lack of encapsulation is one of the main things that causes technical debt in software. “Breaking up the monolith,” as one of my clients put it it is very often the task of working with legacy code. The monolith works as it was supposed to but we find it very difficult to repurpose it and that’s because we didn’t take the time to correctly model the behaviors. We focused on the implementation but not on the entities who are responsible for those implementations. That’s the additional step that object-oriented programming asks us to take.
It’s not enough to just create behavior in an object-oriented program, you must also assign those behaviors to entities who are responsible for performing them. We do this because it allows us to break up a complex program into its constituent parts, which allows us to more easily manage and extend a program in the future.
The notion of hiding implementation details by only saying what the service does and not how that service does what it does is a very important part of Agile software development. This is because it enables us to more easily refactor code.
For example, a critical practice in Agile software development is test-first development where we write a test for a behavior before implementing that behavior. Doing test-first development in this way helps us stay focused on building behavioral tests that are not dependent on the way we implement those behaviors and this is very important because these are the kinds of tests that support us when we go to refactor those behaviors and change the implementation.
If we change an implementation for a behavior without changing the behavior itself and a bunch of our tests break then they’re not helping us, they’re actually hindering us. When we do test-first development correctly and write tests for behaviors then the tests that we do write support us in regression and when we refactor our code, just like they should.
Most fundamentally, we have different ways of processing how something works versus what it is in our brains and when we speak we often semantically confuse these two concepts and implementation details to explain what we’re talking about. This can be a point of confusion and it’s better when communicating to separate out the what from the now when we’re talking and writing.
It’s a subtle thing but is valuable to focus on because by doing so we are also supporting good software development principles and practices.
When we hide how we do something by only showing what we’re doing, it encourages us to form tight contracts and strong interfaces for our methods and behaviors. It supports us in making autonomous objects that are straightforward to understand and work with.
I would even go so far as to say that the central quest of a software developer is to be able to get the job done while hiding as much of the implementation details as possible because the more we can hide the more of our system is changeable in the future without suffering a high cost.
Note: This blog post is based on a section in my book, Beyond Legacy Code: Nine Practices to Extend the Life (and Value) of Your Software.
September 10, 2019
Understand Trade-Offs
When we talk about code qualities and code quality practices we may have a tendency to believe that more is better, universally but that’s not always the case. We want software to run fast but we also want it to be easy to understand when we go to change it. Sometimes these characteristics and qualities can be at odds with each other. This is why it’s so vital for professional software developers to understand the trade-offs of their choices.
I tell the professional software developers in my advanced software design and development classes that they may not do anything differently next week then they did the week before taking my class but at least they will know the trade-offs of their decisions. I believe this is vitally important for professional software developers because we can’t make the right decisions unless we understand both our options in the trade-offs between those options.
One way to understand trade-offs more deeply is to understand the root cause of the situation that you are examining. Very often when building software will see many different options for implementing any particular behavior. Generally speaking, in Agile we prefer to take the simplest actions and build the simplest implementations of the features we are creating, knowing that we can go back later and extend them and because we follow good quality practices it is straightforward for us to do so.
I do want to point out here that this is not the norm in the industry. I fully recognize that so if you work on a team that has a lot of legacy code with the stigma around changing it then it probably got that way for good reason because developers went to try to change that code and found that it wasn’t as easy as they might’ve thought.
Poorly written code can be a bear to change because often times making one change can ripple defects any issues across many unrelated areas because of the way software is developed today. When we don’t follow software development practices that support building quality software then very often we find side effects in systems that might not show up until we go to extend those systems. Just getting a feature to work is not good enough because we often need to extend features and if that is very difficult or impossible to do then we have not done our industry is service.
Instead, we want to follow quality practices that support us in making changeable code so that we can build reliability into our software and dropped the cost of ownership. That’s what quality software is all about, dropping the cost of ownership.
In the physical world, we are willing to pay a premium for quality materials or quality service but in the software world, we really do get quality for free. I know this may be difficult for some people to accept but in my experience as a senior software developer doing quality craftsmanship and software development doesn’t slow you down. In fact, it’s just the opposite, paying attention to the quality practices in building quality code actually makes things go faster.
This is true in the physical world and in every other discipline so why would it not be true in software? If you are a chef who is responsible for producing hundreds of meals a day from your kitchen then you know that you must keep your kitchen clean in order to handle that kind of volume. The same is true in software. In order to gain the benefits of Agile software development that is incremental and just-in-time, we have to make the right trade-offs and follow quality practices as nonnegotiable’s.
So in Agile software development, there are a core set of principles and practices that we follow which we never deviate from. We always strive to make our code CLEAN. However, like any good practitioner, we are also aware that there are contraindications and doing the right thing in one situation would be the wrong thing in a different situation. Medicine has the same thing. In order to fight an infection a doctor might give patients penicillin but if a patient is allergic to penicillin then giving it to them will kill them so the doctor must find a different alternative.
In software development, there are many practices that are the right thing to do in one situation but the wrong thing to do in a different situation. By understanding the trade-offs of our actions and the practices that we follow we can make the right choices for any situation that we are in.
At least, that’s what we strive for.
Note: This blog post is based on a section in my book, Beyond Legacy Code: Nine Practices to Extend the Life (and Value) of Your Software called Seven Strategies for Seven Strategies for Measuring Software Development.


