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