Remix.run Logo
tormeh 16 hours ago

> hexagonal architecture

God help me. Was on a project where this was used to justify so much extra boilerplate. Every class had an interface, and then we used dependency injection to supply the class to something expecting that interface. Actually, it was in Rust, so there were no classes, but that didn't stop us. Absolute waste of time.

humanfromearth9 14 hours ago | parent | next [-]

Interfaces are only necessary to properly abstract away from implementation details that have different change driver assignments. Else it's overkill and arbitrary. This doesn't invalidate hexagonal architecture, it just provides the actual good guideline to know if abstractions and information hiding are necessary.

Check the Independent Variation Principle paper for more info: https://doi.org/10.5281/zenodo.17677316

The IVP provides two directives that help evaluating objectively design options, based on actual business decisional authority structure, not some guy's intuition. With the insights of the IVP, you'll be able to decide effectively.

The paper is long, but you can skip to the parts that you find interesting

littlecranky67 15 hours ago | parent | prev | next [-]

Sounds like a regular setup, at least this is very common in modern C#/.NET when you implement REST services. Nothing to do with Hexagonal Architecture, just inversion of control. There is a very thin DI container baked into ASP.NET and the pipeline. Do you have to use that? No, but it gets complicated very quickly if you don't. So any project that is more than a tech demo/eval uses DI. In other languages/frameworks (i.e. Typescript+NodeJS) this is not very common for some reason.

tormeh 15 hours ago | parent | next [-]

I don't know much about C#, but in Rust this is very much not the norm. In fact, there are technical limitations associated with async traits. This sometimes allowed us a reprieve from the madness, but only sometimes. I guess you can write enterprise Java in any language.

The entire idea was to make it easier to mock components and therefore easier to test code, however all the code connecting the components became untestable, so we were back to square one, struggling to meet our test coverage quota because of the massive amounts of boilerplate.

zelphirkalt 15 hours ago | parent | next [-]

The easiest thing to test will always be pure functions. Many people don't realize this, and how clean it can make your tests look. Provide input, get return value, assert/check it. Sure, at the end of the day you have some IO somewhere. But that will usually be a smaller part of the code base. Codebases that make testing more difficult than that, are making it unnecessarily complicated. Sometimes you depend on some library that forces you or heavily nudges you into another style, that can happen. But what one wants to avoid is a codebase, where one has to know to mock 5 other things because of their side effects.

MangoToupe 13 hours ago | parent | next [-]

> But that will usually be a smaller part of the code base.

Testing how IO composes makes up most of what you want to test because it's such a difficult problem. Reasoning about this in terms of size of the codebase doesn't make sense.

ffsm8 9 hours ago | parent [-]

Thing is though, most of the high level languages have that part "solved" via libraries... Hence lots of people don't see the need to test it, as they expect the library to have sufficiently tested already. That leaves your tests mostly centered around your domain/business logic

Personally I'm just doing web api dev/backend atm and have to say that at least in this domain, pretty much everything actually hard is solved.

The only difficulty is the "interesting" architecture decisions people like the OP introduce, making inherently trivial problems into multi week endeavors which need coordination between dozens if not hundreds of devs

MangoToupe 8 hours ago | parent [-]

> Thing is though, most of the high level languages have that part "solved" via libraries... > ... > Personally I'm just doing web api dev/backend atm and have to say that at least in this domain, pretty much everything actually hard is solved.

Color me extremely skeptical!

tormeh 13 hours ago | parent | prev | next [-]

Exactly. Divide the code in impure and pure functions. The pure functions can be easily unit tested. The impure ones can often be integration tested. The rest you'll just have to live with (or cover with whole-process integration tests). For tests spanning larger chunks of your code base it might be worth it to mock impure code. Never mock pure code - that's madness.

9rx 12 hours ago | parent | prev [-]

Trouble is that in an application the pure functions are just implementation details that don't need to be tested. They get tested by virtue of a correct implementation being necessary to correctly run the application. A library can be a little different story, as there the public interface is quite likely to be pure, but I expect most codebases found in the wild are applications. The discussion happening here seems to be directed at applications.

Granted, it appears Rust gets a lot of use as a cross-language library provider, where the application glue is written in other languages. Perhaps that's why?

littlecranky67 15 hours ago | parent | prev | next [-]

What is the default way in Rust to write REST endpoints and have the REST endpoint use/create handles to your database transaction that is bound in scope to the underlying HTTP request? (i.e. transaction lifetime and commit/rollback is linked to the HTTP request succeeding)

tormeh 13 hours ago | parent [-]

Often each API route will have its own handler function. That function will - usually through many layers of indirection and abstraction - launch queries towards your database.

littlecranky67 12 hours ago | parent [-]

That describe a REST API implementation design, but doesn't really answer the particular question: How is the scoping implemented, i.e. how would any given Rust REST API framework couple a Db transaction to a HTTP scope. I mean, I know how that would look like if the framework ofered you no abstraction or DI whatsoever. You would pass the transaction (and maybe database handle) through all sorts of functions, create some sort of context object (that holds all sort of references to the http connection, database, transaction and whatnot, and call everything manually. Eventually you will end up building a similar custom-made abstraction that the built-in DI container in ASP.NET does. But I would expect there are some frameworks for rust that already deliver that.

tormeh 10 hours ago | parent [-]

Axum is the most popular web library out there, and sqlx the most popular sql library. Together they look something like this: https://github.com/tokio-rs/axum/blob/main/examples%2Fsqlx-p...

wiseowise 14 hours ago | parent | prev [-]

How do you fake implementation without using interfaces/abstract classes?

wwosik 15 hours ago | parent | prev [-]

DI yes. All the crap under the name of "Clean" Architecture - no.

ericmcer 9 hours ago | parent | prev [-]

It is annoying when it is dogmatically followed, but for certain things it makes sense to have a generic interface between your business logic and specific library/service code.

Being all or nothing with it can be tedious though. Like if you are using Postgres... you probably don't need have abstracted adapters that makes it easier to run on some other DB later.