7 ms·
First make the change easy, then make the easy change (2021)
- OJFord 4y agoI don't think it's particularly related to the original framing, but practising this makes for much better commits IMO.
- deleted 4y ago[deleted]
- samcheng 4y agoThis is basically the point of Martin Fowler's seminal book Refactoring. The book struck me as a book on unit tests much more than a book about refactoring itself, for exactly the same reason as described in the article.
- montroser 4y agoThis is absolutely critical to achieve sustainable development in large code bases. However, it is also important to know how to pick your battles. Make the change easy with the critical path and core of the system. But sometimes, especially along the periphery, the right choice is to just make the change and be okay with perpetuating a bad pattern, if it is going to bring disproportionate business value quickly. The key is knowing when to take one approach or the other.
- omegalulw 4y ago> But sometimes, especially along the periphery, the right choice is to just make the change and be okay with perpetuating a bad pattern, if it is going to bring disproportionate business value quickly Agreed. Also tests - if the tests are catching the cases they are supposed to but have anti patterns in the way they are written - it's not always worth it to fix them so long as your new test cases can be easily covered.
- wpietri 4y agoI would add one more conditional to this: > just make the change and be okay with perpetuating a bad pattern, if it is going to bring disproportionate business value quickly ... and if it gets cleaned up eventually. Technical debt should be managed like credit card debt. Absolutely take it on in emergencies, but build the discipline of paying it down when the emergency has passed.
- Archelaos 4y agoI disagree. It should be only cleaned up, if nothing else is more important.
- apalmer 4y agoI think there is a minimum floor that should be allocated to this kind of work. It doesn’t make sense to clean everything to a perfect condition but there should be some amount of ‘keep your room clean’ level of hygiene that’s maintained
- Archelaos 4y agoI think "cleaning up x" should just be handled as any other to-do-list item. That means evaluating its cost and benefits in context with the other items on the list. Then do what is on position one, then on position two, then on position three, ... Re-evaluate the list from time to time.
- wpietri 4y agoFine in theory, but often bad in practice, because in a lot of places prioritization is done by people with little or no technical background and lots of incentive to show progress. There are a lot of ways to do this, but my general preference is a black budget plus explicit "credit card" usage. E.g., the team has high standards for any new feature work and quietly spends 15% of their time every week on continuous technical improvement. If the product manager wants to break the normal standards and take on technical debt (e.g., rush have feature X ready for trade show), then you break the work into "rush to add feature X" and "clean up feature X mess". The first gets done before the trade show, the second after. And personally, if a product manager doesn't honor the deal, I say they get their credit card taken away for a while, because they've proven they can't be trusted to do right by the team and the company.
- Nemi 4y agoHuman communication is a funny thing. We use phrases like “technical debt is bad” and everyone in the room will nod their heads and be in total agreement, but every single person in that room has a different definition of what technical debt is. To some people it means “buggy code”. To others it means “not the way I would have written it”. Yet to others it means “code that is hard to understand”. What it means to me could be any one of those things, but the defining factor is that there is an ongoing ‘cost’ to the code in question. What kind of cost? Usually time-based cost. It takes someone’s time to manage and handle the situation. It could be that the code causes data inconsistencies that have to be managed by hand occasionally. It could be that frequently updated code takes 100 times as long to change as it should. It could be that the code has grown in complexity to the point that I can’t put a junior developer on it, requiring expensive personnel to maintain. It could be that it compromises the user experience, hampering user adoption. In all of these cases, the underlying is that it costs money. Like true debt does. Which is why using the term “debt” is a perfect description. I would argue that if a piece of code is clunky, buggy, or poorly written, but has no clear impact on the business in any way, is it truly technical debt? One might argue that the person making the decision to write it that way made a pretty valuable business decision. It is like a 0% loan. Until that loan starts costing you money, you would be a fool to spend time paying it off.
- gardenhedge 4y agoAh Kent Beck, where would the industry be without him?
- Jtsummers 4y agoProbably making snarky comments about some other programmer whose book(s) they never read and whose contributions they don't understand.
- wpietri 4y agoWas it snarky? Personally, I think he has made a number of really important contributions to our field, and so I read that as sincere.
- Jtsummers 4y agoMaybe I was being ungenerous, but most comments that are one-liners like that, "Where would we be without <Person>?", when it relates to individuals like Beck or Fowler or others here seem to be sarcastic, not sincere. I suppose the original commenter could clarify that.
- wpietri 4y agoGiven the lack of apologetic reply, it looks like you were right!
- gardenhedge 4y agoAlso assuming my comment is snarky and making a snarky comment back is pretty cool.
- gardenhedge 4y agoI mean without Kent Beck we would not have made much progress as an industry. We wouldn't have agile or scrum for example!
- comprambler 4y agoThe same concept applies for system administration, your environment (read codebase) eventually gets so complex that it accumulates odd infrastructure bugs that pile up over time exactly like tech debt. Sometimes you must just spend time figuring out how this infra piece (read code block) that was implemented by someone long gone from the company even works. This usually involves having to fix it as well. You and your team must piecemeal the long term fixes if you want to make any progress towards understanding, scalability and reliability.
- xupybd 4y agoI've noticed with this approach it's important to try not to rush. If you're like me you see the deadline or the triviality of the bug and think I can't charge two days to fix this. But the average time to fix bugs in the code base will reduce if you fix these things.
- civilized 4y agoI've always been curious, is this how atomic changes are accomplished in databases? I always figured if you changed a table, you would set up the data post-change in a separate place, then switch a pointer to point to the new data as the very last operation.
- tremon 4y agoWhat's "this" in your question? I don't see the immediate connection between a programmer's workflow and a database row update. But your understanding of what happens inside the database engine is too simplistic: first of all, the data does not necessarily exist as only one copy because there may be multiple indexes or materialized views covering that data. And each copy needs its own pointer switch as the very last operation. Secondly, the "atomic" change you mention is only atomic from the application point of view. Multiple clients may each have their own isolation levels and execution state, which means that the in-memory "switch" to the new data may happen at different times for different connections (especially for isolation level repeatable-read).
- lolinder 4y ago> In a way, this quote is saying “first do what you should have always been doing; being organized” and “then do what you came here to do in the first place (add a feature, fix a bug).” This is half the meaning of the quote, but only half. Even if you've been as organized as can be, business requirements change and suddenly your code organization is wrong. A good codebase can turn bad fast when people try to retain their original code model in the face of a changing real-world model. Changes become harder and harder, the code becomes more and more complicated, and you end up with a big ball of mud. I suspect this is especially likely to happen when the original architects of the system are long gone. They knew why the system was structured the way it was and would have been able to recognize when the requirements evolved to the point where that design stopped making sense. But subsequent maintainers don't have that picture and either are afraid to make large structural changes or they don't know where the inflection points are that allow adaptation.
- wpietri 4y ago> business requirements change and suddenly your code organization is wrong Agreed, and another important case is when you just learn something. The beginning of a project is when we know the least about the domain. Some early assumptions will be wrong! And that's great as long as we've optimized our team's process for learning. Indeed, that can become a strength, where we release new things specifically to learn. Learn about our users, about our domain, about our technical notions.
- tylerchurch 4y agoYeah, it seems to be a rule that once something is in the hands of users, things that sounded great in theory turn out to be bad in practice. Or things work so well that new (previously unimagined) improvements become obvious.
- guskel 4y agoIf you get organized about building the domain, such as using an ontology, this can be mitigated.
- lupire 4y agoThis is the opposite of TDD's "Red, Green, Refactor". https://www.codecademy.com/article/tdd-red-green-refactor https://www.codecademy.com/article/tdd-red-green-refactor The TDD version protects against making useless changes. The "make change easy" version protects against management stopping work as soon as the tests pass.
- karmakaze 4y agoTDD applied twice is Red, Green, Refactor, Red, Green, Refactor. The only difference here is where the start/stop boundary is: Refactor, Red, Green, Refactor, Red, Green. Combining the two: (optional Refactor), Red, Green, Refactor each time.
- ghoward 4y agoI agree with this, but I phrase it differently. I refer to eliminating technical debt relative to the change before making the change. More details in [1], where I explain my software development process. [1]: https://news.ycombinator.com/item?id=32210402 https://news.ycombinator.com/item?id=32210402
- sfink 4y ago> In a way, this quote is saying “first do what you should have always been doing; being organized” and “then do what you came here to do in the first place (add a feature, fix a bug).” Maybe it's just me, but that's not really how I understand it. Perhaps it's because I generally run into this when I'm thinking about accomplishing something new with a hack. Specifically, I want to do something that was unanticipated in the original design, and "goes against the grain" of what is there now. One solution, as suggested by the above interpretation, is to refactor your codebase in a way that the addition now fits in smoothly. That's a great thing to do a lot of the time. But beware: there isn't a "best" or "perfect" factoring that you can ever reach, no matter how much refactoring you do. There may be some things that are strictly improvements over what came before. But beyond that, a codebase has to optimize for a subset of the possible uses and dimensions of flexibility: what do you want to be easy to change? Your new feature or usage may not be the thing to orient the codebase around. Sometimes, it really is just a one-off, or an experiment, or temporary. In that case, refactoring everything to make it easy is just wrong. The original quote still applies, though. If you inject a hack here, a hack there, you'll quickly end up with a mess. Very often, the better thing to do is to do whatever refactoring is necessary in order to add a general extension mechanism that permits the hack you need, and only then use it to implement the hack. The difference is that the hack will still be messy, it may have hardcoded values, and it may not get along with similar hacks. But its scope is limited and the amount of damage or distortion it can cause to the surrounding design is bounded. I guess fixes are more likely to require refactoring. Features or extensions should be considered very carefully before adapting a large chunk of the system to suit them, which can come at the cost of making it harder to adapt to the original purposes.
- srcreigh 4y agoYour understanding may fit, actually. If the one-off hack is easy to add, it doesn't need refactoring to add it. Maybe ten or twenty one-off hacks can be added before anything gets difficult. It's only later that things become difficult and the code requires refactoring. Or put another way. You are taking something that's difficult to add, and making it easier to add by giving up clean integration with the existing design (ie making it a hack). Maybe this means future cross cutting updates don't automatically work, or you don't get some functionality out of the box. But if you've decided that's acceptable, it lets you put off the extra effort to make something easy via extending the design. Until the cross cutting updates or extra functionality becomes needed too often :)
- ipiz0618 4y ago...unless every time you try to make meaningful changes, people try to urge you to hack in stuff quickly so the code only gets messier to the point the maintainer gets a mental breakdown ;)
- banq 4y agoDon't change it, but replace it! As in CRUD, only CRD; as in append-only event sourcing; as in blockchain; the code represents your business value, similar to the bookkeeping book to record your money value.
- 29athrowaway 4y agoOrganized developer: Find clean plate and utensils. If there are no clean plates and utensils, then wash some. Then eat putting the food on the plate, using the utensils. Then wash your dish and utensils when you are done. Most developers: Eat a little bit of food using any object from the kitchen as utensils, then discard all the rest by throwing it on the floor, because your task was only eating that one bit of food. Bugs and rats will infest the kitchen and eat the leftovers. Then the organized developer will be put in a performance improvement plan, because fuck logic. There's no time to be clean because our performance is being measured on how much food we eat. Company interviews will be about eating a 10 food course using a Victorian cutlery set with 120 utensils. On the first day, the developer will ask where are the forks, the team will reply "we know that forks are the proper utensils for this, but our policy is to eat all food using spoons".
- cgrealy 4y ago>> Organized developer: >> Find clean plate and utensils. If there are no clean plates and utensils, then wash some. Then eat putting the food on the plate, using the utensils. Then wash your dish and utensils when you are done And in almost every organisation I've worked for, someone outside engineering (or frequently inside) will complain about wasting time on clean plates when there are perfectly good scraps of food in the bin.
- theptip 4y agoI generally like this approach. Typically my workflow will be to prototype a feature, see where the existing API needs to be extended, and then carve out a two-PR sequence; I) refactor the code to make the new change easy, II) make the change. One issue with just submitting the refactor PR on its own is it can be hard to tell if the refactor is sensible without seeing the new usage. I like stacked PRs for this case.
- LAC-Tech 4y agoDoes anyone have the original quote by Kent Beck handy? I feel like this is one of those things that seems obvious but is a bit more deep than that.
- crdrost 4y agoA 2012 tweet said, “for each desired change, make the change easy (warning: this may be hard), then make the easy change” and clarifies in a comment “Glad you found it helpful. Sometimes when the work is hard it signals that we're doing it wrong. Sometimes it's just hard.” [1] A 2020 tweet claims to have an older form of this insight c. 2000 which would be written in DOT as digraph { "straightforward?" -> "add feature" [ label = "Y" ]; "straightforward?" -> "isolate change" [ label = "N" ]; "isolate change" -> { "refactor" "create" } -> "straightforward?" } See [2]. So the claim is that new feature development per se should always be “easy” (in the Rich Hickey sense, "adjacent" or "in nearby reach"), but that there should exist potentially a lot of hidden work which is not feature development. For example, you build a log ingester which runs super fast, it buffers the logs and batch inserts them into a NoSQL store, gets very nice throughput... But then you want to add a feature for which your NoSQL storage just doesn't index right or so. The advice here is to abstract the problem first, figure out where it creates friction. Are these for example ad hoc user searches? Or do we know them ahead of time? Those create new structural constraints, for some work that is not directly tracked... Perhaps just adding some appropriate index, computing periodic summary rows, streaming the recorded logs into a MapReduce type cluster, switching to a relational database that better matches your domain, or whatever. None of this work directly yields value to your customer, indeed you are attempting to do a pure refactor with no observable changes to the clients. But, because you abstracted the problem you now have the ability to easily solve a bunch of nearby problems at once, and you use this to solve that target problem. Why would Kent Beck want this? Because he is talking about extreme programming and agility and changing the software in response to user feedback and all that. He needs that freedom to solve nearby problems easily, because the core tenet of Agile is that you ship ≈crappy software and let your customers tell you what's crap about it, rather than pretending to know yourself. “Hey the colors are awful but here's a first pass.” “I don't care about the colors, I care that you think every purchase tracked by the system belongs to a contract.” “Uh. What. I thought you said contracts had the purchases underneath them? We had a long conversation about this!” “Yeah but just use your head! Sure we usually purchase things for contracts. But we also purchase things for bids, or for demo products, or to upgrade our tools...”—that is the Agility problem in a nutshell, I want to find this out before the codebase is 30,000 LOC+ and fixing the broken data model requires three months’ rework! So Kent Beck needs to be able to offer a client a “change/feature” with the understanding that they might say “What. On. Earth. No. Just, no. What?” and say “oh ok my mistake I can tweak this to do what you want, give me a day or two.” My own version of this is the aphorism, “Magic is just cleverly concealed patience.” What I mean is that if you dive into what magicians do, you will often discover that it just required a lot of prep work: and the reason that you assume magic is you can't imagine someone spending that much time on that stupid of a thing. “Who would buy 52 decks of cards just to assemble one that is ENTIRELY made up of the Jack of Spades?” Well, a professional magician would. Some sleight of hand, they fan put a normal deck, swap the decks so that you shuffle the Jack of Spades deck and pick any Jack of Spades you’d like, how about that. Same happens with professional kitchens prepping everything before service. If you cleverly conceal the patience, you get a reputation for working magic at your job. [1]: https://twitter.com/KentBeck/status/250733358307500032 https://twitter.com/KentBeck/status/250733358307500032 [2]: https://twitter.com/KentBeck/status/1218307926818869248 https://twitter.com/KentBeck/status/1218307926818869248
- jrochkind1 4y agoThis makes more and more sense to me the longer I program, and the few projects where I've been able to do it, are the few projects whose code still seems delightfully elegant to me. It is basically taking care of your "technical debt" before it becomes debt at all. (And I agree with other comment threads that it isn't always because of a mistake earlier, it's because the program has been called upon to do new things that weren't anticipated before, it's not so important to decide if they _should_ have been or not). But to do this requires a few things which can be rare: 1. Sufficient "Ownership" of the code to be permitted to do the refactoring. Like if you show up with a PR on an open source project that is new to you, to refactor to first make the change easy... it's going to go nowhere. Internal projects then will depend on the culture and social organization... 2. Time. If it's essentially eliminating your technical debt before it's acquired... well, the reason technical debt is acquired is because for better or worse, choices are made not to spend time on it, whether wisely or unwisely. 3. Skill. It's a craft. The more you do it, the better at it you get. Ideally learning by observing people do it who have already done it more than you. And it also requires having a general mental model of the existing architecture you are changing, which, first see #2 above, and second, can be hard to do if it's a mess. (Also, and this may be the most challenging -- with insufficient skill and sensitivity, your refactorings to "make the change easy" can actually make things worse, make your immediate change easy at the cost of a complicated spaghetti architecture which actually just over-complicates everything).
- JonChesterfield 4y agoRe: 1 the refactor is fairly likely to be ignored until the functional change is also proposed but that's ok, review tools should be able to deal with a patch series two deep.
- jrochkind1 4y agoEven with the functional change proposed, i would say your chances of getting such a relatively "complex" change actually reviewed and merged are lower though, than if you just proposed the smallest change that would result in function; and your investment higher, for higher risk of it just sitting there. I think for this reason and similar, open source code especially can end up subject to accretion of things people are reluctant to change or seriously refactor. Committed non-burnt-out steward(s) can mitigate.
- phyzix5761 4y agoThis post is like 12 words long. How is this of value?
- VladimirGolovin 4y agoThe value is the quote itself. To me, its resonance is much wider than the scope of the post (refactoring). It reminds me of LessWrong’s removal of trivial inconveniences. The post is just an opportunity to bring the quote to our attention, and it can be safely disregarded or interpreted more widely.
- JonChesterfield 4y agoRefactor then functional changes is the right play where reviewers are involved. It separates moving things around and renaming, which is really noisy in a diff tool, from the functional change which hopefully changes much less code. The first review is looking for any functional change in the noise, the second to assess a functional change without any noise. Adding tests in the first phase is great but existing ones shouldn't change. Maximises chance of review finding mistakes / minimises cost to review.
- glenjamin 4y agoSomething I think hasn’t been discussed much in these comments is what the alternative would look like, and why that is undesirable. If you only perform the refactoring after making a change (eg. adding a ticket for cleanup later), then (a) you made it hard to do the change, but also (b) it’s now far less vital to perform the refactoring - as the desired outcome has already been reached. Refactoring for refactorings sake can be hard to figure out what the goal or end-state is. Making a refactoring in order to facilitate a particular desired change acts as a great forcing function for helping you decide what to change and why.
- GSGBen 4y agoQuestion for everyone: do you refactor in a separate pull/merge request, or just a separate commit within the same pull/merge request?
- josteink 4y agoI don’t know about everyone else, but for me it depends on the scale of refactoring. If I myself think that receiving such a mixed PR would be troublesome to properly review, then the refactoring gets to be a PR of its own. If the changes are small enough to all keep in your head at the same time… Then it’s one PR.
- therealdrag0 4y agoI think separate is ideal. But sometimes I only realize the refactor halfway through my changes, at that point I might try to do a chunk on a clean workspace so they can be moved to a separate PR or I’ll just proceed and give generous explanation of all changes in PR with inline comments (comments in PR not in code) and offer to walk reviewers through it.