2

Consider this Java code:

int rowIndex=8;
int offset=0;
libFuncCall.populate("B"+rowIndex+offset, data);

Here I mistakenly missed the fact that, resulting argument is a String though I had rowIndex and offset both integers.
The actual code would have been:

libFuncCall.populate("B"+(int)(rowIndex+offset), data);

Until I logged it, before finding:

log.debug("B"+rowIndex+offset); // It was B80 instead of B8

In my case, first input argument was a cell in Excel spreadsheet. It took me days, to find this bug.

In which code smell catagory would it fall and how should I avoid one?

asked May 22, 2023 at 10:37
3
  • 3
    It is not a code smell, just a misinterpretation of a single piece of code. A code smell is a bad trend in a collection of code files, typically a software product.I would say this is a mistake on the language designers' part. In an attempt to make things easier and better looking they made it more dangerous. Commented May 22, 2023 at 13:58
  • 1
    if this method is the only way to access fields, then it is a code smell - since you address a cell in excel by row & column - not by cellID... even if they look very similar (see #3 on my answer). The pure fact, that the OP did loose some hours on finding the issue is prove for itself. (see #1 on my answer, the interface is not simple enough) - on the other hand, if there are more way to populate cells and the OP did use a misleading one, well... then it would be no code smell... i think we don't have enough data yet to know for sure (+1 for that comment) Commented May 23, 2023 at 5:22
  • Yup. I wanted to keep things easy and raw (revisit-refactor later). As I was providing quickfix and kept the "using library provided Cell object+look more into library provided getCellValueByCellName()" as technical-dept. Also I was trying to have cell cordinates abstracted away by simple String cell names(e.g. B8) in my library-agnostic wrapper class. It backfired. Commented May 23, 2023 at 9:19

4 Answers 4

4

violates KISS:

you have to create an Cell ID where you could provide coordinates - coordinates are way simpler

violates principle of least astonishment

surprise: this cell id is an aggregated key

violates implementation reflects design

cell coordinates are used by design - an cell id is not was specified

of course

the proper method signature would have been libFuncCall.populate(String column, int row)

answered May 22, 2023 at 13:27
2
  • 4
    I think one should add that in case ´libFuncCall.populate` is a 3rd party lib with a fixed API, a solution would be to create a wrapper for that call which requires coordinates and use that wrapper exclusively. Commented May 23, 2023 at 10:58
  • that sounds reasonable @DocBrown! Commented May 23, 2023 at 12:57
2

In addition to @MartinFrank I*d like to add: primitive obsession

While the code smell focuses on types I find it in the way you construct the string.

Whenever I have to combine a literal string with dynamic data I always use String.format() This would change your code to:

libFuncCall.populate(String.format("B%d",rawIndex+offset), data); 

leaving no room for the mistake you made.

answered May 23, 2023 at 21:40
2
  • yes, very very right (+1) Commented May 24, 2023 at 9:00
  • 5
    Using String.format doesn't solve the "primitive obsession", it just works around it - you have to remember to do that each time. Using a specific type encapsulates that logic, so that the easy thing to do is correct: you "fall into the pit of success". So the final code would be libFuncCall.populate(new CellId("B", rawIndex+offset), data);, with the CellId class responsible for calling String.format when you need to interact with a lower layer that expects the identifier as a string. Commented May 24, 2023 at 10:47
2

You're breaking Curly's law. Do only one thing.

Every piece of code you write should do only one thing.

"B"+rawIndex+offset

You want this piece of code to do three things: Add two integers, convert the result of that to a string, concatenate two strings.

It does two of those things, converting to string and concatenating.

You avoid this by being explicit.

int rawIndex=8;
int offset=0;
Integer cellRowNumber = rawIndex + offset;
String cellAddress = "B" + cellRowNumber.toString(); 
libFuncCall.populate(cellAddress , data);
answered May 24, 2023 at 9:44
4
  • Obvious question: How do we know that "B" is a cell column? Commented May 24, 2023 at 17:17
  • @PeterM: The thing is that, I was completely missing the fact that I was in fact dealing with strings rather than integers, thought some java compile mechanism should notify/warn me beforehand. For a moment I thought, "I can only expect this in dynamically-typed python or javascript". Now I know, I could get away with having/using Cell object of/in API. Commented May 24, 2023 at 19:17
  • 1
    @PeterM it should be even better for cellAddress not be a string at all, but a structure representing an actual location: something like: cellAddress.RowNumber, cellAddress.ColumnLetters . If you want to get to the cell later on you have to break it apart later anyway, so best to not join it in a string in the first place. Commented May 25, 2023 at 6:37
  • @PieterB I agree that a structure/class would be a better approach. But I think was being too subtle in my comment. I was trying to imply that at the very least you should define String cellColumn = "B";, and then String cellAddress = cellColumn + cellRowNumber.toString(); or even String cell Address = makeCellAddress(cellColumn, cellRowNumber); Commented May 25, 2023 at 13:10
2

I think the fundamental issue here is primitive obsession, or stringly typed code: your libFuncCall.populate method lists its input as a string, and leaves the caller responsible for formatting that string correctly. Presumably, other methods also need a similar cell reference; and other places in the code generate one.

That leaves a number of responsibilities scattered around the code:

  • Gluing together row and column references (currently the responsibility of the caller)
  • Validating that the string is a sane reference (presumably libFuncCall.populate would need to throw or propagate an exception if I passed "hello, world" as the argument)
  • Splitting the string back up again if needed (and related tasks such as incrementing to get the next cell in the same column, or the same row)

Using a separate row and column parameter (as Martin Frank suggests) would solve the immediate problem, but still leaves these responsibilities scattered: libFuncCall.populate has to recognise that "hello, world" isn't a valid column reference, and know how to combine the two arguments as a string if lower-level functions expect one.

All of these can be encapsulated in a type such as CellReference:

  • If you construct an instance from separate row and column parameters, you can't make the mistake shown in the question
  • The constructor can immediately validate those, so libFuncCall.populate can trust the value ("make invalid states unrepresentable")
  • The CellReference instance can have separate accessors for column and row, or for a combined string, depending on what the ultimate low-level code actually needs
  • The signature of libFuncCall.populate immediately makes clear what it needs, rather than needing additional comments describing the expected format of the string argument

So the final code would look like:

int rawIndex=8;
int offset=0;
libFuncCall.populate(new CellReference("B", rawIndex+offset), data);

A typo of new CellReference("B"+rawIndex+offset) would be a compile error (no single-argument constructor); a typo of new CellReference("B"+rawIndex, offset) would be an immediate run-time error ("B0" is not a valid column reference).

answered May 24, 2023 at 11:07
1
  • new CellReference("B", rawIndex+offset). That sounds reasonable rather than specifying coordinates. Commented May 24, 2023 at 19:42

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.