Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

You wouldn't have to mingle them if they're different types, in fact that would probably be one of the worst design decisions you could make.

In the DOD version you'd create a new record containing 3 arrays instead of 2 (x,y,z) and write or extend your functions to support this new record type.

In the OOP version (NB: blech, it's not OOP, it's just records) you'd also need new functions to support the new type.

There are definitely benefits and drawbacks to both approaches, but what you've written is not a drawback for structure-of-arrays or a benefit for array-of-structures.



> In the DOD version you'd create a new record containing 3 arrays instead of 2 (x,y,z) and write or extend your functions to support this new record type.

But now you're violating DRY: don't repeat yourself. Bugfixes found in Point3D will have to be manually "ported" to Point functions (and vice versa).

> In the OOP version (NB: blech, it's not OOP, it's just records) you'd also need new functions to support the new type.

Those new functions can largely be written as foo3D(Point3D a, Point3D b) {return foo(a.xy, b.xy); }.

DRY is kept, your bugs fixed in foo will automatically apply to foo3D. If you use inheritance (which... probably would be a mistake... but its possible and an "OOP" solution), your Foo functions would automatically work on Point3d.

EDIT: Hmm... maybe Point and Point3D keep the Liskov Substitution Principle and therefore work for inheritance. Maybe inheritance is a valid solution in this case?


So your OOP version creates many layers which impedes understanding and extension as well.

DRY is also not a law which must always be followed. You often don't even know when you're repeating yourself until you do, and the first time you see a repetition is not when you should eliminate it (in most cases). You often see people reference the rule of three here.

The first repetition should make you pay attention. The second lets you know that it's probably (not necessarily) time to refactor. Premature DRY is as bad as premature optimization.


If you don't care about DRY in this case, that's fine. There's more than one problem with your proposed DOD solution.

Your proposed solution violates the open-closed principle. Which means you have to modify "old working code" to get anything done.

If your "solutions" to fixing code issues is "rewrite and extend the old code to cover the new case", then it becomes impossible to build libraries.

----------

Lets say ApplicationFoo has been written using Point2D, either using OOP or DOD principles. In the real world, ApplicationFoo is often written by another team outside of your direct control.

Your ApplicationBar wants Point3D, and realizes that Point2D largely implements the functionality you want (2d-distances, line-drawing, intersections, etc. etc.)

Your solution forces you to rewrite Point2D, which therefore creates a change in ApplicationFoo. (Or more likely, ApplicationFoo is going to refuse to update to the new library: ApplicationFoo is now stuck on Point2D 1.0, while ApplicationBar is using Point2D 1.1. And now your organization is no longer sharing code effectively).


> Your solution forces you to rewrite Point2D, which therefore creates a change in ApplicationFoo.

I don’t see how this is the case. You don’t change Point2D at all, you simply add a second type Point3D. ApplicationFoo can happily continue to use Point2D from your library while you use Point3D.

I think the example of Point2D and Point3D is not very well chosen. Your proposed solution of Point3D = { xy : Point2D, z : Float } is also leads to lots of duplication in the sense that (for example) to add two of these points you would have to do

    { xy = add2D(xy1, xy2), z = z1 + z2 }
where the computation of `z` is a copy of the computations of `x` and `y` in Point2D. So should you detect an error in Point2D you might still have to manually port it to Point3D.

For more complicated operations on points, the maths is mostly completely different in 2D and 3D (the same concepts might not even make sense, especially if you don’t generalize to n dimension right away), so sharing of code between the two points seems difficult.

The main problem with this solution, I think, is that you picked one specific projection from 3D space to 2D space. It might line up with a projection that is relevant to your domain but it probably won’t. So if you use inheritance and call a function requiring a Point2D with one of your Point3D’s you secretly project the point to the xy-plane which seems like something that should be explicit in the code.


> I don’t see how this is the case. You don’t change Point2D at all, you simply add a second type Point3D. ApplicationFoo can happily continue to use Point2D from your library while you use Point3D.

Point2D, under DOD, doesn't exist. There's "array_x" and "array_y". See the article under question. Point2D would be the index into the "x" and "y" arrays, or an "id" returned by the ECS system.

The "x" and "y" fields, under DOD, are dispersed into different areas, allegedly for SIMD / auto-vectorization benefits. I'm trying to discuss the effects of a decision like this.

> I think the example of Point2D and Point3D is not very well chosen.

There are multiple users who understand software engineering who disagree with me, but understand what I'm trying to discuss.

There are also multiple users who prefer to be pedantic and focus on this issue. For the most part, I'm able to ignore these unimaginative users pretty easily. So the example is working pretty well as a filter.

If you wish for more people to participate in the discussion without getting distracted, maybe a better example would have been recycling the example from the article:

    pub struct Enemy {
        name: String,
        health: f64,
        location: (f64, f64),
        velocity: (f64, f64),
        acceleration: (f64, f64),
    }

    pub struct FastEnemy {
        name: String,
        health: f64,
        location: (f64, f64),
        velocity: (f64, f64),
        acceleration: (f64, f64),
        jerk: (f64, f64), // 3rd derivative of location
        jounce: (f64, f64), // 4th derivative
    }

    pub struct GalaxianEnemy{
        name: String,
        health: f64,
        location: (f64, f64),
        velocity: (f64, f64), // Constant vertical velocity
        loopiness: (f64, f64), // cos / sin based horizontal movement
    }
Doesn't really matter. There are plenty of data / classes where you need to just "add one more parameter" to a previously created class to make it perfectly work. The above "GalaxianEnemy" and "FastEnemy" classes follow this pattern.

---------

But if we focus on this pedantry, we wouldn't be able to discuss software engineering. Surely you've come across an example in your programming life where code-reuse would be useful with a subset of parameters, but needed to be extended into an additional parameter?

> The main problem with this solution, I think, is that you picked one specific projection from 3D space to 2D space. It might line up with a projection that is relevant to your domain but it probably won’t. So if you use inheritance and call a function requiring a Point2D with one of your Point3D’s you secretly project the point to the xy-plane which seems like something that should be explicit in the code.

Is it so hard to imagine a video game, where my Point2D and Point3D interpretation is in fact correct? The point of the discussion is to point out software engineering principles: recycle code where possible, open-to-extension but closed-to-modification, and other such principles.


I think you are dramatically underestimating the differences between operations 2D and 3D space. There is zero chance you would want to re-use the vast majority of your 2D code for 3D points.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: