Awesome, this got replies.

Is the idea composition? [...] Increasingly I'm viewing OO design principles as more limiting than they are helpful in shaping my code.
The idea is explicitly about when to choose polymorphic inheritance over composition. I found that useful for 2 parts of my own program:
GUI, which is the classic application domain of class hierarchies. The second is
lix activities, which have a very rigid, small interface -- switch to, perform, hooks on becoming and de-becoming -- and comparatively many implementations of these.
Inheritance incurs a design debt: By the Liskov substitution principle, "B extends A" requires that wherever an A is desired, we can pass a B to that, and the receiver can treat it like an A. We shall not curb functionality. This makes the resulting class hierarchy very rigid. Everything depends on the classes at the beginning, which become very hard to change.
Composition (
class B { A a; ... } instead of
class B : A { ... }) does not take up this debt. We don't have to expose any A functionality in B's interface. Some newer languages have features to forward any method call to B on to the component A as long as B doesn't implement them. This merges the convenience of not redefining methods from A with the convenience of not taking up any design debt.
The function pointers would work as well, and are idiomatic in C to get dynamic binding. We can make our own virtual table instead of having the compiler generate that from the class code.
Why then use the complex builtin language feature instead of composition or simple pointers?
Leverage the compiler, let it check your work statically. This is a widespread mantra in D and C++. In D and the Java world, this is leveraged pretty strongly: On forgetting to implement an abstract method, the compiler will complain. On overriding a method marked final, the compiler will complain. The function pointers would instead crash at runtime to remind of necessary overriding.
In both cases, there's the option to interpret the error/crash as a mistake in the base class design, instead of as a welcome reminder.
When new classes are added, but the root classes remain the same, the if-else-chains would require updating code all over the application. There is no way to check whether one place has been forgotten. With virtual method dispatch, the changes are all bundled together in one specific place, the new class. If we expect our program to grow mainly in this manner -- a reasonable assumption with GUI components and lix skills -- then the class hierarchy might pull its weight.
In any case, in practice you rarely have a choice
Right right. Any kind of overarching design becomes encrusted and hard to change. OO is particularly prone to generate unnecessary rigid complexity.
Choice of problem domain, tooling ecosystem, and language, all of them have an influence on what overarching design is expected. I'm weird in that I love reading about professional software development, but only have a hobby project without any holy design to preserve.
OO with Simon, Part 2Idea for can't-replace-self in containerWe store lemmings, and the lemmings change their behavior during their lifetime. This leads to the following idea:
Don't replace lemmings. Replace the behavior of lemmings.class Lemming:
private Job job
void become(Job j):
assert (j.lemming == this)
job := j
void updateCalledByTheGame():
job.perform()
class Job:
protected Lemming lemming
abstract void perform():
do nothing yet
constructor(Lemming l):
this.lemming := l
this.lemming.become(this) We introduce tight coupling between job and lemming here. (Both know of each other, and can access each other's public interfaces.) I have thought about this for weeks, and can't see how to get rid of it.
I'm also not as opposed to public/protected members as it's traditionally recommended to be. The reason is that in D, we can later rename the member and make it private, and write an accessor property method that's callable exactly with the syntax of the previously-public field. In other languages, you might want to encapuslate behind boilerplate immediately.
class Faller : Job:
constructor(Lemming l):
super(l)
override void perform():
lemming.moveDown
if hit ground:
lemming.become(new Walker(lemming))
class Walker : Job:
constructor(Lemming l):
super(l)
override void perform():
lemming turns or walks ahead
if (no ground):
lemming.become(new Faller(lemming))My biggest dread is that this is overkill for the problem at hand. I had function pointers in C++ with a jump table. However, the C++ lixes had a fixed memory size. There were 2 ints reserved for each job to be used however they saw fit.
Using dynamically allocated classes allowed arbitrary private fields for each job. This has lead to much more expressive code inside the job methods. This expressiveness is not visible in my sample code above, because I'm focusing the hierarchy, not the exact method contents of any single job. But inside these methods, I've experienced the biggest gain in readability.
-- Simon