7 ms·
It's hardly a secret, but it's still not widely understood that one huge advantage of starting with tests is that it forces you to think about how an API will b
by TillE 4y ago
It's hardly a secret, but it's still not widely understood that one huge advantage of starting with tests is that it forces you to think about how an API will be used.
Instead of just writing a class with a bunch of methods that seem useful, you write the tests to figure out what a user actually needs, and iterate on that. And as a wonderful byproduct, you get a suite of regression tests so you can comfortably refactor and add features in the future.
- tgv 4y agoDepends on what you're doing. If you're adding functionality because of a need, you've got a minimal "API" (or rather, interface) already defined. If you ignore that, you might end up with an interface that's not complete and with features that won't be used, but need to be maintained anyway. There's no fast and hard rule, I'm afraid.
- karmakaze 4y agoI often implement top-down, calling non-existent methods that do what I wished they existed to do. Then implement the called functions or even classes. The source should be comprehensible on its own at each level with good naming and separation of concerns. This is a generalization of how to write a recursive function, pretend it exists before you write it.
- mirekrusin 4y agoI write code bottom up. Break problem to first principles, write/reuse primitive, combine them. Fixing bugs is easy as one of underlying things doesn’t work and you can scope it down quickly and patch with test.
- lelanthran 4y agoThe problem with that is that no one, not even you, has 100% accurate foresight. IOW, you will never have your bottom layer done correctly or completely. At some top layer you're going to think "well, looks like I won't be need that function", OR, "Well, looks like I am missing a function for $FOO"/
- garethrowlands 4y agoIt doesn't have to be perfect to be a decent approach sometimes. A spare or missing function isn't necessarily that bad. It's the premise of On Lisp, for example, http://www.paulgraham.com/onlisp.html http://www.paulgraham.com/onlisp.html
- Jtsummers 4y agoThat's what refactoring/rewriting is for. You have your piece-parts, you start combining them at the next layer up. Realize some of them aren't what you need. Maybe you need more, maybe they should be combined in some fashion, or maybe their behavior should change. So you do. It's not like people (well, fools might, but who wants to be a fool?) say, "You know, I already finished that bottom layer, I should never, ever, ever change it. That would be admitting I was wrong and not psychic. I can't have that." Since most people aren't fools they will, instead, say something like, "Oops, that doesn't compose well. Time to fix it."
- lelanthran 4y ago> That's what refactoring/rewriting is for. You have your piece-parts, you start combining them at the next layer up. Realize some of them aren't what you need. Maybe you need more, maybe they should be combined in some fashion, or maybe their behavior should change. So you do. It's not like people (well, fools might, but who wants to be a fool?) say, "You know, I already finished that bottom layer, I should never, ever, ever change it. That would be admitting I was wrong and not psychic. I can't have that." Since most people aren't fools they will, instead, say something like, "Oops, that doesn't compose well. Time to fix it." I'm not saying it never gets fixed, I'm saying it's extra work compared to top-down. Top-down produces only and exactly what is needed to get the current layer to compile, run, and pass/fail. Bottom-up requires that the bottom-most unimplemented layer be written to provide at least what the next higher level needs. And since you cannot predict that perfectly all the time, you will have to come back and refine (not refactor) that layer: remove some functionality that turned out not to be needed, or add some other functionality that turns out was needed. This step is never going to happen in a top-down effort. [PS. At work I've almost always done bottom-up, because that suits the workflow constraints better: 1) Never leave commit a non-working build, 2) never do a PR for an incomplete module, 3) Always make PRs as small as possible, etc. For my own projects/hobbies/experiementation, I used to do the same thing. Now I'm experimenting with the top-down approach and my velocity is a little faster because I am never spending time removing stuff that was written.]
- karmakaze 4y agoWhen it comes to interface design, I draw top & down to middle. Find what interface or mechanism that is the common thing that makes the lower-level agnostic and the upper-level about applying or reapplying that mechanism in varying contexts. Some might call that thing a 'narrow waist' but I don't quite equate it to that although it's a good example. Is the thing you want a stream of bytes, sequence of messages, unsequenced messages, or priority-ordered messages? The problem with bottom-up is that you don't know the context. The problem with top-down is that you do know the context--and can leak it into what should be without. I find the latter works out better if you're aware of avoiding it. How do you avoid not having context without going top-down? An analogy in UX would be Windows vs Mac. Windows builds things then puts the UI on what they've built--in many different kinds of settings places. Mac figures out what information & controls the user should have and how to name and group them. Edit: I actually have a recent real-world example. Making a subsystem for querying and mutating things, what they are doesn't matter. We set out making an 'adjust' operation and a 'move' operation because we wanted to preserve paired decrement/increment amount for moves. We were also certain that we only needed to move between two things, or adjust many things but only of one kind. We got all the way up to the top where the public API was getting close to complete. We discovered use-cases that made sense to to more than those operations. We were able to shuffle things around and ended up with a fully-capable 'adjust' operation that can work on any number of kinds/things, and a fully-capable 'move' operation that can work on any number of kinds/things. On top of that we added checks to only do what we need now. The more general things were substantially harder to make work efficiently which is why we chose not to do it when we were sure we didn't need it. We were wrong.
- mirekrusin 4y agoI don't get what you're trying to say. You know the context. You have the whole problem in front of you. The question is about which direction you're going to solve it. For "you want a stream of bytes, sequence of messages, unsequenced messages, or priority-ordered messages?" the answer is you probably want type parametrized iterator at this level if you can ask this kind of question. To put it in other words you put more attention into trying to find underlying composition of algebras in problems than doing adhoc, inlined imperative constructions littered with if statements that you add every time some issue is discovered.
- lelanthran 4y agoI'm actually in the middle of a hobby project, where I am keeping a devlog. I've repeatedly, throughout my devlog, emphasised that I'm calling functions that don't exist, then I create stubs of those functions (a one liner returning an error), then I populate the stub with actual logic. To me, this is the most natural way of writing code.
- garethrowlands 4y agoThis is sometimes called the London school of test driven development. At least, if you then mock the non-existent methods to make your top level test pass.
- njharman 4y agoI learned TDD as a development method. Too many people imagine TDD is about testing. It's explained right there on the cover Test *Driven* Development. Not a testing method. A development process guided by unit "tests". In quotes as unit tests are at least 51% about forcing you to write small, isolated blocks of code with well defined and simple interfaces. Units. A large percentage of remainder is enabling ruthless refactoring (refactoring being a huge part of TDD's development practice/philosophy) by ensuring you have not violated those interfaces. Only a few percent is actually about having "correct" code.
- garethrowlands 4y agoThis is one motivation for BDD - it doesn't contain 'test' in the title, so it's easier to explain that it's a development method.
- drewcoo 4y ago> starting with tests is that it forces you to think about how an API will be used And tests that describe how an API can be called are BDD. Hold the Cucumber.
- taeric 4y agoI like what you are saying, but I've seen enough test driven development to know that it doesn't force any thinking about how the API will be used. It certainly can help illuminate that, but I've been constantly surprised by the convoluted "use case" tests that I've seen people create and think nothing of it.
- nonethewiser 4y agoHow often do you write methods that you dont use? I only write a method if I'm going to use it. In which case testing doesnt offer that advantage.
- marcosdumay 4y agoI'm willing to accept that there exists somewhere a large mass of developers that don't think about how their code will be used before writing it. But the one thing I can't agree on is that teaching them TDD is either necessary or sufficient for them to start thinking about that. I'm pretty sure that if you can manage to write code without knowing how it will be used, you will write tests that way too. Writing a test doesn't force you to think about your API any more than writing the API.
- jdlshore 4y agoIt "prompts" you to think about the API is probably a better way of putting it. That prompt is helpful.
- pixelrevision 4y agoIt also doubles as documentation that’s forced to stay up to date. Code has a way of becoming hard to follow especially when edge cases and performance enhancements are added.