[HN Gopher] Premature Abstraction
       ___________________________________________________________________
        
       Premature Abstraction
        
       Author : arendjr
       Score  : 82 points
       Date   : 2024-07-18 07:49 UTC (15 hours ago)
        
 (HTM) web link (arendjr.nl)
 (TXT) w3m dump (arendjr.nl)
        
       | Zambyte wrote:
       | Anyone who wants to do a deep dive into understanding effective
       | abstractions, I _highly_ recommend SICP. The full book[0] and
       | lectures[1] are available online for free. You don 't have to
       | know Scheme to follow along.
       | 
       | [0] https://mitp-content-
       | server.mit.edu/books/content/sectbyfn/b...
       | 
       | [1] https://ocw.mit.edu/courses/6-001-structure-and-
       | interpretati...
        
         | morkalork wrote:
         | >You don't have to know Scheme to follow along.
         | 
         | Is a hilarious understatement. No, you don't need to know it
         | when you start the book but when you finish, you certainly
         | will.
        
       | sevensor wrote:
       | Although I agree with the recommendations, I cringe at the
       | definition of abstraction. In a sane world, abstraction doesn't
       | mean defining classes so much as it means identifying important
       | unifying concepts. DRYing your code by moving a method to a
       | common base class isn't abstraction in any important way, it's
       | just adding a level of indirection. In fact, I'd argue that this
       | example is the opposite of abstraction: it's concretion. Now
       | every subclass relies implicitly on a particular implementation
       | of that shared method. Not that doing this is never useful, but
       | it's a mistake to call it abstraction when it's nothing of the
       | sort. No wonder people complain that their abstractions leak.
        
         | withinboredom wrote:
         | I'm currently dealing with a codebase that does this to a
         | ridiculous extent. Like, literally, every change affects the
         | entire project because everything is made of base-classes mixed
         | in weird ways. Every concrete object inherits multiple base
         | classes and no individual behavior. Imagine something like
         | this:                   class Book extends ShelfableItem,
         | Pagable, Authored, Readable, BaseBook {}
         | 
         | It's absolutely insane.
        
           | patmorgan23 wrote:
           | Oof. I know this is an example but all of those look like
           | they would be better served as interfaces.
        
             | withinboredom wrote:
             | Yes. Did I mention there are interfaces too, with almost
             | the same name, but it's only used for the base classes.
             | (Yes, there is only one implementation of all interfaces).
        
             | dakiol wrote:
             | It depends. Of course we don't see the whole picture
             | because it's just an example by OP, but I also find weird
             | that the de facto solution to abstract classes is:
             | interfaces. Sometimes, duplication is better than
             | interfaces.
        
             | favorited wrote:
             | Even if they were interfaces, it screams of the "model your
             | code after physical objects" approach, where a system has 1
             | enormous "Book" type which represents all the things you
             | can do with a physical book.
             | 
             | It seems unlikely that the same type should be "Shelfable"
             | and "Readable" / "Pagable," because they describe distinct
             | sets of operations. When a book is on a shelf, you can't
             | page through it. If you "read" a book on a shelf, you only
             | see the title, author, and maybe some pull quotes.
        
           | neonsunset wrote:
           | Except usually the name of every type the Book inherits from
           | or implements is so long it has to be put on a separate line.
        
         | sevnin wrote:
         | If by abstraction you mean identifying unifying concepts then I
         | cant understand how you reasoned yourself into thinking that
         | identifying a common method and sharing it between multiple
         | classes by the means of the super class is not abstraction. You
         | have identified a commonality - the common code, common method.
         | By your definition it's abstraction.
        
         | mannykannot wrote:
         | I came to say more-or-less the same thing. The author is making
         | some valid points, but the moral is that premature
         | _reification_ of abstract concepts may be harmful, especially
         | if there is something vague about them.
        
       | aswerty wrote:
       | > Post-Architecture is a method of defining architecture
       | incrementally, rather than designing it upfront
       | 
       | For anyone else wondering what it means.
       | 
       | I'm going to be honest, almost all architecture I've seen out in
       | the wild has followed a more incremental approach. But then again
       | everywhere I've worked hasn't separated the architecture/coding
       | roles.
        
         | tracker1 wrote:
         | If you work with C# or Java in a lot of places, such as Banking
         | in particular, you'll definitely see a lot of up-front
         | architecture and excess abstractions early on.
        
       | recursivedoubts wrote:
       | My general take (and w/ the caveat that every system is
       | different) is as follows:
       | 
       | - procedural code to enter into the system (and perhaps that's
       | all you need)
       | 
       | - object oriented code for domain modeling
       | 
       | - functional code for data structure transformations & some light
       | custom control flow implementation (but not too much)
       | 
       | I like the imperative shell, functional core pattern quite a bit,
       | and focusing on data structures is great advice as well. The
       | anti-OO trend in the industry has been richly earned by the OO
       | architecture astronauts[1], but the idea of gathering a data
       | structure and the operations on that data structure in a single
       | place, with data hiding, is a good one, particularly for domain
       | modeling.
       | 
       | In general I think we are maturing as an industry, recognizing
       | that various approaches have their strengths and weaknesses and a
       | good software engineer can mix and match them when building a
       | successful software project.
       | 
       | There is no silver bullet. If only someone had told us that years
       | ago!
       | 
       | [1] - https://www.joelonsoftware.com/2001/04/21/dont-let-
       | architect...
        
         | kingofthehill98 wrote:
         | That's the strategy I always take when designing a system.
         | Funny that I never thought about it before, I think most PHP
         | developers will relate to that aswell.
         | 
         | - Procedural single point entrance into the system (network ->
         | public/index.php, cli -> bin/console)
         | 
         | - OOP core for business logic, heavily borrowed (copied) from
         | Java OOP model
         | 
         | - Functional code inside classes/methods whenever possible
         | (callables/closures, array_map/filter/reduce/walk,
         | illuminate/collections, etc.)
        
         | bunderbunder wrote:
         | OO code for domain modeling might be, to date, the single
         | greatest source of disillusionment in my career.
         | 
         | There are absolutely use cases where it works very well. GUI
         | toolkits come to mind. But for general line-of-business domain
         | modeling, I keep noticing two big mismatches between the OO
         | paradigm and the problem at hand. First and foremost, allowing
         | subtyping into your business domain model is a trap. The
         | problem is that your business rules are subject to change, you
         | likely have limited or even no control over how they change,
         | and the people who do get to make those decisions don't know
         | and don't care about the Liskov Substitution Principle. In
         | short, using one of the headline features of OOP for business
         | domain modeling exposes you to outsize risk of being forced to
         | start doing it wrong, regardless of your intentions or skill
         | level. (Incidentally, this phenomenon is just a specific
         | example of premature abstraction being the root of all evil.)
         | 
         | And then, second, dynamic dispatch makes it harder for
         | newcomers to figure out the business logic by reading the code.
         | It creates a bit of a catch-22 situation where figuring out
         | which methods will run when - an essential part of
         | understanding how the code behaves - almost requires already
         | knowing how the code works. Not actually, of course, but
         | reading unfamiliar code that uses dynamic dispatch is and
         | advanced skill, and nobody enjoys it. Also, this problem can
         | easily be mitigated with documentation. But that solution is
         | unsatisfying. Just using procedural code and banging out
         | whatever boilerplate you need to get things working using
         | static dispatch creates less additional work than what it takes
         | to write and maintain satisfying documentation for an object-
         | oriented codebase, and comes with the added advantage that it
         | _cannot_ fall out of sync with what the code actually does.
         | 
         | Incidentally, Donald Knuth made a similar observation in his
         | interview in the book _Coders at Work_. He expressed
         | dissatisfaction with OOP on the grounds that, for the purposes
         | of maintainability, he found code reuse to be less valuable
         | than modifiability and readability.
        
           | sqeaky wrote:
           | This is a strong argument against inheritance, but that isn't
           | everything about OOP. Just one well supported advanced
           | abstraction (That I would also argue should be rarely used.)
           | 
           | I would argue that just having strong type system and
           | bundling methods with data gets you the vast majority of the
           | usefulness of OOP. Liskov, Open/Closed, Message Passing, and
           | other theoretical abstractions be damned.
           | 
           | EDIT - Where are the good places to use inheritance?
           | 
           | There are only a few I can think.
           | 
           | One is when you are trying to create a system that inverts
           | dependencies by allowing a plugin system or follows some sort
           | of nuanced workflow that others might want to "hook into".
           | But that isn't the only way to do that, maybe other ways
           | would be better like passing in functors.
           | 
           | Another situation I have seen recently is when creating a
           | kind of data or messages that differ only by type and maybe a
           | few small pieces of behavior and they are all known up front.
        
             | moth-fuzz wrote:
             | > One is when you are trying to create a system that
             | inverts dependencies by allowing a plugin system or follows
             | some sort of nuanced workflow that others might want to
             | "hook into".
             | 
             | I'm fairly certain that's _the_ use case of inheritance -
             | at least in the Simula tradition. Classes as a means of
             | lifetime management, moving parts that have well defined
             | steps of operation (methods), and interchangeable parts
             | (subtypes) which you can more or less slot into the larger
             | system (polymorphism).
             | 
             | It's easier to think about classes not as nouns, but as
             | verbs over time (or rather, bounded by time): at a specific
             | moment in the assembly line, call this particular method,
             | at another moment, call that other method...
             | 
             | Object oriented programming in the Simula tradition I would
             | even go as far as to say is just best practices in
             | structured/procedural programming taken to their logical
             | extremes.
        
               | jancsika wrote:
               | > Classes as a means of lifetime management
               | 
               | Wrt plugin systems: at least as the class level, are
               | classes really a means of lifetime management in
               | practice?
               | 
               | IIRC, audio plugin APIs follow the shell command pattern
               | of memory management for loading new classes-- the user
               | dynamically loads a library into a running instance of an
               | application, and there it stays until the application
               | exits.
               | 
               | And even if plugin systems as implemented are actually
               | unloading classes, the user is almost always just
               | restarting the app to make sure it took. :)
               | 
               | Edit: clarification
        
             | recursivedoubts wrote:
             | Agree 100%: static typing (for code completion) +
             | method/data bundling is the major win in OO, and it rarely
             | gets talked about for whatever reason.
             | 
             | It's unfortunate that inheritance became such a major focus
             | of practical OO languages. Would love to see a composition-
             | first OO language. Might have its own problems, but would
             | at least be interesting.
        
               | 1propionyl wrote:
               | Go, Rust, Zig, etc all support static typing and
               | method/data bundling without any explicit language
               | support for implementation inheritance (interface
               | inheritance in general and especially when structural
               | rather than nominal is not nearly as much of an issue and
               | doesn't create strict tree hierarchies).
               | 
               | Rust has support for variance and subtupint so perhaps
               | it's not as pure of an example, but it's pretty heavily
               | restricted.
               | 
               | Zig's support for method/data bundling being used for
               | "objects" isn't even first class so I wouldn't call it OO
               | (object- _oriented_ ) so much as object-orientation-
               | capable with less fuss than if one wanted to build their
               | own objects system in C.
        
               | sqeaky wrote:
               | Even in C++ the last time I thought I might need
               | inheritance I made a simple class/struct with a few
               | members that were `std::function` instances. Instead of
               | needing inheritance this worked and I managed to keep
               | type safety checks on all function return and parameter
               | types. Once upon a time this would have been weird
               | function pointers and `void*` with dangerous casts. Last
               | month when I did it, there were just lambdas passed to
               | typesafe constructors.
        
               | jerf wrote:
               | Whether you'd call "composition-first" is probably asking
               | for a big argument about what "composition first" really
               | means, but Go is certainly a language that syntactically
               | privileges a particular type of composition over
               | inheritance. It doesn't even have syntax for inheritance,
               | and frankly even manually implementing it is rather a
               | pain (best I've ever done requires you to pass the
               | "object" as a separate parameter to every method call...
               | and, yes, I said that correctly, to every _method_ call).
               | 
               | I'm not ready to try to stake a position on the top of
               | some "composition first" hill because the syntactic
               | composition it supports is not something I use all the
               | time. It's an occasional convenience more than a
               | fundamental primitive in the language, the way
               | inheritance is in inheritance-based languages. Most of
               | the composition is just done through methods that happen
               | to use in composed-in values, but it is generally not
               | particularly supported by syntax.
        
           | j45 wrote:
           | The real world definitely isn't OO all the time.
           | 
           | Maybe more functional and event based.
           | 
           | The oscillation between the two as what's in favour is also
           | humorous.
           | 
           | The right thing for the right need for the present and near
           | future, especially the newer the codebase, and the greater
           | the need to learn, is often the way to consider pursuit.
        
             | temporarely wrote:
             | > The real world definitely isn't
             | 
             | Ironically, (ime/o!h;) it turns out this the "root of evil"
             | plaguing _abstraction_ : thinking that software
             | architecture must map to "the real world".
        
               | epgui wrote:
               | I think you have it backwards.
               | 
               | Software exists in the real world, and is used to solve
               | real world problem. In building software we inevitably
               | invent or use abstractions to represent or effect real
               | world things. Abstractions that make it easier to do this
               | are good, abstractions that make it harder to do this are
               | just getting in the way.
        
               | temporarely wrote:
               | 1 - (Application) System exist in the real world, not
               | software. Software exists in machines.
               | 
               | 2 - Computing is used to solve real world problem.
               | 
               | 3 - "In building software we inevitably invent or use
               | abstractions to represent or effect real world things."
               | Here is the problem where we part company.
               | 
               | 4 - Abstractions that inform computing systems are indeed
               | useful.
               | 
               | [edit+ps]
               | 
               | self disclosure: I've reached 'architectural orbit'
               | numerous times in my career. 30 years later, I am sharing
               | a subtle point. Effective software models _cutout
               | attributes_ of real world elements of the problem domain.
               | All attempt to  "model the world" end in tears.
        
               | epgui wrote:
               | This reads like nonsense to me, so I can only assume the
               | disagreement is semantic.
        
               | j45 wrote:
               | For me, software and tech that is for someone, exists to
               | work for people, who are end users.
               | 
               | End users and customers don't exist to serve at the
               | leisure and pleasure of software and it's creators.
               | 
               | Making people work harder than they need to operate
               | software is selfish.
               | 
               | DevOps and DevEx is important, but if no one uses it with
               | those being great, the Customer and their experience are
               | often lost and never gained.
               | 
               | Learning to model something flexible enough for absorbing
               | and quickly implementing the early customer feedback that
               | is relevant is critical to boring things like retention.
               | 
               | Helping customers earn enough to eat every month, helps
               | the tool makers earn enough to eat every month.
        
           | pjc50 wrote:
           | > dynamic dispatch makes it harder for newcomers to figure
           | out the business logic by reading the code
           | 
           | This is definitely a potential problem, but I note that you
           | can also get into this mess without OO in any language that
           | lets you put a (reference to) function in a variable. Or, god
           | help you, operator overloading.
        
           | beryilma wrote:
           | [delayed]
        
         | sqeaky wrote:
         | I hadn't thought about it explicitly like this before, and I
         | think I agree. My more nebulous thought process was something
         | like:
         | 
         | 1. Try to solve the problem purely functionally.
         | 
         | 2. If that failed because of data issue, model the data with
         | objects and simple operations in an OOP style or well thought
         | collection of arrays (game devs answer to OOP causing memory
         | and caching problems, but the end result programming is similar
         | to OOP thought process)
         | 
         | 3. If that is can't happen because some external restriction is
         | impose use the minimal amount of procedural logic to solve the
         | problem and round off as many sharp corners as is practical
         | until it is unlikely any on the team gets cut.
         | 
         | Logically that is very close to inversion of thought process
         | and ordering of operations to what you suggested. But I think
         | we would recognize each others attempts to pick a design
         | paradigm in code.
         | 
         | Now I want to think about this more. Is there some underlying
         | principle here? Is this some kind of underlying principle?
         | Where do domain specific languages fit in? Do other paradigms
         | fit in? What are the bounds of this pattern, where does this
         | process fail?
        
         | epgui wrote:
         | If you think OOP is particularly well-suited to domain
         | modelling, you should try the FP approach to domain modelling.
         | 
         | You will never be able to unsee the complexity inherent to OOP.
        
           | jprete wrote:
           | Do you happen to have a link to an example or explanation?
        
             | arendjr wrote:
             | The keyword to search for is algebraic data types, which
             | are common in functional languages, but for instance Rust
             | also has them.
             | 
             | Here is an example comparing C# with F#, where the latter
             | also algebraic data types:
             | https://blog.ploeh.dk/2016/11/28/easy-domain-modelling-
             | with-...
        
               | neonsunset wrote:
               | The syntax has improved quite substantially since 2016
               | for pattern matching at C#'s end, and it's very easy to
               | model ADTs with records (and the experience of using them
               | even before that was decent with methods accepting
               | lambdas).
               | 
               | Today, you write it in a similar way you would write a
               | match in Rust.
        
             | epgui wrote:
             | The other commenter is _not technically wrong_ to point at
             | "algebraic data types", but I don't think that answer is
             | helpful at all. It's like saying the answer to data
             | modelling is tuples.
             | 
             | I would instead recommend searching for "functional
             | programming and domain driven design".
        
         | Dwedit wrote:
         | When you write a procedure that has to maintain an internal
         | state between calls, changing it into a class makes sense. As
         | for the name, you change the verb (write) into a noun (writer),
         | and you now have a name for the class.
         | 
         | C# will silently create hidden closure classes for you when you
         | use lambdas or yield.
        
           | arendjr wrote:
           | Just know that if you do this, you're injecting statefulness
           | in the center of wherever it was this procedure was being
           | used. If your entire system already has statefulness
           | everywhere, nobody will bet an eye. But if you want to have
           | any chance at creating a functional core or island, it's the
           | opposite of what you should be doing.
        
           | alex-robbins wrote:
           | When you write a procedure that has to maintain an internal
           | state between calls, stopping what you're doing and switching
           | to functional programming makes sense.
        
         | gwbas1c wrote:
         | > These things might be good architectures, they will certainly
         | benefit the developers that use them, but they are not, I
         | repeat, not, a good substitute for the messiah riding his white
         | ass into Jerusalem, or world peace. No, Microsoft, computers
         | are not suddenly going to start reading our minds and doing
         | what we want automatically just because everyone in the world
         | has to have a Passport account.
         | 
         | Priceless.
        
       | t43562 wrote:
       | I like to start with a fairly unambitious bit of procedural code
       | and gradually introduce abstractions when it starts to get
       | complicated or repetitious.
       | 
       | Straight code becomes functions, occasionally a group of
       | functions cry out to become a class.
       | 
       | In C++ this is a huge effort to do - change hurts more there. In
       | python it's much less painful and I end up with a program that is
       | some imperfect composite of object oriented with functions. Next
       | week when I want it to do more I move it further down the road to
       | structure.
       | 
       | I also like keeping side effects in their own ghettos and
       | extracting everything else out of those if possible but I'm not a
       | big functional programming person - it's just about testing.
       | Testing things with side effects is a pain.
        
         | tracker1 wrote:
         | I find that JS/TS also lends itself towards this in terms of
         | Node/Deno/Bun usage for apps. You can have a file/module that
         | simply exports a function, a collection of functions, a class,
         | etc. It's easy to keep it simple and then combine with a mix of
         | procedural, functional and oo concepts as best fits the use
         | case.
        
         | HdS84 wrote:
         | Yes, I'm doing that a lot, too. I'm often astounded how hostile
         | some languages are to later changes - e.g. java always feels
         | resistant to change, while dotnet and especially python are
         | more amenable. I.e. I totally transformed a program from
         | function to oo in python without much sweat - would have been a
         | total pain in dotnet or java
        
       | k__ wrote:
       | Isn't that mostly a question of composability?
        
       | jimmaswell wrote:
       | Fine blog post overall, but the author fell to premature
       | abstraction themselves in declaring that little Foo class bad.
       | It's entirely too generalized for me to say anything negative
       | about at all. Depending on the context, a tiny class like that
       | could be completely sensible or utterly unnecessary.
        
       | epgui wrote:
       | The discussion of procedural code doesn't make sense to me,
       | because it seems to mix together some orthogonal concepts.
       | 
       | Procedural is not the opposite of object-oriented (nor is it
       | particularly contrasting); idiomatic OOP is procedural to a large
       | degree. Effective functional programming happens when you ditch
       | the procedural approach in favour of a more declarative approach.
        
       | commandlinefan wrote:
       | > The real problem with the class Foo above is that it is utterly
       | and entirely unnecessary
       | 
       | I see this sentiment a _lot_ in anti-OO rants, and the problem is
       | that the ranter is missing the point of OO _entirely_. Hard to
       | fault them, since missing the point of OO entirely is pretty
       | common but... if you're creating classes as dumb-data wrappers
       | and reflexively creating getters and setters for all of your
       | private variables then yes what you're doing _is_ utterly and
       | entirely unnecessary, but you're not doing object-oriented design
       | at all. The idea, all the way back to the creation of OO, was to
       | expose actions and hide data. If you're adding a lot of syntax
       | just to turn around and expose your data, you're just doing
       | procedural programming with a redundant syntax.
        
         | HdS84 wrote:
         | As someone who dies a lot of python, TS, dotnet and java - I
         | disagree. The problem of dotnet and java is that everything is
         | a object. And for many cases, I don't need that object at all,
         | it can be a static class - but honestly, the python concept of
         | a module fits a lot better. It's a grouping of functions in a
         | module, not a class holding functions.
        
       | resters wrote:
       | A well chosen abstraction dramatically simplifies code and
       | understanding, and a poorly chosen abstraction has the opposite
       | effect.
       | 
       | When building a system some choices about abstractions can be
       | made early, before the problem domain is fully understood.
       | Sometimes they stand the test of time, other times they need to
       | be reworked. Being aware of this and mindful of the importance of
       | good abstractions is key to good system design.
        
       | feoren wrote:
       | I know HN doesn't like quibbles about site design, but I'm
       | literally having difficulty reading the article due to the font
       | size being forced to be at least 1.3vw. Zooming out doesn't
       | decrease the font size! Downvote if this is boring, but (a) I've
       | never seen a site that did that before, so it's just notable from
       | a "Daily WTF" kind of perspective, and (b) just in case the
       | submitter is on HN: it's actually preventing me from reading the
       | content (without changing it in DevTools anyway).
        
       | m3kw9 wrote:
       | Here's the scenario, hot shot intern comes in, calls a meeting to
       | use generics so things can be done "easier". He does a good job
       | at presenting it and its value, the dumbass team lead oks it.
       | Fast forward 1 week everyone complains behind on how much pain it
       | is to use
        
       | MisterBastahrd wrote:
       | There's no greater joy in life than jumping through an abstract
       | object, an object interface, and a factory method only to find
       | out that the factory only services one object.
        
       | phkahler wrote:
       | >> Often, an abstraction doesn't truly hide the data structures
       | underneath, but it is bound by the limitations of the initial
       | data structure(s) used to implement it. You want to refactor and
       | use a new data structure? Chances are you need a new abstraction.
       | 
       | Data structures _are abstractions_ :-)
        
       | scotty79 wrote:
       | I would love a language that has this gradual evolutional
       | abstracting as a core concern. That makes it easy. Where you can
       | start from simplest imperative code and easily abstract it as the
       | need for this arises.
       | 
       | For example a language that requires "this." or "self." prefix is
       | not such language because you can't easily turn a script or a
       | function into a method of some object.
        
       | aliasxneo wrote:
       | I'm not "formally" trained in software engineering and am
       | primarily self-taught. This area, in particular, has been
       | confusing to me over the years, especially after consuming so
       | many contradictory blog posts.
       | 
       | I tried to model DDD in a recent Golang project and am mostly
       | unhappy with the result. Perhaps in my eagerness, I fell into the
       | trap of premature abstraction, but there's not anything in
       | particular that I can point to that might lead to that
       | conclusion. It just feels like the overall code base is more
       | challenging to read and has a ton of indirection. The feeling is
       | made worse when I consider how much extra time I spent trying to
       | be authentic in implementing it.
       | 
       | Now, I'm starting a new project, and I'm left in this uncertain
       | state, not knowing what to do. Is there a fine balance? How is it
       | achieved? I appreciate the author's attempt at bringing clarity,
       | but I honestly walked away feeling even more confident that I
       | don't understand how this all works out.
        
       ___________________________________________________________________
       (page generated 2024-07-18 23:05 UTC)