Archive

Posts Tagged ‘development’

Spring Python i wprowadzenie do IoC (Inversion of Control)

December 22nd, 2009 Comments off

Jednym z feature’ów Spring Pythona jest kontener IoC, Inversion of Control, poniższy artykuł przedstawia konfigurację kontenera przy wykorzystaniu Spring Pythona w wersji 1.0.

Do czego może służyć Inversion of Control? Oczywiście do odwrócenia kontroli! :-) Cały trick polega na tym, żeby istniał pewien byt – właśnie kontener IoC – który zarządza zależnościami pomiędzy modułami aplikacji. Kontener taki bywa również określany mianem kontekstu aplikacji.

Klasy Pythona są bardzo dobre, moduły są dobre, pakiety, funkcje i metody także są dobre, kontener jednak pozwala w dodatkowy sposób podzielić aplikację na moduły, niezależnie od tego czy poszczególne moduły są pojedynczymi klasami, instancjami, funkcjami czy może po prostu stringami lub integerami.

Na czym polega odwrócenie? Na tym, że moduły aplikacji (nazywane dalej obiektami kontenera, kontekstu) nie tworzą bezpośrednio zależności pomiędzy sobą, np. nie importują siebie nawzajem. Pisząc więc klasę reprezentującą klienta, który wywołuje zdalną usługę serwera i jednocześnie uwierzytelnia się przed tym serwerem, nie importujemy w klasie klienckiej klasy implementującej uwierzytelnianie, a jedynie korzystamy z tej klasy  uwierzytelniającej – pozwalamy kontenerowi na to, żeby nam w runtimie wstrzyknął sam odpowiednia skonfigurowaną klasę, która zapewni uwierzytelnianie. Ostatecznie, klasa, która ma wywołać usługę serwera nie musi sobie zawracać głowy tym, skąd wziąć i jak skonfigurować uwierzytelnianie, prawda? Jej rolą, zadaniem i zmartwieniem jest wywołanie serwera, a nie związane z tym kwestie poboczne. Jeśli serwer kiedyś zmieni (a zawsze następuje to wcześniej niż można spodziewać się) sposób uwierzytelniania to nie będziemy musieli w ogóle dotykać klasy klienckiej – a ostatecznie celem jest to, żeby dobrze przetestowanego i spełniającego na produkcji swoją biznesową rolę kodu nie zmieniać (w każdym razie, nie zmieniać bez potrzeby).

Kluczowe w idei IoC jest to, że kontener konfigurowany jest deklaratywnie, jest to raczej statyczna konfiguracja niż miejsce, w który można poszaleć z pętlami czy wyjątkami. Ma to taką miłą zaletę, że kontener konfigurujemy z boku całej aplikacji, bez zmian w jej kodzie implementującym logikę biznesową.

Inną cechą kontekstu jest to, że obiekty definiowane w nim mogą być pobierane z niego jako singletone’y lub jako zwykłe instancje klas (tzw. prototypy).

IoC znakomicie też ułatwia testowanie aplikacji, całość składana jest z klocków, więc nie ma problemu z tym, żeby prawdziwe klocki, te, które faktycznie tworzą aplikację, zastępować mockami. Dzięki temu łatwo pisze się unittesty.

W wersji 1.0 Spring Pythona, IoC można skonfigurować korzystając przede wszystkim z dwóch składni:

- XMLConfig – pozwala na konfigurację kontenera w XML-u
- PythonConfig – umożliwiwa konfigurację kontenera w samym Pythonie

Od wersji 1.1 w Spring Pythonie dostępny będzie także YamlConfig, jak wynika z nazwy – IoC będzie można też konfigurować w YAML-u i jest to bardzo wygodne, choć jest to już sprawa na inny artykuł.

W dalszej części będę używał PythonConfiga, aczkolwiek dokładnie to samo można osiągnąć korzystając z XMLConfiga (a od wersji 1.1 także w YamlConfigu).

W porządku, załóżmy więc, że chcemy napisać aplikację, która wywoła usługę serwera zwracającą kursy walut. Trzeba więc utworzyć prosty serwer, który będzie zwracał kursy dla, powiedzmy, dwóch walut. Przed serwerem tym trzeba będzie też jakoś uwierzytelnić się.

Serwer będzie napisany w Pyro, bez użycia Spring Pythona, a poniżej jest jego kod źródłowy:

# -*- coding: utf-8 -*-

