[HN Gopher] Premature Abstraction
       ___________________________________________________________________
        
       Premature Abstraction
        
       Author : arendjr
       Score  : 173 points
       Date   : 2024-07-18 07:49 UTC (1 days 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).
        
               | brabel wrote:
               | Having lots of interfaces for common things is not a bad
               | thing. See how Rust traits work... even basic structs you
               | create will probably implement lots of basic traits (some
               | of which can be done automatically, thankfully) like
               | `Display`, `Default`, several `From` or `Into` impls,
               | `Clone`, `Copy` if your type is "light", `AsRef`, `Send`
               | and many more!
               | 
               | This makes code much more reusable as so many functions
               | are written based on those basic traits alone.
               | 
               | Of course, finding the right basic types is really hard
               | and your company seems to have done that badly, but in
               | principle, having some basic types to model very common
               | "things" is a necessary thing.
        
               | withinboredom wrote:
               | The issue isn't the interfaces; it's that there is only
               | one implementation (the base classes) per interface--so
               | why even bother having an interface? Otherwise, I agree
               | with you to a degree.
               | 
               | The main issue with the codebase is that if you want to,
               | say, change the behavior of a Book, you have to go change
               | the behavior of some base class (after working out which
               | one is actually being called). This base class might be
               | used in a Clock, Field, or Filesystem as well--something
               | so conceivably far away that their similar behavior is a
               | coincidence and not really related at all. Then you get
               | to argue with the architect about whether "reading a
               | book" is different than "reading a clock."
        
             | 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.
        
           | rezonant wrote:
           | crying in rails concerns :*-(
        
         | 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.
        
           | chowells wrote:
           | It's not abstraction because it's not presenting a simpler
           | mental model. It's just shoving some code located somewhere
           | else into scope. It's mere indirection.
        
           | mrkeen wrote:
           | Example of abstraction vs indirection:
           | 
           | If I said 'User', we both know exactly what that means. It's
           | so semantically simple that laypeople know what it means. But
           | our implementations could vary wildly. Someone who's just
           | taken Java 101 will be thinking of a class with getName() and
           | setName(). But someone who's just taken SQL 101 will think of
           | a User as an INT or UUID, where features are added by
           | referencing that user's id from different tables. User is
           | abstract because it's understandable and not locked into any
           | particular implementation.
           | 
           | I love Kafka. But it's a PITA to program against, at least in
           | Java. I cannot code directly against it and always need to
           | make my own wrapper classes to construct and poll it. I'll
           | make ResumingKafkaReader and RewindingKafkaConsumerFactory,
           | etc. These are not abstract, because they are very specific
           | about what and how they do things. They are concrete
           | behaviours wrapped with 1-2 levels of concrete indirection.
           | 
           | However, I might inject one of my Kafka indirections into a
           | business logic class, interfaced as a Supplier<User>, which
           | makes it abstract. I can then unit test my class, safe in the
           | knowledge that my class cannot know if a User came from Kafka
           | or just a test stub.
           | 
           | So I push back on the thesis of the article, and double-down
           | on doing things abstractly first and foremost. This is
           | closely related to the dependency inversion principle. Write
           | (and test) your business classes around Users and other
           | abstract things. Once you've done it wrong a few times and
           | eventually gotten it right, then you can start writing the
           | _indirections_ (e.g. AbstractKafkaFactory) which the article
           | rightly claims slow you down in the beginning.
        
         | 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.
        
         | bubblyworld wrote:
         | I had to mull over this for a while, but I think I agree -
         | abstractions at a conceptual level are much more powerful than
         | object-level "compression".
         | 
         | Concepts/domain model/whatever tend to change over time though
         | (at least in the business world, maybe not so much tooling
         | etc). I think that's another source of leaky abstractions -
         | things that conceptually made sense together at one point grow
         | apart, and now you're left with common code that is deeply
         | integrated but doesn't _quite_ fit any more.
        
       | 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.
        
               | mjevans wrote:
               | Go's first class support for typed return tuples and
               | Interfaces is a lovely replacement for inheritance (E.G.
               | an Interface of type blah supports this signature). They
               | function as an API contract, if a given class implements
               | the requirements of the Interface, it can be cast to and
               | used as that anywhere which accepts that interface.
        
               | 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.
        
               | cjblomqvist wrote:
               | Typescript and it's structural typing my be what you're
               | looking for.
        
               | bunderbunder wrote:
               | It's not unfortunate happenstance, it's by definition.
               | 
               | Dynamic dispatch is _the_ defining feature of object-
               | oriented programming. In dynamically typed languages such
               | as Smalltalk, you can get there with duck typing. But a
               | statically typed language needs a statically typed
               | mechanism for dynamic dispatch, and that requires some
               | way of saying,  "Y is a particular kind of X, so all
               | members of X are also in Y." Which is - again by
               | definition - inheritance.
               | 
               | You could remove - or refuse to use - the inheritance
               | (or, equivalently for some purposes, duck typing). But
               | that would also prevent the use of dynamic dispatch, so
               | what you're doing would bee be procedural programming,
               | not OOP, even if you're using an object-oriented language
               | to do it.
        
               | randomdata wrote:
               | _> Dynamic dispatch is the defining feature of object-
               | oriented programming._
               | 
               |  _Message passing_ is the defining feature of object-
               | oriented programming. Dynamic dispatch can be achieved
               | using message passing, but message passing is more than
               | dynamic dispatch.
               | 
               | Ultimately, static typing is incongruent with object-
               | oriented programming. Messages are able to be invented at
               | runtime, so it is impossible to apply types statically.
               | At best you can have an Objective-C-like situation where
               | you can statically type the non-OO parts of the language,
               | while still allowing the messages to evade the type
               | system.
        
             | marcosdumay wrote:
             | > I would argue that just having strong type system and
             | bundling methods with data gets you the vast majority of
             | the usefulness of OOP.
             | 
             | Yes, a module system brings almost all of the advantages of
             | OOP. The one remaining is structure abstraction (things
             | like interfaces on Java derived languages, or type classes
             | on Haskell derived ones).
             | 
             | But well, none of those are even typically associated with
             | OOP. The OOP languages just have those features, like they
             | have variables too.
        
               | josephg wrote:
               | Yep. Rust has all of these features (modules, structs
               | with associated methods and type classes (traits)). But
               | nobody thinks of it as an OO language. In fact, I've
               | heard that many people struggle with rust if they've come
               | from a heavily OO language like Java. You have to
               | structure your code a little differently if you don't
               | have classes.
               | 
               | Modula apparently had many of these features too - and
               | that predated what we now think of as object oriented
               | programming. The good parts of OOP aren't OOP.
        
             | bigstrat2003 wrote:
             | Inheritance is just plain a great way to model a lot of
             | relationships, in my experience, because a lot of things
             | are most easily thought of as "x is a kind of y". I am
             | perennially baffled that people shit on inheritance so
             | much, because I think it's incredibly useful. I find myself
             | often missing inheritance when working in Rust, for
             | example.
        
               | bccdee wrote:
               | Implementation inheritance often leads to code that is
               | just awful to read. If class C extends class B and class
               | B extends class A, then to find out what `new C().foo()`
               | actually does, you need to read through the whole C-B-A
               | hierarchy, bottom to top. If `A.foo()` calls
               | `this.bar()`, you have to start again, from the bottom of
               | the hierarchy. With an inheritance hierarchy of depth _n_
               | , every method call could be going any of _n_ different
               | places. With an interface, there 's a single level of
               | indirection. With composition, the code simply _tells you
               | what happens next_.
               | 
               | If class A and class B both implement interface X, and B
               | wants to borrow code from A, it should just call A's
               | methods--ideally, static methods, but B can keep an
               | instance of A if it wants. Explicit is better than
               | implicit.
               | 
               | Also, I dislike ontological statements like "x is a kind
               | of y." What does that mean? Typically, it's a claim about
               | behaviour: "x offers method w and satisfies invariant v".
               | But the actual blueprint here is an interface, (w,v)--not
               | another object y. The waters get even muddier when we
               | start talking about "is-a" vs "has-a" relationships. It
               | feels like OOP is trying to unhelpfully distance us from
               | what's actually going on with our code. Under the hood,
               | inheritance is no more than syntactic sugar for
               | composition. I think that OOP's focus on the ontological
               | philosophy of inheritance is the reason why it led to so
               | much bad AbstractObserverStrategyFactory-style code.
        
           | 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.
        
               | josephg wrote:
               | > In building software we inevitably invent or use
               | abstractions to represent or effect real world things.
               | 
               | Ehhh. Most abstractions I've written aren't abstractions
               | over the real world. They're abstractions over low level
               | machinery of a computer or program. (Eg there's no HTTP
               | request and response, DB connection or network socket
               | outside of computer software).
               | 
               | The real world isn't object oriented. It's just a bunch
               | of atoms moving around. You can describe physical reality
               | as a bunch of objects that interact via ownership and
               | method calls, but there's nothing natural about that. OO
               | is no better of a way to describe the real world than
               | actors & message passing, or state & events.
               | 
               | Software that models "the real world" usually describes
               | users, money, addresses and things like that. But none of
               | those things are made out of atoms. There is no money
               | molecule. Money is not on the periodic table. They're all
               | just another form of abstraction - one that happens to
               | exist outside the software world, that we can capture in
               | part in a database table.
               | 
               | Interesting abstractions are all invented ideas. Some are
               | useful. Some are elegant to express and use in OO code,
               | and some are not.
        
               | 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.
        
             | bruce343434 wrote:
             | Operator overloading isn't to blame.
             | 
             | If I overload shift-left `<<` for a completely different
             | concept such as "piping", that's my mistake. That's like
             | writing a normal function or method and calling it `foo()`
             | when it has nothing to do with the concept of fooing.
             | 
             | That said, unless you are writing a math library or some
             | container, there's not many good uses for operator
             | overloading.
        
               | pjc50 wrote:
               | > If I overload shift-left `<<` for a completely
               | different concept such as "piping", that's my mistake.
               | 
               | Yours and C++'s.
        
               | frogulis wrote:
               | I think the main difference between the two is, as
               | someone reading and debugging the code, will probably
               | eventually check even those methods that I assume I know
               | roughly what they do. In contrast, I may not even think
               | to check an overloaded operator unless I _already know_
               | that it's overloaded.
               | 
               | Maybe a good analogous method would be an overloaded
               | `.ToString()` in C# that has side effects or returns the
               | full text of the Magna Carta or something.
        
               | bunderbunder wrote:
               | Custom operators of any kind are definitely a problem for
               | learners. I think people just fixate on overloading
               | because that's the only kind of operator customization
               | available in the most popular languages.
               | 
               | The particular problem is that search engines tend to
               | have terrible support for searching for arbitrary
               | sequences of non-alphabetic characters.
        
           | beryilma wrote:
           | But, in a way, OO modeling and design was invented to solve
           | the mess that "banging out" procedural code created in the
           | first place.
           | 
           | You have to model your business domain in software one way or
           | another anyway. Why should it be bad to try to be more
           | methodical about it using OO methods? We do it with
           | relational databases all the time where tables are pretty
           | similar to objects.
        
             | nanomonkey wrote:
             | OO and Relational databases really don't mix well, so much
             | that there are wikipedia articles on the subject [0].
             | 
             | ORMs create some of the worst SQL table layouts that I've
             | ever seen, not to mention they generally cause tons of N+1
             | query problems.
             | 
             | [0] https://en.wikipedia.org/wiki/Object%E2%80%93relational
             | _impe...
        
             | arendjr wrote:
             | I actually have nothing against objects and methods, but
             | that's a very limited subset of OO. I prefer to use
             | algebraic data types for domain modeling, and giving them
             | methods is totally fine too. But I do prefer them to be
             | immutable in most cases, which is also quite
             | counterintuitive from an OO perspective.
        
           | fenomas wrote:
           | I'm not a big thinker on such things, but my general rule is
           | that subtyping is okay only for implementation details.
           | 
           | I.e. it's okay for ConsoleLogger to be a subtype of Logger,
           | but PaidUser probably shouldn't be a subtype of User.
        
             | green_dee wrote:
             | So interfaces > modelling
             | 
             | ConsoleLogger is a Logger because they share a method
             | (log), PaidUser and User can have some common things, but I
             | don't think it's only in the way it behaves, but also in
             | the way you contact/use them
        
           | anymouse123456 wrote:
           | FWIW - OO has the same (or more) problems in GUI toolkits.
        
         | 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.
        
               | nequo wrote:
               | How do you represent sum types in C# today? Can enum
               | members hold records as data, or do you do it
               | differently?
        
             | 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".
        
             | rapnie wrote:
             | Domain Modeling Made Functional [0] by Scott Wlaschin
             | 
             | [0] https://www.youtube.com/watch?v=2JB1_e5wZmU
        
           | brabel wrote:
           | You can try the FP approach today in most languages that are
           | not even FP:
           | 
           | * Rust * Kotlin * Dart * Java
           | 
           | Yep, even Java. Check this out:
           | https://blog.jdriven.com/2021/10/sealed-classes/
        
         | 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.
        
         | dgb23 wrote:
         | > object oriented code for domain modeling
         | 
         | I'm not sure about that.
         | 
         | If we're talking about IT (information processing in general),
         | then the domain model is just data representing facts and
         | should probably be treated as that, and not some metaphorical
         | simulation of the world.
         | 
         | I've come up with a pretty useful test for when to apply OO:
         | 
         | When you need to model a _computational unit_[0] in terms of
         | _operational semantics_, then use OO.
         | 
         | [0] Decidedly _not_ a simulation of a metaphor for the "real
         | world".
         | 
         | ---
         | 
         | Examples:
         | 
         | A resizable buffer: You want operations like adding, removing,
         | preemptively resizing etc. on a buffer. It's useless to think
         | of the internal bookkeeping of a buffer that is represented in
         | its data structure when you use it.
         | 
         | A database object: It wraps a driver, a connection pool etc.
         | From the outside you want to configure it at the start, then
         | you want to interact with it via operations.
         | 
         | A HTTP server: You send messages to it via HTTP methods, you
         | don't care about it's internal state, but only about your
         | current representation of it (HATEOAS) and what you can do with
         | it.
         | 
         | A memory allocator: The name gives away that you can _do_
         | things with it. You first choose the allocator that fits your
         | needs, but then you _operate_ on it via alloc/free etc.
         | 
         | ---
         | 
         | Some of us wince when we hear "OO", because it has been an
         | overused paradigm. Some advocates of OO have been telling us
         | that it is somehow total (similar to FP advocates) and people
         | have been pushing back on this for a while now.
         | 
         | When applied to information processing especially, it becomes
         | ridiculous, complex and distracting. I call this "Kindergarten
         | OO": You to write code as if you explain the problem to a child
         | via metaphors.
         | 
         | Computational objects however arise naturally and are very
         | obvious. I don't care if those are encoded as classes, with
         | closures or if we syntactically pretend as if they aren't
         | objects. They are still objects.
        
         | mejutoco wrote:
         | > 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.
         | 
         | One can do this in a module without OOP.
         | 
         | The idea of mixing data and behaviour/state (OOP) instead of
         | keeping data structure and functions transforming those
         | (functional) is IMO the biggest mistake of OOP, together with
         | using inheritance.
         | 
         | I believe making part of the program data instead of code (and
         | thus, empty of bugs) is such a big advantage. Already lisp was
         | talking about it. Mixing data with behaviour, without a clear
         | delimitation creates a tight-coupled implementation full of
         | implicit assumptions. Outside the class things are clean, but
         | inside they ossify, and grow in complexity. Pure functions with
         | data in data out are such a big improvement in clarity when
         | possible.
        
         | NomDePlum wrote:
         | James Gosling, who I'd consider the father of one of the most
         | popular OO languages gave this advice:
         | 
         | "You should avoid implementation inheritance whenever possible"
         | 
         | My early days of Java where largely building unmaintainable
         | inheritance trees into my code and then regretting it. This
         | quote gave me comfort that it wasn't really that good an idea.
         | 
         | Decent discussion on inheritance Vs composition also found
         | here:
         | https://en.m.wikipedia.org/wiki/Composition_over_inheritance
        
         | voidhorse wrote:
         | Look, I'm going to catch flak for this but at the end of the
         | day the main problem is that Java and C++, the most popular OOP
         | languages, are just _bad_ programming languages.
         | 
         | There are OOP languages out there, most of them older than Java
         | and C++, that actually provide a much better set of knobs and
         | handles for writing sane OO programs.
         | 
         | Java is finally getting a bit better thanks to a lot of market
         | pressure and good ideas from Kotlin. C++ will probably be a
         | mess forever.
        
       | 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
        
           | DarkNova6 wrote:
           | Can you give examples for what you changed?
        
             | HdS84 wrote:
             | Difficult on mobile and I'm without access to k PC ATM.
             | 
             | Basically, my predecessor hat build a python program which
             | had N modules which where applied to a data structure -
             | maybe think of correction steps. First spell checking, than
             | turn manual headlines into actual headlines etc.
             | Originally, they operated on a global data structure, and
             | every new module required calling it on the global
             | structure, so extension was difficult. Worse, every module
             | hat internal state, e.g. the number of spelling mistakes.
             | Reporting these things at the end was cumbersome. There
             | where "required" functions in each module, but it was
             | difficult to newcomers to discover who they where. We
             | changed it so that every step was a class which inherited
             | from a base class, so adding a new step was as easy as
             | inheriting from the base class. Furthermore, we added Auto
             | discovery, so just adding the class in a module was
             | sufficient to get it executed.
        
       | 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).
        
         | arendjr wrote:
         | That's useful feedback, thanks!
        
       | 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.
        
         | teodorlu 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.
         | 
         | This is about how I write Clojure.
         | 
         | I start out with some code that does the thing I want. Either
         | effectfull code that "does the thing" or functions from data to
         | data.
         | 
         | After a while, I feel like I'm missing a domain operation or
         | two. At that point I've got an idea about what kind of
         | abstraction I'm missing.
         | 
         | Rafael Dittwald describes the process of looking for domain
         | operations and domain entities nicely here:
         | 
         | https://youtu.be/vK1DazRK_a0
        
       | 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.
        
         | RaftPeople wrote:
         | > _I 'm not "formally" trained in software engineering and am
         | primarily self-taught_
         | 
         | Welcome to the club and I wouldn't be too worried about it (but
         | definitely read and learn what others have figured out).
         | 
         | Software design and development is still an unsolved problem.
         | The industry has not collectively found a foundational set of
         | standard practices that apply across the board other than some
         | of the most basic (e.g. organization is good).
         | 
         | You can tell that it's not solved by the relentless flow of
         | industry trends that become the new "best practice" until some
         | years later when we figure out "well, that approach has these
         | pros and these cons and tends to fit with these types of
         | problems, but definitely not a silver bullet, let's try the
         | next thing"
         | 
         | Regarding your specific issue on your new project: just be
         | pragmatic, get it working and learn from your decisions, it's
         | all just a collection of pros and cons and the analysis of pro
         | vs con changes depending on the angle you look at it (e.g.
         | short term vs long term, slow changing environment vs fast
         | changing environment, cost to value ratio, etc., etc., etc.)
        
         | marcosdumay wrote:
         | The one point of DDD is that you make a dictionary of all of
         | the domain terms and get everybody to accept them.
         | 
         | You can cut those 4 pages and throw the rest of the book away.
         | But it is one of the best books on software engineering, and
         | only gets better once you do that.
        
       | cryptica wrote:
       | Though I agree about the point about not creating
       | objects/instances where a pure function will get the job done, I
       | disagree with the general stance against OOP. I think OOP is
       | absolutely essential to simplicity. FP tends to lead to too many
       | indirections with data traversing too many conceptual boundaries.
       | FP (if used dogmatically) tends to encourage low cohesion. I want
       | high cohesion and loose coupling. Some degree of co-location of
       | state and logic is important since that affects cohesion and
       | coupling of my modules.
       | 
       | The key to good OOP is to aim to only pass simple primitives or
       | simple cloned objects as arguments to methods/functions. 'Spooky
       | action at a distance' is really the only major issue with 'OOP'
       | and it can be easily solved by simple pass-by-value function
       | signatures. So really, it's not a fundamental issue with OOP
       | itself. OOP doesn't demand pass by reference. Alan Kay emphasized
       | messaging; which is more leaning on 'pass by value'; a message is
       | information, not an object. We shouldn't throw out the baby with
       | the bathwater.
       | 
       | When I catch a taxi, do I have to provide the taxi driver with a
       | jerrycan full of petrol and a steering wheel? No. I just give the
       | taxi driver the message of where I want to go. The taxi driver is
       | responsible for the state of his car. I give him a message, not
       | objects.
       | 
       | If I have to give a taxi driver a jerrycan full of petrol, that's
       | the definition of a leaky abstraction... Possibly literally in
       | this case.
       | 
       | That said, I generally agree with this article. That's why I tend
       | to write everything in 1 file at the beginning and wait for the
       | file size to become a problem before breaking things up.
       | 
       | There are many different ways to slice things up and if you don't
       | have a complete understanding of your business domain and
       | possible future requirement changes, there is no way you will
       | come up with the best abstractions and it's going to cost you
       | dearly in the medium and long term.
       | 
       | A lot of developers throw their arms up and say stuff like "We
       | cannot anticipate future requirement changes"... Well of course,
       | not on day 1 of your new system!!! You shouldn't be creating
       | complex abstractions from the beginning when you haven't fully
       | absorbed the problem domain. You're locking yourself into anti-
       | patterns and lots of busy-work by forcing yourself to constantly
       | re-imagine your flawed original vision. It's easier to come up
       | with a good vision for the future if you do it from scratch
       | without conceptual baggage. Otherwise, you're just seeding bias
       | into the project. Once you have absorbed it, you will see, you
       | CAN predict many possible requirement changes. It will impact
       | your architecture positively.
       | 
       | Coming up with good abstractions is really difficult. It's not
       | about intelligence because even top engineers working in big tech
       | struggle with it. Most of the working code we see is spaghetti.
        
         | arendjr wrote:
         | Thanks! I would just like to clarify I'm actually not opposed
         | to OOP at all, and at several points I tell people it's fine to
         | go in that direction for the problems where you need it. I do
         | try to warn against it as a go-to solution before you've
         | understood what are the problems that actually need fixing,
         | which it sounds like we're pretty aligned on.
         | 
         | Indeed if you pass by value/use immutability where feasible,
         | you already avoid most of the issues I'm warning against, so it
         | sounds like you found a sensible way to apply it while avoiding
         | the pitfalls.
         | 
         | > If I have to give a taxi driver a jerrycan full of petrol,
         | that's the definition of a leaky abstraction... Possibly
         | literally in this case.
         | 
         | :D
        
           | cryptica wrote:
           | My view on FP is that one of its main benefits in terms of
           | avoiding bugs is that it forces you to turn all your 'movable
           | state' into raw messages. The fact that it also abolishes
           | 'unmovable state' is just a quirk of it, not necessarily a
           | benefit.
           | 
           | My main point in support of OOP is that OOP doesn't prevent
           | you from making all your 'movable state' into raw messages
           | too. Properly encapsulated instance state is not dangerous.
           | What I like about OOP is that it offers some additional
           | benefits in terms of high cohesion and loose coupling because
           | co-locating logic with related state helps to write high
           | cohesion, loosely coupled modules. It reduces the amount and
           | complexity of state that needs to be transferred between
           | modules. It allows my messages and function/method signatures
           | to be even leaner than FP allows.
           | 
           | If you want to catch a taxi to the airport, FP still requires
           | that you bring a jerrycan of fuel and steering wheel to give
           | to your taxi driver, the only restriction is that he cannot
           | alter them (no mutations). The benefit of this is that you
           | can fully trust that the integrity of your jerrycan and
           | steering wheel has been maintained after the trip and you can
           | then confidently re-use them for your airplane as well...
           | Anyway, as elegant as that seems in theory, it's not quite
           | how the world works.
        
       | jaynate wrote:
       | Gall's law: "A complex system that works is invariably found to
       | have evolved from a simple system that worked. The inverse
       | proposition also appears to be true: A complex system designed
       | from scratch never works and cannot be made to work. You have to
       | start over, beginning with a working simple system."
       | 
       | Your theory of premature architecture reinforces Gall's law.
       | 
       | This is from the book Systemantics: How systems work and
       | especially how they fail (1977).
       | 
       | https://en.wikipedia.org/wiki/Systemantics
        
         | arendjr wrote:
         | I like this one, thanks!
        
       | TurboHaskal wrote:
       | It seems to me the author just wants multiple dispatch.
        
       | OhMeadhbh wrote:
       | I think there's plenty of good advice in this post, though the OP
       | doesn't talk as much about the evils of premature abstraction as
       | one might like. Still, they do talk about how to avoid it using
       | reasonable programming guidelines.
       | 
       | In the talk about data structures, I was reminded of Fred Brooks
       | quote from MMM: "Show me your flowcharts, and conceal your table,
       | and I shall continue to be mystified; show me your tables and and
       | I won't usually need your flowchart; it'll be obvious." Several
       | people have translated it to something like "Show me your code
       | and conceal your data structures, and I shall continue to be
       | mystified. Show me your data structures, and I won't usually need
       | your code; it'll be obvious," for a modern audience.
       | 
       | Several years ago I was happy to work with several people with an
       | interest in philosophical history. We whiled away the hours
       | thinking about whether these quotes represented something of the
       | tension between Hericlitus (You cannot step into the same river
       | twice) and Plato (everything is form and substance.) So... I
       | think the observation about the alternating utility of form and
       | function is an old one.
        
         | arendjr wrote:
         | Love it! Great quote and very on point.
         | 
         | As for Heraclitus vs. Plato, I think the lesson I'm trying to
         | teach is to not pick a side until you understand each
         | position's implications and which of those might be more
         | beneficial to the problem at hand ;)
        
       | ImHereToVote wrote:
       | This is nothing to be ashamed of. We have all been there. I mean,
       | I haven't.
        
       | agentultra wrote:
       | This seems more like _premature indirection_ than abstraction.
       | 
       | There are other design "paradigms," such as _denotational
       | design_. You start with and build from abstractions.
        
         | vilunov wrote:
         | I used to read and write a lot of Scala and a bit of Haskell
         | code which claims to be very similar to what I think you're
         | mentioning here. There people also start with defining the
         | domain in interfaces (algebras, eDSLs) and data types.
         | 
         | In the end it's still the same indirection and abstraction as
         | in any other Java or Go codebase, and it prevents the developer
         | from easily accessing the actual logic of the program.
        
       | BobbyTables2 wrote:
       | Premature Abstraction is a common problem, affecting developers
       | of all ages. Most are too embarrassed to talk about it.
       | 
       | Fortunately, there are non-invasive therapies that can reduce the
       | frequency of occurrence.
        
         | arendjr wrote:
         | xD
         | 
         | In all seriousness though, you do hit a great point. The moment
         | you stop being embarrassed about your mistakes and set your ego
         | aside, is the moment that you can truly start learning from
         | those same mistakes. At some point it even becomes the only way
         | you can move forward, unless you want to stay boxed inside a
         | niche of expertise defined by your own self-set boundaries.
        
       | gspencley wrote:
       | I've been a software eng professionally for 25 years. Have been
       | coding more like 30 - 35. There is a fundamental principal here
       | that I agree with and it surrounds a code smell that Martin
       | Fowler termed "Speculative Generality" in his book "Refactoring."
       | 
       | Speculative Generality is when you don't know what will have to
       | change in the future and so you abstract literally everything and
       | make as many things "generic" as you possibly can in the chance
       | that one of those generic abstractions may prove useful. The
       | result is a confusing mess of unnecessary abstractions that adds
       | complexity.
       | 
       | However, yet again I find myself staring at a reactionary post.
       | If developers get themselves into trouble through speculative
       | generality, then the answer is clearly "Primitive Obsession"
       | (another code smell identified in "Refactoring") right?
       | 
       | Primitive Obsession is the polar opposite of abstraction. It
       | dispenses with the introduction of high-level APIs that make
       | working with code intuitive, and instead insists on working with
       | native primitive types directly. Primitive Obsession often comes
       | from a well meaning initiative to not "abstract prematurely." Why
       | create a "Money" class when you can just store your currency
       | figure in an integer? Why create a "PersonName" class when you
       | can just pass strings around? If you're working in a language
       | that supports classes and functions, why create a class to group
       | common logical operations around a single data structure when you
       | can instead introduce functions even if they take more parameters
       | and could potentially lead to other problems such as "Shotgun
       | Surgery."
       | 
       | This is not to say that the author is wrong or that one should
       | embrace "premature abstraction." Only that I see a lot of
       | reactionary thinking in software engineering. Most paradigms that
       | we have today were developed in order to solve a very real
       | problem around complexity at the time. Without understanding what
       | that complexity was, historically, you are doomed to repeat the
       | mistakes that the thinkers at the time were trying to address.
       | 
       | And of course, those iterations introduced new problems.
       | Premature Abstraction IS a "foot gun." What software engineers
       | need to remember is that the point of Design Patterns, the point
       | of Abstractions, the point of High-Level languages and API design
       | is to SIMPLIFY.
       | 
       | One term we hear a lot, that I have been on the war path against
       | for the past decade or two is "over engineering." As engineers,
       | part of our jobs is to find the simplest solution to a given
       | problem. If, in your inappropriate use of a given design pattern
       | or abstraction, you end up making something unnecessarily
       | complicated, you did not "over engineer" it. You engaged in BAD
       | engineering.
       | 
       | When it comes to abstractions, like anything else, the key to
       | gain the experience needed to understand a) why abstractions are
       | useful b) when abstractions can introduce complexity and then
       | apply that to a prediction of what will likely benefit from
       | abstraction because it is something that will be very difficult
       | to change later.
       | 
       | All software changes. That's the nature of software and why
       | software exists in the first place. Change is the strength of
       | software but also a source of complexity. The challenge of
       | writing code comes from change management. Being able to identify
       | which areas of your code are going to be very difficult to change
       | later, and to find strategies for facilitating that change.
       | 
       | Premature Abstraction throws abstractions at everything, even
       | things that are unlikely to change, without the recognition that
       | doing so makes the code more complex not less. Primitive
       | Obsession says "we can always abstract this later if we need to"
       | when in some situations, that will prove impossible(ex:
       | integrating with and coupling to a 3rd party vendor; a form of
       | "vendor lock-in" through code that is often seen).
       | 
       | /stream-of-consciousness-thoughts-on-article
        
       | sesm wrote:
       | Premature abstraction and premature optimization are just
       | instances of Heffalump Trap pattern.
        
       | stretch1414 wrote:
       | Wonderful write-up. One way I really try to avoid premature
       | abstractions is co-locating code wherever it is used. If you
       | don't try to put everything in a shared lib or utils, you keep
       | the surface area small. Putting a function in the same file (or
       | same folder) makes it clear that it is meant to be used in that
       | folder only. You have to be diligent about your imports though
       | and be sure you don't have some crazy relative paths. Then, if
       | you find yourself with that same function in lots of places in
       | your code, you might have an stumbled upon an abstraction. That's
       | when you put it in the shared lib or utils folder. But maybe not,
       | maybe that abstraction should stay in a nested folder because it
       | is specifically used for a subset of problems. Again, that's to
       | avoid over-abstracting. If you are only using it for 3 use cases
       | that are all within the same parent folder path (just different
       | sub-folders), then only co-locate it as far up in the file tree
       | as is absolutely necessary for keeping the import simple. Again,
       | it requires due diligence, but the compartmentalization of the
       | folder structure feels elegant in its simplicity.
        
       | ChrisMarshallNY wrote:
       | In my experience, I've always found the devil to be in the [late]
       | details.
       | 
       | I have learned (the hard way), that, no matter how far I go down
       | the rabbithole, in architecture, I am never a match for Reality.
       | 
       | I. Just. Can't. Plan. For. Everything.
       | 
       | I've learned to embrace the suck, so to speak. I admit that I
       | don't know how things will turn out, once I get into the bush, so
       | I try to design flexible architectures.
       | 
       | Flexibility sometimes comes as abstractions; forming natural
       | "pivot points," but it can also come from leaving some stuff to
       | be "dealt with later," and segmenting the architecture well
       | enough to allow these to be truly autonomous. That can mean a
       | modular architecture, with whitebox "APIs," between components.
       | 
       | People are correct in saying that OO can lead to _insane_
       | codeballs, but I find that this is usually because someone
       | thought they had something that would work for everything, and
       | slammed into some devilish details, down the road, and didn 't
       | want to undo the work they already did.
       | 
       | I have learned to be willing to throw out _weeks_ worth of work,
       | if I determine the architecture was a bad bet. It happens a lot
       | less, these days, than it used to. Hurts like hell, but I 've
       | found that it is often beneficial to do stuff I don't want to do.
       | A couple of years ago, I threw out an almost-complete app,
       | because it was too far off the beam. The rewrite works great, and
       | has been shipping since January.
       | 
       | Anyway, I have my experience, and it often seems to be very
       | different from that of others. I tend to be the one going back
       | into my code, months, or years, after I wrote it, so I've learned
       | to leave stuff that _I_ like; not what someone else says it
       | should be like, or that lets me tick off a square in Buzzword
       | Bingo.
       | 
       | My stuff works, ships, and lasts (sometimes, for decades).
        
       | yellowapple wrote:
       | > /// BAD: This mutates `input`, which may be unexpected by the
       | caller.
       | 
       | > [...]
       | 
       | > /// GOOD: `input` is preserved and a new object is created for
       | the output.
       | 
       | Neither of these are good or bad without knowing their context
       | and understanding their tradeoffs. In particular, sometimes you
       | _want_ to mutate an existing object instead of duplicating it,
       | especially if it 's a big object that takes awhile to duplicate.
        
       ___________________________________________________________________
       (page generated 2024-07-19 23:14 UTC)