4 ms·
The problem with the author's case for Go's is-a relationships is that it breaks down the moment you want to pass the object to a function expecting the origina
by optymizer 12y ago
The problem with the author's case for Go's is-a relationships is that it breaks down the moment you want to pass the object to a function expecting the original object.
For example, http://play.golang.org/p/EmodogIiQU http://play.golang.org/p/EmodogIiQU
type A struct { }
type B struct { A } //B is-a A
func save(A) { //do something }
b := &B{}
save(b); //OOOPS! b IS NOT A
If Go had is-a relationships, the code above would be valid. Instead, Go only implements has-a relationships, and simply provides shortcuts to calling B.A.foo() as B.foo().
One could create B.save(), which would call save(b.A), but the very reason you're now proxying the call to save() is because there is no is-a relationship in Go.
We all know about interfaces, but the problem is that is-a relationships do exist, and you can't always use interfaces, because often you want to share the data encapsulated by the objects, not only the behavior. One ends up creating methods to fetch each piece of data, but in code that is supposed to be performant, calling methods instead of accessing fields is suboptimal.
- lloeki 12y agoIOW, the Liskov substitution principle is unsatisfied. The language has chosen by design not to resolve "methods"† up the "hierarchy"†, therefore the only way to reinstate it is to implement func save(B) { }, which has either the benefit of making you think whether you need to persist more fields or make explicit that you don't. And as you said, implementing proxy accessors to satisfy an interface just to simulate inheritance is obviously not the right choice. † neither of which exist, since there's no objects and no inheritance. In simplifying things, this brings a constraint. Given how method resolution is a pain point WRT implementation complexity and performance (see e.g Ruby), this is a reasonable tradeoff. I am glad we have such an interesting choice of languages.
- jaekwon 12y agoJust call save(b.A) or save(&b.A). No need for methods, is performant. I can't think of a reason--besides extra typing--why this wouldn't be sufficient. http://play.golang.org/p/7vE_wN6EBv http://play.golang.org/p/7vE_wN6EBv
- AnimalMuppet 12y agoIt's sufficient, in that it does what you want - it calls save() on b's A. But it's not sufficient if what you want is to treat a B as an A. There is no is-a, so you have to keep treating a B as something different from an A.
- tolmasky 12y agoThis becomes difficult to discuss in the abstract. In my experience, save() is a clear example of exactly why you want composition and not inheritance. When I am saving the B record, I want it to let the A record's save do its thing without interfering. Let's for the sake of argument use Person and Employee instead of A and B so we can use a concrete example. We'll say Person has a name field and an address field. Employee owns a Person, and has an EmployeeID as well as Salary. You want: Employee : Person { string employeeID; double salary; } I want: Employee { Person person; string employeeID; double salary; } If we're saving into JSON, your save contents are mixing with the Person's save contents ("{name, address, salary, employeeID}"). What happens when a "basic income" law is passed, and all of a sudden the Person's implementor decides to add a salary field to the Person object? My method continues working without hiccup, because I was explicit about how I saved: save() { write("{ employeeid:whatever, salary:whatever, person: "); save(person); write("}"); } My save file displays the same encapsulation as my code, and thus behaves correctly when a base class changes by default. On the other hand, in subclass-land you'd now have competing salary fields, so you'd have to explicitly prepare for that, instead of getting it semantically for free. For example, you could start defensively programming by name-spacing the save properties: "{person-name, person-address, person-salary, employee-salary, employee-employeeID}". In my experience this reliance on what "things are" is a bad way to think about programming. It doesn't help anyone to argue the philosophy of whether Employee is a Person or not, because I can easily sidestep the argument by saying: "OK fine, Employees are Persons... but I'm not longer writing the Employee class, I'm writing the EmployeeRecord class. And as such the person instance itself is part of the employee set if it has an associated EmployeeRecord. I have now satisfied the is-a relationship without an is-a language feature". Kind of like how certain Rectangle ARE squares regardless of whether they happen to be members of the Square class. Its a membership requirement, not an instantiation requirement. Its just words.
- tolmasky 12y agoIn my experience its always really telling that there are no incredibly simple is-a relationships to use in inheritance justifications (as opposed to tons of goto real world examples for just about every other feature under the sun). Its always either incredibly abstract (B is-a A), or incredibly contrived (Triangle is-a Shape). I've spent a lot of time in inheritance heavy code, and I've yet to find something that wouldn't be just as, if not more, elegant without inheritance. I've spent the most time in Cocoa (which I believe to be very well designed BTW), but the inheritance there is clearly not needed IMO. I usually find one of the following to be true: 1) Very shallow inheritance trees that could have very easily (and more logically) been replaced with interfaces. For example, NSResponder being everything's superclass even though just about all of its methods are empty implementations ("subclassers responsibility"), aka, it clearly should have been an interface. 2) Confused/strangely complex is-a relationships (mutable array is-a immutable array, what? so if I specifically specify NSArray, I may still get a mutable array and the type system will be happy???). 3) Strange rules around what methods are overridable, and more importantly, how specifically they can be overridden. Why can't I in a UIView subclass override -subviews to ensure that it always has the same subviews? Well, implementation detail, that's why. Normally this would be fine, but since anyone is allowed to muck around in a subclass, suddenly I need to know the way it was implemented. This of course conflicts other parts of the framework where you are definitely expected to override the method and not use a setter. I have yet to be presented with one of these "killer" is-a relationships that must exist. If the sole excuse is "performance", then sure I'll conceded. I guess I don't work in environments where member access is the performance bottleneck so I guess I can't relate.
- noblethrasher 12y agoInheritance is usually a mess because people use it to model objects when they should instead be modeling types: a triangle is not a shape, but a Shape is a sum type of Circle, Square, Triangle, etc.
- NateDad 12y agoShape is definitely much more accurately modeled as an interface than a base class, since it has no functionality on its own.
- npat 12y agoWhy not? type Saveable interface { ... } func save(a Saveable) { .... } save(a); // ok, assuming A is Saveable save(b); // necessarily ok, given than A is Saveable