Restoring testability: the IOSP

This article is a revised version. It was previously published atย refactoring-legacy-code.netย published.

In legacy code projects, there are typically few or no automated tests. The simple recommendation that these should be added does not reflect reality. The code has not been designed with testability in mind. Dependencies and a lack of aspect separation in particular make testing difficult. It doesn't help: the code has to be refactored before tests can be added. However, this leads to the following dilemma:

  • To make the code testable, it must be refactored.
  • Before the code is refactored, tests should first be added.

There is no easy way out of this dilemma, no silver bullet. You just have to start somewhere. Due to the risk involved, a focused and cautious approach should be taken. The use of powerful tools such as TypeMock isolatorin order to replace dependencies with dummies.

But sometimes simple Tool-supported refactoringto improve the situation in terms of testability. It doesn't always have to be a major rebuild of the software. Instead, you should proceed in small steps and regularly and frequently transfer changes to version control. This keeps a step-by-step way back open in case you make a mistake.

However, the complex refactorings should be carried out with a clear focus on improving testability. It is now important not to get bogged down. To do this, it is helpful to focus on an important principle: Integration and Operation must be separated. The Integration Operation Segregation Principle (IOSP) states that a functional unit should either integrate other functional units, then it is a Integration. Or it should contain logic and then not also integrate. In this case, we are talking about a Operation. Operations contribute to solving the problem, while integrations integrate other functional units, joining them together. Integration can be hierarchical. This means that an integration can integrate functional units that are themselves integrations. This results in a tree structure, at the bottom of which are the operations. These are characterized by the fact that they have no dependency on other functional units, because then they would be integration again. The operations are leaves because they themselves do not integrate other functional units.

We find an analogy in the way humans work together. This works smoothly when the people involved are either in the role of Management or Performer act. Here, management corresponds to integration. The task of management is to organize and delegate work processes. The executors are those who carry out the activities but do not delegate themselves. If the roles are mixed up, execution is not as smooth as it could be. If management interferes in the execution, there is at least a lack of clarity. Likewise, things can go wrong when executors delegate parts of their work. A clear separation of aspects helps to ensure that an activity based on the division of labor flows smoothly.

The following example shows a fictitious excerpt from a software system for invoicing. The code is responsible for converting an order into an invoice. To do this, some simple calculations are performed and the created invoice items are then saved

using System.Collections.Generic;

namespace refactoringiosp
{
    public class Accounting
    {
        private readonly RechnungsStore rechnungsStore = new RechnungsStore();
        private const double UStSatz = 0.19;
        public void Create invoice items(IEnumerable order items) {
            foreach (var orderItem in orderItems) {
                var invoice item = new invoice item {
                    Description = order item.description,
                    Unit price = order item.unit price,
                    quantity = orderitem.quantity
                };

                invoice_item.total_net = invoice_item.quantity * invoice_item.unit_price;
                invoiceItem.VAT = VATRate * invoiceItem.TotalNet;
                invoiceItem.SumGross = invoiceItem.SumNet + invoiceItem.VAT;
                invoiceStore.Save(invoiceItem);
            }
        }
    }
}

using System;
namespace refactoringiosp
{
    public class RechnungsStore
    {
        public void Save(InvoiceItem InvoiceItem) {
            Console.WriteLine($ "Save: {invoiceitem.quantity} x {invoiceitem.description} รก {invoiceitem.unitprice}");
        }
    }
}

namespace refactoringiosp
{
    public class Order item
    {
        public string Description { get; set; }

        public double Quantity { get; set; }

        public double Unit Price { get; set; }
    }
}

namespace refactoringiosp
{
    public class Invoice item
    {
        public string Name { get; set; }

        public double Quantity { get; set; }

        public double Unit Price { get; set; }

        public double SummeNetto { get; set; }

        public double VAT { get; set; }

        public double SummeBrutto { get; set; }
    }
}

The problem with this example code is that it violates the IOSP. The method Create invoice items is on the one hand an operation, as it is a Order item into a Invoice item transforms. On the other hand, it is integration, as it is the method Save from the class InvoiceStore calls, i.e. integrates them.

