Od czasu do czasu lubię napisać konsolową grę, która stanowi dla mnie punkt wyjścia do postawienia różnych pytań związanych z samym kodem, podziałami czy też technikami. W przeciwieństwie do większych projektów, praca nad drobnymi programami umożliwia uzyskanie szybszego feedbacku.
Podczas jednej z takich sesji, postawiłem sobie za cel oddzielenie zasadniczego kodu gry (czyli jej logiki) od rzeczy abstrakcyjnych, które nie były bezpośrednio powiązane ze szczegółami gry.
Zgodnie z tym założeniem, wydzieliłem następujące funkcje i klasy o charakterze abstrakcyjnym:
def matrix_find_value(matrix, x):
for row in matrix:
for value in row:
if value == x:
return True
return False
def select(coll, indices):
return [coll[i] for i in indices]
class ParserError(Exception):
pass
def ask(parser, prompt='> '):
while True:
try:
return parser(input(prompt))
except ParserError as e:
print(str(e) + ", try again")
class OneOfNumberParser:
def __init__(self, numbers):
self._numbers = numbers
def __call__(self, s):
try:
value = int(s)
except ValueError:
raise ValueError('Required number')
if value not in self._numbers:
raise ValueError('Not available number')
return value
Największą korzyścią tego kodu jest to, że w przypadku potencjalnych zmian wymagań gry, utworzone funkcje/klasy pozostaną nienaruszone.
Spójrzmy na przykład na funkcję select
. Bez względu na charakter wprowadzonych zmian w grze, funkcja ta cały czas będzie odpowiadać za pobranie wielu wartości z listy po indeksach.
Jedynie w przypadku większych zmian część kodu może okazać się zbędna.
W ostateczności, w przypadku większych zmian w logice gry, część kodu może okazać się co najwyżej niepotrzebna. Istnieje możliwość, że funkcja ask
będzie do usunięcia, gdy docelowa gra zacznie działać w trybie graficznym.
Z drugiej strony można zadać sobie pytanie: czy ask
i OneOfNumberParser
są lepsze od następującej funkcji?
def update(board, symbol):
while True:
try:
value = int(input('> '))
except ValueError:
print('Required number')
continue
if value < 1 or 9 < value:
print('Required is number from 1 to 9')
continue
index = value - 1
if board[index] != '_':
print('The position is taken')
continue
break
board[index] = symbol
Ta funkcja chociaż zawiera szczegóły gry (więc niestety jest podatna na zmiany), to mimo to jest łatwiejsza w zrozumieniu.
Warto zastanowić się nad zasadnością tworzenia abstrakcji.
Z jednej strony abstrakcje pozwalają kontrolować złożoność, pozwalają ukryć wszelkie nieistotne szczegóły, ale z drugiej strony użycie abstrakcji takich jak ask
czy OneOfNumberParser
czynią kod nieco trudniejszy w zrozumieniu.
Gdy nie mamy pewności to lepiej poczekać z odpowiedzią, by dać sobie trochę więcej czasu i dokonać przemyślanego wyboru.
Myślę, że poniższy cytat z tej strony świetnie nawiązuje do tematu abstrakcji:
Don't build technology unless you feel the pain of not having it. It applies to the big, architectural decisions as well as the smaller everyday programming decisions.
– Nathan Marz
Cytat z książki Reguły programowania. Jak pisać lepszy kod również trafnie opisuje to zagadnienie:
Skuteczne programowanie polega na opóźnianiu tego, co nieuniknione.
– Chris Zimmerman