It's easy to miss a higher level concept in an app
… and check why 5600+ Rails engineers read also this
It’s easy to miss a higher level concept in an app
Just yesterday, I finished reading Understanding the Four Rules of Simple Design written by Corey Haines. I definitely enjoyed reading it. The examples are small, understandable and a good starting point for the small refactorings that follow. It’s a short, inexpensive book, but dense with compressed knowledge, and I can only recommend buying it. You can read it in a few hours, and contemplate it for much longer.
One of the examples inspired me to write this blogpost about higher level concepts. What does that even mean? Especially in terms of programming?
Location
First, let’s have a look at this example:
Now you know a bit of the story related to the example from the book.
class Location
attr_reader :x, :y
def neighbors
# calculate a list of locations
# that are considered neighbors
end
end
I looked at it and I was like: hey, how would that even work?…
This is not the right place for this method. It should be in ...
I won’t tell
you yet, because I quickly realized that it could actually work…
This Location
is basically just a Value Object
and it could return all neighbour locations as value objects as well. Let’s try to implement that.
class Location
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
end
def neighbors
return enum_for(:neighbors) unless block_given?
(-1..1).each do |x_axis|
(-1..1).each do |y_axis|
next if x_axis == 0 and y_axis == 0
yield Location.new(x + x_axis, y + y_axis)
end
end
end
end
And you could use it like:
Location.new(0, 0).neighbors.to_a
# => [#<Location:0x00000000fde8a0 @x=-1, @y=-1>, #<Location:0x00000000fde878 @x=-1, @y=0>, #<Location:0x00000000fde850 @x=-1, @y=1>,
# #<Location:0x00000000fde828 @x=0, @y=-1>, #<Location:0x00000000fde7d8 @x=0, @y=1>, #<Location:0x00000000fde7b0 @x=1, @y=-1>,
# #<Location:0x00000000fde788 @x=1, @y=0>, #<Location:0x00000000fde760 @x=1, @y=1>]
This makes sense if you assume an infinite, 2-dimensional Map of square cells, which is the default for Conway game of life. But whenever you say Game to me, first thing I think about is Civilization 5 and its hexagonal Map.
When you say Game to me, especially board game, I think players, maps, movements, rules. Not all of those things make sense for Conway game of life because it is a zero-player game but I think the intuition of Game still applies.
When discussing a different code example in the book, Corey reverses
the dependency between two objects. Instead of Cell
knowing its location
class Cell
attr_reader :location
end
Now the location can know what cell is on it, thus becoming a Coordinate
:
class Coordinate
attr_reader :x, :y
attr_reader :cell
end
So when I think about location neighbors I start to wonder:
- should
Location
know about its neighbors, or - should something else know what other locations are neighbors of a given location?
That something would be probably be a
Map
for me.
What is the better dependency direction here?
Did we miss the concept of Map
perhaps? Could we gain something by adding
it? Would it be more or less intention-revealing? These are good questions
to ask.
I think games are particularly hard to implement right because there are many rules and behaviors that often require knowledge about pretty much anything else that’s happening in the Game. Corey explains it nicely at the beginning of the book when he talks about the better design concept.
Month
A few months ago I wrote a blogpost that shows how to implement a custom YearMonth
class in Ruby that would work with ruby Range.
Basically YearMonth
knows how to compute its successor, the next YearMonth. It also works nicely with iterating
and comparison.
range = YearMonth.new(2014, 1)..YearMonth.new(2014, 3)
# => #<struct YearMonth year=2014, month=1>..#<struct YearMonth year=2014, month=3>
range.each {|ym| puts ym.inspect }
# #<struct YearMonth year=2014, month=1>
# #<struct YearMonth year=2014, month=2>
# #<struct YearMonth year=2014, month=3>
YearMonth.new(2014, 1) <=> YearMonth.new(2014, 3)
# => -1
YearMonth.new(2014, 1) <=> YearMonth.new(2014, 1)
# => 0
YearMonth.new(2014, 3) <=> YearMonth.new(2014, 1)
# => 1
It was pointed out, however, that I was missing a higher level concept: a Calendar
.
Days, Weeks, Months, and Years don’t exist in a vacuum, but are parts of something
bigger: passing time, which we follow by using a Calendar.
And I agree. There are even different kind of calendars in use. I am not sure yet
if I have an intuition how to design a good Calendar
class with a useful API.
And how would I do that so that all chunks of knowledge don’t land in the Calendar
class,
but only in one proper place?
Employee
I was once writing an application for managing employees’ holidays. In Europe you are allowed a certain number of free days depending on how long have you been working, both in your life as a whole and for that particular employer (and other factors as well).
The application that I was working on was meant to be deployed separately to every company. So some of the queries that I wrote were executed across entire sets in database, without scoping per company because the data was meant to be for that one company. That made the code easier in some places, because I could query things globally when verifying the correctness of some business rules.
One pivot later, the product was an SaaS intended to be deployed in one place but to support multiple companies. The concept that I was reluctant to introduce immediately became necessary. Everything had to be scoped per company for each employee currently using the system. It was a multi-tenant application.
Employees don’t live in a vacuum either. There are parts of a company hiring them. I was writing
software for helping companies manage holidays for their employees and there was no concept
of the Company
anywhere in the code. That was the higher-level concept I was missing.