The structure of this code snippet is shown in the following figure:

Dependencies according to IOSP-Clean Code Developer Academy

The example consists of the two classes Accounting and InvoiceStore. Accounting is dependent on InvoiceStore. This dependency is linear here. The aspects Domain logic and Resource access are clearly separated. The two classes are each responsible for only one of these two aspects. And yet it is difficult to test the code automatically. This is because integration and operation are mixed up. The class Accounting contains the domain logic, i.e. it contributes to solving the problem and is therefore an operation. However, it also calls the resource access and thus integrates itself with another functional unit. And it is precisely this violation of the IOSP that makes the code more difficult to test than necessary.

The violation of the IOSP even takes place on two levels here: the method Create invoice items contains domain logic and integrates the resource access call. There is therefore an IOSP violation at the method level. There is also an IOSP violation at class level: the class Accounting contains both domain logic and integration code.

A refactoring should have the following structure:

Dependencies according to IOSP-Clean Code Developer Academy - Stefan Lieser

Integration and operation are now clearly separated. The leaves of the dependency graph are the operations. Since they are leaves, i.e. they are not dependent on any other functional units, an automated test is easy to implement. The functional unit Invoicingwhich is now responsible for the domain logic. The class Accounting behaves externally in the same way as before. However, there is no longer any domain logic in this class, only integration code. The following listing shows the code after refactoring:

using System.Collections.Generic;

namespace refactoringiosp
{
    public class Accounting
    {
        private readonly RechnungsStore rechnungsStore = new RechnungsStore();
        private readonly Rechnungserstellung rechnungserstellung = new Rechnungserstellung();
        public void InvoiceItemsCreate(IEnumerable orderItems) {
            foreach (var bestellposition in bestellpositionen) {
                var invoiceItem = invoiceCreation.InvoiceItemCreate(orderItem);
                invoiceStore.Save(invoiceItem);
            }
        }
    }
}

namespace refactoringiosp
{
    public class Invoicing
    {
        private const double UStSatz = 0.19;
        public Invoice item Create invoice item(Order item order item) {
            var rechnungsposition = new Rechnungsposition {
                Description = order item.description,
                Unit price = order item.unit price,
                quantity = orderitem.quantity
            };
            invoiceItem.TotalNet = invoiceItem.Quantity * invoiceItem.UnitPrice;
            invoiceItem.VAT = VATRate * invoiceItem.TotalNet;
            invoiceItem.TotalGross = invoiceItem.TotalNet + invoiceItem.VAT;
            return invoice item;
        }
    }
}

ย 

Dependencies according to IOSP-Clean Code Developer Academy Seminars and Trainings

After refactoring, the code is much easier to test. This is because the two operations that make up the solution are now clearly separated. One operation contains the domain logic, another the resource access. Furthermore, there is now an integration method that brings the two operations together.

A test strategy should always consist of a few integration tests and many unit tests. This can be implemented well here. The operations can be tested in isolation in unit tests. The method Create invoice item is a function: it receives its input as a parameter and returns the result as a return value. No other methods belonging to the solution are called within the method. This was the aim of the refactorization: to separate operation and integration. This makes the method very easy to test. We can now test the domain logic with many different test data and scenarios without being confronted with resource accesses in every test. Nevertheless, an integration test is required at the upper level to ensure that the operations interact correctly.

In terms of testability, this simple example would also work without refactoring. Through a Extract method Refactoring could be used to extract the domain logic from the original method and to refactor it using internal and InternalsVisibleToย accessible for the test. The aim here is to present the IOSP as a target for a refactoring measure, so I have chosen a simple example. In real legacy code, the blending of aspects and the dependencies are usually much more dramatic than in my example. The basic pattern remains the same: Function units each call their successor, as the following figure shows:

ย 

ย 

