Comrades, tell me how in a decent society it is customary to solve such a problem:

We have, for example, such a class hierarchy

class A_Base { int x; int y; public A_Base(int x, int y) { this.x = x; this.y = y; } } class A_First : A_Base { string name; public A_First(int x, int y, string name):base(x,y) { this.name = name; } } class A_Second : A_First { string surname; public A_Second(int x, int y, string name, string surname):base(x,y,name) { this.surname = surname; } } 

Now we need to add another property in A_Base (and accordingly, initialize it in the constructor)

 class A_Base { int x; int y; int z; public A_Base(int x, int y, int z) { this.x = x; this.y = y; this.z = z; } } 

This means that it is necessary to change the definition of the constructor in the entire class hierarchy, adding a new parameter z there, although we do not change these constructors themselves - we only change the call of the parent. And somehow this is not very good.

 class A_First : A_Base { string name; public A_First(int x, int y, int z, string name):base(x,y,z) { this.name = name; } } 

How are advanced PLO partners to act in such cases? To get a separate class / structure that will be a parameter for the constructor?

 class A_Init { int x; int y; int z; } class A_Base { int x; int y; int z; public A_Base(A_Init init) { this.x = init.x; this.y = init.y; this.z = init.z; } } 

or is there another tricky pattern?

    3 answers 3

    tl; dr

    There may be several solutions, and they depend on the specific situation.

    In this particular case, I would draw attention to the reason for changing the specification. Where did the new parameter z come from, why was it not provided right away?

    The answer to this question will help design the hierarchy correctly. For example (option # 1), subject matter experts cannot describe all possible scenarios. This means that as we work on the program, properties will appear and change, and this is perfectly normal from a business point of view . Then yes, an A_Init structure would be the appropriate solution.

    On the other hand, if this structure stores default values ​​for parameters x , y , z , then is it not better to immediately make an empty constructor or constructor in the hierarchy with only the most necessary parameters, the list of which will not change exactly ©?

    The second method is more correct, including because it will help solve the same problem at all levels of the hierarchy. A_Init does not guarantee that you will not have to start A_Init2 for any of the heirs.

    Because: use defaults wherever possible. Pass through the constructor only initializers for readonly fields.

    Option number 2: there is a preliminary design of the system, a prototype is being written. The prototype is not so scary that changes are made to the code often. Actually, it is being developed to identify bottlenecks. Then the answer is: nothing terrible happens, it should be so.

    The third option: the heirs are in fact not the heirs, and decorators . Then the implementation looks different:

     class A_First : A_Base { A_Base decorating; string name; public A_First(A_Base decirating, string name) { this.decorating = decorating; this.name = name; } } 

    How to understand that we are talking about this situation? Usually, so that A_Base is an abstract class that knows almost nothing, but its successors do the main work.

    The fourth option is an extension of the first option. If the default values ​​exist, but the algorithm for obtaining them is confused, the logic of their receipt / calculation can be put into a separate class, which Eric Evans would call a regulation . Then all hierarchy classes would receive one or more rules in the constructor and be initialized in accordance with the rules of business logic.

    The difference between A_Init and the regulations is not in syntax, but in essence. If A_Init does not contain business logic, but only returns constants, then this is probably not a time limit.

      This is one of the disadvantages of inheritance - if the parent class changes, then the changes will most likely affect all children. And nothing can be done about it. Especially this deficiency will manifest itself in long-term and large programs.

      To avoid such things, you can think about replacing inheritance with composition, or familiarize yourself with designing using the SOLID principles, or look at other paradigms, such as Component-Oriented Programming .

      In the latter, each class is built from components, inheritance is not used, respectively, the problem disappears, and the likelihood of errors is substantially less, but when there are many components, it will be difficult to navigate them.

        As part of the language C # - no way. This is a special case of the problem of a fragile base class .

        You have to understand that changing the base class (from which there are or may be derived classes somewhere) is always a breaking change, and requires a careful review of the entire code.

        therefore

        • Try to plan in advance the basic public classes that are open for inheritance so that their public interface does not need to be changed in the future;
        • try to resist such changes;
        • if, nevertheless, changes to the base class cannot be avoided, notify all possible customers of your base class about your changes, so that they review their code; try to make changes so that the place where the change is required is indicated by the compiler.
          • bad example: your function took an angle in degrees as a double number, now it takes an angle in radians as well as a double
          • A better example: your function took an angle in degrees, now you have a function with a new name that accepts an angle in radians, and the old one is labeled with the [Obsolete] attribute

        And yes, the modification of the base class violates the Open / closed principle of SOLID.