# server.py

import Pyro.core as pyro

class ExchangeRates(pyro.ObjBase):
    """ Minimalny serwer zwracający, dla wybranych walut, kursy wymiany złotego. 
    """
    def get_exchange_rate(self, user, password, currency):
        if user != "foo" and password != "bar":
            return "FORBIDDEN"

        if currency == "EUR":
            return "4.1959"
        elif currency == "CHF":
            return "2.8080"
        else:
            return "UNRECOGNIZED_CURRENCY"

def run_server():
    """ Startuje serwer kursów walut."
    """
    pyro.initServer()
    daemon = pyro.Daemon()
    uri = daemon.connect(ExchangeRates(), "exchange-rates")

    print "Server started, port: %s" % daemon.port

    daemon.requestLoop()

if __name__ == "__main__":
    run_server()

I taki serwer spokojnie nam wystarczy, przejdźmy teraz do klienta, który będzie korzystał ze Spring Pythona. Tak się szczęśliwie składa, że Spring Python wspiera także korzystanie z Pyro, więc z tego też skorzystamy.

Pierwsza wersja klienta:

# -*- coding: utf-8 -*-

# client1.py

from springpython.remoting.pyro import PyroProxyFactory

class Client(object):
    def __init__(self, url, user, password):
        self.url = url
        self.user = user
        self.password = password

        self.proxy = PyroProxyFactory()
        self.proxy.service_url = self.url

    def get_exchange_rate(self, currency):
        return self.proxy.get_exchange_rate(self.user, self.password, currency)

client = Client("PYROLOC://127.0.0.1:7766/exchange-rates", "foo", "bar")
print client.get_exchange_rate("EUR")

Uruchomienie jej zakończy się sukcesem

$ python client1.py
Pyro Client Initialized. Using Pyro V3.7
4.1959
$

Użyjmy teraz IoC do skonfigurowania klienta, na początek przenieśmy tam po prostu wszystkie zmienne, które na pierwszy rzut oka wydają się warte przeniesienia, czyli URL usługi, usera i hasło.

Kontener korzystający ze składni Pythona to zwykła klasa dziedzicząca po springpython.config.PythonConfig. Spring Python zarządza tylko tymi obiektami, które przypisany mają dekorator springpython.config.Object. Pierwsza wersja kontekstu może wyglądać tak:

# -*- coding: utf-8 -*-

# ctx1.py

from springpython.config import Object
from springpython.config import PythonConfig

class ClientContext(PythonConfig):

    @Object
    def url(self):                      
        return "PYROLOC://127.0.0.1:7766/exchange-rates"

    @Object    
    def user(self):
        return "foo"

    @Object
    def password(self):
        return "bar"

a klient korzystający z kontekstu w ctx1.py ma postać:

# -*- coding: utf-8 -*-

# client2.py

from springpython.context import ApplicationContext
from springpython.remoting.pyro import PyroProxyFactory

from ctx1 import ClientContext

class Client(object):
    def __init__(self, url, user, password):
        self.url = url
        self.user = user
        self.password = password

        self.proxy = PyroProxyFactory()
        self.proxy.service_url = self.url

    def get_exchange_rate(self, currency):
        return self.proxy.get_exchange_rate(self.user, self.password, currency)

ctx = ApplicationContext(ClientContext())

url = ctx.get_object("url")
user = ctx.get_object("user")
password = ctx.get_object("password")

client = Client(url, user, password)
print client.get_exchange_rate("EUR")

Tworzymy więc kontekst aplikacji poprzez powołanie nowej instancji klasy springpython.context.ApplicationContext, której przekazujemy instancję naszej konfiguracji z ctx1.py. Do obiektów zarządzanych przez kontekst mamy dostęp przez metodę .get_object(nazwa_obiektu) – ‘nazwa_obiektu’ jest taka sama jak nazwa metody zdefiniowana w ctx1.py, wywołanie tej metody zwróci obiekt z kontenera. Warto zwrócić uwagę na to, że w Spring Pythonie zawsze używamy metody .get_object do pobierania obiektów z kontenera, niezależnie od tego, czy (tak jak tutaj) korzystamy z PythonConfiga, z XMLConfiga czy też – od Spring Pythona 1.1 – z YamlConfiga.

