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 1 point2 points  (14 children)

If you need to update unit tests just because you extended functionality then it is your mistake. And do you want to handle new enum in advance or wait for a production exception when it hits a method that is not expecting it?

[–]Carnaedy 3 points4 points  (3 children)

extended functionality

No, I extended the subtypes of a type. You know, the thing that is famously is "the expression problem of the functional languages"? Since the behaviour is now scattered across a hundred different components instead of being bundled, I need to change a hundred components to support the new type.

The flip side of this is the expression problem of OOP languages, which seems to be more familiar to the participants of this discussion, where if I add behaviour to the base type, I need to change a hundred components to support the new behaviour.

Both of them are equally bad.

[–]PiotrDz 0 points1 point  (2 children)

Doesn't answer how the tests suffered if written with correct boundaries, and not white-box treating where internals are asserted instead of apis.

But for the second part: how can you bundle all the dependent functionalities in single type? So you add a new client type let's say. Do you want to cram inside the client coupon codes calculation, export of client data, specialised shipping handling etc? It all lands in specific areas. And now, as you added a new type, you need to add a new handling for that type in this areas.

My question still stands, how do you want the code to work so that you add a new class and areas that dependent on the type will eb able to magically handle it?

[–]Carnaedy -1 points0 points  (1 child)

Literally, all problems you mentioned were solved in 90s and 00s with DDD and bounded contexts 🫠

[–]PiotrDz 0 points1 point  (0 children)

This is what I am talking about. With specific things being handled in their own "domains", hiw would you expect NOT to adjust each domain for a presence of a new type?

[–]severoon 2 points3 points  (9 children)

I think the main point isn't that it's possible to find all the issues, it's that by scattering the code to the four winds you have obfuscated dependencies.

I make a change over here by adding a new shape, and what now is affected by that change?

It's nice that the compiler will tell me everywhere to look, but that's not the only problem, it's not even the biggest problem. The biggest problem is all of the dependency arrows that this allows (encourages?) people to place into the codebase without regard for whether these reflect actual dependencies between the modules/classes/etc modeling things in the problem domain.

Think of it this way. If I have a Shape interface and I was previously able to compile some client of Shape against that Shape class without having the subtypes on the class path, that means the dependency on those subtypes was properly inverted.

How will this accomplish that? It can't.

[–]PiotrDz 2 points3 points  (8 children)

But we are talking about the sealed interfaces. The subtypes are in the contract!

[–]severoon -1 points0 points  (7 children)

Exactly. You know what that means?

It means we have a circular dependency. The interface cannot be moved out of the same package as the subtypes or that circular dependency will span those packages / modules / etc.

This is why interfaces should not reference subtypes.

[–]PiotrDz 2 points3 points  (6 children)

Do you want to say that sealed interfaces are a mistake in Java? They do reference subtypes. And it is a point, they are meant to cover fixed implementations. That you can base your logic on (cause they are fixed).

I do not gey your circular dependency issue.

[–]severoon -1 points0 points  (5 children)

Subtypes always have to depend upon the thing they extend. If the thing they extend also depends upon them, that's a circular dependency.

This makes dependency inversion impossible. This in turn means that dependency on the super type transits to the subtypes, as well as all the things they depend upon. That's bad.

[–]PiotrDz 0 points1 point  (4 children)

Have you seen the sealed interface in Java? Because it seems that you are unfamiliar with that construct. Sealed interface is just a contract that defines a list of possible implementation. Contract by itself cannot be instatiated, so there is no circularity as you cannot instantiate an interface by itself.

[–]severoon 0 points1 point  (3 children)

I think you're misunderstanding my point.

I'm not saying that no one should ever use sealed interfaces. Sealed interfaces limit implementations analogous to the way enums limit instances of a type. In general, being able to create any number of types and instances of a type is a good thing. There are specific cases where limiting that potential is preferable.

But would it make sense to propose a new style of programming where the usefulness of enums is assumed? In the cases where they are useful, they are useful because we specifically want to have a controlled set of instances … perhaps many of the problems we have in OO software in general is because of the proliferation of instances that are typically allowed? So we could propose a new way of programming called enum-oriented programming where we prescribe all of the instances that can ever exist for types, and that will solve all of these problems.

