0

my following example seems to go into the direction Inheritance VS Composition. But that's not, what i want to ask. I see the concept Inheritance and Composition on one side and the alternative, which I will show, on the other.

Let's say I have the following Base class

class Base
{
    protected final String a = "1";
    protected final String b = "2";

    protected void c()
    {
        System.out.println("I'm Base" + this.a);
    }

    public final void action()
    {
        //uses in any way a, b and c
    }
}

Now I want to have some different functionality based on that.

So I can do the following:

class Child1 extends Base
{
    protected final String a = "super";

    @Override
    protected void c()
    {
        System.out.println("I'm Child1" + this.a);
    }
}

static class Child2 extends Base
{
    @Override
    protected void c()
    {
        System.out.println("I'm Child1" + this.b);
    }
}

With Child1 and Child2 i get two other functionality based on Base. And if I want to I can create a lot of more

The same I can achieve with:

final class Base2
{
    // there exists ways to hide this public things here too,
    // with the help of some interfaces, but that's not relevant for now
    public final String a;
    public final String b;
    private Consumer<Base2> cConsumer = null;

    public Base2(String a, String b, Consumer<Base2> cConsumer)
    {
        this.a = a;
        this.b = b;
        this.cConsumer = cConsumer;
    }

    public void c()
    {
        this.cConsumer.accept(this);
    }


    public final void action()
    {
        //uses in any way a, b and c
    }


}

So the client can use now for example the functionality of Child1 the following three ways:

    public void clientMethod()
    {
        var result1= new Child1();

        var result2= new Base2(
            "super",
            "2",
            base2 ->
            {
                System.out.println("I'm Child1" + base2.a);
            });

        var result3 = Some result from a client-related-specific Builder/Factory

    }

The second solution (result2=...) seems to be, that this would be the composition-way. And yes that is true. But normally a client doesn't do that in that way. Normally there would be factory/builder (result3=...) in between, so that it doesn't make any difference in the view of the client.

So I don't focus on inheritance VS/over composition, but on inheritance/composition VS something "Direct Injection Construction".

So the problem is with the Builder (which creates an composition-Object) or Inheritance, that I must create some extra class to get a Base-Class, which acts like the Client wants to. In my alternative solution (direct create and use of the composition), I don't need that extra class. So I can on demand let the Base class acts like I want to.

So my question is,
when I should let the client define (by injection directly) the final behavior of Base, and when I should "hard" code that behavior into child classes or builder-classes , IF I need that specific behavior only once (in that related CLIENT)

The second question goes into more detail. Maybe it makes sense to "hard code" parts and let other parts "injectable". The question here is, which parts should follow the one or the other principle.

So a concrete example,
where my "injectable" version is used in existing software: Lets say we have an Object were you can register listeneres/add hooks into it. So that kind of configuration do the client on himself. I don't think that this would be done in a inheritance hierarchy or in a hard coded Builder. The same, if i construct a object of the class human. Here the client puts some information into that object to configure it. Maybe the name, the age, the hair-color and so on, maybe the client also inject a function which determine the movement-speed. But this example (the complete Human) could also be modeled the other way. So that I have "fixed" Human-Classes. Which way I should choose? (Yeah I know there could be some decisions-helper, if I need the same Human more than one time; but this decision-case should be hidden/not relevant here)

An other example:
I have a Template-Class but the client should determine how the method xy() in that Template-Class should work. I can create some child classes with that specific behavior. Or i give up and let the client directly choose the specific behavior which the client inject then into the template class and then uses that class.

Maybe there exists any advantages or disadvantages to use sometimes one or sometimes the other, but which advantages and disadvantages (related to the sometimes and sometimes^^)

2
  • 1
    Your "Direct Injection Construction" is just what we call dependency injection. In this case, you're injecting a function, which can be seen as an object with a single method (which is quite literal in Java). Note that Consumer<T> is an interface, which the class generated from your lambda implements. This can be generalized further - you can accept an interface or a base class, and let the client decide which implementation of the interface / base class to inject (so, the two approaches can be combined). Many (most?) design patterns work exactly that way. Sep 6, 2022 at 4:46
  • When I should let the client define (by injection directly) the final behaviour of Base when the consumer can change the behaviour dynamically at will. If the behaviour is rarely or never altered in runtime due to a consumer choice, why should it be dynamic at all?
    – Laiv
    Sep 6, 2022 at 13:53

