I have some raw data I need to do many things to (shift it, rotate it, scale it along certain axis, rotate it to a final position) and I am not sure what the best way to do this to maintain code readability. On one hand, I can make a single method with many parameters (10+) to do what I need, but this is a code reading nightmare. On the other hand, I could make multiple methods with 1-3 parameters each, but these methods would need to be called in a very specific order to get the correct result. I have read that it is best for methods to do one thing and do it well, but it seems like having many methods that need to be called in order opens up code for hard-to-find bugs.
Is there a programming paradigm I could use that would minimize bugs and make code easier to read?
5
Beware of temporal coupling. However, this is not always an issue.
If you must perform steps in order, it follows that step 1 produces some object required for step 2 (e.g. a file stream or other data structure). This alone requires that the second function must be called after the first, it is not even possible to call them in the wrong order accidentally.
By splitting your functionality up into bite-sized pieces, each part is easier to understand, and definitely easier to test in isolation. If you have a huge 100 line function and something in the middle breaks, how does your failed test tell you what is wrong? If one of your five line methods breaks, your failed unit test directs you immediately toward the one piece of code that needs attention.
This is how complex code should look:
public List<Widget> process(File file) throws IOException {
try (BufferedReader in = new BufferedReader(new FileReader(file))) {
List<Widget> widgets = new LinkedList<>();
String line;
while ((line = in.readLine()) != null) {
if (isApplicable(line)) { // Filter blank lines, comments, etc.
Ore o = preprocess(line);
Ingot i = smelt(o);
Alloy a = combine(i, new Nonmetal('C'));
Widget w = smith(a);
widgets.add(w);
}
}
return widgets;
}
}
At any point during the process of converting raw data into a finished widget, each function returns something required by the next step in the process. One cannot form an alloy from slag, one must smelt (purify) it first. One may not create a widget without the proper allow (e.g. steel) as input.
The specific details of each step are contained in individual functions that can be tested: rather than unit testing the entire process of mining rocks and creating widgets, test each specific step. Now you have an easy way of ensuring that if your “create widget” process fails, you can narrow down the specific reason.
Aside from the benefits of testing and proving correctness, writing code this way is far easier to read. Nobody can understand a huge parameter list. Break it down into small pieces, and show what each little piece means: that is grokkable.
1
The “must be executed in order” argument is moot since pretty much all of your code must be executed in the correct order. After all, you can’t write to a file, then open it then close it, can you?
You should concentrate on what makes your code the most maintainable. This will usually mean writing functions that are small and easily understood. Each function should have a single purpose and not have side effects that are unanticipated.
I would create an »ImageProcesssor« (or whatever name suits your project) and a configuration object ProcessConfiguration, which holds all necessary parameters.
ImageProcessor p = new ImageProcessor();
ProcessConfiguration config = new processConfiguration().setTranslateX(100)
.setTranslateY(100)
.setRotationAngle(45);
p.process(image, config);
Inside the imageprocessor you encapsulate the whole process behind one mehtod process()
public class ImageProcessor {
public Image process(Image i, ProcessConfiguration c){
Image processedImage=i.getCopy();
shift(processedImage, c);
rotate(processedImage, c);
return processedImage;
}
private void rotate(Image i, ProcessConfiguration c) {
//rotate
}
private void shift(Image i, ProcessConfiguration c) {
//shift
}
}
This method calls the transformational methods in the correct order shift()
, rotate()
.
Each method gets appropriate parameters from the passed ProcessConfiguration.
public class ProcessConfiguration {
private int translateX;
private int rotationAngle;
public int getRotationAngle() {
return rotationAngle;
}
public ProcessConfiguration setRotationAngle(int rotationAngle){
this.rotationAngle=rotationAngle;
return this;
}
public int getTranslateY() {
return translateY;
}
public ProcessConfiguration setTranslateY(int translateY) {
this.translateY = translateY;
return this;
}
public int getTranslateX() {
return translateX;
}
public ProcessConfiguration setTranslateX(int translateX) {
this.translateX = translateX;
return this;
}
private int translateY;
}
I used fluid interfaces
public ProcessConfiguration setRotationAngle(int rotationAngle){
this.rotationAngle=rotationAngle;
return this;
}
which allows nifty initialization (as seen above).
The obvious advantage, encapsulating necessary parameters in one object.
Your method signatures become readable:
private void shift(Image i, ProcessConfiguration c)
It is about shifting an image and detailed parameters are somehow configured.
Alternatively, you could create a ProcessingPipeline:
public class ProcessingPipeLine {
Image i;
public ProcessingPipeLine(Image i){
this.i=i;
};
public ProcessingPipeLine shift(Coordinates c){
shiftImage(c);
return this;
}
public ProcessingPipeLine rotate(int a){
rotateImage(a);
return this;
}
public Image getResultingImage(){
return i;
}
private void rotateImage(int angle) {
//shift
}
private void shiftImage(Coordinates c) {
//shift
}
}
A method call to a method processImage
would instantiate such a pipeline and make transparent what and in which order is done: shift, rotate
public Image processImage(Image i, ProcessConfiguration c){
Image processedImage=i.getCopy();
processedImage=new ProcessingPipeLine(processedImage)
.shift(c.getCoordinates())
.rotate(c.getRotationAngle())
.getResultingImage();
return processedImage;
}
Have you considered using some kind of currying? Imagine that you have a class Processee
and a class Processor
:
class Processor
{
private final Processee _processee;
public Processor(Processee p)
{
_processee = p;
}
public void process(T1 a1, T2 a2)
{
// Process using a1
// then process using a2
}
}
Now you can replace class Processor
by two classes Processor1
and Processor2
:
class Processor1
{
private final Processee _processee;
public Processor1(Processee p)
{
_processee = p;
}
public Processor2 process(T1 a1)
{
// Process using argument a1
return new Processor2(_processee);
}
}
class Processor2
{
private final Processee _processee;
public Processor(Processee p)
{
_processee = p;
}
public void process(T2 a2)
{
// Process using argument a2
}
}
You can then call the operations in the right order using:
new Processor1(processee).process(a1).process(a2);
You can apply this pattern multiple times if you have more than two
parameters. You can also group the arguments as you want, i.e. you do not
need to have each process
method take exactly one argument.
4