0

In a refactoring task, I have an old class, called OldClass, that will be replaced with the newer better NewClass.

OldClass and NewClass share have a lot in common, and for example share data members with the same name like memberA, memberB... But while they have the same name, these are members of OldClass and NewClass and are not shared through inheritance (nor do I want them to be shared).

They do have a common ancestor, so it would look like this:

      ┌──────────────┐
      │CommonAncestor│
      └───────▲──────┘
      ┌───────┴──────┐
      │         ┌────┴────┐
      │         │NewParent│
      │         └────▲────┘
┌─────┴─────┐  ┌─────┴──────┐
│ OldClass  │  │ NewClass   │
├───────────┤  ├────────────┤
│int memberA│  │int memberA │
│int memberB│  │bool memberB│
└───────────┘  └────────────┘

My problem is that both will exist within the code for a while before the transition is fully over, with a setting at start enabling the switch between old and new mode.

That means that in a lot of place, I will have

 CommonAncestor* c = ...;
 if(newMode) {
     NewClass* n = dynamic_cast<NewClass*>(c);
     n->setMemberA(...);
     n->setMemberB(...);
 }
 else {
     OldClass* o = dynamic_cast<NewClass*>(c);
     o->setMemberA(...);
     o->setMemberB(...);
 }

Which is awful (code duplication, and cast). (Of course, there is a lot more than two members...)

How can I avoid that? What would be a nice way for them to coexist without duplicating every member access and assignation?

The idea is that deleting OldClass, later, should be as easy as possible.


Side note: In the case of instanciation, I avoided duplication by being able to construct an OldClass member from a NewClass one, so:

 CommonAncestor* c;
 NewClass* n;
 n->setMemberA(...);
 n->setMemberB(...);
 if(newMode) {
    c = n;
 }
 else {
     OldClass* o(n); //we can make an Old by copying a New
     c = o;
 }
sayanel
  • 301
  • 2
  • 12
  • 4
    Do the classes have a common interface? Or can you make one? If so create a common abstract baseclass and use that and accept the fact that you have to refactor the calling code too. In the end it is all about decoupling and making sure you can do changes locally. So step 1. Define common abstract baseclass (interface). 2. Let old class implement that. 3. modify client code to use interface. 4. let new class implement the interface. 5. replace old class with new class. Also don't forget to add unit tests for the interface and verify that old and new class behave the same. – Pepijn Kramer Jun 12 '23 at 10:04
  • I can create a common interface for them, which holds every common method. It works, I wonder why I didn't think of something so obvious... If I have members with a public access, are their drawbacks to putting them in the common abstract class, or should I enforce the usage of getter/setter? – sayanel Jun 12 '23 at 10:18
  • 1
    Use getter setters and put them in the interface too. Always remember : where pieces of code meet you have interfaces. At these points you should hide implementation details. Also, I usually design code for unit testability and this requires interfaces + dependency injection so this pattern is now well established in my mind. – Pepijn Kramer Jun 12 '23 at 10:22
  • So yes public members are a drawback. If you now have a public member std::string, you cannot change that anymore. If you have a getter `std::string name() const noexcept` then you can internally start returning strings from a map or vector later and clients will not have to know about these changes. It keeps changes in your code more local. – Pepijn Kramer Jun 12 '23 at 10:28

2 Answers2

2

When you find yourself down-casting with dynamic_cast, something has usually gone wrong in your design:

CommonAncestor* c = ...;
if(newMode) {
    NewClass* n = dynamic_cast<NewClass*>(c);
    n->setMemberA(...);
    n->setMemberB(...);
}

This above example could instead be written as:

CommonAncestor* c = ...;
c->setMemberA(...);
c->setMemberB(...);

This would require setMemberA and setMemberB to be virtual member functions, so calling the base class functions chooses an implementation from the derived class. Even if you weren't allowed to modify CommonAncestor, you could:

  • create a new class OldOrNewClass containing virtual void setMemberA(...), et al.
  • make OldClass inherit from OldOrNewClass
    • OldOrNewClass can inherit from CommonAncestor, so that OldClass has only one parent
  • make NewClass or NewParent inherit from OldOrNewClass
  • downcast your CommonInterface* to OldOrNewClass* and then call the virtual member functions
CommonAncestor* c = ...;
OldOrNewClass* n = dynamic_cast<OldOrNewClass>(c);
n->setMemberA(...);
n->setMemberB(...);

Multiple inheritance is allowed in C++, so you can keep your old class hierarchy and add OldOrNewClass to that. However, if you are allowed to modify CommonInterface, you can save yourself this trouble.

Also see Why do we need virtual functions in C++?

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • I can modify `CommonAncestor`, it's a base class used for a lot of other purposes. I like your solution, but it doesn't solve the problem you raised that "when you find yourself down-casting something has usually gone wrong". I still need the cast to `OldOrNewClass` – sayanel Jun 12 '23 at 11:42
  • @sayanel you would only have to down-cast if you don't control `CommonAncestor`, but you do, so it can be avoided entirely. Down-casting to `OldOrNewClass` is ugly and unavoidable otherwise, but at least we don't need to check `if (newMode)` and down-cast to different things, which is still a great improvement. – Jan Schultke Jun 12 '23 at 11:49
  • Correction: I CAN'T modify `CommonAncestor`. Sorry for the typo. Still improvement I agree – sayanel Jun 12 '23 at 11:52
1

Templates are for code deduplication:

template <typename T, typename F>
void manipulateClass(CommonAncestor* c, F&& functor) { // please give me a better name
  if (T* n = dynamic_cast<T*>(c)) {
    std::forward<Functor>(functor)(n);
  } else {
    throw std::invalid_argument("Class has incorrect type for selected mode");
  }
}

...

auto const setter = [&](auto n) { // all variables captured
  n->setMember(...);
  n->setMember(...);
  // ...
};
if (newMode)
  manipulateClass<NewClass>(c,setter);
else
  manipulateClass<OldClass>(c,setter);

A similar thing can be done for construction. Also, I would recommend using either std::unique_ptr<Class> or std::shared_ptr<Class> instead of Class*.

bitmask
  • 32,434
  • 14
  • 99
  • 159
  • Is manupulateClass a class or a function here? The values given in the `SetMember...` should be passed as arguments of manipulateClass, but I have a lot of member so I should avoid a signature with 20+ parameters... How to I set `memberA` alone? or B? – sayanel Jun 12 '23 at 11:49
  • 1
    @sayanel See my edit. In your situation you can capture all values and pass them from a lambda taking an auto argument. This means you do the branch just once (`if (newMode)`) but you have to implement the common code just once inside the lambda. – bitmask Jun 12 '23 at 11:55