3 ms·
> I don't think Inheritance is always bad - sometimes it's a useful tool. I can only think of one or two instances where I've really been convinced that inheri
by josephg 1y ago
> I don't think Inheritance is always bad - sometimes it's a useful tool.
I can only think of one or two instances where I've really been convinced that inheritance is the right tool. The only one that springs to mind is a View hierarchy in UI libraries. But even then, I notice React (& friends) have all moved away from this approach. Modern web development usually makes components be functions. (And yes, javascript supports many kinds of inheritance. Early versions of react even used them for components. But it proved to be a worse approach.)
I've been writing a lot of rust lately. Rust doesn't support inheritance, but it wouldn't be needed in your example. In rust, you'd implement that by having a trait with functions (+default behaviour). Then have each robot type implement the trait. Eg:
trait Robot {
fn stop(&mut self) { /* default behaviour */ }
}
struct BenderRobot;
impl Robot for BenderRobot {
// If this is missing, we default to Robot::stop above.
fn stop(&mut self) { /* custom behaviour */ }
}
- esailija 1y agoThe problem is of course that there is no useful default behavior you can define when the trait is so isolated and generic.
- jjmarr 1y agoDon't mix implementation and interface inheritance.
- josephg 1y agoIt doesn't have to be "so isolated". The trait can still have required methods that don't have a default implementation. Eg: trait Robot { fn send_command(&mut self, command: Command); fn stop(&mut self) { self.send_command(Command.STOP); } } struct BenderRobot; impl Robot for BenderRobot { // Required. fn send_command(&mut self, command: Command) { todo!(); } } This is starting to look a lot like C++ class inheritance. Especially because traits can also inherit from one another. However, there are two important differences: First, traits don't define any fields. And second, BenderRobot is free to implement lots of other traits if it wants, too. If you want a real world example of this, take a look at std::io::Write[1]. The write trait requires implementors to define 2 methods (write(data) and flush()). It then has default implementations of a bunch more methods, using write and flush. For example, write_all(). Implementers can use the default implementations, or override them as needed. Docs: https://doc.rust-lang.org/std/io/trait.Write.html https://doc.rust-lang.org/std/io/trait.Write.html Source: https://doc.rust-lang.org/src/std/io/mod.rs.html#1596-1935 https://doc.rust-lang.org/src/std/io/mod.rs.html#1596-1935
- RHSeeger 1y ago> First, traits don't define any fields. How does one handle cases where fields are useful? For example, imagine you have a functionality to go fetch a value and then cache it so that future calls to get that functionality are not required (resource heavy, etc). // in Java because it's easier for me public interface hasMetadata { Metadata getMetadata() { // this doesn't work because interfaces don't have fields if (this.cachedMetadata == null) { this.cachedMetadata = generateMetadata(); } return this.cachedMetadata; } // relies on implementing class to provide Metadata fetchMetadata(); }
- Klonoar 1y agoGetters and setters that get specified by the implementing type.
- josephg 1y agoYep. Or ... don't put that in the interface at all. It looks like an implementation concern to me.
- RHSeeger 1y agoBut if there's a lot of classes that implement the same thing, then not duplicating code makes sense. And saying "it's an implementation detail" leads to having the same code in a bunch of different classes. It feels very similar to the idea of default implementations to me; when the implementation will be the same everywhere, it makes sense to have it in one place.
- josephg 1y agoSo to be clear about your example: You have a whole lot of different - totally distinct - types of things, which all need to have the same logic to cache HTTP requests? Can you give some examples of these different types you're creating? Why do you have lots of distinct types that need exactly the same caching logic? It sounds like you could solve that problem in a lot of different ways. For example, you could make an HTTP client wrapper which internally cached responses. Or make a LazyResource struct which does the caching - and use that in all those different types you're making. Or make a generic struct which has the caching logic. The type parameter names the special individual behaviour. Or something else - I don't have enough information to know how I'd approach your problem. Can you describe a more detailed example of the problem you're imagining? As it is, your requirements sound random and kind of arbitrary.
- OskarS 1y ago> The only one that springs to mind is a View hierarchy in UI libraries. I'd like to generalize that a little bit and say: graph structures in general. A view hierarchy is essentially a tree, where each node has a bunch of common bits (tree logic) and a bunch of custom bits (the actual view). There are tons of "graph structures" that fit that general pattern: for instance, if you have some sort of data pipeline DAG where data comes in on the left, goes out on the right, and in the middle has to pass through a bunch of transformations that are linked in some kind of DAG. Inheritance is great for this: you just have your nodes inherit from some kind of abstract "Node" class that handles the connection and data flow, and you can implement your complex custom behaviors however you want and makes it very easy to make new ones. I'm very much in agreement that OOP inheritance has been horrendously overused in the 90s and 00s (especially in enterprise), but for some stuff, the model works really well. And works much better than e.g. sum types or composition or whatever for these kinds of things. Use the right tool for the right job, that's the central point. Nothing is one-size-fits-all.
- aatd86 1y agoturns out that using composition and polymorphism is usually simpler than inheritance in such cases.
- widdershins 1y ago> But even then, I notice React (& friends) have all moved away from this approach. Modern web development usually makes components be functions. But what do those functions return? Oh look, it's DOM nodes, which are described by and implemented with inheritance. I would agree that view hierarchies in UI libraries are one of the primary use-cases for inheritance. But it's a pretty big one.
- josephg 1y ago> But what do those functions return? Oh look, it's DOM nodes, which are described by and implemented with inheritance. Well of course. React builds on what the browser provides. And the DOM has been defined as a class hierarchy since forever. But react components don’t inherit from one another. If the react devs could reinvent the DOM, I think it would look very different than it looks today.
- pixelfarmer 1y agoI will tell you one example with inheritance: The Linux kernel.
- graealex 1y ago> And yes, javascript supports many kinds of inheritance Funny you mention it, since JavaScript has absolutely no concept of contracts, which is one of the most important side-effects of inheritance. Especially not at compile time, but even at runtime you can compose objects willy-nilly, pass them anywhere, and the only way to test if they adhere to some kind of trait is calling a method and hoping for the best. At least that had been the case till ES6 came around, but good luck finding anyone actually using classes in JavaScript. Mainly because it adds near-zero benefits, basically just the ability to overwrite method behavior without too much trickery.
- josephg 1y agoYes. JavaScript is an incredibly dynamic language. If you don’t like that, don’t use it.
- osigurdson 1y agoThe commenter used inheritance and thought it was fine. Probably not necessary to re-write in Rust just to be able to say that it doesn't use inheritance while being functionally the same thing.