Relax, take a step back and start from the business logic

Another year of advent of code and this time I decided to participate. Not only that but I thought it would be a great way to teach myself Ruby.
This means that my train of thought gets interrupted quite often since for every little thing I come up with I have to stop and search how Ruby does it.

Day 3 – Gear Ratios

In today’s challenge the target is to calculate the sum of all numbers that are adjacent, even diagonally, to a symbol. The input looks like this:

467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598..

and a symbol is everything that is not a number or a dot (.).

Head first

Seeing this input combined with the fact that I don’t know Ruby throw me in a crazy rabbit hole where I was searching for two-dimensional arrays at one point and parsing strings at another.

Then I remembered that the adjacent part includes diagonals too so I dropped everything and start thinking of how I will combine numbers from one line with symbols from another.

This is getting big. Should I start smaller? Should I try to approach this in a 2×2 array? Should I do this or that? Chaos!

Start from the business logic

Thankfully after taking a break and drinking some water I realized that my need to answer all my unknowns had taken the best of me and I was viewing things wrong.

It does not matter how the input looks. It is just that, an input. I shouldn’t start from there.
It does not matter what/how Ruby does things. It is just a tool.

What matters is the business logic and in this case its quite simple:

  • Our business entities are Numbers and Symbols.
  • Our business logic dictates that a Number is next to a Symbol if it lies to the area that surrounds it.

Translating this to code:

class Symbol
attr_reader :value, :row, :column
def initialize(value, row, column)
@value = value
@row = row
@column = column
end
end
class Number
attr_reader :value, :row, :starting_column, :ending_column
def initialize(value, row, starting_column, ending_column)
@value = value
@row = row
@starting_column = starting_column
@ending_column = ending_column
end
def is_next_to?(symbol)
return false if (@row <= symbol.row – 2) || (@row >= symbol.row + 2)
return false if (@ending_column <= symbol.column – 2) || (@starting_column >= symbol.column + 2)
true
end
end
view raw gear_ratios.rb hosted with ❤ by GitHub

made things so much simpler:

class GearRatios
def initialize(numbers, symbols)
@numbers = numbers
@symbols = symbols
end
def sum
@numbers.filter { |number| @symbols.any? { |symbol| number.is_next_to? symbol } }
.sum { |number| number.value }
end
end

After writing and testing the business logic all I had left to do was to write the code that will produce our lists for numbers and symbols. In this case it just happens to be a two-dimensional array with string values.

Start from what matters

Being overwhelmed or not, relax, take a step back and start from what matters.

Presentation is important but it shouldn’t drive an approach since it might change often. Input is also important but it shouldn’t matter if we are dealing with a database, a web service or the file system.

Start from the business, make it work and then try to figure out how everything else can be plugged in.

Bring me both your problems and your suggestions

It’s been a little over a year that I have the role of an engineering manager for the first time in my career. Needless to say that I’m still learning. I read the dos and don’ts and try to incorporate them in my everyday work life. Sometimes everything works fine, some others everything goes wrong!

The don’t I keep doing

The one thing that I often find myself doing is providing, immediately, a solution to a problem that was brought to my attention. No “tell me what you think we should do”, no “what approaches have you tried?”, nothing that will spark a dialogue between me and my report. A dialogue that will helps us to figure things out and grow as engineers.

Figuring something on your own, instead of being told about it, has proven to be vital in understanding it better. So, if I want my reports to grow as engineers, I must lead them to a solution rather than give them one. The aforementioned questions must be my first reaction.

The do we can start doing

this part is a message to all my current and future reports

My hope is that writing about this don’t will make me more conscious about this behavior and avoid doing it. But, we are all humans and it will be better to have a safeguard.

So, what I propose is for you to come to me both with a problem and a suggestion as to how we can solve it. This will keep me from offering an answer right away and, most importantly, it will help you grow as an engineer. Forcing yourself to think about a suggestion or to reflect upon the approaches you’ve taken will make you understand the problem and its domain better. That alone will benefit our discussion or, even better, might lead you in a solution without any help!

Let me be a feedback loop for you

(this post was written with the intent to give it to my current, and future, direct reports in an attempt to establish part of our relationship)

Every time we need to solve a problem (or implement a feature) the process is the same

  1. First we think the approach we are going to follow. This is an abstract flow that we believe will solve the problem.
  2. Based on that we divide our approach to one or more components (modules / classes / functions, whatever suits the size of the problem) and assign a certain behavior to each one of them.
  3. Finally, we start implementing each component.

Feedback loop

How do we know if something we implemented is correct?

By getting feedback. Based on it we make improvements and try again. This loop keeps us on track and allows us to deliver something valuable.

This is why we try to have frequent and short feedback loops. This way any corrections occur sooner than later and we don’t spend time and effort into something that might, potentially, thrown away.

Tech lead as a feedback loop

Looking back to the process of solving a problem we could say that two out of the three steps do have a feedback loop to helps us.

In the implementation step we can argue that the compiler / linter / etc gives us immediate feedback on what we wrote. We fix it and move forward.

