[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)