2 Answers 2

2

Your question hinges on the single responsibility principle.

If I use inheritance to implement multiple versions of the same thing the implication is that the majority of the code in the base class is shared over all those versions with minor changes to random parts in each version.

However in real life this is rarely the case. Instead what tends to happen is that all my versions are identical, except for a single responsibility which is handled differently for each version. for example say I have multiple types of shopping cart

ShoppingCartSMS
{
    Buy {
       //work out price
       //save order
       //send confirmation **BY TEXT**
    }
}

ShoppingCartEmail
{
    Buy {
       //work out price
       //save order
       //send confirmation **BY Email**
    }
}

etc. Here its only the way the confirmation message is sent that changes, and its argued that how to send messages is not the responsibility of the ShoppingCart.

The logic is moved out to its own class, which implements an interface, and can now be injected into a single ShoppingCart class to provide the various functionality variations.

Once you separate out the logic by responsibility into multiple classes, using inheritance doesn't make any sense. Indeed if I separate out all three operations, the price calc, saving and confirmation messages into separate classes I can compose them into multiple different shopping cart versions much more simply than the inheritance alternative.

If you answer the question "what is the responsibility of my object?" then the answer to your "which things should I inject?" question becomes obvious.

1

Coupling and Flexibility

Injection allows for code substitution, so is used to reduce coupling in the structure of your code. In most programming languages (certainly Java, C#, C++), inheritance represents the tightest possible form of coupling.

Tight coupling in your code structure can be a form of rigidity; a rigid structure often implies code which can be hard to change, but many code structures are written just once and exist for years without any need to change.

Rigid code could be easier for a human to reason over if the structure is organised in a way that makes it easier to identify how individual pieces of code relate back to their real-world requirements.

Conversely, loose coupling structure can be a form of flexibility; which can lead to code being easier to change, with the caveat that a developer changing some code still needs a way of being able to reason over it and to relate it back to its requirements. (Something which can usually be achieved by thorough automated test coverage).

To Decouple Or Not

The answer of when you should care about coupling is whenever flexibility in the code structure provides some tangible benefit to you right now.

To be clear around tangible benefits, I don't mean hypotheticals that might happen for some presumed "future change", or anything currently shrouded in unknowns, whose requirements aren't yet firmly nailed down, or which nobody has yet agreed to be added to the schedule.

By tangible benefits I mean the things that you can be certain you will need, which are usually only those things which can be realised under the current known requirements, and usually only before the next release into production.

If you need to substitute some code (substituting a function/closure, or in a class) as a means of supporting different behaviour, or you need to isolate some behaviour for unit testing, then rigidity in the structure of your code might cause problems, and injection might help by making it easier to substitute with a different class or function.

Coupling is a matter of judgement and has previously been covered in other threads on this site:

Testing and Refactoring

One of the most reliable ways of measuring whether you have the right balance between coupling and flexibility is to write automated unit tests for the behaviour in your code. Sometimes simply thinking about how to isolate behaviour for testing can immediately highlight where coupling might be a problem - for example, if your tests are doing a lot of extra (and duplicated) work, or creating a lot of mocks, or require a lot of initial setup and parameters.

Behaviours are typically things which your stakeholders will understand, so those are typically the starting point of testing as well as discussions with people about future changes in the system and its requirements (e.g. product owner, users, customers, clients, etc).

If you can isolate individual behaviours in the code structure so that they are easy to write tests for, then that's a strong indicator that you probably have enough flexibility to be able to respond to the changes those people are likely to ask for in future too.

You will usually have some classes and functions which aren't decoupled, but if the tests were easy to write, then that doesn't matter because the presence of those tests make it easier for programmers to understand why the code exists (i.e. what existing behaviour it supports), and then have confidence about not breaking that existing behaviour if they need to refactor in future.

Otherwise if flexibility doesn't achieve anything right now, remember KISS and YAGNI, then just do whatever gets you to the next release.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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