0

For the past few days I've been researching the relationship of abstract classes and dependency-injected via the constructor classes.

It appears that any time that I can have a dependency-injected class:

public interface IServiceC
{
    void methodC();
};

public class ServiceC implements IServiceC
{
    ServiceC(IServiceA a, IServiceB b) { .. }

    void methodC() { 
      //..
      a.methodA(); 
      //.. 
      b.methodB();
    }
};

At some point I will have to provide it with instances say instanceA, instanceB.

But isn't that the same as having an abstract class and a concrete class using the instances?:

public abstract class AServiceC implements IServiceC 
{
    abstract void methodA();
    abstract void methodB();

    void methodC() {
       //...
       serviceA();
       //...
       serviceB();
    }
}

public class CServiceC extends AServiceC
{
    CServiceC(ServiceAArgs argsa, ServiceBArgs argsb)
    {
        instanceA = new CServiceA(argsa);
        instanceB = new CServiceB(argsb);
    }

    void methodA() { instanceA.methodA(); }
    void methodB() { instanceB.methodB(); }

    CServiceA instanceA;
    CServiceB instanceB;
};

This is all assuming we know the concrete classes, eg we have only testing and release configurations. The release code knows the concrete instances while the testing code simply implement the appropriate interfaces.

The abstract classes can then forward only the bottom-layer via the constructor and manage the plumbing.

Am I missing something crucial here? I am trying to applying DI for an embedded firmware written in C in which I must have everything statically allocated. This means that I need to know the concrete instances before passing them to another object that has them as a dependency. Currently I am using callbacks instead of accepting interfaces in the constructor which is basically the same as single-method interfaces but the more I look at this the more abstract classes seem useful since we usually need to own the lower-level components when writing a higher-level one.

1

I think you've missed the point behind Dependency Injection which is Dependency Inversion.

Specifically the caller does not know the real implementation of the service, this necessitates indirection either via virtual (in C++) or function pointers (C).

It has several benefits, particularly with testing as the collaborators can be a test double of some sort. It also provides some flexibility by moving decisions from the doing part of the code, to the configuration part of the code which knows more about the larger desired behaviour.

In the case of your abstract object, the implementation (the actual constructed object) knows the implementations of its collaborates, because it specifically constructs them itself. It does manage to achieve logic sharing (via the base class), but you can't swap either of those two collaborators for any reason without that changing for everyone, or duplicating the derived class. This design also suffers from change ripples as implementations affect the base class, and the base class affects all its implementations.

This really needs an example. I'm going to use a compression program - basic use case is: it reads a file, compresses it, and writes it to another file.

Since most C compilers are also C++ compilers I am going to mostly write C, but using some C++ features so the below is C+.

int read_file(handle h, char* buffer, int length);
int write_file(handle h, char* buffer, int length);

int compress_file(handle from, handle to)
{
    handle h = //obtained by opening file
    const int N = //some length.
    char buffer[N];
    int k;

    while(0 != (k = read_file(from, buffer, N)))
    {
        //compression logic here
        ...
        //buffer and k now represent the compressed data.

        if (k != write_file(to, buffer, k))
        {
            return -1; //failed report some error code
        }
    }

    return 0; //some success code, or maybe the compressed length.
}

int main()
{
    //open handles

    if (0 != compress_file(from, to))
    {
         //handle error
    }

    //cleanup
}

Great done.

Oops, requirements have changed and now we have to compress a file to a network. Unfortunately we have to use a different API.

class NetworkPoint
{
   void write_byte(char a);
}

So how do you get around this? One option is to push the logic for compression out. It is probably the most complicated part of this system, no point in rewriting that.

int compress_buffer(char* buffer_in, int N, int* n, char* buffer_out, int K, int* k)
{
     //logic about compression here
}

compress_file doesn't change much and now there is a second version to cover the new use case of compression to the network.

int compress_file(handle from, handle to)
{
    handle h = //obtained by opening file
    const int N = //some length.
    char buffer[N];
    int k;

    while(0 != (k = read_file(from, buffer, N)))
    {
        compress_buffer(...);

        if (k != write_file(to, buffer, k))
        {
            return -1; //failed report some error code
        }
    }

    return 0; //some success code, or maybe the compressed length.
}
int compress_network(handle from, NetworkPoint& to)
{
    handle h = //obtained by opening file
    const int N = //some length.
    char buffer[N];
    int k;

    while(0 != (k = read_file(from, buffer, N)))
    {
        compress_buffer(...);

        for (int i = 0; i < k; ++i)
        {
            to.write(buffer[i]);
        }
    }

    return 0; //some success code, or maybe the compressed length.
}

