Reflexe_03 Používání dynamického proxy
Použití dynamického proxy jak používat java.lang.refrect.proxy použití proxy k implementaci dekorátorů řetězení proxy skryté nebezpečí využívání proxy 2
Úvod Ukrývání umístění cílového objektu před klientem. Intervence(intercession) je jakákoli schopnost reflexe, která modifikuje chování programu přímo převzetím kontroly nad chováním. Vyvolání metod intervence je jediná možnost zachycení (intercepting) zpráv a jejich možné modifikace. 3
Sekvenční diagram typického použití proxy 4
Problém sledovánítříd Scénář: Zákazník volá na podporu SW se záznamem poruchy. Při zapnutém sledování, je možné rychleji a lépe objasnit vzniklý problém. Sledování (tracing) je užitečné, ale velmi náročné na I/O. Proto zapínání/ vypínání sledování. 5
Problém sledovánítříd Zpomalení z důvodu příkazu if on/off. Jedna z možností vytvořit podtřídy od nesledovaných tříd a zastínit (override) každou metodu se sledováním a voláním nadtřídy. Nevýhody: únavné, vykonávání této možnosti je nudné a mechanické náchylné k chybám, křehké, změna signatury metody přináší problémy. 6
Sledování s využitím proxy Dvě důležité úlohy každého proxy jsou implementace rozhraní a delegování. Javovskátřída Proxyprovádí implementaci rozhraní pomocí dynamicky vytvořené třídy, která implementuje množinu daných rozhraní. Dynamické vytvoření třídy je provedeno třídní metodou getproxyclass()a tovární metodou newproxyinstance(). 7
Terminologie Každá třída konstruovaná těmito továrními metodami je public final podtřída od Proxy, které říkáme třída proxy (proxy class). Instancím od těchto dynamicky tvořených proxiesříkáme instance proxy(proxy instance). Rozhraní, která třída proxy implementuje říkáme rozhraní proxy (proxied interfaces). 8
public class Proxy implements java.io.serializable {... public static Class getproxyclass(classloader loader, Class[] interfaces) throws IllegalArgumentException... public static Object newproxyinstance(classloader loader, Class[] interfaces, InvocationHandler h ) throws IllegalArgumentException... public static boolean isproxyclass(class c1)... public static InvocationHandler getinvocationhandler(object proxy) throws IllegalArgumentException... getproxyclass() dodá třídu proxy, specifikovanou class loadrema polem rozhraní. Pokud taková třída proxy neexistuje, vytvoří se dynamicky. Protože každý javovský objekt je asociován s loadrem třídy (class loadrem), proto getproxyclass() musí mít parametr tohoto loaderu. Jméno každé třídy proxy začíná s $Proxy následované číslem (pořadovým). 9
public class Proxy implements java.io.serializable {... public static Class getproxyclass(classloader loader, Class[] interfaces) throws IllegalArgumentException... public static Object newproxyinstance(classloader loader, Class[] interfaces, InvocationHandler h ) throws IllegalArgumentException... public static boolean isproxyclass(class c1)... public static InvocationHandler getinvocationhandler(object proxy) throws IllegalArgumentException... Všechny třídy proxy mají konstruktor, s parametrem InvocationHandler. InvocationHandler je rozhraní pro objekty, které pracují s metodami, které získali od instance proxy, prostřednictvím jejich proxied rozhraní. Kombinace getconstructora newinstancemůže být použita ke konstrukci instancí proxy. 10
Proxy c1 = getproxyclass( SomeInterface.getLoader(), Class[] {SomeInterface.class ); Constructor cons = c1.getconstructor(new Class[] {InvocationHandler.class); SomeSH je třída která implementuje InvocationHandler. Object proxy = cons.newinstance(new Object[] {new SomeIH(obj ) ); Object proxy = Proxy.newProxyInstance(SomeInterface.getClassLoader(), Class[] {SomeInterface.class, new SomeIH(obj) ); alternativa 11
isproxyclass(), getinvocationhandler() Proxy.isProxyClass(obj.getClass()); zjistí, zda obj odkazuje na instanci proxy. Proxy.getInvocationHandler(p); vrátí InvocationHandler, který byl použit ke konstrukci p. 12
Invocationhandlers Proxyumožňuje programátorům uskutečnit úlohu delegováníprostřednictvím rozhraní InvocationHandleru. Instance invocationhandlersjsou objekty, které zvládají každé volání metody instance proxy. Invocationhandlersjsou také zodpovědniza udržování odkazů na cílový objekt instance proxy. 13
public interface InvocationHandler { public Object invoke( Object proxy, Method method, Object[] args) throws Throwable; Rozhraní InvocationHandler 14
Invocationhandlers Instance proxy předává dál volání metody na její invocation handler pomocí volání invoke(). Původní argumenty pro volání metody jsou předány metodě invoke() jako pole objektů. Navíc proxy instance poskytuje odkaz sama na sebe a na objekt Method vyvolávané metody. Předané parametry metodě invoke() jsou přesně ty objekty, které potřebuje přeposlat reflektivně dalšímu objektu. 15
public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { return method.invoke(target, args); 16
Invocationhandlers Obecnější metoda invoke může provádět prea post- processing parametrů. 17
Sekvenční diagram zpracování 18
Implementace proxy pro sledování Řešení používá invocation handler (vyvolávací ovladač), ve kterém metoda invoke()přeposílá (forward) všechna volání metod cílovému objektu. Toto přeposílání je snadno provedeno s metodou invoke() od metatřídy Method. Řešení zahrnuje vytvoření proxy a invokačního ovladače. 19
Implementace proxy pro sledování Třídní (statická) metoda createproxy() je předána cílovému objektu, který vše introspektivně prověří a vytvoří odpovídající proxy a invokační handler. 20
import java.lang.reflect.*; import java.io.printwriter; public class TracingIH implements InvocationHandler { public static Object createproxy( Object obj, PrintWriter out ) { return Proxy.newProxyInstance( obj.getclass().getclassloader(), obj.getclass().getinterfaces(), new TracingIH( obj, out ) ); private Object target; private PrintWriter out; private TracingIH( Object obj, PrintWriter out ) { target = obj; this.out = out; public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { Object result = null; try { out.println( method.getname() + "(...) called" ); result = method.invoke( target, args ); catch (InvocationTargetException e) { out.println( method.getname() + " throws " + e.getcause() ); throw e.getcause(); Implementace řešení je v createproxy(). Tato statická tovární metodeazabalí svůj argument do proxy, které vykoná sledování. 1. prověří objekt argumentu createproxy() na přímá rozhraní, které třída implementuje. Posílá pole rozhraní do Proxy.newInstance, která vytvoří proxy třídu pro tato rozhraní. 2. je vytvořen TracingIHs aggumentemjako cílovám objektem. 3. je vráceno nové proxy, které přeposílá svoje volání na TracingIH. Toto proxy implementuje všechna rozhraní cílového objektu a je typově kompatibilní. 21
out.println( method.getname() + " returns" ); return result; Uvedené řešení místo změny zdrojového kódu umožňuje zabalit objekty s proxy a dát uživateli odkaz na proxy. 22
Implementace pro sledování Když je vyvolaná metoda proxyna instanci proxy, je nejdříve řízení předání metodě invoke() s následujícími argumenty: proxy instance proxy na níž byla vyvolaná metoda invoke(), method objekt metatřídymethodpro vyvolávanou metodu,
Implementace pro sledování args pole objektů obsahující hodnoty argumentů předávané v metodě volání na instanci proxy. nullžádné předávané argumenty. Primitivní typy zabalené v obalových třídách.
Implementace pro sledování Návratový typ metody invoke Object. Návratová hodnota splňuje pravidla: pokud typ návratové hodnoty je void, nezáleží na návratové hodnotě. Null nejjednodušší řešení. je-li návratový typ metody rozhraní primitivním typ, odpovídající hodnota musí být z příslušné obalové třídy, není-li hodnota návratové metody kompatibilní s metodou rozhraní výjimka ClassCastException.
Implementace pro sledování Pro lepší pochopení: proxyforrover = (Dog) TracingIH.createProxy(rover); Dog rozhraní, rover instance od DogImpl Mechanismus proxy zabezpečí, proxy instance vrácená metodou createproxy() může být přetypovaná na Dog.
Diagram relevantní řádku kódu šedé zabarvení Proxy «interface» Dog «interface» InvocationHandler $Proxy0 TracingIH DogImp1 instanceof instanceof instanceof proxyforrover invocationhandler cíl rover
Poznámka k továrnám Předvedené řešení nemá možnost dynamicky měnit sledování on/off. Aplikace buď použije sledovací, nebo nesledovací verzi. K dosažení možnosti dynamického on/off sledování je použit vzor Abstraktní továrny. Je deklarovaná třída, která obsahuje metodu pro vytvoření instance rozhraní Dog.
Poznámka k továrnám Tato metoda vybírá, zda vytvořit instanci třídy Dog, která umožňujesledování nebo instanci, která neumožňuje sledování.
interface Dog { void initialize( String name, int size ); int method1(); int method2(); class DogImpl implements Dog { private int x = 0; private int y = 0; // konstruktor DogImpl (){ if (!invariant() ) throw new RuntimeException(); public void initialize( String name, int size ) { public int method1() { x++; y--; return 0; public int method2() { x++; y++; System.out.println( "Dog(" + x + "," + y + ")" ); return 0; public boolean invariant() { return x + y == 0;
import java.lang.reflect.*; import java.io.printwriter; public class DogFactory { private Class dogclass; private boolean traceison = false; public DogFactory( String classname, boolean trace ) { try { dogclass = Class.forName( classname ); catch (ClassNotFoundException e){ throw new RuntimeException(e); // or whatever is appropriate traceison = trace; #1 pouze je-li nastaven příznak trasování se vytváří požadavky pro trasování, sledování. public Dog newinstance( String name, int size ) { try { Dog d = (Dog)dogClass.newInstance(); d.initialize(name,size); if ( traceison ) { // #1 d = (Dog)TracingIH.createProxy( d, new PrintWriter(System.out) ); return d; catch(instantiationexception e){ throw new RuntimeException(e); // or whatever is appropriate catch(illegalaccessexception e){ throw new RuntimeException(e); // or whatever is appropriate
Řetězení proxies Silnou stránkou mechanismu proxiesje možnost jejich řetězení. V řetězci se jedno proxy odvolává na další proxya teprve poslední komunikuje s cílovým (reálným) objektem. Je-li provedení správné, vytvoří se efekt skládání vlastností implementovaných každým proxy.
Náhradní rozhraní pro jednotkové testování (stubbinginterfaces) V předchozím textu, proxy mělo cílový objekt. To však není nutné, v některých situacích může invokativníovladačimplementovat metody zcela (tedy bez odkazu na cílový objekt). Konkrétně: test stub(náhrada pro testování) je implementace rozhraní, která je použito k testování během vývoje dříve, než bude kompletní implementace.
Náhradní rozhraní pro jednotkové testování (stubbinginterfaces) Během vývoje topdowntestování pomocí náhrady (stubs) dovolí vývoj a testování horní vrstvy systému, bez požadavků, aby byl k dispozici všechen kód dolní vrstvy. Např. může být testováno chování vrstvy GUI, tím, že se pro dolní business logiku používají stuby. Avšak náhrady (stuby) se musí vytvořit a udržovat více práce.
Náhradní rozhraní pro jednotkové testování (stubbinginterfaces) Během vývoje se hodně mění rozhraní, a proto se musí měnit také náhrady více práce a peněz. Vytvoření a údržba stubsje mechanická a únavná práce. Stubyvyžadují napsání návratových hodnot a výjimek.
Náhradní rozhraní pro jednotkové testování (stubbinginterfaces) Toto předem připravené chování se používá testy k uvědomění si cest kódem během testování. Využívání stubůusnadňuje počáteční testování. Unit testing(testování po málýchjednotkách) umožňuje odhalovat počáteční chyby v kódu.
Návrh stubůpomocí proxy K daným javovskýmrozhraním se vytvoří proxy instance testovacích stubů(náhrad). Tato možnost eliminuje potřebu ručně implementovaných testovacích stubůpři vývoji topdown. Použití stubůvytvářených pomocí proxy podporuje dovednost programování proti rozhraní.
Návrh stubůpomocí proxy
Návrh stubůpomocí proxy Stub rozhraní přidává užitečné metody k proxy např. metody pro získání informací z vnitřku invokačního ovladače bez volání Proxy.getInvocationHandler(). History možnost stubtestováná, která bere v úvahu historii objektu a ta byla definovaná, aby si pamatovala metody volané na stubu během testování.
Návrh stubůpomocí proxy DefaultHistory tato třída implementuje History, která nic nedělá. ReturnValueStrategy aplikace vzoru Strategie, který dovoluje skriptování návratových hodnot a vyhozených výjimek. DefaultReturnValueStrategy tato třída implementuje ReturnValueStrategy, která se použije, pokud není nic specifikováno.
Návrh stubůpomocí proxy WarppedException ReturnValueStrategymůže potřebovat vyhodit výjimku. To se provede zabalením výjimky do WrappedException. To pak dovoluje stuburozeznat reálnou výjimku od skriptované výjimky. StubIH invokační ovladač pro testování možností stubů. To je odpovědné za použití strategie návratových hodnot k určení návratové hodnoty.
Dynamické proxy Kontext: Rozsáhlá aplikace. Problém: Úprava kódu bez zásahu do návrhu. Řešení: Použití dynamického proxy, které nabízí Java.
Dynamické proxy Dynamické proxy nabízí obalit daný objekt proxy objektem. Vnější objekt proxy, bude přijímat (odposlouchávat) všechna volání určena pro vnitřní zabalený objekt. Proxy objekt standardně předává všechna volání vnitřnímu objektu, ale je možné přidat kód, který se vykoná před nebo po vyvolání metody vnitřního objektu.
Dynamické proxy Není možné samozřejmě zabalit libovolný objekt. I přesto umožňují dymanickáproxy úplnou kontrolu nad zabaleným objektem. Dynamické proxy pracuje s rozhraními, které třída objektu implementuje. Volání, která může proxy zachytit jsou volání jednoho z těchto definovaných rozhraní.
Dynamické proxy Proxy se nyní skládá ze dvou tříd. InvocationHandlerdostává všechna volání metod, které směřují na proxy. Řídí tak přístup metod k RealSubjectu. Proxyimplementuje celé rozhraní Subject.
public interface PersonBean { String getname(); String getgender(); String getinterests(); int gethotornotrating(); void setname(string name); void setgender(string gender); void setinterests(string interests); void sethotornotrating(int rating); Příklad seznamky Hodnocení osoby pro seznamku HotOrNotRating() hodnocení 0 10 jako průměr zadaných hodnot jinými osobami
Příklad seznamky Datové atributy k dané konkrétní osobě zadává samotná osoba. HotOrNotRatingzadávají ostatní osoby od 1-10 a počítá se jako průměrná hodnota. Jak zabezpečit různé možnosti zadávání datových atributů?
public class PersonBeanImpl implements PersonBean { String name; String gender; String interests; int rating; int ratingcount = 0; public String getname() { return name; public String getgender() { return gender; public String getinterests() { return interests; public int gethotornotrating() { if (ratingcount == 0) return 0; return (rating/ratingcount); public void setname(string name) { this.name = name;
public void setgender(string gender) { this.gender = gender; public void setinterests(string interests) { this.interests = interests; public void sethotornotrating(int rating) { this.rating += rating; ratingcount++; Tento datový atribut může zadávat kdokoli, mimo konkrétní osobu
Vytvoření dynamického proxy pro PersonBean 1. Vytvořit dva InvocationHandlers InvocationHandleryimplementují chování proxy. 2. Napsání kódu, který vytváří dynamické proxy 3. Zabalit libovolný objekt PersonBeans vhodným proxy.
1. Vytvoření InvocationHandlers Existuje pouze jedna metoda invoke()a nezáleží, která metoda volá proxy, právě metoda invoke()je to, co je voláno v handleru.
proxy.sethotornotrating(9); invoke(object proxy, Method method, Object[] args) return method.invoke(person, args) Tento objekt byl předán ve volání metody invoke() Pouze nyní vyvoláváme metodu na RealSubjectu, s původními argumenty (1) Nechť je na proxy vyvolaná metoda sethotornotrating() (2) Proxy pak vyvolá metodu invoke() InvocationHandleru. (3) Handlerpak rozhodne, co by měl dělat s požadavkem a možná požadvekpošle RealSubjectu. Zde vyvoláváme původní metodu, která byla vyvolána na proxy.
Prohlížení vlastního a cizího beanu Pro zákazníka, který si prohlíží svůj vlastní bean. Pro zákazníka, který si prohlíží bean někoho jiného.
import java.lang.reflect.*; public class OwnerInvocationHandler implements InvocationHandler { PersonBean person; // konstruktor public OwnerInvocationHandler(PersonBean person) { this.person = person; public Object invoke(object proxy, Method method, Object[] args) throws IllegalAccessException { try { if (method.getname().startswith("get")) { return method.invoke(person, args); else if (method.getname().equals("sethotornotrating")) { throw new IllegalAccessException(); else if (method.getname().startswith("set")) { return method.invoke(person, args); catch (InvocationTargetException e) { e.printstacktrace(); return null;
import java.lang.reflect.*; public class NonOwnerInvocationHandler implements InvocationHandler { private PersonBean person; public NonOwnerInvocationHandler(PersonBean person) { this.person = person; public Object invoke(object proxy, Method method, Object[] args) throws IllegalAccessException { try { if (method.getname().startswith("get")) { return method.invoke(person, args); else if (method.getname().equals("sethotornotrating")) { return method.invoke(person, args); else if (method.getname().startswith("set")) { throw new IllegalAccessException(); catch (InvocationTargetException e) { e.printstacktrace(); return null; Je-li volaná jiná metoda vrací se null
PersonBean nonownerproxy = getnonownerproxy(joe); System.out.println("Name is " + nonownerproxy.getname()); try { nonownerproxy.setinterests("bowling, Go"); catch (Exception e) { System.out.println("Can't set interests from non owner proxy"); nonownerproxy.sethotornotrating(3); System.out.println("Rating set from non owner proxy"); System.out.println("Rating is " + nonownerproxy.gethotornotrating()); PersonBean getownerproxy(personbean person) { return (PersonBean) Proxy.newProxyInstance( person.getclass().getclassloader(), person.getclass().getinterfaces(), new OwnerInvocationHandler(person)); PersonBean getnonownerproxy(personbean person) { return (PersonBean) Proxy.newProxyInstance( person.getclass().getclassloader(), person.getclass().getinterfaces(), new NonOwnerInvocationHandler(person));
import java.lang.reflect.*; import java.util.*; Databáze 1/2 public class Database { private Hashtable<String, PersonBean> datingdb; public Database() { datingdb = new Hashtable<String, PersonBean>(); initializedatabase(); public Hashtable<String, PersonBean> getdatabase() { return datingdb; public void initializedatabase() { PersonBean joe = new PersonBeanImpl(); joe.setname("joe Javabean"); joe.setinterests("cars, computers, music"); joe.sethotornotrating(7); datingdb.put(joe.getname(), joe); PersonBean kelly = new PersonBeanImpl(); kelly.setname("kelly Klosure"); kelly.setinterests("ebay, movies, music"); kelly.sethotornotrating(6); datingdb.put(kelly.getname(), kelly);
2. Vytvoření třídy Proxy a vytvoření instance objektu Proxy Vše, co ještě zbývá, je dynamicky vytvořit proxy třídu a vytvořit instanci proxy objektu. Zapíšeme metodu, která vezme PersonBean (vstupní parametr) a ví, jak pro něj vytvořit owner proxy. Toto proxy postoupí volání metody OwnerInvocationHandleru.
Tato metoda vezme objekt person a vrávína něj proxy. Protože má proxy stejné rozhraní, vrací se PersonBean. PersonBean getownerproxy(personbean person) { return (PersonBean) Proxy.newProxyInstance( person.getclass().getclassloader(), person.getclass().getinterfaces(), new OwnerInvocationHandler(person)); třídní metoda newproxyinstancetřídy Proxy parametry: classloader objektu person, a množinu rozhraní, které musí proxy implementovat, a instanci InvocationHandleru do konstruktoru předáme reálný objekt
Postup aplikace K vytvoření dynamického proxy je třeba mít seznam rozhraní, která chcete zachytit. Ten se získá následovně: Class[ ] classes = obj.getclass().getinterfaces(); Dále je třeba class loader a třídu, která obsahuje chování, které chcete vykonávat, když váš proxy zachytí volání. Získání class loaderu: ClassLoader classloader = obj.getclass().getclassloader();
Postup aplikace Samotný proxy objekt musí být instancí třídy, která implementuje rozhraní InvocationHandler z balíčku java.lang.reflect. Uvedené rozhraní deklaruje následující operaci: public Object invoke(object proxy, Method m, Object[] args) throw Throwable; Po zabalení objektu do dynamického proxy, volání určená pro zabalený objekt jsou přesměrována na operaci invoke(), ve třídě, kterou dodáte.
Postup aplikace Váš kód pro metodu invoke() bude potřebovat předat každé volání metody zabalenému objektu. result = m.invoke(obj, args);
import java.lang.reflect.*; import java.util.*; Databáze 1/2 public class Database { private Hashtable<String, PersonBean> datingdb; public Database() { datingdb = new Hashtable<String, PersonBean>(); initializedatabase(); public Hashtable<String, PersonBean> getdatabase() { return datingdb; public void initializedatabase() { PersonBean joe = new PersonBeanImpl(); joe.setname("joe Javabean"); joe.setinterests("cars, computers, music"); joe.sethotornotrating(7); datingdb.put(joe.getname(), joe); PersonBean kelly = new PersonBeanImpl(); kelly.setname("kelly Klosure"); kelly.setinterests("ebay, movies, music"); kelly.sethotornotrating(6); datingdb.put(kelly.getname(), kelly);
public PersonBean getpersonfromdatabase(string name) { return (PersonBean)datingDB.get(name); Databáze 2/2 public PersonBean getownerproxy(personbean person) { return (PersonBean) Proxy.newProxyInstance( person.getclass().getclassloader(), person.getclass().getinterfaces(), new OwnerInvocationHandler(person)); public PersonBean getnonownerproxy(personbean person) { return (PersonBean) Proxy.newProxyInstance( person.getclass().getclassloader(), person.getclass().getinterfaces(), new NonOwnerInvocationHandler(person));
public class ProxyTest { public static void main(string[] args) { Database database = new Database(); Databáze PersonBean joe = database.getpersonfromdatabase("joe Javabean"); PersonBean ownerproxy = database.getownerproxy(joe); System.out.println("Name is " + ownerproxy.getname()); ownerproxy.setinterests("bowling, Go"); System.out.println("Interests set from owner proxy"); try { ownerproxy.sethotornotrating(10); catch (Exception e) { System.out.println("Can't set rating from owner proxy"); System.out.println("Rating is " + ownerproxy.gethotornotrating()); PersonBean nonownerproxy = database.getnonownerproxy(joe); System.out.println("Name is " + nonownerproxy.getname());
try { nonownerproxy.setinterests("bowling, Go"); catch (Exception e) { System.out.println("Can't set interests from non owner proxy"); nonownerproxy.sethotornotrating(3); System.out.println("Rating set from non owner proxy"); System.out.println("Rating is " + nonownerproxy.gethotornotrating()); Databáze
Name is Joe Javabean Interests set from owner proxy Can't set rating from owner proxy Rating is 7 Name is Joe Javabean Can't set interests from non owner proxy Rating set from non owner proxy Rating is 5
Dynamické proxy Proxy je dynamické, protože jeho třída je vytvořena za běhu programu. Proxy je vytvořeno na základě požadavku (množina rozhraní). Naproti tomu InvocationHandlernení proxy, je tom jen takový dispečer, kterého proxy využívá pro zpracování volaných metod. Vytvoření proxy je podle třídní metody: static Proxy.newProxyInstance()
Dynamické proxy Třída Proxy má statickou metodu isproxyclass() pro zjišťování zda se jedná o dynamické proxy. Třídní metoda newproxyinstance() vyžaduje Třídní metoda newproxyinstance() vyžaduje pole rozhraní třídy nejsou dovoleny.
Závěr Implementace vzoru proxy vytvoří zástupný objekt, který řídí přístup k cílovému objektu. Proxy objekt může izolovat klienty od změn ve stavech požadovaného objektu, jako např. při natahování obrázků vyžadujících delší trvání. Problém proxy způsobuje vyžadovaná pevná vazba mezi zástupným objektem a původním objektem. Dynamické proxy může nabízet využití při zachycování volání cílovému objektu.
Shrnutí Kromě vytváření náhrady pro individuální objekty, Proxyje efektivním nástrojem pro přidávání vlastností objektům. Oddělením kódu např. pro trasovánído invokačních ovladačů, usnadní práci vývojářům.