I think my Table class may be doing too much. I feel as though I should perhaps create a separate Input class that deals with editing the table's rows.
My main problem is knowing when to create a new class. Is it true that the table is doing too much? Which things should be made into other classes, and why?
Btw, Step is a class that generates rows with correct data. It generates a dict.
The application outputs a table like this:
Step Danger Battle Input 1 250 13 113 0 19200 74 False Empty 2 252 13 113 113 55552 216 False Empty 3 254 13 113 226 41984 163 False Empty 4 0 26 113 339 38912 151 False Empty 5 2 26 113 452 54528 212 False Empty 6 4 26 113 565 16640 64 False Empty 7 6 26 113 678 5376 20 False Empty 8 8 26 113 791 48384 188 False Empty 9 10 26 113 904 45824 178 False Empty 10 12 26 113 1017 30464 118 False Empty
The rows of the table represent each step a player takes in a game. This is not my game, I have reverse-engineered the mechanics of a popular console game (Step object generates the correct stats).
So, the table will represent the path taken by someone playing the game; as such they will need to add their input to the table. Adding input changes the subsequent values in the table. The table then acts as a record, or a route-planner, for what the user will do when playing the game on a console.
The below code is all from Table class:
from row_maker_inlined import Step
from math import trunc
class Table(object):
''' A table of steps. '''
def __init__(self, dist, start):
''' Accepts a dict of starting stats. '''
self._dist = dist
self.maker = Step(start)
self._rows = [self.maker.step]
self._input_history = {}
self._position = None
self._adj_dist = dist
@property
def rows(self):
return self._rows
@property
def dist(self):
return self._dist
@property
def adj_dist(self):
return self._adj_dist
@property
def input_history(self):
return self._input_history
def makeNext(self):
''' Returns the next row. '''
self.maker.next()
return self.maker.step
def setMaker(self, stats):
''' Sets the maker generator to position n. '''
self.maker.step = stats
def build(self):
''' Builds the entire table. '''
while len(self._rows) < self._adj_dist:
self._rows.append(self.makeNext())
def show(self):
''' Displays the table. '''
print 'Table:\n'
for i in self._rows:
print '{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}\t{7}\t{8}'.format(
i['n'],i['step'],i['o'],i['i'],i['dng'],i['lmt'],i['r'],i['enc'],i['inp'])
The Table accepts input. Here are the methods that deal with input. I keep wondering if a separate Editor or Input class would be better for handling this? And then the Table could have an Input object within it.
def input(self, input, pos):
''' Checks input is valid before adding. '''
if input == 'G' and not self._rows[pos]['enc']:
print '*** Cannot add', input, 'to this row:'
print self._rows[pos]['n'], '\n'
elif (input == 'Empty') and (self._rows[pos]['inp'] != 'Empty'):
self._position = pos
self.clearInput(input, pos)
self.update()
elif (input == 'W') and (not self._rows[pos]['enc']):
self._position = pos
self.addInput(input, pos)
self.update()
else:
self._position = pos
self.addInput(input, pos)
self.update()
def addInput(self, input, position):
''' Accepts an input and an index position. Sets the input at rows[position]. '''
self._rows[position]['inp'] = input
self.addToHistory(input, position)
def clearInput(self, input, position):
''' Clears the input from table[position] to 'Empty'. '''
modified = self._rows[position]
if modified['inp'] != 'Empty':
self._rows[position]['inp'] = 'Empty'
self.removeFromHistory(modified['n'])
else:
print '*** Input is already empty:', modified['inp'], '\n'
def addToHistory(self, input, position):
''' Adds the most recent input to the history. '''
modified = self._rows[position]
self._input_history[modified['n']] = input
def removeFromHistory(self, row_key):
''' Removes input from input history. Row key is 'n' value . '''
del self.input_history[row_key]
def deleteOldInput(self):
''' Deletes input that has been removed due to table resize. '''
last_row_num = self._rows[-1]['n']
# Add old input to list.
old_input = [i for i in self.input_history.iterkeys() if i > last_row_num]
# Iterate through deletion list and delete from input_history.
for num in old_input:
self.removeFromHistory(num)
print '*** Removed old input from input_history.'
This section deal with updating the Table. If, for example, I add input to row 1, this will have an affect on all of the rows below it. The following methods deal with updating or refreshing the table:
def calcWalks(self):
''' Returns the number of walks in the input table. '''
walks = 0
for key, val in self.input_history.iteritems():
if val == 'W':
walks += 1
return walks
def calcDist(self):
''' Returns the adjusted distance that includes walks. '''
# TODO: Add stutter calculations for adding.
return self._dist + trunc(self.calcWalks() * 0.25)
The Table grows or reduces depending on input too:
def checkTableLength(self, length):
''' Checks table size and adds/removes rows. '''
# Table is too short:
if length < self.adj_dist:
# The new row is based on the last row in the table.
self.setMaker(self._rows[-1])
self.build()
print '*** Table extended.'
# Table is too long:
elif length > self.adj_dist:
# The amount of rows to remove.
extra = length - self.adj_dist
self._rows = self._rows[:-abs(extra)]
print '*** Table reduced.'
# Position should point to the last row, so others do not update.
self._position = len(self._rows) - 1
# Keep input history up-to-date by removing input that no longer exists.
self.deleteOldInput()
def update(self):
''' Updates all table data. '''
# Update field distance based on the number of walks.
self._adj_dist = self.calcDist()
length = len(self._rows)
if length != self.adj_dist:
self.checkTableLength(length)
self.updateRows()
def updateRows(self):
''' Recalculates and updates, danger, enc, and input. '''
self.setMaker(self._rows[self._position])
# Update the rows that require updating; starting from the modified row.
for row in self._rows[self._position:]:
updated = self.maker.step
row['dng'] = updated['dng']
row['enc'] = updated['enc']
# Remove redundant input:
if row['inp'] == 'G' and not row['enc']:
row['inp'] = 'Empty'
self.removeFromHistory((row['n']))
# Advance the row maker, in order to generate the next row.
self.maker.next()
def getEncs(self):
''' Returns a list of encounters. '''
return [(i['n'] - 1, i['inp']) for i in self._rows if i['enc']]
My main problem is with the concept of single responsibility. The Table is responsible for almost everything; accepting input, validating input, resizing, editing row values, etc.
What should the Table be responsible for? How can I know? Because the Table is the center of the program, it seems it should be responsible for everything...
I need to get out of the mindset of creating one or two classes with tons of methods, but I don't know how.
It would be very helpful if someone could point out what could be made into new classes. And how those objects will be structured, eg. A Table has an editor object that accepts and is responsible for input.
I really want to learn good OOP design, however that is difficult; programming is a hobby for me, self-taught, no formal teaching.