Let me restate your main issue first: you have a loosely structured Main
class which has become too large over time, and now you are refactoring parts of it to classes A
and B
, to give the program more structure. Still the code in A and B requires access to certain members in Main
which will stay there, and what you observe is that the access from A or B now becomes more effort, at least more effort than it was before when all your code was in a single "god" class.
Actually, that is quite normal. When you refactor some functionality into a new class (or module, or component), you have to make some things explicit which were formerly implicit. For example, it will become explicit which input data the functionality requires, which output data it's function produces, and which access to other functionality in Main
the new class requires. And more explicitness comes for the price of requiring more boilerplate code, that's common.
Technically, you are on the right track. You wrote
Sometimes, when I want to extend the functionality of A, I may need to gain access to additional data members of Main. This requires the redefinition of the constructor signature of A.
This is exactly what happens when the input to some functionality is made explicit. However, it is not necessarily the constructor which has to be extended, sometimes, it is sufficient (or even more correct) to extend the signature of certain functions in A which are called when using its functionality.
It also makes the list of arguments passed to A pretty long (depending on how complex the functionality of A is).
Well, that can happen. It could be a sign that
the functionality in A is indeed quite complex and requires all the data.
one put too many things into A, or too few, one did not make the ideal cutout from Main
when creating A.
certain data members belong into A exclusively, and should not even exist in Main
.
If you are just bothering about the length of A's constructor argument list, you may also consider to rethink if all the data is mandatory and has to be known at construction time of A, or whether there is data which can or should be passed later by some other functions.
It should be clear, however, that noone can tell what the right answer here is by just looking at meaningless names like A and Main, or unknown members and functionalities.
The latter problem can perhaps be solved by gathering the different arguments in a single struct, but I am not sure if this is a good solution.
It is a sensible technique when the grouped arguments form a coherent type, a type for which you can come up with a clear, self-descripting name. Otherwise, it is not. Note there is normally no need to group all arguments together, sometimes you can identify subsets of all arguments which can used to create a new, sensible type.
... I might want to create another class B [...]. Unlike A, it might need access not only to the data members of Main but also to some of Main methods. In this case, I see no other option than passing a reference to Main to the constructor of B (e.g., using *this). However, I would almost certainly never use all the methods of Main in B (but only a small subset of them).
There are several other options here (but again, there is no way to tell which ones are the "best" for your specific case):
The classic OOP solution: create an abstract class X (an "abstract interface") with the subset of methods required by B as pure virtuals, make Main implement X and then make B's constructor consume an object reference of type X instead of typ Main
. Main
will still have to pass this to B's constructor, but now you made the restricted access explicit, which makes it much easier to reason about the behaviour of B, or to write tests for B.
Functional solution: in case B requires only one or two methods from Main, you can spare the interface X, and simply pass member functions of Main as parameters to B, just as data. You may have to learn a few bits about topics like std::function
, lambda expressions and closures in C++.
The solution you definitely should check first, before everything else: is B really a good cut-out from Main? Can you avoid the whole issue by moving a function from Main to B? Can you avoid the whole issue by redefinining the functionality of B?
The hard part, however, is to make the decisions which technique should be applied when, what to put into classes A and B (and what not), and how to design their interfaces. Unfortunately, there is normally no most optimal way and definitely no simple, braindead design pattern which can tell you what to do. The best recommendation I can give you is to discuss your real code with others, more experienced programmers, try out different things and practice. Codereview.Stackexchange might be worth a try, if you cannot find someone in your organisation for reviewing your code.