Ucieczka z niewoli konwencji

Jak wiemy, współczesne frameworki webowe, takie jak Django czy (w jeszcze większym stopniu) Ruby on Rails, opierają się na konwencjach nazewniczych. Konwencje te ułatwiają życie programiście. Na przykład w Django tworzy się klasę (model) ORM, w której to klasie nazwy pól odpowiadają nazwom kolumn w tabeli bazy danych.
Tabelę tworzy sobie Django automagicznie właśnie na podstawie zdefiniowanej klasy modelu. Na przykład:

class Product(models.Model):
    price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name='cena')
    discount = models.DecimalField(default=0, max_digits=4,
decimal_places=2, verbose_name='rabat')

Nawet jeśli nie znasz Pythona szybko odczytasz ten kod. Na tym właśnie polega prostota i przejrzystość Django. Ale proszę sobie teraz wyobrazić, że mamy sklep internetowy, w którym pole z ceną już było, a pole z rabatem chcemy teraz dodać.
Oczywiście bardzo prosto jest napisać funkcję, która wyliczy cenę po rabacie. Możemy zapisać ją do bazy jako new_price. I co dalej? Istnieje już kilkaset wprowadzonych do bazy produktów. Istnieje cały system generujący koszyk, podsumowujący zamówienie, zliczający zamówienia poszczególnych klientów etc.
A my sobie teraz dodajemy rabat... Czeka nas sprawdzanie w dziesiątkach tysięcy linii kodu gdzie jeszcze występuje price, żeby zastąpić tę zmienną przez new_price. Koszmar!
O ile byłoby piękniej, gdybyśmy mogli stare ceny mieć w kolumnie old_price, a price by oznaczało cenę po rabacie! Wtedy system by odczytywał jak poprzednio pole price, ale byłaby to już zmienna "zrabatowana" i mielibyśmy pewność, że jest ona prawidłowo uwzględniania przy składaniu zamówienia!
Co zrobić? Przenieść w bazie wszystkie ceny do nowej kolumny? Ryzykowne i nieeleganckie...
Pomogą nam dwie rzeczy. Po pierwsze - przełamanie niewoli konwencji poprzez użycie parametru db_column. Po drugie - tak zwane Property.
Co to jest Property? Nie zawsze jest rzeczą korzystną trzymać wszystkie dane w bazie SQL. Czasem bardziej się opłaca je przeliczać w locie. Taką właśnie mamy sytuację w wyżej wymienionym przykładzie. Skoro w bazie już mamy kilkaset (albo i kilkadziesiąt tysięcy) pozycji, to przecież łatwiej ceny zrabatowane wyliczać niz je pracowicie zapisywac do bazy SQL. No ale my chcemy mieć pole "price" w naszym obiekcie! No i dobrze. Właśnie Property służy do stworzenia takiego pseudopola w modelu, które jest wyliczane, a nie pobierane z bazy.
Najpierw zmienimy nazwę pola price na old_price:

  old_price = models.DecimalField(db_column='price',\
                                             max_digits=8,\ 
                                             decimal_places=2,\ 
                                             verbose_name="Cena bez rabatu")

Proszę zwrócić uwagę, że to pole będzie pobierało "stare" ceny z bazy danych, a to dzięki parametrowi db_column, za pomocą którego mogę wymusić przełamanie konwencji nazewniczej.
Teraz pozostaje skorzystać z Property:

  def _get_price(self):
       if self.discount:
            rabat = self.old_price*self.discount/100
       else:
            rabat = 0
       return self.old_price - rabat
  price = property(_get_price)

I to wszystko. Warto zwrócić uwagę, że dzięki wstawianiu rabat = 0 rozwiązujemy problem "pustego" pola rabatowego dla produktów, które wprowadzone zostały do systemu przed modyfikacją.