4

I am coming to terms with TDD, and the fact that I need to re-factor some code that I'm (re)writing. I am having a problem that I think is a classic conflict---TDD vs. encapsulation of private methods/data---and I need advice on how to structure my code properly so that everything is testable through its public interface.

I am writing a class which does some heavy computation for a performance critical code. It contains ~10 std::valarrays which, in typical use, may have between 50 and 200 floats. All of the methods pound on some or all of those arrays---usually by zipping through them in loops. Additionally, there is a fist full of constants common to all of the methods.

Because these methods all use the same data, they really do belong in a class together. (In my first iteration of the code (in C) my function call signatures were horrifying but repetitive.)

The class really only needs one or two public methods which returns a comparatively small and simple array of POD structures. So, the input is simple, the output is simple but there is HEAVY computation on a lot of self generated data in between.

A simplified illustration might look something like this:

class HighPerformingDoohicky(){
 private:
  std::valarray a, b, c,
   ...
  float some_constants // all methods depend on these.

  void method1() // pounds on a, b, c.
  void method2() // pounds on c, d, e.
  void method3() // pounds on all of them    

 public:
  HighPerformingDoohicky(a_few_numbers) some_constants{a_few_numbers} : {}
  std::valarray<float> method4() // loop/iterate calls to methods 1, 2 and 3 to produce output
}

So there is a *LOT* of private data so any one datum is only a small part of the generated output. Testing the output methods really doesn't help me develop the numerics and it would be easy for subtle problems to go unnoticed in the public interface.

I am happy to restructure (friend classes maybe?) and I am not fixated on the solution having a particular form as long as it is easy to read, is testable, is correct and is efficient.

Addendum: In case someone finds it illuminating to know more specifically about the calculations here is a summary. Six of the variables are the state space for a dynamical system. One of the methods is an Euler integrator that use the equations of motion (and control) to project a state space trajectory based on initial conditions (input). Another method will compute a set of Lagrange multipliers based on that trajectory. Another method will compute a gradient vector that depends on the trajectory and the Lagrange multipliers. Method 3 will repeatedly call those functions to solve an optimization problem. All of these things (including the gradient) are valarray which are N elements long, with N somewhere between 20 and 200 in normal operation.

6
  • When you say "these methods all use the same data", what do you mean by "the same data"? Is it all just a bunch of floats, or are there some relationships/constraints the floats are expected to satisfy? Or to phrase it as a leading TDD question: Is there any set of float values for which calling method1, method2 or method3 is simply incorrect, and no expected output/changes can be defined?
    – Ixrec
    Commented Sep 4, 2015 at 21:57
  • Have you considered bringing your real code to Code Review SE?
    – RubberDuck
    Commented Sep 5, 2015 at 1:21
  • @Ixrec Yes, all shared data are just floats or valarrays of floats. It would be incorrect to call the methods out of order. Arrays a, b and c need to be worked on before c, d and e. There is a chain of logical dependency for numerical stability. I suspect the details of the calculations may be too esoteric, but I'll tack them on to the end of the question in case they are illuminating.
    – Timtro
    Commented Sep 5, 2015 at 1:52
  • @RubberDuck I didn't know about it! I'll check it out, but I'm hopeful that I may get a general answer here that may help others (but maybe not). Thanks so much for telling me about Code Review, though. I'm sure I'll be bugging people soon there too!
    – Timtro
    Commented Sep 5, 2015 at 1:56
  • 1
    You're welcome @Timtro. Just be sure to stop by the help center first. The TL:DR; version is that the code must be real code, work as intended, and be your code. I can't help but think betters answers will happen by looking at the current implementation details.
    – RubberDuck
    Commented Sep 5, 2015 at 1:59

1 Answer 1

5

Six of the variables are the state space for a dynamical system. One of the methods is an Euler integrator that use the equations of motion (and control) to project a state space trajectory based on initial conditions (input). Another method will compute a set of Lagrange multipliers based on that trajectory. Another method will compute a gradient vector that depends on the trajectory and the Lagrange multipliers. Method 3 will repeatedly call those functions to solve an optimization problem. All of these things (including the gradient) are valarray which are N elements long, with N somewhere between 20 and 200 in normal operation.

I do not know the internals of your class, but this description sounds a lot like different responsibilites, which could be separated to different classes, each one with its own public interface. The fact some of them operate on the same data does not make it mandatory to put all of them into the same class - noone forbids you to design classes taking a reference to the data of another class if that helps you to get things in a better shape. The key to make this work is as always to get the different abstractions right (and in C++: by having a clear plan for ownership of the data).

However, if you really think you are not able to design this program in more separated fashion, may be for the sake of performance, then it may be a tradeoff between different design goals - performance vs. the "do not test private functions" mantra. Actually, I would not hesitate to question the latter in such a situation a bit. Sometimes adding "maintenance hatches" to your classes can help - public helper methods, which will allow you to access some internals/inner workings only for testing purposes.

3
  • You're not wrong, and I'm not opposed to the idea of splitting this up. My hesitation is in knowing the mathematical relationships that underpin the computation and the introduction of overhead. But I am much more concerned with making testable code than I am with premature optimization. If I were to split this up, what might be the most efficient way to give resulting fragments access to the data? I would be tempted to make the smaller classes as friend classes and when they are created within HighPerformingDoohicky, passing them a this pointer to get at the data. Thoughts?
    – Timtro
    Commented Sep 5, 2015 at 16:06
  • I also think you have a really good point about "maintenance hatches". I think I am trying WAY to hard to protect the data. I would still want users to be careful with calling the methods carelessly. What do you think of naming them so as to make it clear that they aren't to be used casually. Perhaps with underscore pre/postfixes.
    – Timtro
    Commented Sep 5, 2015 at 16:13
  • 1
    @Timtro: Using friend classes will undermine the benefits of separate classes, I would advise against it. For further details, better ask on Code Review.
    – Doc Brown
    Commented Sep 5, 2015 at 20:07

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.