For the behavior step we get the feedback through tests. Is the component’s behavior the expected one? How about the components API? Can we use it easily in our tests? Answering these question helps in having solid components.

But what about the approach step? This is where the tech lead comes in.

Right before writing any code, thus putting effort into something, prepare a simple list with the steps you are about to take and discuss them with your tech lead. If there is any feedback use it to improve your approach and have another meeting.

Is it time consuming?
No. In case the approach needs improvement after the implementation the time to fix it will be longer.

Do you feel bad on spending your TL’s time?
Don’t. It is, literally, their job to help you.

I think that data classes help in violating the YAGNI principle

Let me ask you something. You are the reviewer in a PR that creates a simple calculator which needs to know how to add numbers. Just that. There are no reasons to make us think that the calculator will need more functionality.

Despite that the PR includes a calculator that can add, subtract and multiply. What do you do as a reviewer?

I want to believe that you will, respectfully, discuss the removal of the extra functionality otherwise the calculator will violate the YAGNI principle and add (a) more code for the developers to maintain and (b) more ways to couple the project with the calculator. And all that with no immediate benefit.

Do we need all that functionality?

The same goes with data classes. There is no reason to have a class that can be uniquely identified by all of its properties if we don’t use its instances this way. There is no reason to have an extra getter for every property if we don’t use, extensively, the destructuring declaration. There is no reason to have a copy mechanism if we never copy instances!

If it is there it is going to be used

I recently removed the data keyword from one of our oldest classes and I noticed that many of our newest tests started to fail in compilation. The compiler could not find the copy method which was used to create dummy values from other dummy values by changing one property per test.

When to create a data class?

Here is my thought process when trying to decide the type of class I’ll use:

Q: Is this class anything but a domain entity or value object?

A: Then a simple class is just fine.

Q: Is this class a domain entity? Meaning that it can be uniquely identified by a subset of its properties (ex: an id)?

A: Then a simple class with an implementation of equals/hashCode will be enough.

Q: Is this a value object? Meaning that it can be uniquely identified by all of its properties?

A: Yes.

Q: How many properties?

A: One. Then a value class is a must.

Q: Are you sure its just one?

A: Turns out its more! We’ll use a data class.

Don’t do it for the test

Our test code is the first consumer of our production code. Changing the production code, in this case change/create a class as data, to write more quickly a couple of tests will result in having tests that can easily break every time the production code changes since the tests know too much about the code’s internals and not its behavior.

Don’t be afraid to throw your work away

There are times, especially in large code bases, that you might be working towards a solution and get stuck because of something that you did not foresee.
That was my case this past week. In order to unblock the development of a new feature I decided to change the API of some classes. The changes would make the integration with the feature much easier and intuitive.

I moved some code, deleted some other, made a few additions and after 3 days I had the API I aimed for. Unfortunately this new API, even though it was great for the new feature, it did not play well with a certain flow. A flow that was not affected by the old API.

In other words by fixing one thing I broke another. So I did the only logical think to do.. I deleted the branch I working on!

Always weigh things

I have to admit that deleting a piece of code that you have worked for hours is not an easy decision. Especially when it looks and behaves as you have designed it. The urge to keep changing things in order to make all flows work is quite strong.

This is where you have to weigh things. Is it worth the effort? Do we have the time to invest? Will the final code be clean, scalable, readable?

In my case the decision to move forward and try to include the broken flow would mean tieing things together (bad code) and also adding a couple more days of work (more time). It wasn’t worth it.

Clean mind

A benefit of throwing a solution is that you can now see the other routes that where there from the start but you were too focused to notice them. Be it that you are no longer occupying your mind with the previous solution’s graph, be it that you have to figure something out, almost always you’ll find another way to tackle things.

In my case the new approach was way simpler and easier. The old API was left untouched and the entire integration was achieved from a different point that up until the deletion I hadn’t given it much attention.

Micro throwing

Throwing implementations is great for small things too like functions or new classes.
Every time I develop one of them, if I start to feel that things are slowing down I don’t think of it much, I just reset --hard and start over (it always helps if you already have a couple of tests to back you up).
Having one route crossed out and knowing, at least some part of the solution, I find the second, third etc implementation to be much faster.

Code review: don’t just request a change

When reviewing a PR I try to elaborate on my proposals for two reasons:

1. Out of respect to my colleague

Every piece of code is part of an effort that took time and thought. Requesting a code change by simply asking it (i don't like it, change it) or by dictating it (do it this way) demotes all the work that has been done.

I figured that, since my colleague spent a few hours to come up with a solution I owe her/him more than a few seconds!

2. I solidify my knowledge or even better, learn something new

By trying to justify the reason behind a request I end up understanding better why I prefer a solution or a format over another.

When I provide an example or when I do a mini investigation to help me clarify why something must change I often find that (a) many things that I thought I knew were not as I had them in mind and (b) I now understand a concept much better because I had to write about it. You don’t master something if you can’t explain it!

PS: never forget that it might say “request a change” but in reality you start a conversation between two professionals that will eventually benefit of the project