and now I want to add methods to calculate the three different centers of a triangle, but only for triangles. With inheritance, I would extend Shape, restrict it to three vertices, and simply add the methods. What's the most elegant, idiomatic way to tackle this problem in Rust, without inheritance?
To take a stab at answering myself: I might make Shape a trait with the perimeter and area methods undefined, and make a Triangle struct that implements that trait while also providing those extra center methods. I would do this knowing that the area implementation for other shapes would invoke the triangle area method along the way. I would then make a ComplexShape struct with the alternative implementation of area().
On the one hand, this example is one of oop's great promises: reusability. On the other, this example is also a trap that many oop devs fall into. If it's possible to add more than 3 vertices to a shape, but it's not possible with a triangle, then a triangle isn't a shape. One possible fix: move the vertices out of shape and give it a pure virtual instead, probably one that returns an immutable slice (mutable would turn a square into something else). The result is equivalent to this:
It's even better than making one all-encompassing DrawingObject trait because if something implements AffineTransformable, it'll pick up implementations for scale, rotations, flips, and transformations for free.
Similarly, there might be traits like ClosestPoint that are only implementable for simple objects like points, lines, and circles. If you want to calculate the closest point for something complex like a spline, you'd need to explicitly approximate the spline (which is expensive and therefore shouldn't happen implicitly) and do closest point on each of the line segments.
Each type of object (lines, points, circles, etc.) got its own custom struct and didn't try to reuse internal details from other primitives - that way you don't fall into the "is a square a rectangle or is a rectangle a type of square?" problem.
All the answers so far have provided static or up-front approaches, where triangles are a different type or distinguished via an enum. However, what if you want to be able to build a polygon and then, if it is a triangle, compute triangle properties? In that case, it would make sense to use an optional operation.
This way you can have just a Vec<Shape> for a collection of shapes, and there is no requirement that triangles must be handled distinctly until there is a reason to care. Checking if a shape is a triangle is checking the Vec's length, which is basically the same as checking an enum discriminant.
Ooh, I like this. Turning a Shape into a Triangle dynamically is cool. It could also have a method to turn it back into a Shape. It would be especially useful if triangles could be implicitly casted back into shapes, sort of like upcasting. Is there a trait for implicit casting like that? It seems like there could be, implementing it with the type you want to cast into as a generic. The compiler would simply have to execute the conversion instead of generating an error when a Triangle is passed as a Shape parameter. Even if the conversion must be explicit, it would still be useful to implement a trait that enables type casting via an as expression.
If Shape were a trait then you can coerce Triangle into dyn Shape. But there is no implicit conversion of concrete types. In general, Rust won't ever do potentially-expensive things like that implicitly (in this case, it would have to allocate a new Vec of points).
Rust code solves this problem via impl From<Triangle> for Shape.
After some thought, here's my preferred solution: Shape should be a trait with the two methods. The perimeter and area methods are reused a lot across implementations, but that code reuse can be handled with dependency injection a la the strategy pattern. OOP programmers, note this separation of common interface and code reuse. This may feel like inheritance with extra steps, right up until you encounter the need for another perimeter or area calculation for a new kind of shape, and then it will be easy to add without the need to override anything or refactor anything major.