Mamy więc kontekst, w którym trzymana jest podstawowa konfiguracja. Domyślne użycie dekoratora @Object tak jak powyżej oznacza, że dany obiekt jest singletonem. Możliwe jest także podanie explicite, że obiekt ma być singletonem; można także podać, że obiekt ma być prototypem, czyli przy każdym jego pobraniu z użyciem .get_object będzie zwracana jego nowa instancja.

Pójdźmy teraz dalej i zdejmijmy z klasy Client odpowiedzialność za tworzenie proxy do wywoływania serwera – przede wszystkim, klient ma serwer przez proxy wywoływać, a nie pamiętać o tworzeniu proxy (niech się tym zajmie kontekst), po drugie, w przykładzie tworzymy tylko jednego klienta, ale w rzeczywistości instancji klasy Client możemy tworzyć wiele i nie ma sensu tworzyć nowego proxy dla każdej instancji, niech proxy będzie singletonem. Przy okazji przeniesiemy też do kontekstu definicję klienta, i będzie ona prototypem, przy każdym pobraniu klienta z kontekstu będziemy otrzymywali nową instancję klasy Client.

Tak może wyglądać kontekst ctx2.py

# -*- coding: utf-8 -*-

# ctx2.py

from springpython.remoting.pyro import PyroProxyFactory
from springpython.config import scope, Object, PythonConfig

from client3 import Client

class ClientContext(PythonConfig):

    @Object
    def url(self):                      
        return "PYROLOC://127.0.0.1:7766/exchange-rates"

    @Object    
    def user(self):
        return "foo"

    @Object
    def password(self):
        return "bar"

    @Object
    def proxy(self):
        proxy = PyroProxyFactory()
        proxy.service_url = self.url()

        return proxy

    @Object(scope.PROTOTYPE)        
    def client(self):
        client = Client()
        client.proxy = self.proxy()
        client.user = self.user()
        client.password = self.password()

        return client

Z kolei client3.py mocno zmniejszył się

# -*- coding: utf-8 -*-

# client3.py

class Client(object):
    def __init__(self, proxy=None, user=None, password=None):

        self.proxy = proxy
        self.user = user
        self.password = password

    def get_exchange_rate(self, currency):
        return self.proxy.get_exchange_rate(self.user, self.password, currency)

Potrzebujemy teraz czegoś, co uruchomi całość, np. app1.py:

# -*- coding: utf-8 -*-

# app1.py

from springpython.context import ApplicationContext

from ctx2 import ClientContext

ctx = ApplicationContext(ClientContext())

client = ctx.get_object("client")
print client.get_exchange_rate("EUR")

Co teraz osiągnęliśmy? Oddzieliliśmy od siebie komponenty. Klient nie wie -  i dobrze, skoro nie musi tego wiedzieć – skąd bierze się proxy, ważne, że może je wywołać. Nie wie nawet jakiego rodzaju jest to proxy, akurat teraz jest to Pyro, ale kiedyś może być to komunikacja z serwerem przy użyciu innej technologii. Proxy także nie wie skąd bierze się jego konfiguracja, akurat teraz URL jest zapisany bezpośrednio w kontekście, ale kto wie, może za miesiąc będzie pobierany z LDAP-a czy z innego źródła danych – ponownie, nie powinno być zmartwieniem proxy skąd wziąć URL, URL powinien być dla proxy po prostu dostępny, a jego udostępnieniem zajmuje się kontekst. Komponenty zajmują się jedynie wykonywaniem swojej pracy, a nie zadaniami konfiguracyjnymi, nie są ze sobą powiązane zależnościami w kodzie. Można je więc spokojnie rozdzielić, osobno dystrybuować, osobno developować, i nie przejmować się tym w jaki sposób zostaną w przyszłości użyte – ważne jest tylko to, że dobrze spełniają swoje biznesowe zadania.

Sprawdźmy teraz w jaki sposób IoC ułatwia nam napisanie unittestów – nie możemy oczywiście zakładać, że będziemy mieli zawsze dostęp do serwera (zresztą, o to chodzi w unittestach, żeby testować pojedyncze unity, w różnych warunkach izolacji od otoczenia), napiszemy więc fake’owe proxy, który będzie nam symulowało prawdziwy serwer i wstrzykniemy je do kontekstu w miejsce prawdziwego proxy.

# -*- coding: utf-8 -*-

# test_client.py

# stdlib
import unittest
from decimal import Decimal

# Spring Python
from springpython.config import Object
from springpython.context import ApplicationContext