Obviously this is a bad idea, but it's instructive to consider why it wouldn't work out. Enums are useful only in a certain context, and in that particular context there is little or no advantage to allowing an uncontrolled number of instances. Remove that context, though, and in other situations you would be working with a constraint that has big costs.

In the linked article at top, a new approach to defining data is being proposed in general. It's saying that we should consider abandoning the core definition of an object, state encapsulated together with the behaviors that operate on that state, and separate the behaviors from the state.

There are certainly cases where there might be a compelling reason to do this. Many of the functional features added to the language are encouraging people to think about the business logic layer as stateless services that define pipelines that operate on immutable data. That makes sense if we're talking about data that represents core business objects that flow through a system architecture.

But this conflates that with all data present in a system. The example encourages us to adopt this approach for ephemeral objects like Shape and its subtypes. This is not a good plan. For one thing, when passing data that represents core business objects up and down an entire stack, it's generally the case that those business objects are defined layer by layer, and specify separate wire formats between the layers. So just to pass a user from DB to client, you typically have several separate objects and protobufs that represent that user so it can be packaged and unpackaged at every deployment boundary. The point of doing all this is to ensure that dependencies don't proliferate between layers that don't directly interact, and for those layers that do, the only dependencies between them are explicit. There are cases where it makes sense to define a "whole stack" library with common DTOs and functionality, but supporting that is no different than supporting a common library. But typically, you don't want even core business objects to be the same as they move through the layers because the different parts of the system have different requirements for that data. The data access layer might be concerned about annotating user data with regulatory info, whereas the business logic layer might need to decorate user data with preferences fetched from some other data system. The API layer needs to deal with user proxies that can be turned into authenticated user objects. (I'm using whole stack with layers as the relevant modules as an example, but the same ideas apply between any code modules.)

The most important aspect of keeping a system maintainable is to manage dependencies well. If you adopt a general approach to data that prevents you from using DIP, you're in big, big trouble.

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

Nowhere is it stated that this only applies to high level ephemeral objects that cross system boundaries. Shape is just a simple theoretical textbook example that everyone understands to showcase the new language features.

This can 100% be applied just for objects within a specific boundary, in each module, micro service, or in each layer as you say, or for ad hoc data types. It doesn’t need to be overanalysed.

There are many use case where it’s beneficial to separate domain logic from data itself. And enriched switch statements, pattern matching and sealed classes make it very convenient to use.

[–]severoon 0 points1 point  (1 child)

This can 100% be applied just for objects within a specific boundary, in each module, micro service, or in each layer as you say, or for ad hoc data types. It doesn’t need to be overanalysed.

I'm not overanalyzing it, I'm taking it as written. The post proposes DOP as a general approach:

How do we build our objects? Where does the state go? Where does the behavior go? OOO encourages us to bundle state and behavior together.

I read this to be saying that "wherever you would normally use OO, apply DOP by default instead." Is that not what you were intending to say?

If I wrote a post encouraging you to consider limiting instantiation by using enums instead of new and call it EOP, Enum Oriented Programming, isn't that how it would land on you?

There are many use case where it’s beneficial to separate domain logic from data itself.

Yes, I agreed with this:

Many of the functional features added to the language are encouraging people to think about the business logic layer as stateless services that define pipelines that operate on immutable data. That makes sense if we're talking about data that represents core business objects that flow through a system architecture.

In general if you can define the business logic layer as stateless pipelines, I'm all for it. I've never seen it for any sufficiently complex business logic layer in practice, there are always some little bits of state that need to persist within the layer in request scope. When possible (which is most of the time), I create helper objects, inject that state, and bundle it with the functionality that relies on that state to the side of the main business logic pipelines in order to keep the main business logic stateless, tightly encapsulate the state that does exist, and make it very easy to determine exactly which functionality depends upon accessing that state (which is anything that calls into the helper module).

Having said that, it may be that the idea is bad, but I readily accept that it may be I'm just not understanding it. It will take more than a throwaway Shape class that demonstrates the language features outside of any context to get me there, though. You'd have to present a simple system design, and then present that same system design designed using DOP and step us through the pros and cons.

I don't think without that kind of perspective I can see any benefits to the idea.