-1

Let's say we have an undo/redo system and we have a superclass storing an object that should later form a tree of the attributes that can be undone/redone.

This would require every class that should get involved into undo/redo to extend from this one superclass, depending on how big the system grows, this could result in 40, 50 or who knows how many direct subclasses.

Would this be a bad practice? Is there something as "too many direct subclasses"?

Edit: Every attribute that can be undone is an instance of e.g. ValueBoolean, ValueInt etc.. These values all have an id name and are registered to a root to form a tree e.g. "editor" has the children "enabled" "input" etc. This root and some serialization code are what is in the superclass I mentioned. Now when something is undone the system searches the tree for the id (with the path to properly find children).

I did not write this system, I am trying to understand whether it is well designed. It seemed a little bit weird to me that everything would have to extend one class.

5
  • 1
    The phrase "too many sub classes" is subjective. How would we judge that there are too many sub classes? We do not have enough information to answer your question. Jul 2 at 18:53
  • 2
    If you're going to have 40 or 50+ classes anyway - having them all extend the same parent might not necessarily be a problem, but perhaps there's a way to reduce the number of classes through composition (if you can find a way to reframe the problem, maybe you can achieve your 50+ variants by combining a small number of classes). Also, you could potentially rethink how the undo-redo stack works - if you can push/pop something like a memento object, maybe you don't need a superclass. Jul 2 at 19:02
  • 2
    Not per se, but it is uncommon. Because when you have a lot of the same things (in this case direct descendants of some particular class) you are likely to spot some common traits or categories that justify classification (arguably the point of OO programming). The subclasses could be all different in their own unique way though. Jul 3 at 18:07
  • 2
    Beginners sometimes use types & class hierarchy to organize their thinking. However, sometimes simple classes using instance variables, with little hierarchy, can get the job done, and when possible, this is preferred.
    – Erik Eidt
    Jul 4 at 14:59
  • those are interesting points. I also do think that especially the serialization code could be a separate class where the instance of it would be used in places where it is needed. It could be better to separate different concepts e.g. serialization into a separate class instead of passing the functionality down with inheritance, right?
    – Chryfi
    Jul 4 at 18:32

2 Answers 2

5

I don't think the number of descendants (in itself) is necessarily a problem.

But I question whether inheritance is likely an ideal way to implement this behavior anyway.

By putting the undo/redo at the base of the tree, you're asserting that everything that derives from it must also support undo/redo. That may not work well, however, with an existing hierarchy. In an existing hierarchy, you might well have a few fairly specific nodes that should support undo/redo, but that support is (at least more less) distinct from the existing hierarchy.

In a fair number of cases, it's also the context that determines whether something should support undo/redo, not the object itself. So, you may have some objects of a class that should support undo, but others that shouldn't.

There are a couple of fairly common ways of implementing undo/redo. The command pattern is one. And what you're talking about might be pretty much an implementation of Command (or not--you haven't given enough detail to be sure). To implement the Command pattern, your base class would define something like execute and undo methods, so anything derived from it will be able to do or undo it's "thing" on command. This does work, but tends to be fairly intrusive on the objects that need to support undo/redo. It also tends to have fairly tight coupling between the mechanism for saving/retrieving/restoring state, and the policy for the pattern(s) you do/don't support in doing so (so, for example, this often doesn't support things like "redo A, C, and D, but not B").

Another common method is the memento pattern. This is basically kind of a generic way of storing an object's internal state. You normally have an Originator (the thing you want to be able to undo/redo), a memento (the internal state itself) and a Caretaker, which gets and saves the memento, and handles actually saving and restoring Mementos to implement the undo/redo as needed. The advantage here is that it decouples the undo/redo implementation from the objects you want to be able to undo/redo. All the object needs to be able to do so produce or consume a memento, and deciding how/when to tell it to do that is up to the (entirely separate) caretaker class. This tends to make it easier to add support for non-linear undo/redo patterns, such as the one outlined above, of redoing A, C, and D, but skipping B.

2
  • Thank you for the detailed explanation. I will have a closer look at the patterns you mentioned. Perhaps I could have provided more detail. I will edit my question but you have answered it anyways. I wanted to know whether it would be weird to have too many subclasses.
    – Chryfi
    Jul 2 at 22:13
  • You can have one function that tells whether UNDO / REDO is available, and one that performs it, and the default implementation returns "UNDO not available" / does nothing. If you don't implement it, you do nothing.
    – gnasher729
    Jul 6 at 12:53
0

The bad practice here is using subclassing in the first place.

My opinion is that 99% of problems you encounter in object-oriented design are better solved with two different mechanisms than subclassing:

  • Interfaces -- if you have multiple classes that all have the capability to do something common, perhaps it's interface what you want and not subclassing
  • Composition -- if interface doesn't solve the issue cleanly but you rather want to have the functionality of class A in class B, then in many cases it's better to have a class A member in class B rather than make class B a subclass of class A

In my opinion, here it sounds like interfaces would be better than subclassing. Subclassing has problems when you want class C to be both subclass of class A and class B. Some languages only allow inheriting from one class. Other languages allow multiple inheritance, but it's generally considered so complex and problematic that even if your language allows multiple inheritance, you may not want to use it.

Of course, in some languages supporting multiple inheritance, interfaces are actually done by subclassing. However, in that case you just make the interface be a class with nothing but abstract methods.

If you find it likely that implementing the interface could benefit from some help, for example due to some methods being possible to implement by calling other methods, and think that subclassing therefore would be advantageous in this case, there's another possibility: you may define a set of helper functions to help you in implementing the methods that call other methods. Then subclasses have a choice: they may use those helper functions, or they may implement the methods in some other way.

1
  • 1
    You know that languages allowing full MI don't need and generally don't provide an extra feature for restricted MI? Jul 3 at 18:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.