# Nasza aplikacja
from ctx2 import ClientContext

class FakeProxy(object):
    """ Fake'owe proxy, używane w unittestach aplikacji klienckiej.
    """
    def get_exchange_rate(self, user, password, currency):
        return "4.3726"

class TestClientContext(ClientContext):
    """ Kontekst używany do testów aplikacji klienckiej.
    """

    @Object
    def proxy(self):
        return FakeProxy()

class TestClient(unittest.TestCase):

    def setUp(self):
        self.ctx = ApplicationContext(TestClientContext())

    def test_client_exchange_rate_decimal(self):
        client = self.ctx.get_object("client")
        rate = client.get_exchange_rate("EUR")

        # Załóżmy, że nie wiemy jaką dokładnie wartość zwraca serwer; wiemy
        # tylko, że jest to Decimal.
        rate_decimal = Decimal(rate)

        # Jeśli powyżej nie wystąpił wyjątek to znaczy, że wartość zwrócona
        # przez klienta była faktycznie możliwa do konwersji na typ Decimal,
        # czyli test powiódł się.

if __name__ == "__main__":
    unittest.main()

Na czym polega test? Na sprawdzeniu tego, czy klient zwraca wartości możliwe do skonwertowania na typ decimal.Decimal. Nie chcemy oczywiście uruchamiać do tego całego serwera, tworzymy więc subklasę kontekstu, w której metoda ‘proxy’ jest przeciążona, zwraca nasze fake’owe proxy, które w tym prostym przykładzie zwraca zawsze taką samą wartość – docelowo proxy to byłoby o wiele bardziej rozbudowane i symulowałoby faktyczną komunikację z serwerem, zwracałoby więcej kursów i obsługiwałoby uwierzytelnianie – ale dla potrzeb przykładu takie proste proxy w zupełności wystarczy do zilustrowania zasady, że wystarczy utworzyć testowy kontekst przeciążający te metody (czyli zwracający te obiekty), które są nam potrzebne w teście. Ponownie – trzeba zauważyć, że nie zmienialiśmy w ogóle kodu aplikacji, aplikacja jako taka nic nie wie, że tym razem jest poddawana testom i że proxy jest fake’owe.

Mam nadzieję, że to proste wprowadzenie zachęciło Was do korzystania z IoC i Spring Pythona. Nie jest to jednak koniec możliwości, ani Spring Pythona, ani IoC jako sposobu na modelowanie zależności komponentów aplikacji. Więcej informacji o Spring Pythonie można znaleźć tutaj, a dla osób zainteresowanych konfigurowaniem kontekstów w YAML-u jest dokumentacja do zbliżającej się wersji 1.1 Spring Pythona. Spring Python ma także swoje forum, listę mailingową i grupę na LinkedIn, można tam spokojnie wstąpić i zawsze ktoś pomoże lub doradzi :-)

@fourthrealm

Share

Prosty przykład nieklarownego procesu developmentu

September 12th, 2009 No comments

Spędziłem sobie właśnie kilka godzin na szukaniu błędu w kodzie, po czym okazało się, że błąd był w kodzie biblioteki, z której korzystałem, i błąd ten występuje tylko w trunku SVN-owym tejże biblioteki. Oczywiście są dwie szkoły, “kod w trunku może nie kompilować się/nie działać, stabilne są branche” i “cały rozwój jest w branchach, a trunk musi się kompilować/działać”. Osobiście jestem z tej drugiej szkoły, ale to nie ma znaczenia, ważne jest to, żeby dokumentować to, co się wybrało, proces musi być jasny. Wystarczy krótka informacja na stronie projektu w sekcji “For developers” i zaoszczędzi się innym sporo czasu. Jeśli ktoś ma ochotę na inne dobre rady na temat prowadzenia projektów i community, w szczególności opensource’owych, to zachęcam do przeczytania książki The Art of Community, napisanej przez community managera Ubuntu, bardzo pouczająca lektura.

W trakcie szukania błędu przeczytałem przy okazji release notes dla WebSphere MQ 7.0 i rozbawiła mnie informacja na temat JMS-owych bundli OSGi WMQ “The OSGi bundles shipped with WebSphere MQ classes for JMS client are not working.” Krótko i na temat. Nie mamy Pańskiego płaszcza i co nam Pan zrobi. ;-)

@fourthrealm

Share
Categories: Software Tags: , ,