2

I'm working on some code where the structure looks like:

Class A {
  Client client;
  B b;
  void process(request) {
    data = fetchData(client, data);
    b.process(data);
  }
}

class B {
  C c;
  void process(data) {
    data2 = someProcessing(data);
    c.process(data2);
  }
}

class C {
  D d;
  SqsClient sqs;
  void process(data2) {
    data3 = someProcessing(data2);
    d.saveToDB(data3);
    sqs.sendData(data3);
}

Note that all of the process methods are void (as in they don't expect any return value). The idea is to take some input data, transform it, save it in DB and enqueue it. The easiest way I can think of doing this is by using Composition, but it looks like a chain of dependencies now. I was looking at Chain of Responsibility, but in this case I want every handler in the chain to always do something, so I don't think that fits. What would be the right approach to arrange my code?

2

2 Answers 2

1

I agree with @Zapadlo that this isn't too much of a problem given that the classes are cohesive. In a more general setting though, you may want to be more flexible with that kind of dependent computations.

In that case, you can look into traditional composition of functions to arrive at a more general abstraction:

Consider your first class B (A seems to init the chain by fetching the data initially). It takes data as input, let's say of type T. It does some side-effect and computes a new view of the data, called data2 in your code, of type S. The same structure is present for C. In terms of functions this is basically the Function<T, S> composed with the Function<S, U>.

Normally, a Function should be side-effect free. In particular, when you enter the realm of functional programming. Depending on your and your team's affinity to this topic, you may just use a different name for the underlying interface altogether. Say, a DataHandler<T,S> that takes data of type T, does something with it and return some updated data of type S. It's basically the same thing though. (The name DataHandler is not very good though). On the other hand, if you are familiar with FP, you can go take the fun route and rip out the side-effecting parts using monads.

Either way, abstracting the data-handling and -modification part into its own separate interface removes the direct dependencies between these classes. The underlying idea for this operation is the dependency inversion principle (DIP). The resulting caller code is then responsible for the actual composition, so there will be a single dedicated place in the code that creates the dependency chain B->C->D, while the individual classes are not bothered by that chain.

Also, there is a good chance that after refactoring based on the DIP, you will find that the resulting code handling the data transformation and forwarding to a dependency looks very similar or even identical in the different classes. Then you can continue to refactor, following the Don't Repeat Yourself (DRY) principle if you feel it's worthwhile.

1
  • This is spot on. If the order of operations needs to be exactly that, then CoR is an unnecessary overkill. By hardcoding dependencies like this, you ensure that the order will be exactly like this. Putting interfaces as members types would go a long way to provide for future decoupling, but if it is not likely to have different implementations, it is also an overkill, and you would be doing something that you aren't going to need. Commented Nov 13, 2017 at 14:07
1

CoR would be an overkill in this case. You could do it like that, of course, but you would just make the code more complicated, without some clear benefit. Your classes clearly depend on each other. You could decouple them a bit by using interfaces, but if you are not going to have more than one implementation of each interface, then that is an overkill too.

I would just suggest one modification, though. Based on your code, you really do not need to have B member of A class, or C member of B class. Unless there are other reasons for that (which I do not see from this code), it is a completely unnecessary coupling and a waste of space. You could simply make those local variables, like this:

Class A {
  Client client;

  void process(request) {
    data = fetchData(client, data);
    B b = new B();
    b.process(data);
  }
}

class B {

  void process(data) {
    data2 = someProcessing(data);
    C c = new C();
    c.process(data2);
  }
}

class C {
  SqsClient sqs;
  void process(data2) {
    data3 = someProcessing(data2);
    D d = new D();
    d.saveToDB(data3);
    sqs.sendData(data3);
  }
}

Your Answer

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

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