Thursday, February 28, 2013

Abstractions

Writing a decoupled program depends on abstractions. This allows objects to use another object without caring about the details of how it is implemented. When I began my apprenticeship, over a month ago, I didn't fully understand why this is so useful. I understood why a program should have a minimal number of dependencies but couldn't make the logical jump to why abstractions are so important.

One of the first steps in my Tic-Tac-Toe program was building the board class. It was a minimal class and I stored the individual squares as a 1D array. I exposed the squares array as a public, read-only variable. Thus my game class could simply look at the desired square and check if it was empty or get its value. There wasn't an square_empty? method because I had granted access to my array.

class Board
  attr_reader :squares

  def initialize
    @squares = Array.new(9)
  end

  def set_square(index, value)
    @squares[index] = value
  end
end

If I had wanted I could have gotten rid of set_square and replaced attr_reader with attr_accessor and the effect would have been the same. I was having an issue with this code though. My game presented options to the player for squares 1 through 9. My array indexes were 0 through 8. So when I made a move or checked a square, I had to remember to subtract 1 from the square index. This was annoying enough that I modified my set_square method to do that automatically.

class Board
  attr_reader :squares

  def initialize
    @squares = Array.new(9)
  end

  def set_square(index, value)
    @squares[index-1] = value
  end
end

This left me with nagging issue. When I checked if a square was empty, I needed to subtract 1 from the index but not when I made a move. I realized that this inconsistency was pointless and that the only solution was to remove attr_reader. It wasn't doing anything helpful and was in fact making my game harder to work with. So I added the necessary methods to my board to replace the missing functionality and had this.

class Board
  def initialize
    @squares = Array.new(9)
  end

  def set_square(index, value)
    @squares[index-1] = value
  end

  def empty_square?(index)
    @squares[index-1].empty?
  end

  def square(index)
    @squares[index-1]
  end
end

Initially, this felt less efficient. After all, there are more lines of code doing the same functions as before. The immediate benefit I saw was that I no longer had to worry about subtracting 1 from the square index in my game class. The second benefit I didn't notice until later. This third version of my board allowed me to store the squares any way I saw fit. I could create any data structure I wanted for them. I could put them in a linked list, a hash, or even an array of square objects. This is where the usefulness of abstractions became clear to me. No matter how stored the squares now, it would not affect my game class. All I had to do was provide an interface to work with and I could store my squares on a computer at the South Pole and retrieve them over a network if I wanted.

This is a fairly simple example but I think it demonstrates the flexibility that abstractions allow. At the beginning of this project, I didn't understand why the flexibility mattered. I saw it as unnecessarily long code. It was only after a couple redesigns and multiple refactorings that I saw the benefits to me. It allowed me to make changes to how a class worked without affecting other classes that used it. It amazes me that I didn't see how useful this was at first. I guess that's one benefit of doing this apprenticeship.

No comments:

Post a Comment