That is code duplication.

If another end-point is needed it will add another duplicate. If the fundamental business logic changes, there are two (or more) places that have to be kept up to date. Clearly this is fine in some cases but in others this will get out of hand quickly.

So what does the "compress" business logic actually need? It needs to "obtain data", preferably in a buffer, then it needs to do some "compression", then it needs to "send data" that was just compressed. Turns out each "" is a service.

class Reader
{
    virtual int read(char* buffer, int N)=0;
}
class Writer
{
    virtual int write(char* buffer, int N)=0;
}
class Compressor
{
    virtual int compress(char* buffer_in, int N, int* n, char* buffer_out, int K, int* k)=0;
}

Just to be clear an interface should never have any implemented functions, nor should it have any data members.

If you find some repeating structures implementing an abstract class for some of the service implementations may work, just don't pollute the interface definition itself.

This allows that behaviour to be compartmentalised.

class FileWriter : public Writer
{
    int handle;

    ...implement to write to a file handle...
}
class NetworkWriter : public Writer
{
    NetworkPoint point;

    ...implement to write to a NetworkPoint...
}

And now the Dependency becomes invertable.

int compress_data(Reader& from, Compressor& compress, Writer& to)
{
    handle h = //obtained by opening file
    const int N = //some length.
    char buffer[N];
    int k;

    while(0 != (k = from.read(from, buffer, N)))
    {
        compress.compress(...);

        if (k != to.write(to, buffer, k))
        {
            return -1; //failed report some error code
        }
    }

    return 0; //some success code, or maybe the compressed length.
}

If we need a new kind of reader, writer, or compression algorithm, this business logic does not care. Just implement the service and pass it in.

Now we already have Dependency injection going on. That is what those function parameters are. The caller "injects" the dependencies into this function. The function doesn't have direct control of those services, but it can interact with them.

Now lets presume that this business logic is itself a configurable service, say it should be run by a task manager.

class Task
{
   virtual void execute()=0;
}

class CompressionTask
{
   std::shared_ptr<Reader> reader;
   std::shared_ptr<Compressor> compressor;
   std::shared_ptr<Writer> writer;

   CompressionTask(std::shared_ptr<Reader> reader, std::shared_ptr<Compressor> compressor, std::shared_ptr<Writer> writer)
       : reader(reader), compressor(compressor), writer(writer)
   {}

   void execute()
   {
       compress_data(*reader, *compressor, *writer);
   }
}

CompressionTask takes several injected dependencies, it does not care about them too much, but it does ask for some life-time guarantees (shared_ptr). These dependencies are hidden from the TaskManager, and aside from their function the implementation is hidden from business logic.

Someone somewhere will have to create the CompressionTask object, and will have to know enough about the dependencies to either create them, request them, or be passed them. This piece of code however should have all of that information available to make those decisions. The rest of the code base can ignore it - treating it as either a generic collaborator or a source of dependencies.

Inverting the dependencies above achieved:

  • Multiple compressible data sources
  • Multiple compressible data sinks
  • Multiple compression algorithms
  • De-duplication of business logic
  • Hiding configuration decisions from the greater code base
  • Moving configuration decisions to one place were the knowledge to make that decision exists
  • Reduced resistance to requirement changes
  • The inversion is achieved by the abstract classes. The extended class is the equivalent of the injection of the specific instances at the injection site. – Liarokapis Alexandros Nov 15 '18 at 9:19
  • Fair point, you can achieve the Inverse and Injection of dependencies that way. Using derivation to supply the collaborators ties the life-times of the collaborators to the derivation, and it also opens up opportunities for adapting the public interface. This can be a powerful tool, it can also be debilitating - as logic distributes through an arcane type tree reducing re-usability. If types need to be tightly bound by lifetime, I'd reach for c++ templates, or for some form of referable data Bean. These maintain tune-able separation of logic, of dependencies, of life-time, and of injection. – Kain0_0 Nov 18 '18 at 23:35
