1. 21
  1.  

  2. 29

    I prefer Casey Muratori’s suggestion: write the part you know how to write, first.

    Both top down thinking and bottom up thinking are needed. Focusing only on top down results in a bunch of fancy architecture that does nothing useful.

    Focusing only on bottom up results in code you won’t need now or later.

    Focusing on what you know you need, right now, to take you a step closer to the business goal, results in continuous progress towards that business goal.

    1. 3

      Focusing only on top down results in a bunch of fancy architecture that does nothing useful.

      I’ve not found this to be the case, at least using the stepwise refinement I’m used to.

      If one starts with the general goal “enjoy a sandwich”, and then subdivides that into “(make sandwich), (eat sandwich)”, and then subdivides that into “((get ingredients, assemble ingredients), (pickup sandwich, masticate sandwich, swallow))”, we see no overly abstract steps. There may be the temptation, of course, to over generalize any of the particular steps, but not doing that is just a matter of discipline–or laziness.

      What I see as a bigger issue is the use of frameworks and libraries that insert weird black boxes and keep one from seeing the entire path of the refinement–this in turn can get people feeling lost when it’s time to figure out what to do.

      1. 2

        Just to explore the terminologi; In your example, if you then start out with working on “get ingredients”, I think I would call that bottom-up. I mean, I would never start work on anything, that doesn’t have an overall goal or purpose, like “enjoy a sandwich”.

        I would probably characterize top-down, as if you start working on the parts connecting “get ingredients” and “assemble ingredients”, before actually doing “get ingredients” (or “assemble ingredients”).

        (not saying my interpretation is necessarily correct.)

      2. 1

        This exactly.

        And add continuous refactoring in the mix. Preparing an architecture for the future is what gets you in trouble, so you build it to be the best fit for what you build now and you adapt it when you are adding something new later.

      3. 15

        The problem with being so prescriptive about how people “should” write code is that everyone works and thinks differently, and what’s right for you is certainly wrong for others.

        1. 10

          Alternatively: don’t write any code… and instead draw diagrams! I don’t think it matters if you start coding from the top-down or bottom-up, if you don’t have a good high-level diagram overview both are going to end up with incorrect abstractions, or links between components that have to be hacked in.

          In terms of designing the architecture of a program, there is absolutely no substitute for a high-level diagram, one that is very clear and purposefully designed. You should be able to go through and run the various inputs and outputs in your head: X goes to Y, comes out Z goes to A, etc. Then, mark the risk points – or areas where you aren’t entirely sure if a library/API exists, or if the library/API you are using will work in the way you want. De-risk those portions, find their constraints and limitations, and then start building.

          In my opinion, if you are doing either of top-down code or bottom-up code you are already in a bad spot. By the time you start coding the project for real (ignoring de-risk coding where you learn the constraints of your libraries/APIs), it shouldn’t matter which direction or way you are coding, because at that point there shouldn’t be unknowns. The pieces should already be defined and the shared structures between them should already be known.

          Of course, you will always miss something, but it should never be big enough that it compromises your entire architecture, or else you did not spend enough time planning and de-risking.

          1. 4

            This probably works well if you’re a visual thinker, but not everyone is.

            When I conceptualize the architecture of a system I’m building, what’s in my head isn’t boxes and arrows. Occasionally I’ll be required to draw a diagram and it is invariably a disaster. Not because the design is poor, but because I seem to lack the ability to render the dimensionless, non-geometric abstraction in my head as a comprehensibly laid-out graph of lines and boxes on a 2D surface. Maybe it’s a mild form of aphantasia; I don’t know. But ask me to write a design document talking through my idea in words, and I’ll do a bang-up job of it that people will still refer back to a year after the system has gone live.

            Which I think isn’t actually counter to what you’re suggesting, as it still serves the purpose of enumerating the pieces and concretely thinking through how they fit together before diving into implementation.

            1. 2

              You are completely right, it doesn’t have to be a diagram. I’m an extremely visual thinker, so I’m very used to it being a diagram, but a design document works just as well. In any case, I think we’re on the same page about the goal! Thanks for the correction.

          2. 2

            I was reading ‘let over lambda’ yesterday and having all sorts of revelations, one of which was regarding ‘top-down coding’:

            You cannot teach beginners top-down programming, because they don’t know which end is up. —C.A.R. Hoare

            Link to relevant section: https://letoverlambda.com/textmode.cl/guest/chap5.html#sec_2

            This fits with my experience, I have been obsessed with programming language theory since I started trying to learn how to program and fell into the analysis paralysis of which language to choose. Now after having slowly learned a little bit about all of the good things; referential transparency, hygienic macros, advanced type systems, immutability, formal verification, cryptographic concerns and general computer science such as formal languages and automata theory.

            I have finally reached the conclusion that I want a language that has pretty much none of those things!

            A lisp-2 macro system looks like the holy grail in many respects. To be fair I think Idris 2 is also a contender, being able to make general algebraic objects, equipping them with operations and proving that those operations are correctly used is basically unbeatable imo… But if anything stands a chance then it’d be principled use of common lisp, which is probably the only ever “complete” language.

            At least it looks to me that if I start investing in building my own tools in common lisp then I will be able to reach for all of the above on a need basis by implementing DSLs in macros. It may be a long road but I get the feeling that the common lisp ecosystem will be steadily built up, the many implementations and the focus on portability..

            Idk yet, I’m intrigued enough that I am going to investigate it and I would appreciate experience reports.

            1. 2

              At question surely is the complexity of the problem or system to be modeled, together with the degree to which it is already known.

              If sufficiently complex, humans need to start breaking a problem into smaller pieces, that can be worked on independently and later recombined into a working whole. This of course is the design process of “decomposition”, typically in software performed in terms of “functionality”.

              While top-down design is inevitable if the system is sufficiently complex, using Functional Decomposition or some other method, the other factor is how well the problem domain and technical concerns are already known by those seeking to implement them. If well known, as in the example stated in the article, parts can be implemented “bottom-up” because the implementers already have (or think they have) some idea of what needs to be produced and how.

              1. 1

                If you start from main when all the components you have to write are still fuzzy you’ll probably have to scrap most of it by the time you actually get to writing those parts, because there’s gonna be a lot of issues popping up that you didn’t consider when you started writing it, and if you stick to that API it’s only going to create problems.

                1. 1

                  I generally appreciate this article as I’ve found myself in peculiar circumstances in the past where going bottom-up wasn’t the best idea.

                  However, I’m going to have to push back against the dismissal of unit tests, especially if you’re going greenfield (which it seems like this article is targeted at). I read the article linked and frankly this quote -

                  Most of the value of a unit test comes when you change the original (tested) code

                  Couldn’t be more wrong, especially in the context of greenfield component building. Unit tests exist to ensure the component holistically functions correctly, and as this article asserts - oftentimes this means relying on other components/apis. If any of those components alter or break in unexpected ways, our original component may fail in unexpected ways as well.

                  Basically: Making a change in place A can have an effect in place B.

                  In practice, being able to automate the testing of all components is often the only thing keeping legacy code in check. Let’s not brush it off so quickly

                  1. 4

                    I agree and was annoyed by this line in the article:

                    The correct way to architect and write a program is top-down

                    I completely agree that the correct way to architect a program is top-down. I completely disagree that the correct way to write a program is top-down. Conflating the design and implementation phases of a program is pretty worrying for someone giving advice on how to design and build programs.

                    My general rule is design top-down, implement bottom-up. When you have a top-down design, you know what the components should be. When you write code bottom-up, you don’t have to write much before you can start writing useful tests.

                    My corollary is: refactor early and often. Once you actually implement bits of a complex system, you will discover that some of the assumptions that you made in your design are not actually valid. Unit tests help here as well, because any change to the component’s behaviour will affect some tests and you can then check that the behaviour has changed in the way that it should, not some other unexpected way.

                  2. 1

                    Linked document is about designing programs top-down, not writing code top-down. The difference might seem meaningless, but I think it is huge.

                    I am writing my Go code top-down. An alternative is for example Python PEP8 - imports first, then global variables, classes, functions.

                    Here is an example of a code written top-down. You start reading at the top of the file, and you learn details of the implementation as you progress.

                    func NewFoo() Foo { 
                        return Foo {} 
                    }
                    
                    type Foo struct {
                    }
                    
                    func (f Foo) Stuff() string { 
                        return fooStuff 
                    }
                    
                    const fooStuff = "foo-stuff"
                    

                    Here is the save code as above, not structured top-down but with a pep8-like style. It does not matter how the functionality is implemented, the position of code in the file is dictated by the type of instruction.

                    const fooStuff = "foo-stuff"
                    
                    type Foo struct {
                    }
                    
                    func NewFoo() Foo { 
                        return Foo {} 
                    }
                    
                    func (f Foo) Stuff() string { 
                        return fooStuff
                    }
                    
                    1. 1

                      Write code bottom up, it will save you a lot of effort