6

Below is an excerpt of Fundamentals of Software Architecture, page 65, which I am reading:

The instability metric determines the volatility of a code base. A code base that exhibits high degrees of instability breaks more easily when changed because of high coupling. For example, if a class calls to many other classes to delegate work, the calling class shows high susceptibility to breakage if one or more of the called methods change.

The chapter exposes a couple of metrics to help architects understand the code base.
Reading that passage, I instantly cross checked that with Dependency Injection - which, in my understanding, is a design principle devised to provide low coupling and testability.

If that is right, aren't DI and Instability metric contradictory? DI encourages delegating responsibilities to dependencies (whenever this makes sense/does not impair cohesion) - while Instability seems to frown on doing that - because that makes the component less stable, since it is more susceptible to break.

Do I misunderstand this metric, DI, or both?

2
  • 1
    DI calls interfaces rather than classes
    – Ewan
    Jan 8, 2022 at 18:20
  • @jonrsharpe: Hey Jon, I was just experimenting to see what would be the best forum to post this and get help. I do delete the duplicate as soon as responses start coming in. Deleted there.
    – Veverke
    Jan 8, 2022 at 18:57

5 Answers 5

12

DI just makes dependencies explicit and configurable at run time, but it does not change the number and kind of dependencies - at least not directly.

For example, when a class delegates persistence to a repository class and uses 5 different member function calls for this, with DI those 5 different member function calls will go through an interface, and without DI they would just be hardcoded directly (but still the same calls, without any differences in syntax and semantics). So DI does not really change that metrics, those two things are just orthogonal.

Of course, to make DI possible, to achieve a certain goal like testability, one may need to refactor a class or component first, to move the parts which should become "injectable" into another component. So in this sense, DI promotes to refactor a code base into smaller components, and with more smaller components, the instability metrics can increase.

However, this is not "a bad thing". If you want to get the instability metrics down to zero, you could simply put the code of a whole large system into one component, maybe just one class (a.k.a. "big ball of mud"). Obviously, this is not desirable, and it shows that the application of such metrics just by looking at the numbers can easily become misleading.

To apply the instability metrics correctly, one needs to compare two versions of a system after it was structured down to the required or desired number of components. The metrics then may help to find a more optimized distribution of the responsibilities between those components. However, the metrics is usually not helpful to identify the components which should be made "injectable" through DI.

3
  • Changing injected dependency methods would also not increase the susceptibility to break in the class that relies on the dependency ?
    – Veverke
    Jan 8, 2022 at 19:00
  • @Veverke: in the class that relies on the dependency the code does not really differ, maybe Philip Kendall's example makes this more clear.
    – Doc Brown
    Jan 8, 2022 at 21:15
  • @Veverke: I added some more details; I guess I now got where your idea is coming from, maybe my extended answer can clarify a few things.
    – Doc Brown
    Jan 9, 2022 at 8:36
3

The instability metric and dependency injection are complimentary, not opposed. The misconception comes from believing dependency injection increases instability. Instead, think about why you would introduce dependency injection in the first place. The more a class collaborates with other objects, the more dependencies it has. Introducing dependency injection allows you to move the initialization logic for dependecies elsewhere. A large number of dependencies increases the instability metric. Adding parameters to a constructor or method to utilize DI should actually decrease the instability metric slightly, since initialization logic is moved out of the current class.

The second misconception about the instability metric is that the goal is low instability. If a class has many things to do, it simply has many things to do, and depends on many other objects to do its job. A high instability score just identifies where bugs are likely to crop up. This is a tool for the programmer to assess risk. When a high risk piece of code is identified, then you can think of techniques that allow you to manage that risk, not eliminate risk. This is where dependency injection comes into play.

Refactoring code for dependency injection makes it easier to write unit tests. A programmer can execute thousands of unit tests in a few seconds. This gives you very fast feedback if a code change breaks something. Unit tests are also easier to debug, which makes fixing bugs easier and faster. This is how you manage the risk of code that has a high instability score. Sometimes you just cannot make code less prone to breaking. The best thing you can do is make breaking changes easy to find, debug, and fix.

Complex collaboration logic creates a high instability score, which may prompt the developer to refractor code for dependency injection in order to write unit tests. This gives developers quick feedback about whether or not a code change has broken that "unstable" code. It does not reduce the number of times that code will break.

2

I think you are misunderstanding what dependency injection is. The classic dependency injection refactoring would be to change something like

class A {
  InterfaceB b;

  A() {
    b = new BImpl()
  }  

  int doStuff() {
    return b.doOtherStuff() * 2;
  }
}

where the dependency on BImpl is hard-coded in A to

class A {
  InterfaceB b;

  A(InterfaceB b_) {
    b = b_;
  }

  int doStuff() {
    return b.doOtherStuff() * 2;
  }
}

where the dependency is provided. Note that there is no change in the number of other classes called during this refactoring.

You seem to be more talking about refactoring a "god class" where all the functionality is contained in A with no delegation at all. While doing that is a prerequisite for dependency injection, it's a very different concept.

0

If the Foo class needs the result of calling now() on some time stamp object it’s got a dependency. But asking for that result to be handed to Foo rather then making Foo call for it itself is a very different dependency.

DI let’s you move the call dependency out of Foo and makes the result dependency explicit. Foo is now more loosely coupled and deserves a lower metric.

But something somewhere still has to call now(). DI doesn’t lower the metric for the whole code base. It just lets you move the problem around.

That might seem pointless but it’s nice to have stable places for complex business to live. And nice to have unstable construction code free of complex business rules.

So the metric is useful when you understand where to apply it. It is not an absolute.

0

DI reduces instability

Actually you've got it backwards. Dependency injection reduces instability. I'll explain.

Consider the following simple function:

int AddNumbers()
{
    return 2 + 2;
} 

This function adds 2 and 2. If you want to add different numbers, you have to change the code. Therefore it has very high instability.

Now consider this function:

int AddNumbers(int a, int b)
{
    return a + b;
}

We've moved the dependencies to parameters. Now you can add any two numbers without changing code. Therefore this function is much more stable.

Dependency injection is exactly the same, but instead of converting data dependencies to method parameters (as in the above example), you are converting behavioral dependencies to constructor parameters.

This example illustrates. Before dependency injection:

static class DataRetriever
{
    static public int A 
    {
        get 
        {
            //Retrieve A from somewhere
        }
    }

    static public int B
    {
        get 
        {
            //Retrieve B from somewhere
        }
    }
}

class DoMath
{
    public int AddNumbers()
    {
         return DataRetriever.A + DataRetriever.B;  //static dependency!
    }
}

After modifying to use DI:

interface IDataRetriever
{
    public int A { get; }
    public int B { get; }
}

class DoMath
{
    protected readonly IDataRetriever _data; //Injected

    public DoMath(IDataRetriever data)
    {
        _data = data;
    }

    public int AddNumbers()
    {
         return _data.A + _data.B;
    }
}

With the first program you can only add A and B from the DataRetriever. If you want to get your numbers from somewhere else, you have to change the code. With the second example you can get A and B from any source you want, as specified by the caller. You don't have to change code, so it's more stable.

Dependency injection removes a static dependency and makes it a parameter instead. It increases stability and decreases instability.

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.