How to update contracts and preserve storage in inheritance

Let's say I have the following contracts:

contract A {
    uint x;
    uint [50] _gap;
}

contract B {
   uint y;
   uint [50] _gap;
}

contract MainV1 is A, B {
    function execute() external {
       // some code
    }

   uint [50] _gap;
}

Now, let's say update is needed for MainV1 which requires(updating execute function + inheritting from contractC)

Because of inheritance and the way it works, it might really become harder to directly add contractC (contract MainV2 is A, B, C {. that looks correct, because in the inheritance chain, first A's variables come, then B's and then C. No collision or overriden happened for variables. But this case is very simple and won't always be easy to follow. so another way we could do this is:

contract MainV2 is MainV1, C {} 

This is so much easier !

but now, problem emerges:

if we also needed to update execute function, it's not possible anymore. To work around this, MainV1 should have had execute as virtual function and in MainV2, we would override and update it.

Asking this to you OZ because you have lots of experience with this ! which way do you choose ? if we go with virtual way, that means all the function I should make virtual. Want to hear your opinion on this. I am using same approach with gap as you have.

Thank you..

There is some discussion in this thread about different approaches.

For your example, if you want to do contract MainV2 is A, B, C, then you need to reduce the storage gap at the beginning of MainV2 to make space for the storage used by C.

(Note: for the upgrades plugins to recognize the gap, it needs to be named as __gap)

Another way is to keep all storage variables used for your Main contract in a separate base contract, and not have any storage variables in the Main contract itself (this is discussed in the above thread).
Then you can do contract MainV1 is A, B, V1Storage then upgrade to contract MainV2 is A, B, V1Storage, C, V2Storage

Thanks for the answer.

but my main question is something else. The below is easiest to maintain due to storage, but then because of this, when writing MainV1, I need to make all functions virtual in order to be able to override it in MainV2 in case needed.

contract MainV2 is MainV1, C {} 

That is fine if you prefer that approach. Keep in mind MainV2 would need to continue marking functions as virtual in case you have MainV3 later.