[HN Gopher] Master Hexagonal Architecture in Rust
       ___________________________________________________________________
        
       Master Hexagonal Architecture in Rust
        
       Author : milliams
       Score  : 73 points
       Date   : 2024-09-12 08:23 UTC (5 days ago)
        
 (HTM) web link (www.howtocodeit.com)
 (TXT) w3m dump (www.howtocodeit.com)
        
       | keyle wrote:
       | Could someone TLDR me what Hexagonal architecture means?
       | Hexagonal architecture brings order to chaos and flexibility to
       | fragile programs by making it easy to create modular applications
       | where connections to the outside world always adhere to the most
       | important API of all: your business domain.
       | 
       | okay.
        
         | Kostarrr wrote:
         | Well here's the take from the Author itself:
         | https://alistair.cockburn.us/hexagonal-architecture/
        
           | FridgeSeal wrote:
           | That's a mighty lot of words, to say "functional core,
           | imperative shell".
           | 
           | Maybe I'm being glib, but damn, a whole article, and
           | boatloads of fancy new terminology, just to re-state what the
           | author succinctly landed on in the _first_ paragraph:
           | 
           | > Create your application to work without either a UI or a
           | database so you can <do a bunch of nice stuff and make your
           | life easier>".
        
             | rapnie wrote:
             | Indeed. And some great HN discussions of a great article to
             | get inspired on the pattern:
             | 
             | - https://news.ycombinator.com/item?id=18043058 (Sep 2018,
             | 127 comments)
             | 
             | - https://news.ycombinator.com/item?id=34860164 (Feb 2023,
             | 39 comments)
        
               | FridgeSeal wrote:
               | I love a good rabbit hole, thank you!
        
         | n42 wrote:
         | Domain driven design with more hexagons and less Martin Fowler
        
         | andyferris wrote:
         | Another similar/related idea to Google is the "onion
         | architecture".
        
           | andyferris wrote:
           | (Because the diagram gives a better TLDR than than the
           | hexagonal equivalent diagram, IMO)
        
           | keyle wrote:
           | yeah thanks I kept looking for a reason for the 6-way or
           | 6-layers or something.
        
         | mmcromp wrote:
         | https://youtu.be/JubdZIdLQ4M?si=GkZ3tomeqIYzYyPk
         | 
         | Best explanation by far, worth the 5min watch
        
         | itronitron wrote:
         | The bullshit remover condenses that down to the following:
         | 
         | > Bullshit.
        
         | goodpoint wrote:
         | Pretty much nothing.
        
         | sethammons wrote:
         | Step one: ignore the prefix, this has nothing to do with
         | hex/six. And "ogon" meaning sides or struggle. The sides
         | represent interfaces.
         | 
         | This arch really means use interfaces based on business use
         | cases / domains. Call the User service/module and pass user ids
         | into a billing service/module. Each service is over a defined
         | interface (adaptor) that allows separation of concerns and
         | separate data stores. You could use an in-memory port of the
         | billing service and a real db for the user service service
         | because both implementations leverage the same adapter code
        
         | regularfry wrote:
         | No explicit dependencies in the core business domain,
         | everything coordinates via interfaces defined by the needs of
         | the core.
         | 
         | That's pretty much it. Everything else flows from there.
        
         | aswerty wrote:
         | I'm always surprised that this style of architecture isn't
         | discussed in terms of functional purity [1].
         | 
         | To me, a hexagonal architecture essentially means creating an
         | abstraction at the point between pure and impure functions.
         | 
         | The realization of this approach essentially means the core of
         | your application should be entirely pure and as large as
         | possible. And your impure adapters, at the application
         | boundary, (e.g. a rest api, db client, file system, system
         | clock, etc.) should be as small as possible and impure.
         | 
         | Doing this well essentially allows you to get the best of both
         | worlds - highly coupled code (i.e.your pure functionality) and
         | highly decoupled code (i.e. your pure-to-impure functionality).
         | 
         | Another good reason for leveraging "functional design" as an
         | argument is that many of those skeptical of architectural
         | patterns are ironically heavily onboard the "functional design"
         | bandwagon. So it is a strong argument in a political sense
         | also.
         | 
         | [1] https://en.wikipedia.org/wiki/Pure_function
        
           | keyle wrote:
           | Yes after 25 years this is how I see it. Isolate state into
           | its little dirty corner, think in terms of data structure and
           | its transformation. This has kept me sane for decades. It's
           | simple but so many devs just haven't seen the light yet.
        
       | Kostarrr wrote:
       | Cool article on how to abstract things in Rust. I must admit, I
       | usually write the "bad rust application".
       | 
       | Total nitpick: For `CreateAuthorError::Duplicate`, I would return
       | a 409 Conflict, not a 422 Unprocessable Entity. When I see a 422
       | I think utf-8 encoding error or maybe some bad json, not a
       | duplicate key in a database.
        
         | slau wrote:
         | I agree that a duplicate key problem is 409. However I disagree
         | that 422 is for encoding issues. Quite the contrary, 422
         | specifically says that "the server understood the content type
         | of the request entity, and the syntax of the request entity was
         | correct, but it was unable to process the contained
         | instructions."[1]
         | 
         | So it's more "your request didn't make logical sense" more than
         | "your request was missing a closing bracket". That's just a
         | 400.
         | 
         | [1]: https://developer.mozilla.org/en-
         | US/docs/Web/HTTP/Status/422
        
       | phamilton wrote:
       | Has anyone ever actually moved a mature application from one
       | database to another and found that the code around calling the DB
       | was a major painpoint?
       | 
       | I'm all for the unit testing argument, but in an active SaaS
       | business I've never seen the hypothetical database change where a
       | well architected app makes it smooth. I have certainly moved
       | databases before, but the performance and semantics changes dwarf
       | the call sites that need updating. Especially in Rust, where
       | refactoring is quite straightforward due to the type system.
        
         | yuppiepuppie wrote:
         | My anecdotal experience tells me that it never works in a high
         | scale product environment. Having managed and lead 2 teams that
         | maintained a legacy system with hex-arch and we had to move DBs
         | in both. We ended up rewriting most of the application as it
         | was not suitable for the new DB schema and requirements.
        
         | mrkeen wrote:
         | I strongly believe in this principle, but I've also seen
         | colleagues try to future-proof the database (via interfaces) in
         | the wrong place.
         | 
         | If your DBUserStore happens to know directly about SQL, that
         | class is the wrong place to try to introduce flexibility around
         | different DBs, ORMs, SQL dialects, etc. Just hard-code it to
         | PostgresUserStore and be done with it.
         | 
         | Instead, put the interface one level up. Your PostgresUserStore
         | is just a place to store and retrieve users, so it can
         | implement a more general UserStore interface. Then UserStore
         | could just be a HashMap or something for the purposes of unit
         | tests.
         | 
         | Also, if you have some autowiring nonsense that's configured to
         | assume "one service has one database", that's bad. Singletons
         | used to be an anti-pattern and now they've been promoted to
         | annotation and/or basic building block of Java microservices.
         | 
         | When it comes time to live-migrate between databases, your
         | service will need to stand up connections to both at once -
         | _two_ configurations, not one, so architect accordingly.
        
           | chuckadams wrote:
           | > Singletons used to be an anti-pattern
           | 
           | Singleton was never the antipattern, it was the GoF
           | implementation using the class to manage the singleton
           | instance that everyone eventually ran from. Object lifecycles
           | like Singleton are managed these days by a module system or a
           | DI container.
        
         | pjmlp wrote:
         | I have, back when we were selling a CRM product in the dotcom
         | wave.
         | 
         | We could do AIX, HP-UX, Solaris, Windows NT/2000, Red-Hat
         | Linux, with Oracle, Informix, DB2, Sybase SQL Server, Microsoft
         | SQL Server, Access (if you were feeling crazy, just for local
         | dev).
         | 
         | It wasn't that the customers would switch database, or OS,
         | rather the flexibility allowed us to easily adapt the product
         | to customer needs, regardless of their setup.
        
           | regularfry wrote:
           | That's a subtly different situation, as you've presented it
           | here. In that case you know up-front what the set of
           | databases you need to support are, so you can explicitly
           | design to them. One promise of Hexagonal Architecture is that
           | you should be able to get the benefits of being able to move
           | underlying stores _without_ knowing in advance the precise
           | products that you might want to move to.
           | 
           | Depending on the early history of your product that might be
           | the same; or it might not. If you know from day one that you
           | need to support two databases rather than one, that would be
           | enough to cause design choices that you wouldn't otherwise
           | make.
        
             | pjmlp wrote:
             | It was still a product, a different kind product, but still
             | product being developed and sold in boxes (back when that
             | was a thing).
             | 
             | Also it wasn't like we developed all those OS and database
             | backeds for version 1.0, and didn't do anything else
             | afterwards.
             | 
             | Which OSes and RDMS to support grew with the customer base
             | and added to be plugged into the product in some way or
             | fashion.
        
             | mrkeen wrote:
             | > If you know from day one that you need to support two
             | databases rather than one, that would be enough to cause
             | design choices that you wouldn't otherwise make.
             | 
             | I disagree (strongly _in favour of_ of DI  / ports-and-
             | adapters / hexagonal).
             | 
             | I don't want my tax-calculation logic to know about one
             | database, let alone two!
             | 
             | Bad design:                 class TaxCalculator {
             | PGConnection conn;         TaxResult calculate(UserId
             | userId) {..}       }
             | 
             | Hypothetical straw-man "future-proof" design:
             | class TaxCalculator {
             | MagicalAbstractAnyDatabaseInterface conn;         TaxResult
             | calculate(UserId userId) {..}       }
             | 
             | Actual better design:                 class TaxCalculator {
             | UserFetcher userFetcher;         PurchasesFetcher
             | purchasesFetcher;         TaxResult calculate(UserId
             | userId) {..}       }
             | 
             | I think a lot of commenters are looking at this interface
             | stuff as writing _more code paths_ to support _more
             | possible databases_ , per the middle example above. But I
             | do the work to keep the database out of the TaxCalculator.
        
               | yencabulator wrote:
               | That sounds like a codebase that doesn't contain a single
               | JOIN..
        
               | imron wrote:
               | Not advocating for or against, but having worked on
               | systems like this, the joins here would happen in the
               | Fetchers.
               | 
               | That is, User is the domain object, which could be built
               | from one or more database tables inside the Fetcher.
        
       | dvt wrote:
       | This is essentially one stop short of dependency injection[1]
       | (even cited by the original "hexagonal architecture" author back
       | in 2005[2]). I've been writing a lot of Rust this year, and even
       | though part of me really likes it, I do miss Go's dumb
       | simplicity. I have a feeling that you could spend entire sessions
       | just architecting some fancifully-elegant pattern in Rust and not
       | really getting any actual work done (just how I used to in Java).
       | 
       | [1] https://www.martinfowler.com/articles/injection.html
       | 
       | [2] https://alistair.cockburn.us/hexagonal-architecture/
        
         | mrkeen wrote:
         | I agree that this goes hand-in-hand with DI, but are you for or
         | against it? E.g, Is Go simple because it _allows_ switching out
         | real DBs for hashmaps in unit tests, or because it _forbids_
         | it?
        
           | regularfry wrote:
           | You absolutely can do the interface wrangling that Hexagonal
           | wants in Go, if that's something you want to do. I've built
           | apps that way. When I've gone sort of half-way and allowed
           | explicit dependencies in the core (like on file I/O and so
           | on) what I've regretted is those impurities, not the
           | surrounding interface architecture.
        
       | hi-v-rocknroll wrote:
       | The sample code reminds me of PHP and Perl CGIs from 1999 where
       | concerns are jumbled together. For a tiny hobby site that does
       | one thing, sure, it's fine but for scalable, maintainable
       | patterns there must be order, separation of looser concerns, and
       | layering.
        
         | SPBS wrote:
         | Too much layering. Code like this makes a huge song and dance
         | around what is ultimately issuing an SQL query to the database
         | and returning the results. The solution must always scale scale
         | scale, never mind that simple problems should have simple
         | solutions. A simple solution can always be refactored into a
         | more complex solution, but not the other way round. It's
         | _always_ a safe bet to start with a simple solution.
        
       | pistoleer wrote:
       | > If you ever change your HTTP server
       | 
       | Never needed to. Premature optimization.
       | 
       | > We have the same issue with the database
       | 
       | What issue?? Cross that bridge when you get to it...
       | 
       | > To change your database client - not even to change the kind of
       | database, just the code that calls it - you'd have to rip out
       | this hard dependency from every corner of your application.
       | 
       | I really doubt this is such a big deal... Bit of ctrl+f for that
       | module and paste the new one. All database libraries have a
       | "connect()" and a "query()".
       | 
       | I'm so far convinced that this article is written for people who
       | have too much time to waste on things that will probably never
       | happen just so they get to feel smart and good about something
       | that's not even visible.
       | 
       | Imagine if we built bridges this way: yes, right now we have
       | settled on a stone masonry design, but what if we want to move to
       | steel trusses in the future???
       | 
       | Why can software engineers not accept that it is unreasonable to
       | expect one single human creation to last forever and be amendable
       | to all of our future needs? Cross the bridge when you get to it.
       | Don't try to solve future problems if you're not 100% certain
       | you're going to have them. And if you _are_ certain, just do it
       | right the first time, rather than leaving an opening for a future
       | person to "easily" fix it up. And sometimes? A new bridge has to
       | be built and the old one torn down.
        
         | lnxg33k1 wrote:
         | It is much easier to change applications that are designed
         | properly, regardless of the premature optimization, using
         | adapters and interfaces is costless, and give you priceless
         | peace of mind
        
           | pistoleer wrote:
           | If only we could all finally agree on the definition of
           | "proper design"!
        
             | lnxg33k1 wrote:
             | For me, one that allows the application to be extended,
             | maintained and understood easily
        
               | sethammons wrote:
               | The problem word, i think, is "extended." To me, that
               | means adding functionality to the feature set, to others
               | it may mean "changing the db" - and the two designs will
               | be different.
        
               | onli wrote:
               | So as simple as possible, without unnecessary
               | indirections and useless abstraction like interfaces that
               | make reading the code hard, right? ;)
               | 
               | There really are very different perspectives on what
               | makes code easy to manage, and interfaces (especially
               | ones used only for a single class) are a sure-fire way
               | for me to make code unmanageable. But java enterprise
               | coders disagree. That can clash hard.
        
               | lnxg33k1 wrote:
               | What is about interfaces that makes reading code hard? It
               | is a contract, for me reading just a list of operations
               | that are available without the concrete code, is even
               | clearer/cleaner.
               | 
               | Not sure java is bad for indirections, java is bad for
               | boilerplate and lack of defaults, imho. But I usually
               | don't use frameworks/stlib directly, so I create my own
               | boilerplate that is reusable
        
               | onli wrote:
               | It's about the indirection. In the calling code, instead
               | of knowing which specific object you are working with -
               | and thus easily being able to check its implementation -
               | you only know which interface it implements. Getting to
               | the implementation can be very time consuming, especially
               | in a system with hexagonal architecture or similar
               | indirect patterns and dependency injection. While
               | searching the code you then lose the context of the
               | problem you were working on.
               | 
               | So I'm not talking about having one or two sensible
               | interfaces as a contract, but codebases where every class
               | has at least one interface, for "decoupling". Pure hell.
        
               | solidninja wrote:
               | In my experience, using tools that do not support the "Go
               | To Implementation" shortcut makes it hard. In IntelliJ,
               | Ctrl+Shift+Click will take you to the possible
               | implementors of an interface.
               | 
               | Concrete example from work today - we have a trading
               | application and there are many paths that lead to alerts
               | of some kind. Alerts are usually raised inline with any
               | business logic (as should be - they intrinsically
               | coupled). Alerts however can be delivered differently -
               | via SMS, other messaging systems and/or log messages. The
               | different places where the alerts need to be generated do
               | not _need_ to know how the alert is going to be
               | physically delivered to its destination - they just need
               | to generate it. Without an interface (or at least a type
               | alias for a function) - it would make being able to say
               | e.g. this alert is a direct phone message vs. a chat
               | message in some channel because of the type it is - much
               | harder.
        
               | onli wrote:
               | Sure! There are valid use cases for them. I was
               | specifically talking about unnecessary usage :)
               | 
               | In the system I worked on the shortcut did not work - too
               | many options or some consequence of the dependency
               | system, additional layers of indirection.
        
               | pistoleer wrote:
               | You can't just extend a house willy nilly. You have to
               | set up scaffolding, destroy some walls, have temporary
               | rigs to keep the ceilings from collapsing, etc. Unless
               | you want to live in a "shipping container" building, but
               | even then there's a non 0 effort, and the shipping
               | container constraint comes with its own set of issues.
               | 
               | In real life, you just can't have it both ways. There's
               | always a trade off. If the trade makes sense go for it I
               | say, but I think people aren't always honest (to me or
               | themselves) about the exact trade they are making.
        
           | choeger wrote:
           | > using adapters and interfaces is costless,
           | 
           | Hah, no, they're not. Every production app is read many, many
           | times by several different developers, mostly to search for
           | bugs. Reading across adapters, services, and interfaces make
           | that much harder. Especially, if the architectural pattern
           | was implemented slightly differently every time by various
           | authors.
           | 
           | Abstraction _always_ comes with a price. Often, it is well
           | worth to pay (or we would still code everything in C or
           | assembly), but sometimes it 's just a waste.
        
             | solidninja wrote:
             | I'm always fascinated by the amount of comments that
             | devalue separating concerns and reducing coupling by using
             | traits and modules. Maybe if you're exclusively writing
             | serverless functions you don't need much code anyway, but
             | the idea that you can go and read a piece of code that
             | deals with the database separately from a piece of code
             | that deals with your HTTP request encoding (and see how
             | they meet in the middle via a few method signatures) is a
             | pretty powerful one in my experience.
        
           | pjmlp wrote:
           | While I agree with the idea, they certainly have a
           | development cost in complexity.
        
       | attheicearcade wrote:
       | I prefer the first example, to be honest. Much of the time your
       | API is more or less a wrapper around the DB, so why introduce
       | more indirection? I don't really buy the testing argument since
       | the interesting stuff which really needs testing is often in the
       | queries anyway. Swapping out dependencies is not a huge issue in
       | a language like rust, the compiler gives you a checklist of
       | things to fix. I also don't like that you call this function
       | which handles transactions internally, inevitably you'll end up
       | with a handler calling two different functions like this
       | resulting in two transactions when they should be atomic.
       | 
       | At $work we're slowly ripping out a similar system in favour of
       | handlers calling database functions directly, such that they can
       | put transactions in the right place across more complicated sets
       | of queries. Code is simpler, data integrity is better.
        
         | Kinrany wrote:
         | > Much of the time your API is more or less a wrapper around
         | the DB, so why introduce more indirection?
         | 
         | That's an easy scenario. General utilities should aim to make
         | hard things possible and then minimize the overhead in easy
         | scenarios, so the fact that this is more complex than the
         | dumbest thing that could possibly work, is not by itself a good
         | argument against.
        
         | mrkeen wrote:
         | It _is_ for testing, but you don 't indirect over class A
         | because you want to test class A, you do so to test class B.
         | 
         | By all means, write a test to make sure the queries actually
         | work on the database. It will be slow, stateful and annoying,
         | but still probably worth it.
         | 
         | But you don't want to bring that slow-statefulness over to
         | every other upstream test in the entire system: want to test if
         | the permission system (in some outer class) is
         | allowing/blocking correctly? You'll need to start up a database
         | to do so. Is your test making sure that an admin with
         | permissions can create a user (without error?), well it's going
         | to start failing due to USER_ALREADY_EXISTS if you run the test
         | twice. To avoid that you'll need to reset and configure its
         | state for every single invocation.
        
           | LargeWu wrote:
           | > To avoid that you'll need to reset and configure its state
           | for every single invocation.
           | 
           | Good testing frameworks just do this for you.
           | 
           | I generally prefer focusing testing efforts around what you
           | describe - spinning up the entire system under test - because
           | _that 's how the system is used_ in real life. There's
           | definitely times you want to test code in isolation
           | statelessly, but I find that engineers often err on the side
           | of that too much, and end up writing a bunch of isolated
           | tests that can't actually tell you if an http call to their
           | API endpoint performs the correct behavior, which is really
           | what we want to know.
        
         | globular-toast wrote:
         | It's not really about being able to swap out the db or
         | something, that's just a bonus. It's about being able to write
         | a proper domain model (ie. entities and business rules etc.)
         | that is clear and testable and independent of details like
         | persistence, serialisation, i/o protocols etc. If your system
         | is just CRUD, then you absolutely don't need anything like
         | this, but if you have business rules (high level stuff) and
         | intermingle it with low level details then it quickly becomes a
         | mess that is hard to reason with.
         | 
         | But you should totally do what you're doing until it breaks.
         | You only start looking into better architecture when things are
         | not working. Being aware of something like this means you'll
         | have something to look up if/when you run into the problems
         | that inevitably crop up in simple "db wrapper" type APIs.
        
       | FridgeSeal wrote:
       | Hmmmmm.
       | 
       | Mixed feelings about this.
       | 
       | "Oh our http handler knows about the db"
       | 
       | Ok? Its job here is more or less "be a conduit to the db with
       | some extra logic".
       | 
       | It "knows about a lot of things" but it's also exceedingly clear
       | what's going on.
       | 
       | The main function is fine. I'll take a "fat" main function over
       | something that obscures what it's actually doing behind a dozen
       | layers of "abstraction". Nothing like trying to isolate a
       | component when you're fighting an outage and nobody can figure
       | out which of the 16 abstract-adapters or service-abstracted
       | launched the misbehaving task.
       | 
       | The original code might be "messy" but it's at least _obvious_
       | what it's doing, and it can pretty clearly be pulled apart into
       | separate logic and IO components when we need to.
       | 
       | This all just feels a bit...over engineered, to say nothing of
       | the insulting tone towards the other learning resource.
        
         | FridgeSeal wrote:
         | > Hard-coding your handler to manage SQL transactions will come
         | back to bite you if you switch to Mongo
         | 
         | I uuuhh, I hate to tell you this, but uh, if you're swapping
         | Postgres to Mongo, and you think that hardcoded queries are
         | going to be your migration pain points, I have some bad news
         | for you. The difference in semantics (for any such change, not
         | just db) will bite you first, and wil bite a _lot_ harder.
         | 
         | This idea of "we can abstract over everything and then swap
         | them out at our leisure" works less than we'd all like to
         | imagine it does, and crucially building everything to
         | accommodate this utopia, will leave you with mountains of code
         | that will do nothing other than make your teammates hate you
         | and obscure your actual logic.
         | 
         | > AuthorRepository
         | 
         | Oh hello C#. So instead of cursing everyone who has the
         | misfortune of working on your C#-flavoured-rust codebase of
         | having to write literal pages of boilerplate to inevitably add
         | a new struct/type to the codebase, I suggest leaning into
         | idiomatic Rust a bit more. Personally, I'd make
         | read/delete/upset traits, whose methods take a handle to a
         | connection pool. Logic for sql then lives inside these
         | implementations, and can be brought into scope only when
         | necessary. Something like `my_struct.upsert(&conn).await?`. We
         | have locality of behaviour, we've separated IO details out, we
         | have all the same advantages with about 50% less code-noise.
        
           | neonsunset wrote:
           | Did you mean Java-flavoured?
           | 
           | In C#, EF Core already implements a repository with the way
           | its API is structured so most of the time writing another one
           | on top of it is an anti-pattern.
        
       | Kinrany wrote:
       | I wish Rust had a good dependency injection library. More
       | specifically, a library that solves the problem of having long
       | chains of constructors calling each other.
       | 
       | The two main use cases are routing frameworks and tests.
       | 
       | Axum already has this, but it is married to HTTP a bit too much.
        
         | tcfhgj wrote:
         | What do you need a DI library for?
         | 
         | What's the problem with constructor injection of the
         | dependencies using traits?
        
           | Fluorescence wrote:
           | I'm curious how mutability is meant to work e.g. in C# I
           | might have a service that caches costly results in memory
           | that I share across a number of other services operating in
           | the same thread.
           | 
           | Accessing something that might update it's internal cache
           | needs to be mutable so i) this need for mutability is viral
           | up the call chain ii) we can't share mutable references... so
           | it's going to be a pain in the butt and need to sidestep
           | compile guarantees somehow. Having an out of the box best
           | solution for common scenarios like this would be nice to see
           | at least.
        
             | tcfhgj wrote:
             | The solution is interior mutability - the concrete solution
             | depends on the requirements (e.g. throughput)
        
           | solidninja wrote:
           | The problem is manual wiring (as always). It is fairly
           | convenient to declare the source of your dependencies
           | (somewhere around main) and have them be automatically wired
           | in the sub-component graph, all without having to write out
           | the chains of code to call constructor parameters. Also
           | simplifies refactoring, as compile-time DI is mostly done on
           | type and not on name or parameter position.
        
       | Avi-D-coder wrote:
       | This is such bad advice that I honestly couldn't tell if it was a
       | parody or not until I read the comment section--it's not.
       | 
       | Attempting these design patterns is a common part of getting over
       | OOP when new to Rust. The result: over-abstracted, verbose,
       | unmaintainable C++/Java written as Rust. Every layer of
       | indirection ossifies the underlying concrete implementations. The
       | abstractions inevitably leak, and project velocity declines.
       | 
       | I have seen the same types and logic literally copied into three
       | different repositories in the name of separation of concerns.
       | 
       | Luckily people usually get over this phase of their Rust career
       | after a couple of failures.
       | 
       | If you'd like to skip that part, here are a few rules:
       | 
       | 1. Always start with concrete types. Don't abstract until you
       | have at least two, preferably three, concrete implementations.
       | 
       | 2. Separation of concerns is a myth.
       | 
       | 3. K.I.S.S.
        
         | tcfhgj wrote:
         | I don't want to invalidate your experience, but I would like to
         | see what your claims and conclusions are based on.
        
           | flohofwoe wrote:
           | Not the parent, but what's really missing in the article is a
           | complete code listing of what the initial code has been
           | turned into after the refactoring to really hammer home the
           | absurdity of the advice (fwiw I was also scratching my head
           | for a while whether this is satire, because the end result
           | would look a lot like 'Java Hello World Enterprise Edition:
           | https://gist.github.com/lolzballs/2152bc0f31ee0286b722).
           | 
           | The original code fits on one page, is readable from top to
           | bottom, and doesn't contain any pointless abstractions that
           | worry about 'future problems' that never actually come to
           | pass in the real world anyway.
           | 
           | If the code no longer fits the requirements, no big deal,
           | just throw away those 40 lines and rewrite them from scratch
           | to fit the new requirements. That will most likely take much
           | less time than understanding and modifying the refactored
           | 'clean code', because there's a pretty good chance that the
           | new requirements don't fit the abstractions in the refactored
           | version either (and IME that's the typical scenario,
           | requirement changes are pretty much always unpredictable and
           | don't fit into the original design, no matter how well
           | thought out and 'flexible' the original design was).
        
             | tempodox wrote:
             | Architecture astronauts have to learn this lesson the hard
             | way.
        
         | rob74 wrote:
         | The same applies to Go too. They may be quite different
         | languages, but trying to apply OOP patterns feels alien in both
         | of them...
        
           | tcfhgj wrote:
           | Dependency inversion is a OOP pattern?
        
             | Avi-D-coder wrote:
             | Yes, the dependency inversion principle is not a commonly
             | held principle in FP or imperative paradigms.
        
               | asimpletune wrote:
               | Wow when I read this comment I did a double take and had
               | to go to Wikipedia... then I realized dependency
               | inversion is not the same thing as inversion of control
               | and things made much more sense.
               | 
               | I guess part of the confusion came from how _dependency_
               | injection is a form of _inversion_ of control... the
               | words are all very similar to _dependency inversion_.
        
               | aswerty wrote:
               | I think your identification of that distinction is
               | entirely too generous. Typically the derision of
               | dependency inversion extends to inversion of control
               | since they are cut from the same cloth. One just focuses
               | on what is being inverted and the other the process of
               | inversion.
        
               | mrkeen wrote:
               | I haven't internalised what inversion of control means,
               | but I'm very strong on the distinction between dependency
               | inversion and dependency injection frameworks.
               | 
               | With DI, you stop your business logic from knowing about
               | your Postgres database.
               | 
               | With DInjF, not only does business logic still know about
               | Postres, but now it knows about Spring too! (The upside
               | is that it takes fewer lines of code to get from 0 to
               | spaghetti)
        
               | tcfhgj wrote:
               | So passing functions to functions instead of explicitly
               | calling them is what exactly then?
        
               | ramchip wrote:
               | Dependency _injection_
        
               | Avi-D-coder wrote:
               | Higher order functions can be used for dependency
               | injection.
               | 
               | Dependency injection and the Dependency inversion
               | principle are not one and the same.
               | 
               | The principle makes a claim, that inversion is a good
               | onto itself.
               | 
               | Injection is a tool not a claim.
        
               | tcfhgj wrote:
               | The principle is not making any claim. It is simply a
               | method to achieve something.
        
             | mrkeen wrote:
             | The three DI patterns I've used in Haskell are:
             | 
             | 1) Record of functions. Probably the most common, and
             | pretty analogous to calling a constructor in OO. If you've
             | heard "objects are a poor man's closures, and vice-versa",
             | that's what this is referring to. You build a closure with
             | as many dependencies as you can pass in at startup time
             | (fields in OO), then later you can just call one of the
             | functions inside it, with the remaining parameters (method
             | call in OO).
             | 
             | 2) final tagless. After you're comfortable with an
             | Optional<Value> that _might_ return a Value, and Future
             | <Value> that will return a Value _later_ , you can venture
             | further down that path and work with a Writer<Value> which
             | will return a value _and do some logging_ , or a
             | Reader<Env, Value> which will return a value _while having
             | read-access to some global config_. But what if you want
             | many of these at once? You end up with a ReaderT Env
             | (Writer Value) or some nonsense. So instead you can write
             | your code in terms of m <Value>, and then _constrain_ m
             | appropriately. A function which can _access Git_ , _do
             | logging_ , and _run commands on the CLI_ might have type:
             | (Git m, Logging m, Cli m) => m Value.
             | 
             | But that function does not know what m is (so therefore
             | does not _depend on it_ ) You might like to have different
             | m's for unit tests, integration tests, a live test
             | environment and a live prod environment, for instance.
             | 
             | 3) Free Monad & interpreter pattern. A nifty way to build a
             | DSL describing the computations you'd like to do, and then
             | you can write different interpreters to execute those
             | computations in different ways. For instance, you could
             | write a network/consensus algorithm, and have one
             | interpreter execute the whole thing in memory with pure
             | functions, for rapid development, debugging and iterating,
             | and another interpreter be the real networked code. It's
             | fun, but someone wrote an article about how Free Monads are
             | just a worse version of final tagless, so I haven't used
             | this in a while.
        
               | fire_lake wrote:
               | With (3) it's hard to have different instruction sets for
               | different parts of your program. Ideally, a function that
               | logs only uses the log instruction set and this is
               | reflected in the type. Once you start down this road, you
               | end up at final tagless anyway.
        
               | tome wrote:
               | Yes, and for interested parties it's probably worth
               | elaborating that Git might be something like
               | class Git m where             clone :: Url -> m GitRepo
               | currentHead :: GitRepo -> m CommitHash
               | 
               | and Logging might be something like
               | class Logging m where             log :: String -> m ()
               | 
               | which is very similar to defining                   data
               | GitDict m = MkGitDict {             clone :: Url -> m
               | GitRepo,             currentHead :: GitRepo -> m
               | CommitHash         }              data LoggingDict m =
               | MkLoggingDict {             log :: String -> m ()
               | }              class Git m where gitDict :: GitDict m
               | class Logging m where loggingDict :: LoggingDict m
               | 
               | So the "record of functions" style and the "final
               | tagless" style are equivalent, except that the former
               | passes operations manually and the latter passes
               | operations implicitly. The former can be considered more
               | cumbersome, but is more flexible.
               | 
               | If you're interested in how my effect system Bluefin does
               | this, you can have a look at https://hackage.haskell.org/
               | package/bluefin-0.0.4.2/docs/Blu...
        
             | layer8 wrote:
             | Dependency Inversion is a recipe for how to invert the
             | dependency between two components (by introducing a third
             | component on which both depend and which can be grouped
             | with either side, hence allowing to invert the dependency
             | at will). It's not inherently tied to OOP.
             | 
             | Incidentally, one thing it glosses over is the creation of
             | the components, which may prevent completely inverting the
             | dependency.
        
           | ladyanita22 wrote:
           | I wonder why is Go[1] considered object-oriented while Rust
           | is not[2], when basically both have the same approach towards
           | objects (struct + methods).
           | 
           | [1]: https://en.wikipedia.org/wiki/Go_(programming_language)
           | 
           | [2]:
           | https://en.wikipedia.org/wiki/Rust_(programming_language)
        
         | ejflick wrote:
         | > 2. Separation of concerns is a myth
         | 
         | I don't fully understand the quote from Djikstra where he first
         | talked about this but I'm sure he didn't mean it as it's
         | interpreted today: "draw invisible boundaries in random places
         | because _best practices_. "
        
         | gpderetta wrote:
         | Premature generalization is the root of all evil.
         | 
         | (yes, this is indeed a generalization)
        
           | mamp wrote:
           | But unfortunately it's not premature. It's been a problem for
           | so long!
        
         | globular-toast wrote:
         | Keep things as simple as possible, but no simpler. The point of
         | this isn't to introduce complexity for no reason, it's to free
         | up the domain model of an application from low level details
         | like persistence, i/o, network protocols etc. What's the Rust
         | way to do that?
        
           | Avi-D-coder wrote:
           | What the author calls bad code is one way of writing
           | idiomatic Rust. There are more complex techniques.
           | 
           | It's recommended to not split the low level details from.
           | your business logic, in fact it's not just recommended the
           | compiler slowly forces your hand.
           | 
           | If you write overly abstract code like the author recommends
           | you will leave a large amount of performance on the table.
           | Code like that doesn't play nicely with lifetimes, by trying
           | to separate memory management from business logic you're left
           | with only the least restrictive scheme owned heap allocated
           | data.
           | 
           | The Rust type system teaches you not separate concerns,
           | without giving up the ability to reason about your code.
        
           | vilunov wrote:
           | I'm sorry, but depending on abstract classes does not "free
           | the domain model of an application of low level details". The
           | details are always there, but they could be tucked away
           | inside other classes (structs/types) that higher ones depend
           | on. These low level classes need not to be abstract, it will
           | just make discovering code harder and provides nothing to
           | improve separation of concerns.
        
         | aswerty wrote:
         | > This is such bad advice that I honestly couldn't tell if it
         | was a parody or not until I read the comment section--it's not.
         | 
         | There is a large body of content on why concepts discussed in
         | the article are championed. And also a large body of content on
         | how they are misused (and I agree that can be - even to a huge
         | degree).
         | 
         | So while, I think it is fine to judge that ascribed benefits
         | are not worth the cost (or are not even realizable in a typical
         | development team). Or argue why the benefits of an architecture
         | like this doesn't work for Rust in particular - which may be
         | the case since many of these patterns are oriented towards
         | design in the enterprise applications space. But ascribing the
         | approach as being a "parody" is not at all constructive.
         | 
         | The patterns of hexagonal architecture isn't in any way coupled
         | to OOP even if some of the terminology is highly aligned with
         | languages in the OOP space. In fact the well regarded (at least
         | by me) Mark Seeman has an article how Ports and Adapters,
         | another name for Hexagonal Architecture, is inherently
         | functional [1]. And this resonates with my experience.
         | 
         | I have seen the pattern implemented well across: Python,
         | Typescript, Javascript, .Net, Scala, and Go. And while the
         | systems languages such as: C, Rust, and beyond are quite
         | distinct from the previously discussed languages. There is
         | certainly space for debate on the viability of the application
         | of these patterns.
         | 
         | [1] https://blog.ploeh.dk/2016/03/18/functional-architecture-
         | is-...
        
         | BlackFly wrote:
         | This sort of response on second thought seems like a knee-jerk,
         | but in the off chance that you might be open to seeing the
         | perspective that values a hexagonal architecture.
         | 
         | You always have two concrete implementations: the production
         | application and the testing application. Otherwise you yolo
         | things into prod, or run only manual/integration tests. That
         | can work for a while, but many people find it unsavory.
         | 
         | It is pretty easy and sometimes useful to make three
         | implementations: http server, CLI, test. Maybe you want to use
         | files in CLI and a db for a server.
         | 
         | It has always been a good idea to isolate persistence and
         | transport concerns from the business logic and that doesn't
         | change in Rust. Don't dependency drill SQLite up and down every
         | call stack. If your application is small enough and will stay
         | so, then separating it is more a question of habit than
         | anything.
         | 
         | But you shouldn't abstract everything nor try to separate
         | everything! It was a persistence layer before, in the hexagonal
         | architecture it becomes adapter implementations of a port.
         | Transport layer is similar. Separation of concerns in this case
         | means that you have a concrete dependency (an http server, a db
         | etc) that isn't part of your logic.
        
           | hitchdev wrote:
           | The best way to conceptualize hexagonal is as a kind of
           | crutch to accomodate the inability of unit tests to
           | effectively fake stuff like the db and their tendency to
           | tightly couple to everything.
           | 
           | It's not intrinsically good design but it does improve unit
           | testability (which sometimes has value and sometimes has zero
           | value).
        
           | Avi-D-coder wrote:
           | I am partial to property testing logic and integration
           | testing servers. This frequently requires some level of
           | separation, the key is to do it only at the right points.
           | 
           | Don't start by saying how can I unit test this tiny bit of
           | logic against several mocks, start with a simple integration
           | test of your real routes.
           | 
           | As you add abstractions you are trading maintainable
           | straightforward code for more granular testing. It's a hard
           | trade off not a set of principles for good code.
        
         | seanhunter wrote:
         | Yeah. The hallmark is they reference some document by Martin
         | Fowler. That's a red flag for me.
         | 
         | I would add - one of the symptoms of this type of over-
         | abstraction is what you could call the "where tf" problem. Any
         | time you need to do anything it's really hard to figure out
         | where
         | 
         | 1) ...the thing you're trying to fix actually happens so you
         | can fix it
         | 
         | 2) ...the new feature you're trying to add should actually be
         | added
         | 
         | 3) ...it's going wrong when it's going slow/not
         | scaling/stalling somehow
         | 
         | ...because in reality the answer to the question "where" is
         | always "all over the place". And that means you typically need
         | to make several small changes in various places to do anything.
         | So you've papered over the intrinsic complexity of the system
         | and you have a really nice looking whiteboard but the
         | complexity is now distributed in a bunch of places so it's
         | conceptually complex and much harder for a dev to actually get
         | their arms around the whole system and understand it fully. And
         | you now have a false sense of security because you have great
         | test coverage but the kind of problem you now face isn't caught
         | by tests. Because typically the type of problem you hit is you
         | should have made 6 changes in different places to implement
         | your feature but you've forgotten one and only implemented 5.
         | So the system is now semantically broken in some way even
         | though all the tests pass.
        
       | pantulis wrote:
       | I am not exactly fond of Hexagonal Architecture although I don't
       | deny the merits of the idea and I think it's useful. That said
       | the important thing is that the article was very well written and
       | I've enjoyed reading it.
        
       | atemerev wrote:
       | Rust 2 Enterprise Edition
        
       | resonious wrote:
       | In my experience working in teams, it is _very hard_ to get
       | people to adhere to these kinds of "clean code" separation of
       | concerns style architectures. Even just nudging someone in the
       | right direction and saying "hey we should have some kind of
       | boundary here so that X doesn't know about Y" doesn't seem to
       | result in any kind of long term shift. They'll say OK, adjust
       | their code, and at that point you've already doubled the time it
       | took them to work on the task. Then 6 months later, the same
       | person will come in and break the boundary because they need to
       | implement a feature that is hard to introduce without doing so.
       | Even among people who read books on this stuff, it seems like
       | very few of them are capable of actually carrying out the
       | practice.
       | 
       | And what's the point again? To make it so that you can switch out
       | Postgres for MongoDB later? To make your web app now accessible
       | over XMPP? It feels like a lot of work to enable changes that
       | just don't happen very often. And if you wrote your app with
       | Postgres, the data access patterns will not look very idiomatic
       | in Mongo.
       | 
       | I think X11 in *nix land is an interesting example of what I
       | mean. X11's client-server architecture allows a window to be
       | "powered" by a remote machine as well as local. But it's dog
       | slow. Straight up streaming the entire desktop frame by frame
       | over VNC is usually smoother than using X11 over the network. I
       | think we just haven't reached a point yet where we can develop
       | good apps without thinking about the database or the delivery
       | mechanism. (I know X11 isn't exactly new, but even with decades
       | of advancements in hardware, X11 over the internet still loses to
       | VNC)
        
       | anacrolix wrote:
       | it's good in theory, and sometimes it pans out as your project
       | evolves. however if you did this from the get go, you would never
       | actually get anything done.
        
       | John23832 wrote:
       | The example of a "bad" rust application is literally how any Axum
       | application is written. I don't have the time to go look, but I'm
       | pretty sure that the Axum examples are written as well. There's
       | nothing wrong with it.
       | 
       | If you want to test, test at the integration layer using
       | testcontainers[0] or something. Not everything has to resemble a
       | Spring style dependency injection/inversion of control.
       | 
       | [0] https://github.com/testcontainers/testcontainers-rs
        
       | seanhunter wrote:
       | I absolutely hate this "we'll get on to why hexagons in a moment"
       | style that seems to be becoming more and more prevalent, where
       | you make something seemingly the subject but you refuse to even
       | define what it means for absolutely ages.
       | 
       | Tell me the thing that's in the headline first. I'm not going to
       | read your article if you don't do this. It's not that I'm not
       | intellectually curious, it's that I don't like being messed
       | around.
        
       | 33a wrote:
       | This seems really badly argued. The second version seems much
       | worse and harder to extend. Looks like classic ORM style database
       | abstraction wrapped with hand written types. This type of code
       | usually leads to inflexible data models and inefficient n+1 query
       | patterns. Relational algebra is inherently more flexible than
       | OOP/ML-style type systems and its usually better to put as little
       | clutter between your code and the db queries as possible in
       | practice.
        
       | SPascareli13 wrote:
       | I remember a few years ago a very good engineer in our company
       | shared a very similar article about hexagonal architecture in
       | golang, and a lot of people started using it, including myself
       | for some time.
       | 
       | Now he's not here anymore, and posts in his linkedin about
       | simplicity and how people overcomplicate things in software so
       | much. This shows me that you should be careful when taking advice
       | from other engineers, they learn and move on from what was
       | previously a "best practice", while you might get stuck thinking
       | it's worthy it because "that one very good engineer said it was
       | how it should be done".
        
       | belval wrote:
       | I might just be burned out but does anybody actually have time at
       | work to do the "better" solution shown in the article? Never mind
       | if it is actually better or not, but adding interfaces to
       | abstract my DB package and actively decoupling everything seems
       | like what was taught during my software engineering classes but
       | rarely ever put in practice.
        
         | vilunov wrote:
         | Unfortunately, my predecessors had plenty of time. Now I'm left
         | with a burning project and half my time spent on clicking "go
         | to implementation" of the next interface and hoping the first
         | one will be the real one (actually full-text searching for it
         | because Scala has no working IDEs).
        
       | agubelu wrote:
       | Hiding your logic and intent behind 10 layers of abstraction is
       | also spaghetti code.
       | 
       | Hell is full of single-implementation abstractions.
        
       ___________________________________________________________________
       (page generated 2024-09-17 23:02 UTC)