The arrows show the flow of data from one functional unit to the next. This is usually implemented in such a way that the dependencies run in the same direction. This means that testability is not as simple as it could be. It is necessary to work with mock-ups in order to be able to test functional units in isolation. Mock frameworks offer this functionality. But why go to so much trouble when it can be done more simply? The following illustration shows a different structure of dependencies in which the IOSP is adhered to:

Dependencies according to IOSP-Clean Code Developer Academy - Seminars and trainings

The data flows in the same way as in the original design. However, the dependencies are now clearly different. The integration of the functional units is separated out here as an independent aspect. This means that the dependencies disappear in the places where they cause the most damage: the dependencies are removed from the domain logic and shifted to methods specifically responsible for the dependencies. With new code, you should ensure compliance with the IOSP right from the start. For legacy code, the IOSP is a very effective target for refactoring.

In the article "DIP or IOSP?" for more details on the Integration Operation Segregation Principle (IOSP).

Our seminars

course
Clean Code Developer Basics

Principles and tests - The seminar is aimed at software developers who are just starting to deal with the topic of software quality. The most important principles and practices of the Clean Code Developer Initiative are taught.

to the seminar "
course
Clean Code Developer Trainer

Conducting seminars as a trainer - This seminar is aimed at software developers who would like to pass on their knowledge of Clean Code Developer principles and practices or Flow Design to others as a trainer.

to the seminar "

