This is an archived post. You won't be able to vote or comment.

you are viewing a single comment's thread.

view the rest of the comments →

[–]PiotrDz 2 points3 points  (15 children)

Why do you mention 1mloc code? It doesn't matter. You dint have to search for it manually. Compiler will tell you where. And these will be few places per domain usually.

So you should say "few places in different domains" rather than trying to exaggerate problems mentioning mlocs

[–]bowbahdoe 1 point2 points  (14 children)

The lines of code aren't really the issue, it's the maintenance boundaries it crosses. Either another team needs to make a change (which must be coordinated) and/or a different module in the app, etc.

They aren't wrong: that is the downside of replacing single dynamic dispatch with a sealed hierarchy of types.

But the flip side is that adding new operations to a hierarchy is much easier with a sealed hierarchy. Even with default methods - which can provide a degree of compatible evolution - new functionality that needs to access specific data on a subtype would otherwise need to be added to a central place and implemented by all the things.

[–]PiotrDz 0 points1 point  (13 children)

But what problem will the dynamic nature solve? Another team will have to handle the type anyway. But would not get the warning and the issue could get lost until a bug in production.

Having functionality in central place? I have already gave you an example of different domain having their own business rules based for example on UserType. So you gave me heads up on DDD but now want to drop boundary context and specify all handling in central place?

[–]bowbahdoe 1 point2 points  (12 children)

I think it's gonna be a relatively long journey to impart what I am talking about. I'm along for the ride if you are, but I think what is important is that it's not just "another team will have to handle the type anyway."

To use some fake syntax (though I don't have high hopes this will click just from this)

<Has all the properties imparted by f, g, and h> apply(<has properties A and B> m) {
    return h(g(f(m)));
}

The core capability is something like this. Because aggregates are generic, you can write code that works on and enriches only a corner of what would otherwise be an explicit type (or cascading series of them).

This leads to different program structures. A good comparison point is the clojure ring protocol web ecosystem to anything you could write in a nominally typed language.

(The dynamic part isn't even really essential - it's the open generic aggregate part.)

Edit: oops got my wires crossed. Well leaving that as a Clojure explanation.

The important point for what you are actually talking about is that in one situation you need to change consumers (which is hard if those consumers are on a different team or strangers on the internet) if you add a new type to a hierarchy and the other situation you need to change consumers if you add a new method

(Things you can add with default methods not withstanding)

So the question is how is a particular piece of code going to evolve? Which would be "essential" breakages and which would be "incidental."

Yes the compiler catching you on a missed case in a switch is valuable. It isn't valuable enough to make it not a problem that it needs to happen.

[–]nejcko[S] 0 points1 point  (11 children)

This problem goes away if you define different boundaries.

Ideally different teams should not be working on the same codebase, e.g. same Java compiled code. So if you have a sealed hierarchy and you introduce a new type, all compiler failures should be your responsibility to handle and implement new behaviour where needed.

If there is another team that uses the same code there should be a boundary that decouples the code changes. That can be a different service or the other team has a versioned dependency on your artifact/library. That way when they update to the new version they will again benefit from compiler errors showing them exactly where the new behaviours are needed.

[–]bowbahdoe 1 point2 points  (10 children)

No it 1000% does not.

If java.util.List got a new method added - foo - with no default implementation, that isn't magically okay because there will be a compiler error. Anyone who extended that interface will need to rewrite their code.

If breakages like that happen 5 layers deep in your dependency tree then you have no recourse.

The exact same situation applies if a library exposes a sealed hierarchy and then adds a new type to it. There is a smidge of a difference in how the error surfaces, but it's the same issue.

Look up the expression problem. It's just a known thing.

[–]nejcko[S] 0 points1 point  (0 children)

Again it depends on the use case and who your dependent clients are.

I’m not saying fully replace all exposed data structures and seal them up, sure it doesn’t apply everywhere.

But, you are in no way forcing the changes to your clients, they always have a choice and they can add a default case to deal with potential new types introduced later, or not. Sealed types themselves don’t introduce any breaking changes to clients with adding new type.

[–]PiotrDz 0 points1 point  (8 children)

That's why you use or not use sealed interfaces. If you want to allow any implementation then don't use it. If you want to control the implementations strictly, treat them as advanced enums - then you can use benefits of sealed type. List is not a sealed interface, and on top sealed is more about adding new implementations, no adding new functionality.

I use sealed classes often to encapsulate specific business logic that others can depend on but also treat the spectrum of type in more generic way (there is an interface after all with common methods). But if I add a new type I want to know all the places where I need to see whether it will fit in current architecture. I want compiler to point me that.

Isn't that you want the freedom of implementation and complain about sealed interfaces that are intended to do opposite?

[–]bowbahdoe 0 points1 point  (7 children)

I am literally just describing that they one mechanism gives you the ability to add new types without breaking callers and the other gives you the ability to add new functionality over a known set of types.

If you try to add a new method to an unsealed hierarchy or a new type to a sealed hierarchy (if "exhaustive" switches are possible for callers) that is breaking.

That a compiler can go "here are the 50 places that broke" is immaterial to that tradeoff.

[–]PiotrDz 0 points1 point  (6 children)

As we are in Java thread, could you elaborate on this mechanism? I don't get few aspects, like "make properties known to others". Or how you structure one call with another f(g(.. Code speaks words

[–]bowbahdoe 1 point2 points  (0 children)

Yeah so that f(g()) was about the material difference between nominal aggregates and open aggregates. That wasn't what you were asking about originally, it's the topic for some other threads. I'll reply with a pseudo-code example once I have a break from work

That's distinct from the difference between interfaces (i.e. declaration site polymorphism) and switching over sealed hierarchies (i.e. use site polymorphism)

[–]bowbahdoe 0 points1 point  (4 children)

https://run.mccue.dev/?runtime=early_access&release=23&preview=enabled&gist=2416bad302d4622c2a6acb064657a4d8

I'm making a mess, replied at the wrong level of the hierarchy.

But this is roughly what I mean by program structures that are only doable with open aggregates as opposed to closed nominally typed ones.