1. 18
  1. 13

    What the colleague was proposing sounds like premature abstraction. I see this quite often, mostly with junior and medior programmers but sometimes in seniors too (hell, I still catch myself doing it) - they start inventing all kinds of abstractions and patterns “in case this code needs to change”. But the thing is, you never know how the code will change. The things you make configurable might actually never change, and the things you baked in as assumptions might change several times.

    I find it’s best to write the code to be as straightforward as possible, so it’s easy to understand. Easy to understand code is easier to change too, because you’ll have an easier time figuring out the repercussions of your changes. Code that is factored in a way that makes it easy to understand and modify in various dimensions while still being efficient is the mark of a true craftsman.

    As you get more experienced, you’ll notice the kinds of things that tend to change a lot (data models, database layouts etc) and you’ll develop a style that favors “premature abstraction” for these kinds of things. If you’re lucky, your past experience affects the future work too and you’ll be right on the money with your abstractions. They were still a bit premature in a way, but because you’re working in a similar context you see the same patterns over and over and you and your colleague will thank your prescience that allowed the easy changes.

    However, be wary of carrying over these abstractions to other kinds of work, as your hard-won experience will actually betray you. For example, people working on frameworks and libraries tend to favor decoupling database code so that it can work on multiple databases. This is often the right thing to do for libraries, but in end-products it just makes code needlessly complex and slow - there it makes more sense to maximally leverage the features of the database you chose in the first place.

    1. 6

      I agree with all sjamaan has written. Also, I want to add:

      What the colleague was proposing sounds like premature abstraction.

      I call that YAGNI.

      I was going to joke by starting my comment as “I stopped reading at ‘My initial proposal was to add a class…’”. I was a heavy Python class user (inherited from University Java class, and the Python community), now that I have seen how much pain it is, and how much easier it is to work and debug with simple data types and functions (possibly passing function as arguments to do inversion of control).

      I did not write “you do not need classes or disjoint types, and anything beyond dict and list is overkill”. My current approach is to start new code as simple as possible but not simpler. To be able to grasp that, I learned standard Scheme that does not have OOP builtin.

      I quote the great last sentence of the previous comment that is my approach to build “abstractions” on top of SQL databases:

      in end-products it just makes code needlessly complex and slow - [in end products] it makes more sense to maximally leverage the features of the database you chose in the first place.

      1. 1

        The story is slightly editorialized to (hopefully) by applicable to a larger audience. The actual language being used doesn’t even have classes. In general I agree that OOP is a tool to use sparingly.

        1. 1

          (opinion) OOP is a great tool for API discovery

          I’ve not found it useful for much else tbh.

      2. 4

        I agree that what my colleague proposed was premature abstraction, at least from my perspective. But as you note, oftentimes experience will favor certain premature abstractions. This colleague was also at a high senior level (at least in title), so I like to give benefit of the doubt that they know what they are doing.

        What is interesting is that “code as straightforward as possible” also suffers from ambiguity. From your comments, I believe you and I agree on what that looks like, but someone from a different background may completely disagree. My colleague might argue that their proposal was very straightforward! The absolutely fascinating bit is that “decoupled code” and “code that is simple” is something we all know as the goal, but the true definitions of what these mean are not actually defined.

        I thought it was just so utterly strange that two entirely different approaches can both be justified with the same reasoning – and is there any way to “prove” that one approach is more correct than the other? Or are we just basing it all on our personal background experience?

        1. 4

          I don’t think you can “prove” that one approach is better/more correct than the other, because that’s highly subjective. Everybody will be bringing in different baggage. My gut says that “straightforwardness” as I call it should be measurable - the less indirections, classes, methods and conditionals you have, the more straightforward the code is.

          But even this relatively objective measure can be perverted, I’m sure, because programmers are just so damn good at that. Just think “code golf” to reduce the LOC count - that doesn’t necessarily make it more readable/straightforward.

          1. 3

            I lean towards agreeing that “proving” one approach over the other is impossible. Then I guess the question is, if everyone has different, subjective, ideas of what “straightforward code” and “decoupled code” is, does it even make sense to have “straightforward and decoupled code” as the north star for software engineering? If none of us agree on where that north star is in the sky, we’re all going in different directions with entirely different maps of the world.

            This is mostly just a philosophical question, one which I find myself considering. The engineer/scientist in me truly wants to define what good code looks like, but if its fundamentally a problem about people then that is clearly impossible.

            1. 3

              It’s a very good question indeed. I think maybe it’s much less important as to where your particular north star is than to have a consistent vision of where that star should be in a given team/project. That way, you get a consistent codebase that perhaps some onlookers find horrible and badly engineered, but everyone on the team agrees is a joy to maintain.

              At least in my own experience, in a team where people are aligned on a certain style of engineering, you get a codebase that has an internal coherency. If different parts are done by people with different viewpoints, you get a Frankenstein monster that has no sense of identity. I’ve seen this happen, and it can be very difficult to reconcile this without somehow “enforcing” one view on those that disagree. And even talking about it can be difficult, because it’s something that’s very difficult to adequately put into words.

              I think this is something that “style guides” try to do but fail at miserably because they’re only focused on the superficial syntactical details. But choosing (for example) whether to use interfaces or callbacks in a codebase goes much more deeply than that. And to make matters worse, there may be cases where there’s a good technical reason to deviate from the common convention.

              1. 2

                That is a good point, perhaps the only thing that does matter is what the team agrees on. Being aligned on the north star is certainly important, but where the north star is might not be. Definitely something I’m going to need to spend some time mulling over.

                Then there is the question of whether a particular north star will “dominate” so to speak. For instance, complex abstractions are oftentimes difficult to replace while more straightforward approaches are oftentimes easier. The paradox is that straightforward code is usually refactored until it is complicated and difficult to change, while complicated code remains complicated. Does a team or project’s north star inevitably drift towards complicated over time? My instinct says yes, which I feel has some interesting consequences.

                1. 1

                  Does a team or project’s north star inevitably drift towards complicated over time? My instinct says yes, which I feel has some interesting consequences.

                  hm, I’d have to agree that, in general, products tend to become more complicated due to the pressures from changing requirements, and the code follows suit. With one caveat: I’ve seen some refactorings that made needlessly complicated code much clearer (but possibly more abstract and complex to understand from scratch). Sometimes all it takes is some time away from the code to realise how it can be made simpler by finding the right abstraction for that particular part of the code. And sometimes it takes multiple iterations of refactorings in which one realises that eventually entire parts can simply be dropped.

                  1. 2

                    As an aside, I think the fact that products will grow more complex over time is a great reason not to start out making things complex right away. That complexity will probably be in different places than you expect, just like performance bottlenecks will be in different places than you expect. So premature abstraction is like premature optimization in that sense, and also in the sense that it is silly to design code in a way that you know will be slow/difficult to adapt to changing requirements - it just takes a lot of experience to see where to apply optimization and abstraction ahead of time.

              2. 1

                In the long term this can be measured in money: how much product earns and how much it costs to support it. Earnings incorporate values like customer satisfaction. Spends can tell about team velocity and maybe even employee churn.

        2. 7

          I can relate to this topic. Background: I have started with Java and later transitioned to Go. At this point, I have more Go experience than Java. I will concentrate on these two languages. But I believe that concerning the topic we can roughly compare two groups of languages: (Go, C, Rust, etc.) and (Java, .NET, Ruby, etc.).

          I have noticed that traditions (or cargo-cult) in Java are solid. You always apply some layered architecture, you always use interfaces, and you always test against your mocks. Often you do not ask a lot of questions with well-established legacy corporate systems. And very often, your framework will dictate how to structure your code.

          In Go, and I believe in C as well, things are more straightforward. Due to the niche (infrastructure in case of Go) that language occupies, in most cases, you have a minimal system that is fine-tuned to solve one particular problem, interacting with well-known external systems. In Go, you declare an interface only when you can not avoid having one. Overall approaches are much more practical, and this allows you to write simpler code. If there is no framework, you have more freedom. In Go, the standard library is your framework.

          Sure, nowadays, we have more extensive Go applications that adopted some enterprise patterns. Still, an excellent project will strike you as a minimal sufficient implementation to solve concrete tasks.

          Comparing my experiences with both approaches: you get a more reliable solution when you test against the real deal (DB, queue, fs, etc.) and avoid extensive use of mocks. You get a more straightforward and more readable solution when you use fewer abstractions. Simple code makes handling (rare) incidents at 3 AM way easier.

          The enterprise approach gives you the flexibility to switch vendors rapidly, a requirement that can not be ignored in the real world.

          There is a middle ground when abstraction and complexity are extracted into a separate library and maintained independently. Then you can have both: simplicity and flexibility. From my experience, “gocloud.dev” is an excellent example of such an approach. This set of libraries abstracts away different cloud implementations for you. You can keep your applications small and simple and still have an opportunity to switch vendors and perform tests.

          To conclude, I want to say that collaboration between people with radically different views often makes the product better. It is hard, and staying respectful is the key (reminder to myself).

          1. 4

            Thanks for your thoughts! I think you capture the trade-offs between the two approaches well, and I agree, there do seem to be two groups of languages in this regard.

            I have noticed that traditions (or cargo-cult) in Java are solid.

            I have found this as well. Every colleague I have worked with that had an extensive Java background knew the name of every design pattern, and would describe all code and approaches as combinations of these. Personally, I’ve just understood the concepts behind the various design patterns, otherwise I just apply them where they fit naturally (only remembering the name of a tricky few which are handy for explaining to junior engineers so they can research them more). Completely different approaches!

            To conclude, I want to say that collaboration between people with radically different views often makes the product better. It is hard, and staying respectful is the key (reminder to myself).

            I couldn’t agree more. The combination of different views and backgrounds is incredibly important for a robust product. Staying respectful, identifying trade-offs, and justifying decisions (without cargo-culting in either direction) is extremely difficult and what I think is the true mark of an expert.

          2. 7

            The author suggestion of keeping higher level modules lighter is expressed as “Push complexity downwards” in the excellent book A Philosophy of Software Design.

            One challenge I’ve found is that maintaining the “lightness” aspect of a module is not enforceable, especially when you have many people/teams working on it. And keeping everything “decoupled” using the indirection is painful to understand and maintain.

            I’ve started following Go’s approach, create boundaries between code that needs to evolve separately and expose a simple and generic API. Within the boundary, keep the code simple and don’t think too much about coupling.

            (side note: not shilling for a book, i genuinely found is useful especially since it’s not prescriptive)

            1. 4

              In this problem, actually measuring the coupling induced by both approaches may be useful. One of the good treatments can be found in Yang’s thesis: Measuring Indirect Coupling.

              1. 2

                Probably the most useful site on the subject https://connascence.io/

                Connascence is a software quality metric & a taxonomy for different types of coupling. This site is a handy reference to the various types of connascence, with examples to help you improve your code.