16 thoughts on “Testbarkeit wieder herstellen: das IOSP”

  1. Hello Stefan,
    For me, control structures have always been excluded from integrations until now. You use a foreach loop in your Create Invoice Item integration.
    In your opinion, where do we have to speak of domain logic in control structures and where not? Does it perhaps depend on the use of domain expressions (in the sense of programming language expressions)? A while loop with a termination condition from the domain logic would therefore not be permitted in integrations, for example.

    1. Hello Denis,

      Yep, you have to be very careful with control structures in the integration. A foreach is ok because no domain logic is used. A for that iterates over all elements is also ok. However, if there is a condition in the for that contains domain logic, for example, it is no longer pure integration. The same applies to while loops, as you have written.
      The biggest risk I see is that the next developer who adds to the code doesn't understand the concept and then adds domain logic. Then you have an IOSP violation in the code. So all developers should know what the IOSP is all about so that operational parts don't creep back into the integrations.
      Greetings
      Stefan

  2. Web-Site: Your new article does not appear in the "Automatic testing" category. It can practically only be found via the search...
    ... and I didn't get an email notification that you had replied. I was kind of expecting that. ๐Ÿ™

  3. Hello Stefan,

    A question about the "order item" data structure. Is it ok if all operations share the same data structure (or are passed by the integrations) and the operations pick out the data they need, or should you keep the operations as lean as possible and only pass the parameters you really need?

    Greetings,
    Reinhard

    1. In fact, this must always be weighed up. Operations should only bind to the data that they themselves require. It therefore makes sense in some cases to provide special data structures. However, I would not do this in the specific example. Here, only a few operations are dependent on the data structure and all operations belong to the same topic. Only if the topics are different or only a fraction of the data is required would I introduce another structure.

  4. What do I do if I need more data in the logic, but I can only get it through operation calls, e.g. read it from the DB? For example, I transform one data structure into another (e.g. my internal structure into one for an external service call) and need DB lookups.

    What if there is already a case distinction at the "upper level", e.g. "search with input for existing data. If there are hits, do A (multiple operations), otherwise do B (multiple hits)"?

    1. If the domain logic requires data from the DB in between, it is split into two parts, plus the resource access. The integration then first calls the first part logic, then the DB, then logic again.

      The case differentiation takes place at the top level. The decision itself is the task of an operation. Reacting to the decision and then either going "to the left" or "to the right" is the task of integration.

      1. Avatar of Michael K.
        Michael K.

        "The task of integration is to react to the decision and then either go "left around" or "right around"."

        But don't I then have a "domain-bound" IF branch in the integration? What Erik describes is exactly my problem (the "left-around-right-around scenario") that I have in understanding IOSP. While I recognize the value of the rule on a theoretical level, I find it very difficult to implement practically.

        Basically, the integration should only contain sequences of method calls, should the rule be hard "no control structures" (a foreach loop is ultimately just shortened syntax for an unconditional sequence, so it fits in your example).

        But if IF branching is allowed in integration under certain conditions, I would be interested to know how to recognize which ones do not violate the principle and which ones do.

        1. Hello Michael,

          I think an if in the integration is acceptable if the expression is completely outsourced to a method. So the following would be fine

          if(CustomerIsCreditworthy(customer)) { ... }

          However, the following would not be okay:

          if(customer.Status == Status.Gold) { ... }

          Then domain logic would be in the expression of the if statement.
          A good rule for me in practice is whether the branch expression can be tested in isolation. If the expression is outsourced to a method, this is the case.

          foreach and for in a canonical way are also fine, because it is only a matter of calling a method on each element.

          1. Avatar of Michael K.
            Michael K.

            Wow, thanks for the quick reply ๐Ÿ™‚

            In extreme cases, your example would look like this (please ignore syntax errors, I'm more into Python):

            Integration:
            if(hasGoldStatus(customer)) { ... }

            Operation:
            public boolean hatGoldstatus(customer kunde): {
            if (customer.status == status.gold) {
            return True;
            }
            else {
            return False;
            }

            Doesn't that bloat the code incredibly?

          2. Theoretically, you can write your entire program in the main method. Why don't you do that? Why do you split it into methods?
            I am concerned here with two values:
            Changeability - The program should be able to be changed even years later
            Correctness - I want to use automated tests to ensure that everything still works even after changes have been made.

            Both changeability and correctness are improved when I pull the expression out into a method. This is not about optimizing for writing and creating. Instead, we better optimize for reading and modifying. If more methods are needed, fine.

          3. Avatar of Michael K.
            Michael K.

            Hello Stefan,

            Thanks again for the quick reply.
            "Theoretically, you can write your entire program in the main method. Why don't you do that? Why do you split it up into methods?"

            I think that came across incorrectly: I'm not interested in a fundamental discussion about whether the principle is enforceable or sensible. Perhaps by way of background: I am not an experienced developer, but would describe myself as an advanced beginner at best. I'm looking for ways to improve my code and that's how I stumbled across the clean code principles.

            While the other principles in the "red degree" make sense quite well, I found the IOSP very difficult to understand and implement. As a beginner, I simply find it difficult to weigh up how strictly I should adhere to rules such as "no control structures in integrations", and I am looking for more or less clear rules. Since your blog was one of the few sources with a comprehensible example, I just wanted to ask. I am very grateful that you took the time to answer!

  5. Hello Stefan,

    I read your article with interest and I actually have one more question. Your picture at the end of the article, where a structure consistently adheres to the IOSP principle. How do you proceed if the functions f1, f2, f3 and f4 not only have a control flow, but also an alternative?

    This would mean that after each integration step f I would have to check whether the call was successful. So f1 successful, then f2 - if successful then f3 and so on. How would you solve the whole thing?

    Thank you

  6. Hello Stefan,

    I have a question about I/O and API calls:
    Would you classify API calls that contain no logic but only the call itself, e.g. a DB query or file accesses, as operators or integrators? (see also https://clean-code-developer.de/die-grade/roter-grad/#Integration_Operation_Segregation_Principle_IOSP)
    I think that something like this would be more of an integrator, since there is no logic in it that wants to unit-test.
    How do you see that and why?

    Best regards

    John

    1. Hello Johannes,

      The IOSP rules are actually very simple. The following is permitted in each case:
      Integration:
      - Calling other methods of my solution

      Operation:
      - Calling framework/runtime methods (so-called APIs)
      - Expressions

      In this respect, it does not matter which framework API you call. Whether File IO or String API, the call is then part of an operation. Maybe you don't want to unit test all operations. Of course. Nevertheless, the call to a framework API belongs to the operation category.

      The description on the CCD website is still a little unclear, I will revise it when I get the chance.

      Best regards
      Stefan Lieser

Leave a Comment

Your email address will not be published. Required fields are marked *

en_USEnglish