1

I am trying to applying DI for an embedded firmware written in C in which I must have everything statically allocated. This means that I need to know the concrete instances before passing them to another object that has them as a dependency.

I'm having some trouble following your discussion of dependency injection versus abstract classes given that your code is written in C, and therefore has no notion of abstract classes or interfaces as such, anyway. That said, you can still emulate DI in C by defining structs that contain function pointers to the needed methods. For your example:

IServiceC.h

#ifndef ISERVICE_C_ISERVICE_C_H
#define ISERVICE_C_ISERVICE_C_H

typedef struct IServiceC IServiceC;
struct IServiceC {
    void (*methodC)(void* _this);
};

void IServiceC_methodC(void* _this);

#endif

IServiceC.c

#include "IServiceC.h"

void IServiceC_methodC(void* _this) {
  IServiceC* self = (IServiceC*)_this;
  self->methodC(self);
}

ServiceC.h (Similar for ServiceA and ServiceB)

#ifndef SERVICE_C_SERVICE_C_H
#define SERVICE_C_SERVICE_C_H

#include "../ServiceA/IServiceA.h"
#include "../ServiceB/IServiceB.h"
#include "IServiceC.h"

typedef struct ServiceC ServiceC;
ServiceC* initServiceC(IServiceA* a, IServiceB* b);

#endif

ServiceC.c

#include <stdlib.h>
#include <stdbool.h>
#include "ServiceC.h"

struct ServiceC {
    IServiceC base;
    IServiceA* a;
    IServiceB* b;
    bool initialized;
};

static void methodC(void* _this) {
    ServiceC* self = (ServiceC*) _this;
    //..
    IServiceA_methodA(self->a);
    //.. 
    IServiceB_methodB(self->b);
}

// if you have a fixed upper bound on how many you need,
// then make this an array and have initServiceC act like
// a simple allocator.
static struct ServiceC _instance = { { .methodC = methodC }, NULL, NULL, false };

ServiceC* initServiceC(IServiceA* a, IServiceB* b) {
    if (!_instance.initialized) {
        _instance.a = a;
        _instance.b = b;
        _instance.initialized = true;
    } else {
        // TODO: throw a warning?
    }
    return &_instance;
}

main.c

#include "ServiceA/ServiceA.h"
#include "ServiceB/ServiceB.h"
#include "ServiceC/ServiceC.h"

int main() {
    IServiceA* a = (IServiceA*)initServiceA();
    IServiceB* b = (IServiceB*)initServiceB();
    ServiceC* c = initServiceC(a,b);
    IServiceC_methodC(c);
    return 0;
}

The C standard guarantees (C11 6.7.2.1 §13) that you can cast a pointer-to-a-struct to a pointer-to-the-first-entry in that struct, so any ServiceC* can be safely cast to an IServiceC*. As a consequence, this approach is limited to single inheritance, including interfaces.

You can browse the code for this answer (along with Services A and B) on repl.it, here.

You can see from here that different service implementations of the same interfaces would be just as easily substituted in the call to initServiceC in main. At no point here was any memory dynamically allocated (except, perhaps, in the puts implementation) for or by the services. As you would expect, the interface IServiceC is unaware of the existence of A or B.

  • I don't have a problem with implementing interfaces and abstract classes in C, this is easily done with function pointers as you suggested. I am mostly asking about the equivalence of the abstract class aproach. We can have abstract classes perform the inversion and use concrete classes to perform the injection. Something like this. blog.ploeh.dk/2018/02/19/abstract-class-isomorphism – Liarokapis Alexandros Nov 15 '18 at 9:24
1

TL;DR

When we use new in the constructor, we're hard coding a dependency.

When we accept an object in the constructor (instead of calling new) we're allowing the class to be configured by the caller.


You're pushing the configuration of Concrete-Service-C into its constructor.  Well, this configuration has to go somewhere, of course — however, with dependency injection: that can go outside of the class; while without DI: there is a firm dependency.  Dependency injection approaches try to kick this configuration problem upstairs; whereas, what you're doing requires separate classes for test and development that duplicates the construction approach (of the specific sub-types) from (common) arguments.

All that really boils down to is that the classes are a little more brittle in the latter approach: a simple test cannot use a test CServiceA with a real CServiceB; that would require another class rather than just another test.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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