This is really kind of the whole idea behind the "red, green, refactor" mantra. The point isn't to write perfect code right out of the gate, it's to write working code. Once the test is green, then you can refactor as necessary with the confidence that you aren't going to accidentally break things (as the test will backstop you).
Ironically, I think this is something that affects seasoned programmers more than new ones; the more you know about the trade, the more voices you have saying "no, that's wrong! Don't do that!", which can freeze progress. The way I combat it is by writing down pseudocode in my source code - literally just english, non-compiling pseudocode - which I then "refactor" into working code (thus the first "green" is "it parses"). By making step 0 the expression of the idea rather "writing the fist line of code", I can get right into the process rather than getting hung up on the "how".
Sometimes I'm surprised at the lack of parallels between writing long-form articles and writing code. How many people draw a general outline of their program, what they want each part to do, and how the whole thing should be layed out? Who plans before they start coding it?
I think these types used to be derogatorily referred to as "cowboy coders" in the 90's by overly process oriented developers who came from an era of mainframes and punchcards.
I think a big shift in modern software development came about when people finally realized that developing software is by nature a "messy" process. Loose text files, REPL sessions filled with red lines on the console, randomly scribbled boxes with arrows on loose sheets of paper strewn about the room; traversing object graphs in your head while on the drive to work. There is no "outline" to frame any of that. The outline only exists after the software has been built.
Maybe I've been spoiled by working with embedded C++ on Arduinos, where we have 3 sections -- Initialization, Setup function, and Loop function. We set variables and include libraries in the Initialization section at top. We run first-time code and open serial & other connections in Setup. Most code goes into the Loop section, which is the function that does the heavy lifting. Extra functions (i.e. the math to convert inches to mm) can be written below, and called in either Setup or Loop.
This is why programming language history is so important. Every programming language has a large number of opinions/assumptions. Code structure is one of them. The Arduino language, based on C/C++, has assumed a simple code structure that helps people learning embedded programming, because the Arduino platform was designed to help people learn.
Yup; which is why the people getting hired for those projects would flounder if asked to make a large individual contribution without much handholding; and also why those types of project team structures yield such incomprehensibly shitty software (at ridiculous cost to the client.)
Seriously? Many of the enterprise devs I know do make large individual contributions in their own (side) projects, or in smaller projects within the organization. Part of enterprise dev is that you are developing within a series of constraints, not least the thought in mind that there is likely to be someone maintaining this code in 10 years time, and it's unlikely to be the team building it today.
I would agree about the general outcome, though - enterprise software often is built _despite_ the politics between various departments and vendors, and much of the solution is as a result of compromise and integrating with "not quite suitable for current purpose" legacy systems. That is where a large amount of the cost goes. (It's also important to remember that what we're building today is also going to become the legacy system of tomorrow, and to include that thought in the design).
developing software is not messy process. what is disturbing is that you think it is and that is normal state. Sure it is messy if you develop by accident so lets see what sticks.
Code is a planning language, so rather than boxes and noodles or pseudocode, I tend to sketch out my plans in code itself. In SICP this is called "design by wishful thinking": you pretend that you already have a library of primitives that do the nitty-gritty work and write the high-level bits of your program using those primitives. Then, you go back and implement the primitives.
Indeed, the "design by wishful thinking" was a real eye opener for me. It is one of my favourite parts of SICP.
It can also be explained as way to postpone decisions you can't clearly make right now. First, you pretend to have made the decision, and later on you implement the decision "you wish you had made".
Definitely an effective technique. Personally I like doing this on a whiteboard so I don't get distracted by errors or Compiler/IDE notifications and can just sketch mostly in realistic looking code.
For smaller segments though, I'll just write out function signatures, an then go back and fill em out or restructure them.
I really like your common English approach to coding. As a designer I often do the same with a few key points that my client and I have discussed with some additional influences taped up around my desk.
Also kind of related - programming by "wishful thinking". You first lay out the flow of the program as calls to high level functions which don't yet exist, and then go back and fill in the functions as you can. The first few SICP video lectures are all about this style, and I found the idea simple but very useful to avoid trying to hold too much of a problem in your head at once.
Yes, exactly. By writing pseudocode, I end up writing business code rather than implementation code, which inherently assumes the a high-level interface to the nitty-gritty details. This leads to an approach where the idea is expressed at a high level first, rather than getting stuck in the implementation details of some small piece of the problem.
It's kind of "breadth-first search" on software development, while the classic approach is more "depth-first search".
On the other hand, it is also important to quickly get into the state of a running program, because only that allows for iterative development where you can catch bugs shortly after you wrote them, rather than having tons of bugs after weeks of (not sufficiently tested) work.
I've always heard it as top down development, as opposed to bottom up development. I always say that's what I'm going to do, and then end up not doing it.
I've found what I think is the SICP book, but I can't track down the video lectures - can you provide some links? Thanks - I thought I was the only one programming by "wishful thinking"!
Using a visual git client greatly helps with this approach, especially if you are coding in multiple files at once. Once you have "green" status, look over the changed lines in the git GUI and look for lack of patterns, code smells, etc. This separates the coder in you from the reviewer in you.
Perhaps I'm misunderstand you, but what difference do you see for this use case between visual and command-line git clients? I do this all the time with git-diff on the command line, and it might just be a failure of my imagination, but I can't think of how the process would be any different with a visual client. Can you give an example?
Different strokes. I'm like you: I do a git status from the CLI for peace of mind. Others in my company gotta have their SourceTree, and hate the CLI. I disagree, but it's their choice.
Recently (as in a crazy experience over the past week), I've been feeling the same way described by you, and by the OP; but I've scaled this up to a much higher abstracted level.
The only way I can really describe it is (in terms of what you just said): "red, green, refactor" your life, not just your job. Constantly question everything you're doing, and find out what you don't like about it. Find the root cause of that feeling, and figure out what you can do to fix it. Find and take the steps to complete that.
Edit: I will add though, that the trick is to do so without stopping progress, as you said.
What keeps me from coding is not necessarily optimization (well, at least its connotation connected to speed), but maintainability...By that I mean the constant battle between, "Do I write the boilerplate I need to set this up right?" and "Let me just copy/paste/mock all of that". To me, this is a more sapping concern than optimized code, because taking too many shortcuts makes the project harder to maintain in its midstages...even worse, the shortcuts can cause sloppiness to cascade into the design of all the other dependent objects and interfaces.
Doing some nominal TDD has helped quite a bit for me, because it does require writing code, but not code that I have to worry too much about (and if it happens to be a pretty shitty, shortsighted test, I can delete it without it, theoretically, hampering the app) screwing me over. Sometimes this physical act is enough to get me past the code-writing block.
Also, if I actually do the testing right, it prevents regression, improves code quality, etc. etc.
I think it ultimately comes down to "choose the right tool for the job". TDD is just one tool that you have in your toolbox to accomplish whatever project you are working on. That doesn't mean that TDD is appropriate for every project.
As a security professional as well as developer, I agree with the OP. Simply put, you're going to screw things up no matter what you do. Be smart about it, plan for the worst, and seek outside feedback into your security processes; you're never going to find all the edge cases, even if that's your day job.
I remember when Facebook's front page code leaked. I am no PHP expert, but it was pretty ugly code. Probably today you couldn't get hired at Facebook if you wrote code like that.
I've heard many arguments where people will tell you, it won't scale. Or you're doing it all wrong by writing quick and dirty procedural code. Quite frankly scaling is about the nicest problem to have if you're a startup. Without traction your ugly code simply isn't going to matter. With traction you will gain funding and the ugly code problem quickly disappears like I assume it did at Facebook.
> Quite frankly scaling is about the nicest problem to have if you're a startup.
I'm certain I stole it from someone here on HN years ago (whoever it was, I owe you a beer!), but I've always called this a "Maserati Problem". That is, it's something I can think about while I'm driving down the road in my Maserati that I've purchased with money my startup has made already.
Scaling, in most cases, is a problem very much like "where am I going to store all this money that's pouring in?!" If you have it, you've already won.
Hindsight is always 20/20. It's difficult to judge a piece of code after the fact. The quality of the code, including maintainability, is a result of various trade-offs. We don't know what compromises the developer was making at the time. We don't know what was the highest priority at the time. May be they were short of money and working code however shitty produced at the shortest amount of time was the most important. Suck to be the guy doing the maintenance but if there's no working product, there would be no maintenance.
We programmers have a ugly tendency to judge less than stellar codes written by others. It made us feel superior to pick apart mistakes others have made. The fact is software development is a series of technical and non-technical trade-offs. I certainly had fell in the same trap before. These days I just appreciate they can ship a working product and get it off the ground.
That article is simplistic. Sure, write code that you won't care about in the future like you don't care about it, but if you take that approach with anything requiring longterm reliability or maintenance then you're setting yourself up for failure. Essentially: Think about it. Use techniques and tricks where appropriate, but at least consciously make the decision to do a less than stellar job and document your reasoning. A couple of lines of comments justifying some weird and verbose approach might show to others that it wasn't the lack of mental capacity to come up with another implementation.
I believe the spirit the article was written in has value. Obviously one should use common sense and still apply the best practices that they know of at the time.
I can identify with this. Some of the best work I've ever done has been some of the worst code, just because of the paradigm shift involved: "get shit done" instead of "write nice code".
After writing tons of code, I've come to realize that 80% of the time, it takes about the same time to do it "right" (scalable/secure/modular) as it does to do it "OK".
Ex: Are plaintext passwords any faster to build/code than encrypted+salt passwords? Nope.
And, it's fine to intend to refactor later. But, too many organizations have no tolerance later to let engineers do things that don't have visible results. I've suffered through a stint of handling "legacy code" like that & it was painful & demoralizing.
I disagree. Too simplistic. It's my job as a developer to try and think of all the ways my project can fail and bog down before hand (within reason on the performance). I've got a list of items I am actually working on and will post it above my desk when done:
1. Let me think about this some more before giving you an answer.
2. I will use my debugger.
3. I will create a new branch for this task.
4. I have commented my code and commented it well.
5. I have accounted for fail scenarios.
6. I have considered multiple solutions to this task.
7. I have not begun hacking away immediately at a task.
One thing I've learned from having to do rapid prototype development is to just put fingers to keyboard and go.
If I find myself getting stuck in the cycle of "this can be written better, I'll just spend 2 hours improving it rather than finishing the framework" I stop, I think about it, then I leave the working-but-probably-crap code in place and move on.
Once the framework is in place, then I start making things better, one area at a time.
I program the way that I imagine I would sculpt. If I had to sculpt a head, I wouldn't start by sculpting the perfect ear, and then sculpting the perfect eye, etc, because who knows what will happen. Maybe in the course of sculpting the eye, I need to move the ear, and then all that work goes out of the door.
Instead, I would sculpt the entire piece of art roughly, almost unrecognizably, and then target large parts of the sculpture. Details like ears and eyes might even never get done, depending on how the requirements change for the sculpture.
I always approach writing software the same way, so the first iteration is just a hardcoded piece of software that just shows me that it will actually work, and then I iterate and start smoothing out all of the sharp edges, of which there are a ton.
I'm by no means a pure functional programmer, but I've found some of the mantra from that paradigm helps when building the early blocks of a program as it allows you to break down your application into core problems which can be tackled individually. Plus it allows your application to scale easier and makes it easier to rip out and replace code as you start to add complexity.
But of course, before starting any major project, the first thing any developer should do is map out at least a basic mental design of the program - even if that map isn't written in pseudo-code or in a form of a flow chart.
in other words, FP evolved to solve hard problems. You don't need to understand monads to model a cash register. You probably do need monads if you want to write a bug free distributed asynchronous system.
I don't know that you need monads, but a lot of recent languages seem designed to guide programmers to certain types of good design. You don't need the language to emulate the design ideas.
The reason pure functional programming is good for "ripping out and replacing" code is because your code units necessarily have fewer interdependencies.
a pure functional language is really good for writing dumb code that "just works" in the sense that it does what you need it to do even if it isn't very pretty or fast, while at the same time keeping it reasonably maintainable because the code will not be rife with accidental complexity and assumptions about global state.
Be careful going overboard with pure functional programming. It could become one of the immobilizing factors the blog was talking about. Is the code pure FP? Can I get rid of the states? Am I doing correct FP? Should I write generic function for more abstraction? Am I doing DRY enough?
Indeed. It's about knowing what tools are best to use when. My comment was more about how some (albeit not all) of the concepts of functional programming can help tackle the first few stages of larger projects and can often help to lower the chances of spaghetti code.
I can relate to this. There is a real risk in freezing up when you attempt for your first writing of code to be the fully architected solution.
I would still go one step further and at least apply the strategy pattern/polymorphism when you have a large if/else/case block going beyond 2 or 3 conditions in a controller or model or something. (assuming MVC web apps)
The fact is you can write out some ugly code directly in a controller where it might get some code working fast and prevent the freeze up of your output, but once working you should immediately move it to the appropriate place like a model or library, and apply a basic OO strategy pattern (perhaps not with factory/interfaces etc until needed later) where appropriate to cut down on spaghetti/nested conditionals.
This really does not take much more time than stopping at working code that is ugly, and is a good middle ground where it is not painful to return to later.
There is still no good excuse for "taking a dump in the corner" of your code base just to get a marginal gain in output.
Occasionally, I find myself “stuck” when writing code – the coder’s “writer’s block” I guess. This quote by Ward Cunningham is both inspiring and truthful:
-- Once we had written it, we could look at it. And we’d say, “Oh yeah, now we know what’s going on,” because the mere act of writing it organized our thoughts. Maybe it worked. Maybe it didn’t. Maybe we had to code some more. But we had been blocked from making progress, and now we weren’t. We had been thinking about too much at once, trying to achieve too complicated a goal, trying to code it too well. Maybe we had been trying to impress our friends with our knowledge of computer science, whatever. But we decided to try whatever is most simple: to write an if statement, return a constant, use a linear search. We would just write it and see it work. We knew that once it worked, we’d be in a better position to think of what we really wanted. --
Next time you’re stuck, just write the simplest thing that could possibly work!
-- So when I asked, “What’s the simplest thing that could possibly work,” I wasn’t even sure. I wasn’t asking, “What do you know would work?” I was asking, “What’s possible? What is the simplest thing we could say in code, so that we’ll be talking about something that’s on the screen, instead of something that’s ill-formed in our mind.” I was saying, “Once we get something on the screen, we can look at it. If it needs to be more we can make it more. Our problem is we’ve got nothing.” --
you need to learn calculus if you want to model relativity. you only need fifth grade math to model a cash register.
if your team builds apps with 500k LOC, you need to be constantly focused on abstracting and factoring out patterns or you will drown in your defect rate before the project even ships. If your team builds medical device software, if you aren't doing this, you'll get someone killed.
If you're building a personal blog, your time is better spent by pasting jquery snippets.
There is a spectrum here and most projects fall somewhere in between.
Of course. And to add an example of the other part of spectrum (opposed to medical device software) - bunch of simple 2d game prototypes. There's people who do such jobs, and many similar, who'd be a fool not just "push the buttons" all the time, with basically just a quick forethought related to structurization and code quality.
Just be prepared that if you write a service that does not scale, that will eventually bite you painfully bad with massive amount of data going nowhere but stuck in the stupid relational database with terabytes of data.
I don't know--my impression is that most people go to far in the other direction. They write code with no thought at all for the future, and then shortly have nightmarish debugging sessions in spaghetti code that's impossible to extend or reuse. All in the name of a rather extreme "worse is better" philosophy.
I've certainly experienced this with some of the people I worked with recently. Anything you win in the short term gets lost several weeks--if not days--later when everything is a horrible mess. Don't even think about coming back to that code months later!
I've found that putting in a little bit of care for the future pays off even in the short term. Perhaps not on the scale of hours or days, but definitely weeks. Which is still pretty immediate.
However, how you do this is also very important. The handy rule I've been using is simple: simplicity. Improve your code by making it do less not more. If you can make code more maitainable or general by simplifying, do it. If it would require adding new concepts or mental overhead, reconsider. Try to reuse existing abstractions as much as possible.
This usually--but not always--means making your code more functional. Do you really need to write this operation in place? Do you really need to tie everything together with a bunch of implicit state changes? Probably not! This is not to say that you should never use state: just be sure to only use it when it fits well and makes sense. And be explicit about it.
The functional style (at least in languages like Haskell) also lends itself very well to reusing simple and general abstractions. The key idea here is that these abstractions are not about doing more: they're about doing less. More general code simply leaves you less room to make mistakes. If you're working with lists of numbers, there are all sorts of arithmetic mistakes you can make; if you manage to write the same code against lists of anything, the only mistakes possible will be in list handling.
Haskell just makes this more practical by providing abstractions with a great "power-to-weight ratio": ones that manage to be simple, general and yet still expressive. Code with functors or monoids or traversables is easier to write than more concrete code and yet still flexible enough for many useful tasks. As a bonus, it's also more reusable. For free.
So the key idea is reduction: all these abstractions work by stripping concrete types of power. A functor can do much less than a list. A monoid can do much less than a number. This lets you write nicer code without becoming an "architecture astronaut" because instead of adding structure, you're really taking it away.
I've found that programming like this really helps in maintaining and extending the code later on. But it doesn't slow me down when I'm writing either--I actually save time because I spend less getting time getting the code correct in the first place.
These days, I've started being able to make large changes or add complicated features and have them work on the first try. Not every time, but surprisingly often. Certainly much more often than before! In large part, I think this is because of writing the code with an eye towards simplicity.
So the important insight: take the simple route, not the easy route.
Great advice IMHO. One trick that I found useful for myself (good chunk of my code is "hobby" code that I myself revisit some months later, so not really "production" problem but nonetheless almost the real-world maintenance example):
- make the function fit in your head. Literally. Make it fit in one screen, if you can. If it does not, think how you can make it fit on the screen - everything, including variable definitions.
- avoid branching as much as possible unless it is the 'exit' branches. Avoiding branching sometimes can be done by tricks like passing function pointers. Do it - it makes code much more expressive and easier to read.
These two heuristics allow me to make the code much simpler to understand for $(me+6months). I saw the same patterns in real-world code at $work as well - using the above two principles makes the code dramatically easier to support later.
One screen is way too big! Make each function do a single thing. Even in verbose languages, you should be able to get most of them down to 5 lines or less - and this isn't actually difficult (except for coming up with names for all of them) the transformation is actually very mechanical, so much so that if you're using an IDE it can probably "extract method" for you, but even doing it by hand is trivial once you get the knack of it.
Indeed. 25 is the maximum - mostly the shorter, the better.
Though at least for me there is somewhere a limit (2-3 lines?) where the returns start to diminish - there's more noticeably more typing involved, and more mental context switching while debugging.
If you write code too slowly, go faster. If you write code too quickly, go slower.
You can tell if you're too slow when you've spent two weeks thinking about the program and have neither working code nor a detailed design, or if you've created a detailed design only to realize upon implementation you'd gone about it entirely the wrong way.
You can tell if you're too fast if you find yourself spending more time cleaning up messes of poorly-thought-out spaghetti code.
This is like in go. If your walls are too brittle, play more conservatively. If your walls are too thick, loosen up. You can make generally better moves in go just by recognizing if you are too conservative or too aggressive. Same with poker.
The problem that comes up is by the time you hit the tipping point of more cleaning then feature additions you end up having to do the fast solutions to fit deadlines.
I think when you take on the debt of poor code you have to remember that what it will take to fix that problem will continue to increase the longer you are away from that code.
Now I don't think you should consider fixing things for scale poor code that is it's own beast which makes the most sense to handle once you have the problem as you can't really tell what the problem will be until you have it.
Sometimes you'll want to plan ahead because the nature of the problem means you'll have a harder time cleaning things up later.
Other times, you want to move quickly because creating castle-in-the-sky architectures is not the best use of your time (especially if it becomes paralyzing).
Experience helps you determine which type of situation you're dealing with.
How long have you been programming, and how many jobs have you had writing code in an actual business? Your profile says you're a student, so I'm curious how you can feel justified in writing such a thesis.
Personally, after 16 years in the industry, I do stress about my code as described in the article... Not about making it work, because that will happen regardless, but about how. It affects my ability to code, because "make it work" is far drowned out by "make it work the way your peers will accept it." Many times, that "acceptable" approach is something I find far less intuitive, and far less maintainable... Also, that approach has changed so many times over the years and with each different team, it's hard to keep track. I can build far more in a weekend, on my own terms, than I can build in a week by someone else's. I am not complaining... I actually like to learn those terms, and I enjoy it as long as people are constructive about it rather than condescending. It's trying to pre-determine those terms that is the block, for me.
wow, SO experience is now something one puts on top of the Achievements section! (I don't say it in a bad way, I'm just surprised things have evolved to such a point)
You're going to see a lot more code from people who err on the side of too little thought rather than too much though for a pretty obvious reason: people who are too scared to start coding... aren't writing any code for you to read later.
Also, while it's easy to identify code which has had too little thought put into it, it's often very difficult to identify code which has had too much thought put into it. The best solutions don't look like the result of lots of hard work, but rather make hard problems look like simple problems that would be trivial to solve.
> Improve your code by making it do less not more.
Exactly. The problem is that code can tend to be either crap (spaghetti) or too much code (too many methods and layers of extraction). So, I'd go one step further: code should do less, not more, and should be clear and understandable, without making unnecessary sacrifices. What is clear to me isn't clear to everyone else, but striving for clarity and simplicity isn't a bad thing.
This is a great post. I just wrote about a project I worked on in February[1]. When I first built the website, it wasn't optimized and now I realize that I greatly over estimated how many resources I thought the site would need.
Last week, after getting to the first page on Google's search results, I'm getting about 12,000 hits a week. Monitoring shows my daily CPU usage is nearly 0%, and memory usage around 32MB. Today(3 months later) I finally did some SEO, added compression, caching, and a CDN to get drop page load time from ~2 seconds to under 500ms.
129 comments
[ 2.9 ms ] story [ 143 ms ] threadIronically, I think this is something that affects seasoned programmers more than new ones; the more you know about the trade, the more voices you have saying "no, that's wrong! Don't do that!", which can freeze progress. The way I combat it is by writing down pseudocode in my source code - literally just english, non-compiling pseudocode - which I then "refactor" into working code (thus the first "green" is "it parses"). By making step 0 the expression of the idea rather "writing the fist line of code", I can get right into the process rather than getting hung up on the "how".
I think a big shift in modern software development came about when people finally realized that developing software is by nature a "messy" process. Loose text files, REPL sessions filled with red lines on the console, randomly scribbled boxes with arrows on loose sheets of paper strewn about the room; traversing object graphs in your head while on the drive to work. There is no "outline" to frame any of that. The outline only exists after the software has been built.
I would agree about the general outcome, though - enterprise software often is built _despite_ the politics between various departments and vendors, and much of the solution is as a result of compromise and integrating with "not quite suitable for current purpose" legacy systems. That is where a large amount of the cost goes. (It's also important to remember that what we're building today is also going to become the legacy system of tomorrow, and to include that thought in the design).
Design is messy. The implementation need not.
> “Design Patterns”, “Code architecture”, “Scalability”, “OOP”, “Maintainability”
Apart from Scalability others can be implemented from day one.
Scalability is about a dumb proxy first. Change in Data structures next.
In software, you have two choices.
1) Dumb code that works aka kludge.
The author is advocating kludge programming.
(I want to use this word distinctly from hacks, which are fewer and atleast clever)
I advocate this.
2) SQlite, Games
I advocate this too. Software that is meant to last, requires rigor.
My point is, the author is considering all programming to be engineering programming. It's not. And I do hate project managers that confuse the two.
I even concede that you can mixmatch the two (testing for eg). Why don't people see this middle ground ?
It's surprisingly effective.
It can also be explained as way to postpone decisions you can't clearly make right now. First, you pretend to have made the decision, and later on you implement the decision "you wish you had made".
aka coding by intention
For smaller segments though, I'll just write out function signatures, an then go back and fill em out or restructure them.
Great comment!
On the other hand, it is also important to quickly get into the state of a running program, because only that allows for iterative development where you can catch bugs shortly after you wrote them, rather than having tons of bugs after weeks of (not sufficiently tested) work.
It makes laying out the program much simpler and gives me a birds eye view of how easy it's going to be to maintain.
The only way I can really describe it is (in terms of what you just said): "red, green, refactor" your life, not just your job. Constantly question everything you're doing, and find out what you don't like about it. Find the root cause of that feeling, and figure out what you can do to fix it. Find and take the steps to complete that.
Edit: I will add though, that the trick is to do so without stopping progress, as you said.
Doing some nominal TDD has helped quite a bit for me, because it does require writing code, but not code that I have to worry too much about (and if it happens to be a pretty shitty, shortsighted test, I can delete it without it, theoretically, hampering the app) screwing me over. Sometimes this physical act is enough to get me past the code-writing block.
Also, if I actually do the testing right, it prevents regression, improves code quality, etc. etc.
http://techcrunch.com/2007/08/11/facebook-source-code-leaked...
I've heard many arguments where people will tell you, it won't scale. Or you're doing it all wrong by writing quick and dirty procedural code. Quite frankly scaling is about the nicest problem to have if you're a startup. Without traction your ugly code simply isn't going to matter. With traction you will gain funding and the ugly code problem quickly disappears like I assume it did at Facebook.
I'm certain I stole it from someone here on HN years ago (whoever it was, I owe you a beer!), but I've always called this a "Maserati Problem". That is, it's something I can think about while I'm driving down the road in my Maserati that I've purchased with money my startup has made already.
Scaling, in most cases, is a problem very much like "where am I going to store all this money that's pouring in?!" If you have it, you've already won.
We programmers have a ugly tendency to judge less than stellar codes written by others. It made us feel superior to pick apart mistakes others have made. The fact is software development is a series of technical and non-technical trade-offs. I certainly had fell in the same trap before. These days I just appreciate they can ship a working product and get it off the ground.
What's so ugly about it? It's easy to understand which for me is the single most important thing when writing code.
But, if you know better, do better please.
After writing tons of code, I've come to realize that 80% of the time, it takes about the same time to do it "right" (scalable/secure/modular) as it does to do it "OK".
Ex: Are plaintext passwords any faster to build/code than encrypted+salt passwords? Nope.
And, it's fine to intend to refactor later. But, too many organizations have no tolerance later to let engineers do things that don't have visible results. I've suffered through a stint of handling "legacy code" like that & it was painful & demoralizing.
1. Let me think about this some more before giving you an answer.
2. I will use my debugger.
3. I will create a new branch for this task.
4. I have commented my code and commented it well.
5. I have accounted for fail scenarios.
6. I have considered multiple solutions to this task.
7. I have not begun hacking away immediately at a task.
8. I have attempted exploiting my code.
If I find myself getting stuck in the cycle of "this can be written better, I'll just spend 2 hours improving it rather than finishing the framework" I stop, I think about it, then I leave the working-but-probably-crap code in place and move on.
Once the framework is in place, then I start making things better, one area at a time.
Instead, I would sculpt the entire piece of art roughly, almost unrecognizably, and then target large parts of the sculpture. Details like ears and eyes might even never get done, depending on how the requirements change for the sculpture.
I always approach writing software the same way, so the first iteration is just a hardcoded piece of software that just shows me that it will actually work, and then I iterate and start smoothing out all of the sharp edges, of which there are a ton.
But of course, before starting any major project, the first thing any developer should do is map out at least a basic mental design of the program - even if that map isn't written in pseudo-code or in a form of a flow chart.
Have to add bugs to haskell code ?
gosh it seems impossible.
a pure functional language is really good for writing dumb code that "just works" in the sense that it does what you need it to do even if it isn't very pretty or fast, while at the same time keeping it reasonably maintainable because the code will not be rife with accidental complexity and assumptions about global state.
I would still go one step further and at least apply the strategy pattern/polymorphism when you have a large if/else/case block going beyond 2 or 3 conditions in a controller or model or something. (assuming MVC web apps)
The fact is you can write out some ugly code directly in a controller where it might get some code working fast and prevent the freeze up of your output, but once working you should immediately move it to the appropriate place like a model or library, and apply a basic OO strategy pattern (perhaps not with factory/interfaces etc until needed later) where appropriate to cut down on spaghetti/nested conditionals.
This really does not take much more time than stopping at working code that is ugly, and is a good middle ground where it is not painful to return to later.
There is still no good excuse for "taking a dump in the corner" of your code base just to get a marginal gain in output.
Occasionally, I find myself “stuck” when writing code – the coder’s “writer’s block” I guess. This quote by Ward Cunningham is both inspiring and truthful:
-- Once we had written it, we could look at it. And we’d say, “Oh yeah, now we know what’s going on,” because the mere act of writing it organized our thoughts. Maybe it worked. Maybe it didn’t. Maybe we had to code some more. But we had been blocked from making progress, and now we weren’t. We had been thinking about too much at once, trying to achieve too complicated a goal, trying to code it too well. Maybe we had been trying to impress our friends with our knowledge of computer science, whatever. But we decided to try whatever is most simple: to write an if statement, return a constant, use a linear search. We would just write it and see it work. We knew that once it worked, we’d be in a better position to think of what we really wanted. --
Next time you’re stuck, just write the simplest thing that could possibly work!
-- So when I asked, “What’s the simplest thing that could possibly work,” I wasn’t even sure. I wasn’t asking, “What do you know would work?” I was asking, “What’s possible? What is the simplest thing we could say in code, so that we’ll be talking about something that’s on the screen, instead of something that’s ill-formed in our mind.” I was saying, “Once we get something on the screen, we can look at it. If it needs to be more we can make it more. Our problem is we’ve got nothing.” --
Source: http://www.artima.com/intv/simplest.html
if your team builds apps with 500k LOC, you need to be constantly focused on abstracting and factoring out patterns or you will drown in your defect rate before the project even ships. If your team builds medical device software, if you aren't doing this, you'll get someone killed.
If you're building a personal blog, your time is better spent by pasting jquery snippets.
There is a spectrum here and most projects fall somewhere in between.
I've certainly experienced this with some of the people I worked with recently. Anything you win in the short term gets lost several weeks--if not days--later when everything is a horrible mess. Don't even think about coming back to that code months later!
I've found that putting in a little bit of care for the future pays off even in the short term. Perhaps not on the scale of hours or days, but definitely weeks. Which is still pretty immediate.
However, how you do this is also very important. The handy rule I've been using is simple: simplicity. Improve your code by making it do less not more. If you can make code more maitainable or general by simplifying, do it. If it would require adding new concepts or mental overhead, reconsider. Try to reuse existing abstractions as much as possible.
This usually--but not always--means making your code more functional. Do you really need to write this operation in place? Do you really need to tie everything together with a bunch of implicit state changes? Probably not! This is not to say that you should never use state: just be sure to only use it when it fits well and makes sense. And be explicit about it.
The functional style (at least in languages like Haskell) also lends itself very well to reusing simple and general abstractions. The key idea here is that these abstractions are not about doing more: they're about doing less. More general code simply leaves you less room to make mistakes. If you're working with lists of numbers, there are all sorts of arithmetic mistakes you can make; if you manage to write the same code against lists of anything, the only mistakes possible will be in list handling.
Haskell just makes this more practical by providing abstractions with a great "power-to-weight ratio": ones that manage to be simple, general and yet still expressive. Code with functors or monoids or traversables is easier to write than more concrete code and yet still flexible enough for many useful tasks. As a bonus, it's also more reusable. For free.
So the key idea is reduction: all these abstractions work by stripping concrete types of power. A functor can do much less than a list. A monoid can do much less than a number. This lets you write nicer code without becoming an "architecture astronaut" because instead of adding structure, you're really taking it away.
I've found that programming like this really helps in maintaining and extending the code later on. But it doesn't slow me down when I'm writing either--I actually save time because I spend less getting time getting the code correct in the first place.
These days, I've started being able to make large changes or add complicated features and have them work on the first try. Not every time, but surprisingly often. Certainly much more often than before! In large part, I think this is because of writing the code with an eye towards simplicity.
So the important insight: take the simple route, not the easy route.
- make the function fit in your head. Literally. Make it fit in one screen, if you can. If it does not, think how you can make it fit on the screen - everything, including variable definitions.
- avoid branching as much as possible unless it is the 'exit' branches. Avoiding branching sometimes can be done by tricks like passing function pointers. Do it - it makes code much more expressive and easier to read.
These two heuristics allow me to make the code much simpler to understand for $(me+6months). I saw the same patterns in real-world code at $work as well - using the above two principles makes the code dramatically easier to support later.
Though at least for me there is somewhere a limit (2-3 lines?) where the returns start to diminish - there's more noticeably more typing involved, and more mental context switching while debugging.
You can tell if you're too slow when you've spent two weeks thinking about the program and have neither working code nor a detailed design, or if you've created a detailed design only to realize upon implementation you'd gone about it entirely the wrong way.
You can tell if you're too fast if you find yourself spending more time cleaning up messes of poorly-thought-out spaghetti code.
I think when you take on the debt of poor code you have to remember that what it will take to fix that problem will continue to increase the longer you are away from that code.
Now I don't think you should consider fixing things for scale poor code that is it's own beast which makes the most sense to handle once you have the problem as you can't really tell what the problem will be until you have it.
Sometimes you'll want to plan ahead because the nature of the problem means you'll have a harder time cleaning things up later.
Other times, you want to move quickly because creating castle-in-the-sky architectures is not the best use of your time (especially if it becomes paralyzing).
Experience helps you determine which type of situation you're dealing with.
Personally, after 16 years in the industry, I do stress about my code as described in the article... Not about making it work, because that will happen regardless, but about how. It affects my ability to code, because "make it work" is far drowned out by "make it work the way your peers will accept it." Many times, that "acceptable" approach is something I find far less intuitive, and far less maintainable... Also, that approach has changed so many times over the years and with each different team, it's hard to keep track. I can build far more in a weekend, on my own terms, than I can build in a week by someone else's. I am not complaining... I actually like to learn those terms, and I enjoy it as long as people are constructive about it rather than condescending. It's trying to pre-determine those terms that is the block, for me.
Seems he's fairly experienced. Anyway such things are highly subjective, and YMMV
Also, while it's easy to identify code which has had too little thought put into it, it's often very difficult to identify code which has had too much thought put into it. The best solutions don't look like the result of lots of hard work, but rather make hard problems look like simple problems that would be trivial to solve.
Exactly. The problem is that code can tend to be either crap (spaghetti) or too much code (too many methods and layers of extraction). So, I'd go one step further: code should do less, not more, and should be clear and understandable, without making unnecessary sacrifices. What is clear to me isn't clear to everyone else, but striving for clarity and simplicity isn't a bad thing.
Surely, you mean abstraction?
Last week, after getting to the first page on Google's search results, I'm getting about 12,000 hits a week. Monitoring shows my daily CPU usage is nearly 0%, and memory usage around 32MB. Today(3 months later) I finally did some SEO, added compression, caching, and a CDN to get drop page load time from ~2 seconds to under 500ms.
[1] https://medium.com/look-what-i-made/5d808ce36277