P-programowanie https://www.p-programowanie.pl Blog poświęcony programowaniu we wszystkich popularnych językach i maturze z informatyki. Fri, 30 Mar 2018 19:15:36 +0000 pl-PL hourly 1 https://wordpress.org/?v=4.9.6 Jak pozbyć się singletonów w Angularze? https://www.p-programowanie.pl/angular/jak-pozbyc-sie-singletonow-w-angularze/ https://www.p-programowanie.pl/angular/jak-pozbyc-sie-singletonow-w-angularze/#respond Tue, 27 Mar 2018 23:46:09 +0000 https://www.p-programowanie.pl/?p=3600 Praktycznie każdy serwis w Angularze jest domyślnie singletonem. Od tej reguły istnieją pewne wyjątki jednak tylko w ściśle określonych przypadkach. Niestety, nie zawsze chcemy aby serwis, którego używamy, zachowywał się jak singleton. W moim przekonaniu Angular nie dostarcza prostego, intuicyjnego mechanizmu aby programista mógł wybrać, jakiego rodzaju serwisu potrzebuje. W tym artykule podzielę się z Tobą kilkoma sposobami, których używam aby osiągnąć cel – czyli instancyjność serwisów.

 Wstrzykiwanie zależności w Angularze

Angular jest wyposażony we własny mechanizm wstrzykiwania zależności i chwała mu za to. Jest to jedna z cech tego frameworka, za którą go pokochałem. Programując przykładowo w ASP.NET MVC bądź Node.JS programista sam musi zadbać o jakiś kontener IoC (ang. inversion of control). W .NET bardzo prosty i popularny jest Ninject, w JS można użyć przykładowo InversifyJS. Użyć, czyli w tym wypadku dodać do projektu, i odpowiednie skonfigurować. W bardziej zaawansowanych przypadkach może to sprawić problemy początkującym programistom, szczególnie, jeśli chcemy do mechanizmu DI (ang. dependency injection) dodać obsługę połączenia z bazą danych (np. wzorzec Unit of work). Pojawiają się wtedy problemy z transakcjami, zakresami transakcji, pulą połączeń itd.

Całe szczęście, zarówno AngularJS jak i Angular2+ załatwił sprawę z góry. Wszystkie serwisy są wstrzykiwalne poprzez wbudowany mechanizm DI. W AngularJS każdy serwis był singletonem i nie było na to lekarstwa. Wynika to z faktu, że posiadał on jeden globalny injector.

W Angular2+ może występować wiele injectorów a cały mechanizm DI jest hierarchiczny. DI w Angular2+ to naprawdę rozbudowany temat i na pewno napiszę o tym osobny artykuł. Z tego względu pominę wiele technicznych szczegółów i opiszę tylko jaki jest czas życia serwisów.

Wstrzykiwalne serwisy

Aby serwis był wstrzykiwalny należy poprzedzić jego definicję dekoratorem @Injectable. Użycie tego dekratora jest sygnałem dla mechanizmu DI, że ma brać ten serwis pod uwagę. Drugim i ostatnim krokiem, jest dodanie serwisu do tablicy providers w module, w którym się znajdujemy. To wszystko. Zdefiniowaliśmy wstrzykiwalny serwis.

Aby pobrać instancję serwisu można użyć następujących sposobów:

// domyślne
constructor(private customService: CustomService) {
	this.customService.testMethod();
}

// poprzez dekorator @Inject
constructor(@Inject(CustomService) private customService) {
	this.customService.testMethod();
}

// poprzez instancję injector'a
constructor(private injector: Injector) {
	this.customService = Injector.get(CustomService);
}

W większości przypadków korzysta się z podejścia domyślnego. Czasem aby pobrać uchwyty do niestandardowych obiektów, które nie są serwisami (np. uchwyt OpaqueToken lub InjectionToken) trzeba użyć dekoratora. Czasem natomiast aby zapobiec błędowi cicrular dependency lub cyclic dependency (szczególnie podczas korzystania z HTTP_INTERCEPTORHttpClient) trzeba skorzystać z instancji injectora. Nie jest to jednak w tym artykule ważne.

Wszystkie wstrzyknięte w ten sposób (każda z powyższych metod) serwisy, są singletonami.

Jeden injector dla wielu modułów

Komponenty i dyrektywy są zahermetyzowane wewnątrz modułów, w których zostały zadeklarowane, jednak nie dotyczy to serwisów. Wynika to z faktu, iż podczas fazy kompilowania wszystkie moduły Angulara są scalane w jeden główny moduł, a wszystkie serwisy lądują w jednej, wspólnej tablicy dostępnych serwisów. M.in. dlatego serwisów nie trzeba eksportować (w odróżnieniu do komponentów i dyrektyw) reużywając je pomiędzy modułami. Przez to nie jest też ważne, czy serwis użyjemy w tym samym module, w którym go zdefiniowaliśmy, czy w całkiem innym module aplikacji. Serwis zawsze będzie dostępny wszędzie i będzie singletonem.

Dzieje się tak ponieważ, wszystkie scalone moduły Angulara podczas fazy kompilacji dostają jeden wspólny injector. Wiążą się z tym kolejne konsekwencje np. możliwość nadpisywania (przesłaniania) serwisów pomiędzy modułami, jednak to nie temat na ten wpis.

Kiedy próbujemy uzyskać uchwyt do serwisu poprzez konstruktor, odpytujemy injector o uchwyt do interesującego na serwisu. Jeśli zrobimy to pierwszy raz, zostanie niejawnie utworzona jego instancja (leniwie), a za każdym kolejnym razem zostanie zwrócona ta sama, poprzednio utworzona instancja. Pojawia się więcej pytań niż odpowiedzi, który injector odpytujemy?

Hierarchiczne wstrzykiwanie zależności

W Angularze DI jest hierarchiczne, a tak naprawdę hierarchiczne są injectory. Podobny mechanizm zapewnia biblioteka InversifyJS, której można użyć w np. w NodeJS. Jeśli posiadamy kilka zdefiniowanych modułów, podczas kompilacji są one scalone w jeden i wyposażone w jeden wspólny injector. Konstruktor każdego komponentu i serwisu szuka tego injectora hierarchicznie.

Wyobraźmy sobie strukturę jak na powyższym obrazku. Serwis dostarczony w głównym module aplikacji będzie singletonem w każdym komponencie w aplikacji, bo cała aplikacja posiada wspólny injector. Użycie serwisu CustomService w każdym komponencie będzie oznaczało operowanie na tej samej instancji.

Jedynym sposobem na pozbycie się singletona jest utworzenie nowego injectora. Można tego dokonać na następujące sposoby:

  • moduł, w którym deklarujemy serwis, doczytać leniwie (ang. lazy loading)
  • dodać tablicę providers w dekoratorze komponentu,w którym chcemy utworzyć nowy injector (przełamać łańcuch DI). Następnie dodać do tablicy interesujący nas serwis.
  • utworzyć serwis ręcznie operatorem new (bez korzystania z DI Angulara, bez dekoratora @Injectable)
  • tworzyć ciągle nowe serwisy rozszerzające (ang. extends) bazowy serwis (dziedzicząc jego logikę i funkcje)

 

W poniższych akapitach opisze własne doświadczenia i przemyślenia na temat tworzenia nowych injectorów.

Leniwie doczytany moduł

Angular udostępnia mechanizm leniwego doczytywania modułów poprzez odpowiednią konstrukcję routingów. Jakie niesie to ze sobą wady i zalety nie naddaje się do opisania w tym artykule. Ważne jest natomiast to, że każdy doczytany leniwie moduł posiada osobny injector. Jest to zrozumiałe, ponieważ w momencie kompilacji aplikacji Angularowej leniwie doczytany moduł fizycznie nie istnieje (nie jest doczytany), więc nie można uwzględnić jego serwisów podczas scalania modułów.

Leniwe doczytywane moduły są więc jednym ze sposobów osiągnięcia instancyjnych serwisów – wręcz narzuconym. Wygląda to następująco:

W zależności od tego, czy będziemy chcieli użyć CustomService czy CustomService2 mechanizm DI odpyta różne injectory. Albo główny, powstały w wyniki procesu kompilacji poprzez scalenie wszystkich nie-leniwych modułów, albo drugi utworzony podczas doczytania leniwego modułu. Odwołując się w komponencie nr. 5 do serwisu CustomService korzystamy z domyślnego injectora. Natomiast prosząc o CustoService2 korzystamy z nowego injectora leniwego modułu.

Aby uwspólnić injectory w leniwie doczytywanych modułach można posłużyć się metodą forRoot(), która blokuje powstanie dodatkowego injectora nawet w przypadku leniwych modułów. Z tej metody korzysta np. RouterModule i kilka innych fajnych Angularowych bibliotek, jednak jej opis to temat na osobny wpis.

Leniwe doczytywanie modułów aby osiągnąć instancyjne serwisy? Nie za bardzo. Coś tam się da, jednak nie jest to na pewno zadowalający efekt.

Redeklaracja serwisu w komponencie

Każdy serwis w Angularze deklarujemy w tablicy providers w jakimś module – czy to w głównym czy dodatkowym. Zapewnia to wspólny injector i oczywiście jedną singletonową instancję. Redeklaracja serwisu w dekoratorze komponentu powoduje utworzenie nowego injectora. Kod wygląda następująco:

@Component({
	selector: 'custom-component', 
	template: 'This is just a test component.',
	providers: [ CustomService ] // przełamanie łańcucha DI
})
class CustomComponent {
	constructor(private customService: CustomService) {
		// customService pochodzi z nowego injectora(!)
	}
}

Efekt działania powyższego kodu dla komponentu nr. 4 miałby następujący efekt:

Zarówno komponent nr. 4 jak i wszystkie jemu podległe prosząc o serwis CustomService dostałby instancję pochodzącą z nowego injectora. Komponentu należące do injectora będącego wyższej w hierarchii dziedziczenia (nr. 1, 2 i 3) dostaną inną instancję niż komponenty znajdujące się niżej w hierarchii (nr. 4, 5 i 6).

Należy pamiętać, że nowy injector przechowuje tylko i wyłączenie konkretny, redeklarowany serwis z tablicy providers umieszczonej w dekoratorze komponentu. Reszta serwisów ciągle będzie wczytywana z głównego injectora.

Rozwiązanie to jest bardzo skuteczne i ogólnie polecane. Jakie są jego wady? Często korzystając z metody forRoot() decydujemy się dostarczyć do serwisu jakąś dodatkową konfigurację już na etapie importu (podobnie jak w RouterModule). Korzystając z takiego rozwiązania decydujemy się na wielopostaciowość instancji danego serwisu, co może wiązać się z chęcią dostarczenia także unikalnej konfiguracji. Niestety nie za bardzo jest jak ją dostarczyć, chyba że jakąś osobną metodą (np. jakiś setter).

Ręcznie tworzony serwis

Opiszę moje ostatnie doświadczenie, które generalnie skłoniło mnie do napisania tego wpisu. Pewnego razu w pracy musiałem stworzyć uniwersalny moduł wyszukiwarki, który mógłby być użyty w wielu miejscach w projekcie. Był to moduł wyodrębniony do większego, reużywalnego cora.

Podejście 1

Początkowo był to zwykły singleton, do którego za pomocą składni metody forRoot() przesyłałem odpowiednią konfigurację np.:

@NgModule({

    imports: [
        CoreSearchModule.config({
            method: HttpMethods.GET,
            endpoint: '/api/articles',
            resultsPerPage: 10
        }),
    ],
	
})
export class AppModule { }

Rozwiązanie fajne, jednak wstrzykiwanie konfiguracji jest bezsensu. W innym miejscu w projekcie ktoś może chcieć strzelać do innego endpointa. Pierwsza myśl? Umożliwić przesłanie nowej konfiguracji jako parametr opcjonalny metody search(). Po chwili namysłu stwierdziłem – bezsensu. Serwis jest singletonem, nie mogę nadpisywać jego konfiguracji w rożnych miejscach aplikacji.

Podejście 2

Stwierdziłem, że serwis wyszukiwarki nie będzie serwisem @Injectable, tylko zwykła klasą TypeScripta. Chcąc stworzyć wyszukiwarkę do artykułów należałoby stworzyć ArticleSerachService dziedziczący z kolei z SearchService. Serwisy rozszerzające dałoby się już normalnie wstrzykiwać. Fajnie? No fajnie, działałoby. Każdy „pod serwis” miałby osobną instancję w injectorze, każdy mógłby mieć inną konfigurację. Jednak to podejście do rozwiązania problemu wydawało mi się bezsensowne. Po co tworzyć sztuczną warstwę nieprzydanych serwisów, tylko po to aby wygrać z singletonami Angulara?

Podejście 3

Stwierdziłem, że pokonam Angulara jego własną bronią – injectorami. Redeklaracja serwisu wyszukiwania w każdym komponencie, gdzie tylko będę chciał użyć serwisu wyszukiwarki. Rozwiązanie początkowe wydawało się fajne, nawet polecane przez dokumentację. Jednak niesie ze sobą kilka problemów:

  • brak możliwości dostarczenia konfiguracji
  • brak możliwości 2 osobnych instancji w obrębie jednego komponentu
  • totalny syf w kodzie (kto obcy kiedyś będzie wiedział, że w komponencie nr. 28 ktoś przełamał łańcuch DI redeklarując serwis?)

Szybko z tego podejścia zrezygnowałem.

Podejście 4

Ostatnia deska ratunku. Przerobiłem od deski do deski cały mechanizm DI Angulara w poszukiwaniu dobrego rozwiązania. Przerobiłem wszystkie możliwe operatory i strategie opisywane w różnych miejscach w internecie. Nic ciekawego nie znalazłem. Zrezygnowany stwierdziłem, że to czas przestać używać DI Angulara.

Utworzyłem zwykłą klasę – niewstrzykiwalną – pozbawioną dekoratora @Injectable. Tworzyłem instancję serwisu operatorem new w jakim miejscu chciałem, a konfigurację dostarczałem przez konstruktor. Prawie myślałem, że task jest skończony, ale stwierdziłem – beznadzieja. Nie po to Angular zapewnia mechanizm DI aby teraz robić takie kwiatki. Jak to później testować?

Ostatecznie satysfakcjonujące rozwiązanie wymyśliłem w ostatniej chwili. Opiszę je w kolejnym akapicie.

Funkcja konstruująca – pseudo fabryka

Dość długo myślałem nad rozwiązaniem swojego problemu: dostarczenie elastycznego, reużywalnego, konfigurowalnego serwisu, który byłby gotowy do użycia w każdym miejscu projektu, korzystając przy tym DI Angulara. Zakręciłem się myślami wokół wzorca budowniczy oraz wzorca fabryki. Stwierdziłem, że budowniczy może rozwiązać mój problem, jednak po chwili facepalm – typescript nie obsługuje klas zagnieżdżonych niezbędnych do stworzenia statycznego budowniczego. Użycie budowniczego w koncepcji GoF (ang. Gang of Fours) nie miało w tym wypadku sensu.

Ostatecznie stworzyłem rozwiązanie przypominające prostą statyczną fabrykę. Postanowiłem wykorzystać serwis Angularowy w postaci singletona, który za pomocą funkcji konstruującej (ang. constructor function) zwróci nową instancję interesującego mnie serwisu, który już nie jest zależy od mechanizmu DI Angulara – dzięki temu, nie jest singletonem. Wykonane kroki:

  1. Dodałem singletonowy serwis SearchService wstrzykiwalny przez DI Angulara (z dekoratorem @Injectable)
  2. SearchService zawiera tylko jedną metodę createSearch(), która dodatkowo przyjmuje odpowiednią konfigurację
  3. Powyższa metoda zwraca nową instancję serwisu SearchEngine, który nie jest już serwisem obsługiwanym przez DI Angulara
  4. Serwis SearchEngine przyjmuje interfejs z konfiguracją i wystawią całą funkcjonalność wyszukiwarki.

Myślę, że takie podejście to bardzo dobre rozwiązanie w przypadku radzenia sobie w wymagających przypadkach Angularowych singletonowych serwisów. Przykładowe użycie mojego rozwiązania wygląda następująco:

@Component({
    selector: 'custom-component', 
    template: 'This is just a test component.'
})
class CustomComponent {
	
    constructor(private searchService: SearchService) { }
	
    searchArticles() {
        this.searchService().createSearch<Article>({
            method: HttpMethods.GET,
            endpoint: '/api/articles',
            resultsPerPage: 10
        }).search({
            surename: 'Kowalski',
            group: 23
        }).subscribe((result) => {
            console.log(result);
        });
    }
}

Oczywiście instancję zwróconą przez metodę createSearch<T>() można także zapisać i reużywać:

@Component({
    selector: 'custom-component', 
    template: 'This is just a test component.'
})
class CustomComponent {
	
    private articlesSearch: ISearch<Article>;
    
    constructor(private searchService: SearchService) {
        this.articlesSearch = this.searchService().createSearch<Article>({
            method: HttpMethods.GET,
            endpoint: '/api/articles',
            resultsPerPage: 10
        });
    }
	
    searchArticles() {
        this.articlesSearch.search({
            surename: 'Kowalski',
            group: 23
        }).subscribe((result) => {
            console.log(result);
        });
    }
}

Dlaczego zdecydowałem się na taką metodę? Po co mieszać serwis zarządzany przez DI Angulara z instancyjnym serwisem? Oprócz listy wniosków , które opisałem w akapitach wyżej pojawia się kolejna bardzo ważna zaleta. Serwis zarządzany przez DI Angulara może dostarczyć naszemu „sub-serwisowi” inne serwisy także zarządzane przez mechanizm DI. Mój serwis do wyszukiwarki korzysta z HttpClient, który musi być serwowany przez DI Angulara. Dzięki obudowującemu singletonowi mogę zawsze pobrać instancję tego serwisu:

@Injectable()
export class SearchService {

    constructor(private http: HttpClient) { }

    /**
     * @description Factory method which return new instance of `SearchEngine<T>`
     * @param `searchConfiguration` base search configuration
    */
    createSearch<T>(searchConfiguration: SearchConfiguration) {
      return new SearchEngine<T>(searchConfiguration, this.http);
    }

}

Graficzne przedstawienie tej koncepcji wygląda następująco:

Kilka plusów tego podejścia:

  • brak singletonów
  • możliwość „wstrzyknięcia” konfiguracji
  • możliwość kilku różnych instancji serwisu w jednym komponencie
  • całość dobrze opisana TypeDoce’m, więc każdy programista bezproblemowo się w tym odnajdzie
  • nadal korzystamy z DI Angulara (dla serwisu fabrykującego), co zwiększa skalowalność projektu
  • nadal możemy wstrzykiwać i używać serwisy zarządzane przez mechanizm DI Angulara

Metodę createSearch<T>() oczywiście można pominąć, konfigurację wrzucić w konstruktor, a ten dalej zwróciłby nową instancję wyszukiwarki. Ta mała nadmiarowość z mojej strony miała dać do myślenia programiście kiedyś pracującym na tym kodzie – aby wiedział, że coś się tam dzieje, że nie jest to tylko zwykłe wywołanie konstruktora z parametrami. Nie jest to standardowe zachowanie serwisów Angularowych, więc lepiej to podkreślić.

Co sądzicie o takim podejściu? Być może ktoś zna inne, alternatywne rozwiązania radzenia sobie z niechcianymi singletonami?

]]>
https://www.p-programowanie.pl/angular/jak-pozbyc-sie-singletonow-w-angularze/feed/ 0
Budowniczy https://www.p-programowanie.pl/wzorce-projektowe/budowniczy/ https://www.p-programowanie.pl/wzorce-projektowe/budowniczy/#respond Sun, 25 Mar 2018 21:20:53 +0000 https://www.p-programowanie.pl/?p=3553 Budowniczy to jeden ze wzorców projektowych używanych w programowaniu obiektowym. Zalicza się on do rodziny wzorców konstrukcyjnych. Dzięki użyciu budowniczego oddzielamy proces tworzenia obiektu od jego reprezentacji. Jest to dość prosty wzorzec, który jednak sprawia problemy, ze względu na różne warianty w jakich występuje. W tym artykule przedstawię dwie podstawowe implementacje tego wzorca oraz opiszę różnice, jakie między nimi zachodzą.

Różne odmiany budowniczego

Na samym początku należy zdać sobie sprawę z tego, że istnieją dwie popularne odmiany tego wzorca, jedna opisana przez GoF (ang. Gang of Four) a druga (będąca modyfikacją pierwszej) opisana przez Joshue Blocha. W większości przypadków mówiąc o wzorcu budowniczy ma się na myśli jego pierwszą odmianę – w tym artykule także będę się do niej odnosił. Wersja statyczna budowniczego zostanie opisana w jednym z akapitów poniżej.

Po co używać wzorca budowniczy?

Budowniczy ma za zadanie rozwiązać pewien powtarzający się problem programistyczny – konkretniej zapewnia oddzielenie procesu inicjalizacji obiektu od jego reprezentacji. Decydując się na użycie tego wzorca można osiągnąć następujące korzyści:

  • logika mówiąca o tym jak obiekt ma być zbudowany będzie oddzielona od implementacji tej logiki
  • spełnia zasadę otwarty na rozbudowę, zamknięty na modyfikacje (open/closed principle) – łatwo dodać do kodu nowych budowniczych
  • spełnia zasadę odwrócenia zależności (dependency inversion principle)
  • sprzyja samodokumentującemu się kodowi (widząc poszczególnych budowniczych wiemy, co dostarczą)
  • daje doskonałą kontrolę nad etapami budowania kompozytu (np. hermetyzacja obsługi błędów dla danego kroku)

Rzadko zdarza się aby za pomocą wzorca budowniczy inicjalizować proste klasy DTO. Częściej natomiast, gdy złożony model składa się z wielu typów referencyjnych na zasadzie kompozycji.

Kiedy używać budowniczego

Budowniczy najczęściej używany jest wtedy, gdy istnieje potrzeba zbudowania złożonego obiektu (tzw. kompozytu). Złożony obiekt może oznaczać tworzenie „super klasy” – odpowiedzialnej za zbyt wiele rzeczy, a więc złamanie pierwszej zasady SOLID. W pierwszej kolejności należy więc spróbować uprościć złożony model, jednak jeżeli okaże się to niemożliwe, należy zastanowić się nad implementacją wzorca budowniczego. Szczególnie, jeżeli nasz złożony kompozyt ma być budowany na wiele różnych sposobów i stoi za tym jakaś logika.

Należy rozważyć użycie wzorca budowniczy gdy:

  • obiekt, który tworzymy jest złożony i nie da się go uprościć
  • nie da się utworzyć instancji obiektu poprzez jednorazową operację (wieloetapowa inicjalizacja)
  • obiekt, którzy tworzymy, będzie budowany wiele razy w różny sposób

Spójrzmy na taki, niezbyt fajny, kod:

var smallCar = new Car();
if (budget > 4500)
{
    smallCar.Wheels = "Aluminium rims 17 inches";
}
else
{
    smallCar.Wheels = "Steel rims 15 inches";
}
smallCar.Addons = new List<string>();
smallCar.Addons.Add("CD radio with MP3");
smallCar.Addons.Add("CD radio");
smallCar.Engine = EngineFactory.CreateEngine("120 HP engine");

W powyższym kodzie widzimy inicjalizację obiektu klasy Car. Prawdopodobnie chodzi o jakąś uboższą wersję, ponieważ tak możemy wywnioskować z nazwy zmiennej smallCar. Kod przeplatany jest logiką, w pewnych miejscach zostały użyte instrukcje warunkowe a gdzie indziej wzorzec prostej fabryki.

Kod jest i tak stosunkowo prosty, ponieważ brakuje w nim obsługi błędów, czyli instrukcji try/catch.  Reużycie kodu np. w celu zbudowania większego samochodu polegałoby na skopiowaniu tego kodu w całości i zmianie poszczególnych parametrów. Byłoby to podejście bardzo imperatywne, raczej mało obiektowe. W dużym projekcie wielu programistów konstruowałoby instancje klasy Car na swój sposób. Chcąc zmienić proces ich budowy, należałoby to zrobić w wielu miejscach.

Tak mogłaby wyglądać prosta implementacja budowniczego dla powyższego kodu:

// kierownik - logika inicjalizacji obiektu
class CarDirector
{
    public void ConstructCar(CarBuilder builder)
    {
        builder.BuildWheels();
        builder.BuildAddons();
        builder.BuildEngine();
    }
}

// budowniczy - interfejs implementacji
abstract class CarBuilder
{
    protected Car car;

    public abstract void BuildWheels();
    public abstract void BuildAddons();
    public abstract void BuildEngine();

    public Car GetResult
    {
        get { return this.car; }
    }
}

// konkretny budowniczy - implementacja 
class SmallCarBuilder : CarBuilder
{
    public SmallCarBuilder()
    {
        this.car = new Car();
    }

    public override void BuildWheels()
    {
        car.Wheels = "Steel rims 15 inches";
    }

    public override void BuildAddons()
    {
        car.Addons = new List<string>();
        car.Addons.Add("CD radio with MP3");
        car.Addons.Add("CD radio");
    }

    public override void BuildEngine()
    {
        car.Engine = EngineFactory.CreateEngine("120 HP engine");
    }
}

static void Main(string[] args)
{
    CarDirector carDirector = new CarDirector();
    CarBuilder smallCarBuilder = new SmallCarBuilder();

    // buduj według logiki CarDirector używając implementacji z SmallCarBuilder
    carDirector.ConstructCar(smallCarBuilder);

    Car smallCar = smallCarBuilder.GetResult;
}

Dzięki użyciu wzorca główna metoda programu znacząco się uprościła. Uzyskaliśmy jednolity interfejs polimorficzny służący do tworzenia instancji samochodu.

Rola poszczególnych elementów wzorca

Oto jego podstawowe elementy wzorca, które zostały użyte w powyższym przykładzie:

  • budowniczy (CarBuilder) – dostarcza abstrakcyjny interfejs służący do budowania finalnego produktu
  • konkretny budowniczy (SmallCarBuilder) – dostarcza implementację metodom budowniczego (implementacja)
  • kierownik (CarDirector) – konstruuje obiekt z wykorzystaniem jakiegoś budowniczego (logika)
  • produkt (product) – finalny złożony obiekt, dostarczony przez kierownika, zbudowany za pomocą jakiegoś budowniczego

Widząc pierwszy raz implementację wzorca budowniczy można odnieść wrażenie, że tych klas jest trochę za dużo. To naturalne, szczególnie jeśli rozumie się zasadę działania wzorca fabryki abstrakcyjnej. Poniżej przedstawię swoją krótką analizę tego wzorca, która być może rozwieje kilka wątpliwości:

  • najważniejszymi elementami wzorca jest kierownik oraz konkretny budowniczy. Kierownik skupia logikę budowania, konkretny budowniczy skupia implementację tej logikii
  • budowniczy jest nic nieznaczącym interfejsem (w przykładzie klasa abstrakcyjna) aby zapewnić wspólny interfejs polimorficzny dla konkretnych implementacji budowniczych (spełnienie 4 zasady SOLID odwrócenie zależności)
  • nie możemy zrezygnować z kierownika, chociaż wydaje się nadmiarowy. Niektóre implementacje tego wzorca rezygnują z niego jednak nie jest to wtedy wzorzec budowniczy zaprezentowany przez GoF.
  • dzięki oddzieleniu logiki od implementacji, możemy zmienić logikę budowania obiektu w jednym miejscu (kierownik) i zostanie ona zmieniona w całym systemie – w każdym budowniczym, a one nie będą tego świadome
  • można dojść do wniosku, że połączenie kierownikakonkretnym budowniczym bardzo przypominałoby fabrykę abstrakcyjną

Obsługa błędów

Obsługa błędów potrafi rodzić pewną konsternację wynikającą z tego, że jest wiele miejsc na ich obsłużenie. Zasada jest jednak ta sama co wszędzie – błędy należy zgłaszać tak szybko, jak tylko jest to możliwe. Oznacza to, że wyjątki powinny być rzucane na etapie wywoływań poszczególnych kroków budowniczego.

W statycznym budowniczym (o którym napisałem w akapicie niżej) pewne błędy muszą zostać obsłużone w metodzie budującej. Wynika to z faktu, że programista projektant danego rozwiązania nie ma panowania nad tym, które metody płynnego interfejsu zostaną wywołane i w jakiej kolejności. Oznacza to, że niektóre walidacje możemy wykonać w poszczególnych krokach budowniczego, jednak główną walidację obiektu musimy przeprowadzić w metodzie budującej zwracającej instancję produktu. Dzieje się tak np. gdy chcemy sprawdzić czy zbudowany obiekt samochód został (w ogóle) wyposażony w silnik.

Statyczny budowniczy

Myślę, że w wystarczający sposób opisałem cechy i zalety wzorca budowniczy, który został zaproponowany przez GoF. W programowaniu obiektowym częściej jednak korzysta z się z prostszej wersji budowniczego, która niestety jest nazywana tym samym terminem. Z tego względu czasem mogą pojawiać się pewne niezrozumienia, szczególnie – przykładowo – na rozmowach rekrutacyjnych.

Płynny interfejs

Statyczny budowniczy polega na użyciu metody zwanej płynnym interfejsem (z ang. fluent interface). Polska nazwa brzmi dość głupio, polecam zapamiętać tę angielską.

Podejście płynnego interfejsu polega na takim projektowaniu metod w paradygmacie programowania obiektowego, aby każda z nich zwracała instancję klasy, w obrębie której się znajduje. Standardowy przykład płynnego interfejsy może wyglądać następująco:

class Calc
{
    private int result = 0;

    public Calc Add(int number)
    {
        Console.WriteLine("dodaje");
        return this;
    }
    public Calc Sub(int number)
    {
        Console.WriteLine("odejmuje");
        return this;
    }
    public int GetResult() {
        return this.result;
    }
}

static void Main(string[] args)
{
    int result = new Calc().Add(5).Add(10).Sub(8).GetResult();
}

Wywołanie poszczególnych metod płynnego interfejsu w danej kolejności zwróci wynik 7. Teraz zapewne widzisz, skąd wzięła się nazwa płynny interfejs. W ten sposób bardzo często projektowane są przeróżne API (z ang. fluent API) oraz właśnie statyczny budowniczy. Nie ma znaczenia ile razy zostaną wywołane metody, za każdym razem zwracana jest instancja tej samej klasy więc możemy wywołać je ponownie.

Po co korzystać ze statycznego budowniczego

Statyczny budowniczy został po raz pierwszy zaproponowany przez Josha Blocha, w pewnej książce związanej z programowaniem w języku Java. Zauważył on, że dzięki konstrukcji budowniczego z wykorzystaniem płynnego interfejsu można osiągnąć mechanizm dający wiele korzyści:

  • uproszczenie trudnych konstruktorów
  • budowanie skomplikowanych obiektów (ale już bez oddzielenia logiki od implementacji!)
  • tworzenie obiektów niemutowalnych (ang. immutable)
  • długi/wieloetapowy proces inicjalizacji obiektu końcowego

Pozostałe zalety wzorca budowniczy opisane w akapitach wyżej, nie występują we wzorcu budowniczego statycznego opisanego Blocha.

Jak stworzyć statyczny budowniczy

Zasady tworzenia statycznego budowniczego są takie, że nie ma zasad. Dlaczego? Budowniczy opisany przez GoF jest sklasyfikowany jako wzorzec konstrukcyjny, a ze względu na jego zakres jako wzorzec obiektowy. Oznacza to, że po pierwsze, skupia się na procesie inicjalizacji obiektów, a po drugie, bazuje na powiązaniu klas poprzez kompozycje (mimo, że dziedziczenie także występuje).

Statyczny budowniczy nie jest oficjalnym wzorcem projektowym, jest podejściem do tworzenia budowniczego korzystając przy tym z płynnego interfejsu. Jest to bardziej metoda, niżeli przepis na daną architekturę. Przez to, bardzo trudno formalnie opisać jak powinien wyglądać, a co ważniejsze jego implementacja silnie zależy od języka.

W językach silnie typowanych (Java, C#) można statyczny budowniczy utworzyć w następujący sposób:

  1. Dodać do interesującego nas modelu zagnieżdżoną klasę statyczną budowniczego
  2. Dodać do budowniczego odpowiednie metody budujące przestrzegając przy tym płynnego interfejsu
  3. Dodać metodę build(), która zwróci instancję klasy

Oto przykładowy kod:

class Car
{
    private readonly Engine Engine;
    private readonly List<string> Addons;

    public Car(Engine engine, List<string> addons)
    {
        this.Engine = engine;
        this.Addons = addons;
    }

    // statyczny budowniczy
    public class CarBuilder
    {
        private Engine Engine { get; set; }
        private List<string> Addons = new List<string>();
        public CarBuilder AddEngine(Engine engine)
        {
            if (this.Engine != null)
            {
                throw new InvalidOperationException("Already added!");
            }
            this.Engine = engine;
            return this;
        }
        public CarBuilder AddAddon(string addon)
        {
            this.Addons.Add(addon);
            return this;
        }
        public Car Build()
        {
            return new Car(Engine, Addons);
        }
    }
}

static void Main(string[] args)
{
    Car car = new Car.CarBuilder()
        .AddEngine(EngineFactory.CreateEngine("120HP"))
        .AddAddon("CD radio")
        .AddAddon("Navigation")
        .Build();
}

Jak widzisz kod jest naprawdę przejrzysty. Warto zwrócić uwagę na to, że budowniczy posiada logikę walidującą poszczególne etapy inicjalizacji modelu wewnątrz obiektu budującego. Model jest więc w dalszym ciągu jest pozbawiony zbędnej logiki.

W różnych językach podejście do statycznego budowniczego może być inne. Ostatnio tworząc budowniczego na platformie Angular2 stworzyłem dwie osobne klasy, gdzie jedna była wstrzykiwalnym (@constructable) serwisem zarządzanym przez mechanizm DI Angulara, a druga była budowniczym. Musiałem tak zrobić, ponieważ mój budowniczy w znacznej mierze miał po prostu uprościć konstruktor i zabezpieczyć logikę inicjalizacji obiektu, a tworzenie klas zagnieżdżonych w TypeScript jest zabronione.

Oczywiście, mogłem użyć tzw. funkcji fabrykującej zwracającej nowy kontekst, jednak wybrałem inną metodę: z osobnymi klasami. Zdecydowałem się na to, ponieważ samo nazwanie klasy przedrostkiem Builder w pewien sposób dokumentuje kod dla przyszłego programisty. Nikt nie będzie się musiał zastanawiać dlaczego użyłem funkcji fabrykujących, a nazwa Builder sama przedstawia intencje twórcy (czyli moje).

Podsumowanie

Wzorzec budowniczy jest fajny, statyczny budowniczy też. Dobrze jest rozumieć podstawowe różnice między nimi. Poniekąd są to wzorce podobne, jednak pewne konsekwencje ich stosowania są zupełnie inne. Budowniczy nie ma poważnych wad, a jedną z największych może być jego nadużywanie tam, gdzie nie jest potrzebny – jednak to dotyczy każdego innego wzorca.

]]>
https://www.p-programowanie.pl/wzorce-projektowe/budowniczy/feed/ 0
Studia nie uczą programowania https://www.p-programowanie.pl/studia-praca/studia-nie-ucza-programowania/ https://www.p-programowanie.pl/studia-praca/studia-nie-ucza-programowania/#comments Thu, 28 Sep 2017 21:45:15 +0000 https://www.p-programowanie.pl/?p=3450 Istnieje wiele sposobów na osiągnięcie sukcesu w IT. Jednym z najpopularniejszych sposób wejścia do branży jest ukończenie studiów informatycznych. Jest to bardzo dobry start na rynku, jednak wielkie zastrzeżenia można mieć do samej metodyki. Czego można spodziewać się po ukończeniu studiów? W artykule skupię się przede wszystkim na nauce programowania.

Studia nie nauczą Cię programowania

Programowanie, tak jak cała branża IT, rozwija się tak szybko, że nikt nie jest w stanie umieć wszystkiego. Co kilka miesięcy lub lat powstają nowe wersje języków programowania oraz nowe platformy. Niektóre są podobne do poprzednich, inne całkowicie przebudowane. Zmienia się także sposób podejścia do planowania poszczególnych rozwiązań i architektur systemów informatycznych.

Aby nie trzymać Cię w niepewności, napiszę na samym początku: studia informatyczne nijak nie nauczą Cię programować. Piszę to z pełną świadomością jako zawodowy programista i absolwent studiów informatycznych. Nieważne, jak renomowaną uczelnię wybierzesz, czy będzie to uczelnia publiczna czy prywatna, czy studia dzienne czy zaoczne. Jakie są tego przyczyny?

Hermetyczne środowisko profesorów

Problem z uczelniami wyższymi leży w hermetycznym środowisku akademickim. Wykładowcy akademiccy nie zdobywają doświadczenia zawodowego przy komercyjnych projektach, nie spędzają nad programowaniem 8h dziennie. Oprócz tego ogranicza ich rama programowa i struktura narzucona przez Ministerstwo Nauki i Szkolnictwa Wyższego. Wszelkie technologie nauczane na studiach są totalnie przeterminowane i bezużyteczne.

Dosłownie nikomu nie radziłbym w dzisiejszych czasach zaczęcia nauki programowania od języka C++. O ile był on dawniej sławny, o tyle teraz jest bezużyteczny. Ciągle chętnie używają go programiści niskopoziomowi, embedded i elektronicy. Nie nadaje się on natomiast do nauki i projektów komercyjnych. Istnieje wiele nowszych języków wysokiego poziomu, które pozwolą nauczyć się programować szybciej i efektywniej.

C++ jest natomiast stałym bywalcem każdej uczelni wyższej. Czarna konsola napawa niechęcią, wielu mechanizmów w C++ brakuje, a z kolei z wielu się już nie korzysta (bo są lepsze rozwiązania).

Brak możliwości zatrudniania programistów

Przez deficyt na rynku pracy programistów ciągle brakuje. Stąd biorą się dzisiaj ich wysokie zarobki. Żadna uczelnia wyższa nie jest w stanie zatrudnić doświadczonego programisty do prowadzenia wykładów lub ćwiczeń. Co ciekawe, jest to spowodowane względami finansowymi, ale także – jak wspomniałem wcześniej – hermetycznym środowiskiem akademickim. Każdy dba o swoją posadę i nikomu nie byłoby na rękę, gdyby jeden z przedmiotów wziął ktoś „spoza” uczelni.

Jaki jest tego efekt? Na wykładach z C++ wykładowca nie umie wytłumaczyć, do czego służą wskaźniki. Nie umie znaleźć sytuacji praktycznej, w jakiej dałoby się je wykorzystać. Później nie dość, że studenci uczą się archaicznego języka, to słuchając o wskaźnikach czują się jak w kosmosie. Oczywiście, podkreślam, nie chodzi o nieumiejętność wytłumaczenia wskaźników. Wykładowcy akademiccy przeważnie bardzo dobrze umieją teorię, ale w przekazywaniu wiedzy liczy się też otoczka.

Bardzo sucha teoria

Na studiach poznaje się bardzo wiele teoretycznej wiedzy, bez jakichkolwiek przykładów jej wykorzystania. Nauka wzorców projektowych, algorytmów, metodyk wytwarzania programowania przebiega płynnie, ale student nawet nie wie, po co się tego uczy. Oczywiście ta wiedza kiedyś się przyda, jednak w momencie studiowania jest jej za dużo i jest zbyt niepoukładana.

Dobry student po dobrej uczelni, wkraczając na rynek pracy programistów, jest początkowo bezużyteczny. Potrzebuje dużo czasu i opieki doświadczonego programisty, aby móc coś sensownego tworzyć.

Praktyka? Projekty na studiach

Jeżeli zastanawiasz się, dlaczego na studiach informatycznych nie ma więcej praktyki, od razu odpowiem – jest. Istnieją duże przedmioty nazywane „projektami semestralnymi”, które pochłaniają w sylabusach ogromne ilości godzin i są poświęcone właśnie praktycznemu wykonywaniu jakichś programów.

Problem z projektami jest natomiast taki, jak w pierwszych akapitach tego artykułu:

  • prowadzą je wykładowcy z wiedzą sprzed 5-10lat
  • prowadzą je wykładowcy bez doświadczenia zawodowego
  • studenci nie mogą z nikim kompetentnym skonsultować architektury i fundamentów projektu
  • prowadzący je wykładowca kompletnie nie ma czasu dla studentów, grupy są za duże, a czasu za mało

Efektem tych zajęć są totalnie „słabo wykonane systemy”. Bez żadnych wzorców projektowych, mechanizmów, rozwiązań architektonicznych. Pełne instrukcji warunkowych, które są później bohaterami zabawnych memów w branży IT. Wszystkie projekty są pisane w PHP. Oczywiście bez żadnej dodatkowej platformy, bez wspomnienia o wzorcu MVC. Po prostu zwykłe instrukcje PHP powplatane w kod HTML, często przechowujące informacje w plikach tekstowych.

Przykład z moich studiów, którego doświadczyłem. Profesor od programowania polecił napisać w C++ grę w kółko krzyżyk. Był to projekt semestralny, od prowadzącego nie dostaliśmy żadnej instrukcji ani nakierowania. Efekt? Nikt nie zastosował algorytmu minimax, każdy oddał projekt z 400-stoma instrukcjami warunkowymi obsługującymi każdą możliwą kombinację kółka i krzyżyka na planszy.

Takie projekty nie dość, że nic nie dają, to utrwalają bardzo złe nawyki.

Niechlubna para C++ i PHP

Para języków C++ i PHP to ta podstawowa, poznawana na studiach informatycznych. Jeżeli przeglądnąć fora internetowe, to właśnie w tych językach jest powrzucanych setki „programów studenckich” będących plątaniną instrukcji warunkowych. Autorzy postów przeważnie proszą o dopisanie czegoś, bo brakuje do zaliczenia. W innym wypadku pytają czemu program nie działa; w kodzie brakuje wcięć, czasem średników czasem jakieś klamerki.

Wielu studentów, kończąc uczelnie wyższą, wie że:

  • programy dekstopowe to te pisane w C++
  • strony internetowe to te pisane w PHP i HTML

Na tym wiedza się kończy. Na studiach nikt nie porusza nic innego. O JavaScriptcie panuje przekonanie takie jak w książkach z 2003 roku, czyli że nie powinno się tego używać. Strona z JavaScriptem to nie jest prawdziwa strona. JavaScript służy tylko do tworzenia kalendarzy i zegarków.

Tymczasem mało kto wie, że obecnie JavaScript jest najczęściej używanym językiem na świecie, a od 2008 roku przeżywa bardzo silny rozwój. Mało kto wie, że stronę można zrobić w czymś innym niż PHP, że aplikacji internetowych nie robi się już w modelu request-response tylko na reużywalnych asynchronicznych komponentach. Mało kto wie, że w wielu dziedzinach odchodzi się od aplikacji desktopowych i wprowadza się aplikacje internetowe i chmury.

Dlaczego o tym piszę w dość agresywnym tonie? Po prostu, ludzie kończąc studia nagle dowiadują się, że istnieje 20 języków programowania, a do każdego około 5-20 platform programistycznych. Z czego na studiach poznali te, które nie są już używane.

Czy warto studiować?

Artykuł niesie proste przesłanie: studia jeszcze z nikogo nie zrobiły programisty. Jeżeli chcesz być programistą, warto skończyć studia informatyczne, ale pamiętaj, dzięki nim nie staniesz się programistą. Być może ukierunkujesz się na wiele aspektów, o których nie miałeś pojęcia. Ale nic poza tym.

Studia warto skończyć. Jak wspomniałem wcześniej, wiedza teoretyczna ze studiów przyda się w dalszej ścieżce zawodowej. Więcej na ten temat opisałem w artykule dlaczego warto studiować informatykę? Jeżeli masz możliwości (przede wszystkim te finansowe), bez wątpienia skończ studia. Jeżeli chcesz nauczyć się programować, przeczytaj następny akapit.

Jak nauczyć się programowania

Do nauki programowania potrzeba tylko jednego: chęci i wielu godzin ćwiczeń. Doskonałymi źródłami wiedzy są książki oraz kursy internetowe. Wybierz jakiś język, który jest aktualnie na topie i po prostu zacznij. Rankingi języków (czyli popularność i opłacalność) na stan dzisiejszy dostępne są w internecie.

Mylisz się jeżeli sądzisz, że to co napisałem to lanie wody albo pusty akapit. Programowanie zmienia się tak szybko, że jeżeli nie umiesz uczyć się sam, to nie jest to zawód dla Ciebie. W programowaniu nie ma przerwy, nie ma najmądrzejszych. Trzeba ciągle poszerzać swoją wiedzę i być na bieżąco. Jeżeli spoczniesz na laurach i zasiedzisz się w jednej technologii na 2 lata, to możesz być do tył względem rynku.

Jestem też sceptyczny, jeżeli chodzi o „szkoły programowania”. Mimo że być może dają efekty, to jest to strata kasy. Wydasz dużo kasy, nauczysz się technologii X, a za jakiś czas i tak będziesz musiał sam nauczyć się nowej technologii Y. Do tego trybu ciągłego poszerzania wiedzy trzeba się przyzwyczaić – inne dziedziny nauki nie są tak dynamiczne.

Uwaga! Wybierając kurs, wybierz ten najnowszy! Tak jak studia, tak jak całe programowanie, tak samo kursy i książki szybko się dezaktualizują. Przykładowo: ucząc się JavaScriptowej platformy Angular na dzień dzisiejszy musisz znaleźć kurs Angular2. Kursy Angular1 sprzed 2 lat są już totalnie zdezaktualizowane, a technologia jest martwa. Jeżeli wydasz pieniądze na książkę Angular1 (inna nazwa AngularJS), wydasz pieniądze w błoto. Co śmieszne, Angular1 i Angular2 nie są do siebie ani troszeczkę podobne, wiedzy z jednego nie da się wykorzystać w drugim (za pół roku pojawia się natomiast Angular5 :)) Potwierdza to konieczność ciągłego procesu nauki, od którego nie ma ucieczki.

Możesz raz nauczyć się fizyki lub matematyki, następnie uczyć tego przedmiotu w szkole średniej i sobie żyć. Nie da się natomiast raz nauczyć programowania i zasiąść w strefie komfortu w jakimś projekcie. Projekty informatyczne mają do siebie to, że ich czas życia jest dość krótki.

Wnioski

Wniosków po tym artykule może być wiele. Nie chcę, abyś pomyślał, że nie warto studiować! Chcę, abyś wiedział, że studia nie nauczą Cię programowania. Jeżeli chcesz programować zacznij sam, już teraz. Naucz się uczyć się samemu. Zainwestuj w dowolne, aktualne źródło wiedzy i ćwicz.

Studia nie są niezbędne do nauczenia się programowania. Deficyt na rynku jest tak duży, że programistami zostaje coraz więcej osób, które ze studiami informatycznymi nie mają kompletnie nic wspólnego.

]]>
https://www.p-programowanie.pl/studia-praca/studia-nie-ucza-programowania/feed/ 11
Czy matematyka jest potrzebna programiście? https://www.p-programowanie.pl/studia-praca/matematyka-potrzebna-programiscie/ https://www.p-programowanie.pl/studia-praca/matematyka-potrzebna-programiscie/#comments Fri, 25 Aug 2017 13:52:27 +0000 https://www.p-programowanie.pl/?p=3265 Pytanie o matematykę i programowanie jest jednym z najczęstszych pojawiających się na mojej skrzynce e-mailowej. Z tego powodu postanowiłem napisać kolejny artykuł, opisujący zależność pomiędzy matematyką a programowaniem. Jeżeli zastanawiasz się do czego matematyka jest potrzebna programiście, w tym artykule poznasz mój punkt widzenia na ten temat.

Związek pomiędzy matematyką i informatyką

Zaczynając ten artykuł należy wspomnieć, że informatyka jako dziedzina nauki wywodzi się bezpośrednio z matematyki. Dawniej stanowiła jej część, jednak przez jej niesamowicie szybki i daleko idący rozwój została uznana za osobną dziedzinę nauki. Informatyka z roku na rok ewoluuje a celem tego procesu jest szybsze osiąganie wyników za pomocą narzędzi jakie udostępnia.

Gdyby pokusić się o narysowanie piramidy informatyki, z całym przekonaniem jej podstawą byłaby właśnie matematyka. Z samej definicji wynika, że jest to dziedzina nauki wysoce abstrakcyjna, opierająca się na świecie cyfrowym.

Nauka programowania jest jak nauka matematyki

Z moich własnych obserwacji i doświadczeń wynika, że nauka programowania niczym nie różni się od nauki matematyki. Obydwie te nauki, jako że ze sobą bardzo związane, są naukami nienamacalnymi.

Ucząc się matematyki przechodzimy przez wszystkie klasy szkoły podstawowej, średniej i wyższej. Chyba każdy z nas poznał to uczucie, kiedy ucząc się podstaw matematyki w szkole podstawowej miał wrażenie, że nigdy nie będzie w stanie być lepszym i rozwiązywać trudniejszych zadań. Zadania ze studiów wyglądają wtedy jak magia, a uczeń czuje się przytłoczony ogromem materiału. Mimo tego niekomfortowego wrażenia lata edukacji mijają, a ludzie nie wiedząc kiedy stają się coraz lepsi. Zaczynając naukę od tabliczki mnożenia, docieramy do wielomianów, funkcji, szeregów, granic, całek, różniczek itd. Małymi krokami wszystko zaczyna się układać.

Tak samo przytłaczająca wydaje się nauka programowania. Zaczynając jego naukę od totalnych podstaw a więc podstawowych konstrukcji programistycznych, całość wydaje się bezsensowna. Programy pobierające i wyświetlające dane na czarnej konsoli wydają się niepotrzebne, a człowiek ma wrażenie, że nigdy nie będzie wstanie napisać skomplikowanego systemu. Tak jak w przypadku matematyki, jest to wrażenie złudne, ponieważ przez te początkowe etapy po prostu trzeba przejść.

Dlatego właśnie warto studiować

Podobieństwo w nauce matematyki i informatyki jest jednym z argumentów, dla których warto iść na studia informatyczne. Na studiach otrzymuje się odpowiednio poukładane porcje materiału, które podawane są odpowiedniej kolejności. Po 5 latach studiów i wielu pozornie nieprzydatnych przedmiotach, znacznie poszerza się nasz horyzont w dziedzinie informatyki. Nie tylko programowanie, nie tylko sieci informatyczne, nie tylko grafika inżynierska tylko całe fundamenty nauki jaką jest informatyka.

Nie idąc na studia nauka programowania może być przytłaczająca i ciężka. Nie mówię, że nie da się go nauczyć samemu, jednak przypomina to sytuację chęci nauczenia się matematyki ze studiów nie umiejąc tabliczki mnożenia ani ułamków. Człowiek pozostawiony sam sobie może mieć problemy z odpowiednim przejściem przez wszystkie etapy nauki, poprzez te najbardziej prymitywne (czyli pętle i wyświetlanie cyferek w czarnej konsoli) aż po bardziej złożone (kolorowe okienka, interfejsy użytkowników, wzorce projektowe, metodyki).

Dlatego programowanie jest trudne

Na koniec warto wspomnieć o interesującym fakcie, dlaczego programowanie wydaje się pozornie tak trudne? Odpowiedź jest oczywista: jego nauka jest jak nauka matematyki, a w Polsce jego nauczanie jest bardzo słabe. Jest to względnie nowa dziedzina nauki, a społeczeństwo nie nadąża z jego wprowadzeniem do szkół. Patrząc na ogromne zarobki programistów, globalną cyfryzację i wielki deficyt na rynku pracy oczywistym wydaje się, że programowanie powinno być przedmiotem szkolnym prowadzonym choćby w minimalnym wymiarze godzin – tak aby zaszczepić podstawy.

Matematyki uczymy się całe życie, pewnie około 15 lat. Programowania nie uczymy się nigdzie, wcale. Dlatego stając nagle przed decyzją „zaczynam uczyć się programowania„, człowiek staje przed ogromnym wyzwaniem opanowania potężnej dziedziny nauki, bez jakichkolwiek podstaw wiedzy. Wtedy nie wiadomo czego się łapać, a poznawanie podstaw wydaje się bezsensowne.

Gdzie ta matematyka w informatyce?

O tym, że informatyka jest powiązana z matematyką można dyskutować długo. Niektórzy się z tym zgadzają, inni stanowczo zaprzeczają. Ja jako programista skupię się oczywiście na matematyce w programowaniu. Na samym początku trzeba zrozumieć, że matematyka to nie tylko operacje algebraiczne, nie tylko analiza matematyczna. Wiele osób sądzi, że matematyki w programowaniu nie ma i wtedy zastanawiam się, czy oni chcieliby pisać kod całkami, żeby tę matematykę zobaczyć?

Dla mnie bardzo ciekawymi przedmiotami na studiach informatycznych były matematyka dyskretnametody optymalizacyjne. Gdybym miał zdefiniować matematykę dyskretną nazwałbym ją pomostem pomiędzy światem matematyki a programowania. Te dwa przedmioty są tak ściśle powiązane, że w zależności od specjalizacji nauczyciela akademickiego mogą bardziej przypominać zajęcia z programowania lub matematyki. Matma dyskretna jest nauką o zbiorach dyskretnych (nieciągłych). Jej przykładowe poddziały to:

  • logika matematyczna – bardzo fajne i przydatne. Poznaliśmy logikę Boolowską i jej prawa, prawa de Morgana, operacje na zdaniach, aksjomaty, tautologie, kwantyfikatory, implikacje, indukcje matematyczną, podstawy teorii mnogości, dopełnienia zbiorów itd. Na kolokwiach musieliśmy np. uprościć jakieś skomplikowane zdania używając praw z logiki Boola. Opiera się na nich cała elektrotechnika, służą między innymi do upraszczania obwodów elektronicznych i znajdują zastosowanie oczywiście w projektowaniu instrukcji warunkowych w programowaniu. Np. zgodnie z prawem zaprzeczenia koniunkcji instrukcję if !(p&&q) można zapisać jako if (!p || !q). Z rachunku logiki Boola korzysta się świadomie lub nieświadomie w całym życiu zawodowym, wykorzystywałem tę wiedzę także podczas programowania mikrokontrolerów AVR.
  • kryptografia – czyli jak działa szyfrowanie, jak działają szyfry jednostronne lub dwustronne. Jak to się dzieje, że asymetrycznego algorytmu RSA nie da się złamać i dlaczego są nim zabezpieczane np. komunikacje w grach MMORPG pomiędzy klientem a serwerem. Jak ważną rolę pełni brak wzoru na liczby pierwsze w algorytmach asymetrycznych i dlaczego cała kryptografia obecnie nam znana zawali się w momencie powstania komputerów kwantowych.
  • teoria gier – czyli jak program ma podejmować właściwe decyzje, dlaczego gra kółko krzyży jest grą o sumie stałej i dlaczego można za pomocą algorytmu minimax napisać robota komputerowego, który nigdy nie przegra. Dlaczego w szachach nie jest to już tak proste. Dylemat więźnia i problem wyboru obarczony stratą lub zyskiem.
  • teoria grafów – w informatyce po prostu magia. Często wykorzystywane tam, gdzie nigdy byś się tego nie spodziewał. M.in. problem znajdowania najkrótszej drogi w grach (algorytm Dijkstry), problem mostów, drzewa, reprezentacja problemów za pomocą grafów, problem najlepszego sąsiada. Nie wiem czy wszyscy wiedzą, ale dzięki teorii grafów mamy takie portale jak jakDojade.pl.

Czy czytając powyższe punkty jest sens się zastanawiać, czy matematyka jest związana z programowaniem? Opisałem bardzo ogólnie wyłączenie jeden przedmiot, którym jest matematyka dyskretna.

Być może, ktoś czytający ten artykuł chciałby poznać więcej relacji pomiędzy matematyką a programowaniem. Spróbujmy wypisać problemy programistyczne przypisane do działów matematyki:

  • operacje algebraiczne/analiza matematyczna  – pisanie wszelkiego rodzaju algorytmów, zwracanie uwagi na złożoność obliczeniową, raczej są to dedykowane i wyspecjalizowane aplikacje desktopowe. Może to być choćby program do obliczania maksymalnego naprężenia w prętach, który napisałem na 2 roku studiów jako forma zaliczenia mechaniki. Umiejętność odróżnienia algorytmów O(n^2) od O(ln(n)), rozumienie które warto optymalizować. Umieć zapisać funkcję matematyczną f(x) = x^2 jako funkcję w programie.
  • statystyka – w programowaniu raczej mało, ewentualnie zajmowanie się big data
  • optymalizacja – gdy pierwszy raz zetknąłem się z tym na studiach byłem zafascynowany. Związane z programowaniem liniowym i matematyką dyskretną. Przykładowo po poznaniu algorytmu optymalizacji liniowej simplex napisałem swój kalkulator dietetyczny dostępny pod adresem www.kalkulator-diety.pl. Jego zadaniem jest dobranie takich ilości produktów które wybiorę, aby odpowiadały mojej osobie (czyli automatyzuje układanie diety żywieniowej). Jest to też temat mojej pracy magisterskiej.
  • algorytmy genetyczne – stosunkowo nowość, mają tak szerokie zastosowanie, że aż Ciężko wszystko wypisać. Służą do zaprogramowania sztucznej inteligencji, przewidują pogodę, umieją grać na giełdzie forex. Mój współlokator używa ich do pisania aplikacji automatycznie układającej plan zajęć.
  • algebra liniowa – dużo tego jest w programowaniu gier komputerowych (nie używając silników graficznych wysokiego poziomu). Trzeba umieć obracać obiekty w przestrzeni, wszystko opiera się na macierzach i przekształceniach trygonometrycznych. Co najważniejsze trzeba robić to optymalnie, a nie metodą prób i błędów. Używane także we wszelkiego rodzaju obróbce grafiki (filtry, rozpoznawanie kształtów, analiza klatek, oprogramowanie do fotoradarów).

W obecnym projekcie używamy maszyny stanów (maszyny turinga) do definicji stanów dokumentów, też była omawiana na matematyce dysrketnej. Na koniec napiszę krótko: nie chcę mi się więcej wymyślać. Nie ma się co sprzeczać nad tym, że informatyka to matematyka, a w programowaniu też można jej spotkać całkiem sporo. Jeżeli ktoś sądzi, że w programowaniu nie ma matematyki, to zastanówmy się czego by się spodziewał? Pisania programu ułamkami? Przecież nie tylko na „liczbach” kończy się matematyka.

Jednak rozważmy dalej, czy można programować nie umiejąc matematyki?

Trzeba programować szybciej

Aby dopełnić ten artykuł i rozwiać wszystkim wszelkie wątpliwości trzeba przeanalizować jeszcze jeden ważny aspekt. Trzeba odpowiedzieć sobie na pytanie: dokąd zmierza informatyka a w szczególności programowanie? Programowanie jako ważny podzbiór informatyki rozwija się nieprawdopodobnie szybko. Praktycznie nie da się nadążyć za wszystkimi nowymi językami i platformami programistycznymi. Po co one powstają? Oczywiście po to, aby było szybciej i lepiej.

Przechodząc przez programowanie od najniższej warstwy (tzw. programowanie embedded), polegało ono na operowaniu instrukcjami procesora (czyli język assembler). Później powstał nieobiektowy język C, później jego starszy brat hybrydowy język C++. Mimo, że bardzo potężny wytwarzanie oprogramowania ciągle było zbyt wolne. Zaczęły powstać języki wysokiego poziomu czyli Java i C#. Programowanie stało się teraz ultra szybkie. Co będzie dalej? W między czasie modny jest JavaScript, ponieważ jest totalnie niezależny od platformy na jakiej się wykonuje. Do JavaScripta powstało wiele platform takich jak AngularJSReactBackboneKnockout.js. Wszystko po to aby było.. szybciej! Stworzony został serwer Node wraz z setkami tysięcy gotowych modułów (sławny node_modules, który często ważą 50GB, czyli 1000 razy więcej niż sam projekt).

Wszystkim zależy na czasie, programowanie rozwija się w stronę szybkiego wytwarzania kodu. Programiści są drodzy, a pisanie systemów informatycznych trwa bardzo długo. Pisanie programów „wolno” nie ma sensu, ponieważ zanim powstaną już stają się przeterminowane. W tym momencie pojawiają się określenia tzw. programisty oraz dewelopera.

Programista a deweloper

Na różnicę pomiędzy programistą a deweloperem nakierował mnie kiedyś wykładowca od mechaniki. Na zajęciach wywiązała się ciekawa dyskusja, na temat różnicy pomiędzy jednym a drugim. Po krótszej chwili doszliśmy do wniosku, że programista to osoba odpowiedzialna typowo za klepanie kodu. Za napisanie algorytmu, który ma działać tak i tak, który ma pobrać jakieś dane i zwrócić wynik. Programista nie zajmuje się niczym więcej. Fajnie gdyby programista miał wysokie umiejętności matematyczne w momencie gdy pisze bibliotekę, która będzie odpowiedzialna np. za przeprowadzanie obliczeń numerycznych.

Z drugiej strony deweloper (ang. software developer) jest osobą, na którą trzeba spojrzeć nieco szerzej. Jest on odpowiedzialny za większą koncepcję, ma wymyślić sposób, umieć połączyć kilka technologii lub kilka rozwiązań. Dla programisty z krwi i z kości ważne mogą okazać się umiejętności matematyczne/fizyczne/mechaniczne lub inne, w zależności w jakim projekcie pracuje. Dla dewelopera ważniejsze może się okazać znanie wielu technologii, umiejętność wykorzystania gotowych rozwiązań i bibliotek dostarczonych przez programistów.

Gdzieś kiedyś usłyszałem, że programista opracowuje skomplikowane biblioteki, które mają działać najszybciej jak to możliwe, a deweloper ma umieć z tych bibliotek korzystać. I tak np. pisząc gry deweloperzy korzystają z gotowych silników graficznych, którą są napisanie idealnie pod względem wydajności, i nie jest im do tego potrzebna matematyka. Z drugiej strony, nad silnikiem graficznym musieli pracować programiści, który na pewno nie mięli problemów z algebrą liniową.

Podział na programistę i dewelopera nie jest moim wymysłem. Choć w naszym kraju nie jest ten podział zbytnio respektowany, to jednak odwrotnie jest w USA. Tam stanowisko programisty (ang. programmer) a stanowisko dewelopera (ang. software developer) jest po prostu inaczej definiowane, te osoby odpowiedzialne są za inne rzeczy.

Czy można programować nie umiejąc matematyki

Każdy medal ma dwie strony. Mimo, że matematyka jest przydatna oczywiście można programować bez niej. Przede wszystkim matematyka nie jest potrzebna front-endowcom (ang. front-end developer). Tworzenie szablonów, pisanie HTMLa lub reguł CSS nie wymagają żadnych skomplikowanych obliczeń. Głębszy front-end czyli programowanie w jakimś frameworku JavaScript także nie będzie od nas wymagać matematyki. Tworząc aplikację w Angular, React lub czystym JavaScript potrzebne dane najpewniej zostaną dostarczone przez back-end.

Podsumowując artykuł powiedzmy otwarcie, matematyka niezbędna programiście (deweloperowi) nie jest. Można naprawdę dużo zarabiać i nie używać matematyki. Natomiast nie postrzegajmy matematyki jako tylko i wyłączenie obliczeń algebraicznych, bo ich jest stosunkowo mało. Patrzmy na nią jako zdolność analitycznego i abstrakcyjnego myślenia, która zawsze może przydać się projektując skomplikowane fragmenty systemu. Błyskotliwość, szybkość znajdywania rozwiązań są cechami pożądanymi w naszym zawodzie. Oto kilka ważnych wniosków:

  • w informatyce i programowaniu jest bardzo dużo matematyki – jest to po prostu dziedzina związana z matematyką i bazująca na matematyce, jeżeli sądzisz inaczej to jesteś ignorantem
  • matematyka to nie tylko ułamki i całki – szczególnie w kontekście programowania. Doskonałym przykładem jest matematyka dyskretna, która wprowadza w świat pomiędzy matematyką a programowaniem, uczy logicznego myślenia, ukazuje mechanizmy które warto znać i umieć z nich korzystać
  • można być dobrym matematykiem i nie umieć programować – no wiadomo, tak się zdarza, znam realne przypadki (nie znam na to wytłumaczenia)
  • można być dobrym deweloperem i nie umieć matematyki – można zajmować się np. aplikacjami internetowymi, nie pakować się w problemy, których nie umiemy rozwiązać (czyli dedykowane aplikacje bazujące na skomplikowanych obliczeniach)
  • nie można być dobrym programistą i nie umieć matematyki – ale chyba wszystko zależy od kontekstu, jak rozumiemy różnicę pomiędzy programistą a deweloperem i czy taką granicę w ogóle uznajemy. No nie oszukujemy się, będąc matematycznym zerem silnika do gry nie napiszemy, symulacji fizycznej nie napiszemy, biblioteki do obliczeń metodą MES nie napiszemy, biblioteki rozwiązującej całki metodą Newtona-Cotesa nie napiszemy. Możemy co najwyżej, jako deweloper, wykorzystać istniejące.
  • wielu sławnych programistów, którzy są nieprzeciętnie dobrzy, są z wykształcenia matematykami
  • bardziej potrzebni są deweloperzy niż programiści – jest mało projektów, które wymagają wysokich zdolności matematycznych. Nikt nie zatrudni matematycznego zera do pisania takiej aplikacji jak MatLab, jednak powiedzmy sobie szczerze takich stanowisk jest mniej. IT stoi mocno na aplikacjach bankowych, systemach z rodziny ERP służących do zarządzania firmami itp. Tam matematyki nie ma. Jeżeli ktoś mądry napisał bibliotekę do obsługi formatu Mp3 (dzwięk w formacie mp3 zapisany jest w postaci szeregu furiera), to każdy inny program będzie z niej korzystał. Nikt nie wprowadza swoich kodowań audio, tylko korzysta z istniejących i sprawdzonych.

Na sam koniec warto jeszcze wspomnieć, że umiejętności matematyczne mogą nam się przydać podczas szukania pracy. Wierzcie lub nie, jednak na Krakowskim rynku IT są firmy, które na rozmowie rekrutacyjnej dają zadania z czystej matematyki i każą je rozwiązać na kartce. Na szczęście takie firmy to odosobnione przypadki.

]]>
https://www.p-programowanie.pl/studia-praca/matematyka-potrzebna-programiscie/feed/ 3
Fabryka abstrakcyjna – robisz to źle https://www.p-programowanie.pl/wzorce-projektowe/fabryka-abstrakcyjna/ https://www.p-programowanie.pl/wzorce-projektowe/fabryka-abstrakcyjna/#comments Fri, 11 Aug 2017 23:53:19 +0000 https://www.p-programowanie.pl/?p=3197 Fabryka abstrakcyjna i wszystkie jej odmiany są rodziną konstrukcyjnych wzorców projektowych. Dzięki fabryce otrzymujemy interfejs, służący do generowania różnych obiektów, które go spełniają. Fabryka abstrakcyjna i metoda wytwórcza są bardzo często mylone ze sobą. W wielu źródłach zaprezentowane są ich błędne implementacje, a programiści często sami nie wiedzą, której wersji fabryki chcą użyć.

Programowanie bez fabryki

W normalnym podejściu do programowania obiektowego, wszelkie instancje nowych obiektów tworzone są za pomocą słowa kluczowego new. Aby utworzyć instancję musimy związać się z jej konkretnym typem, podać go zaraz za rozkazem new. Dodatkowo musimy znać i wypełnić konstruktor. Przy takim podejściu programista nie jest w stanie w łatwy sposób zmienić sposobu tworzenia danego obiektu.

Fabryka i wszelkie jej odmiany służą temu, aby odciąć się od podawania konkretnego typu. Zamiast tego, wykorzystana zostaje tzw. metoda fabrykująca zwracająca instancję, która nas interesuje. Krótko mówiąc, zamiast używania rozkazu new i przypisywania instancji do jakiejś zmiennej, wywołujemy metodę fabrykującą np. CreateLoggerInstance() i to ona jest odpowiedzialna, za zwrócenie instancji.

Po co używać wzorca fabryki?

Dzięki użyciu fabryki programista zyskuje abstrakcyjną warstwę, odpowiedzialną za tworzenie instancji obiektów w jakiś sposób powiązanych wspólnym interfejsem. Uzyskujemy wtedy kod, który jest bardziej skalowalny, łatwiejszy na rozbudowę. Oto najważniejsze zalety używania fabryk:

  • spełnia zasadę odwrócenia zależności (dependency inversion principle). Rodzina tworzonych obiektów spełnia wspólny interfejs, którym następnie się posługujemy. Wpływa to bardzo pozytywnie na testowalność kodu (przykład mechanizmu IoC). Przykładowo zamiast tworzyć nowe klasy new Apple()new Banana() itd. posiadamy metodę fabrykującą zwracającą obiekty interfejsu IFruit.
  • skupienie logiki w metodzie fabrykującej, dzięki czemu zmiany w kodzie można wprowadzić w jednym miejscu systemu.
  • dostarcza dodatkową warstwę abstrakcji dzięki czemu hermetyzuje (enkapsuluje) odpowiednią logikę wewnątrz fabryki. Hermetyzacja jako filar programowania obiektowego niesie ze sobą szereg kolejnych korzyści m.in. uproszczenie kodu, brak powtarzalności, ukrycie logiki pomiędzy warstwami.
  • upraszcza proces inicjalizacji skomplikowanych klas (w przypadku rozbudowanych konstruktorów).
  • w łatwy sposób pozwala na reużywalność kodu (procesu inicjalizacji danej rodziny klas) w innym miejscu systemu
  • spełnia zasadę otwarty na rozbudowę, zamknięty na modyfikację (open/closed principle). Do fabryki w dość łatwy sposób można dodać dodatkową klasę, mając przy tym pewność, że nie zepsujemy czegoś co aktualnie działa.

Powyższe zalety fabryk wynikają z bardzo szerokiego spojrzenia na ten wzorzec projektowy. Trzeba pamiętać, że fabryki nie są lekarstwem na całe zło programowania obiektowego. Często są nadużywane w miejscach, gdzie nie powinno ich być.

Kiedy użyć wzorca fabryki

Zapamiętać należy zasadę: fabryk używamy tam, gdzie chcemy odciąć się od tworzenia instancji klas posługując się konkretnym typem. Może być to spowodowane np. skomplikowaną logiką tworzenia instancji. Np. wiemy, że chcemy uzyskać instancję spełniającą jakiś interfejs, ale jaki to będzie konkretny typ, zależy od dodatkowych parametrów – wtedy używamy fabryki. Powody za użyciem fabryk:

  • klasa ma skomplikowany, przeciążony wielokrotnie konstruktor. Oznaczać to może oczywiście błędnie zaprojektowaną klasę, ale może też wskazywać na to, że utworzenie instancji klasy potrzebuje dodatkowej logiki.
  • utworzenie instancji klasy jest poprzedzone instrukcją warunkową if. Z całą pewnością Taką logikę należy zahermetyzować wewnątrz fabryki
  • w momencie pisania programu nie wiesz, której instancji potrzebujesz (bo np. jest to uzależnione od parametru zwracanego z API). Ponownie, logikę hermetyzujemy wewnątrz fabryki.

Fabryka abstrakcyjna (i jej odmiany) jest jednym z pierwszych wzorców projektowych, które poznaje programista (obok singletona). Z tego powodu, początkujący programiści często próbują wrzucać fabryki tam, gdzie nie są one potrzebne. Pojedyncza metoda zwracająca instancję jest natomiast bardziej antywzorcem, niż dobrym nawykiem.

Idealny przykład do użycia fabryki to kod, który wygląda mniej więcej tak:

IManageArea manageArea;
    Employee employee = employeeService.Load("Managment", 42);

    if (employee.isManager)
    {
        manageArea = new GlobalManageArea();
    }
    else
    {
        manageArea = new LocalManageArea();
    }

    manageArea.FireEmployees(1000);
    // itd

Pomijając sensowność kodu, widać tutaj logikę, od której zależy utworzenie instancji klasy. Całość powinna zostać zahermetyzowana wewnątrz fabryki.

Kiedy nie używać wzorca fabryki

W następujących sytuacjach użycie wzorca fabryki może okazać się błędne lub co najmniej nadmiarowe:

  • nie tworzyć fabryki „na wszelki wypadek”, bo może kiedyś będę chciał zainicjalizować obiekt w inny sposób (złamanie zasady YAGNI).
  • nie tworzyć fabryki, jeżeli w jej wnętrzu znajduje się pojedynczy wielki switch bez logiki, zwracający instancję klas z domyślnym konstruktorem. Jest to redundancja i wprowadzenie nowej warstwy abstrakcji (na wszelki wypadek), tam gdzie nie jest ona potrzebna.
  • nie obudowywać kontenerów IoC w fabrykę, bo przeważnie nie jest to konieczne (kontener IoC nie jest fabryką, ale spełnia podobne zadanie i daje podobne efekty)

Jak wielu programistów tyle opinii, czy aby na pewno słuszne jest założenie, aby nie tworzyć fabryki na wszelki wypadek, bo być może kiedyś będę potrzebował zainicjalizować obiekt w inny sposób. Pozornym argumentem popierającym tę tezę jest to, że zahermetyzowanie inicjalizacji w osobnej warstwie umożliwi nam w przyszłości zmianę sposobu inicjalizacji w jednym miejscu. Jest to też jedna z zalet wzorca fabryk abstrakcyjnych. Nie należy jednak pchać fabryki wszędzie i próbować rozwiązywać nią problemów, do których nie jest dedykowana.

Wykorzystywanie fabryk jako osobnego pojemnika do inicjalizacji obiektów na dłuższą metę okaże się zgubne. Języki obiektowe C#, Java (oraz pół-obiektowe) C++ wymagają związanie klasy z typem za pomocą słowa kluczowego new i nie zbyt wiele programista może z tym zrobić. Użycie prostej fabryki, nie „będzie” bardziej spełniać zasad SOLIDu, a dobry i testowalny kod da się osiągnąć innymi mechanizmami takimi jak dziedziczenie, kompozycja czy generyczność. Tworzenie prostych fabryk doprowadza w skrajności do sytuacji posiadania „super klasy” z dziesiątkami metod CreateXXXInstance() – co ostatecznie nie ma to nic wspólnego ze wzorcem fabryki.

Prosta fabryka (simple factory)

Prosta fabryka jest najczęściej używanym rodzajem fabryki. Doskonale sprawdza się w nieskomplikowanych przypadkach. Jest prosta do zaimplementowania a jednocześnie daje programiście korzyści, które wynikają ze stosowania wzorca fabryk. Ta odmiana wzorca, moim zdaniem, ma szczególne upodobanie wśród początkujących programistów, którzy chcieliby gdzieś zastosować jakiś wzorzec, a simple factory jest jednym z prostszych do użycia. Oto przykładowy kod:

enum ShapeType
    {
        Square = 1,
        Triangle = 2
    }

    interface IShape { }

    class Square : IShape { }
    class Triangle : IShape { }

    class ShapeFactory
    {
        public IShape CreateShape(ShapeType shapeType)
        {
            switch (shapeType)
            {
                case ShapeType.Square:
                    return new Square();
                    break;

                case ShapeType.Triangle:
                    return new Triangle();
                    break;

                default:
                    throw new ArgumentException();
            }
        }
    }

    static void Main(string[] args)
    {
        ShapeFactory shapeFactory = new ShapeFactory();

        IShape triangle = shapeFactory.CreateShape(ShapeType.Triangle);
    }

Kod jest trywialnie prosty. Tworzenie poszczególnych instancji zostało zahermetyzowane w osobnej warstwie abstrakcji, którą jest klasa fabryki. W akapicie wyżej napisałem, że nie ma sensu tworzyć fabryk, których jedyną zawartością jest wielki, nic nie wnoszący switch, jednak jest to tylko przypadek testowy.

Wadą prostej fabryki jest to, że nie spełnia drugiej zasady SOLID czyli Open/closed principle. Mimo wielu zalet jakie uzyskał programista decydując się na tę fabrykę, rozbudowa jej o nowe elementy niesienie ze sobą konieczność ingerencji w klasę fabryki. Konkretniej oprócz rozszerzenia interfejsu IShape o nowe klasy, należy rozbudować instrukcję switch wewnątrz klasy fabryki.

Można też zastosować wersję statyczną. Metoda zwracająca instancje może być oznaczona jako static wtedy nie trzeba będzie za każdym razem tworzyć instancji fabryki. Wybór oczywiście zależy od specyfikacji systemu, jednak ja stanowczo trzymam się od klas statycznych z daleka. Klas statycznych nie da mockować, wcześniej czy później są z nimi problemy (szczególnie platforma ASP.NET MVC). Użycie klas statycznych często oznacza brak możliwości wprowadzenia testów jednostkowych.

Metoda fabrykująca (factory method)

Metoda fabrykująca to najczęściej używany rodzaj fabryki, wśród programistów, którzy dobrze rozumieją działanie poszczególnych rodzajów fabryk. Jest to odmiana najbardziej uniwersalna. Oprócz niesienia ze sobą wszystkich korzyści wynikających ze wzorca fabryki, spełnia też pierwszą zasadę SOLID czyli open/closed principle. Dlaczego? Ponieważ kod odpowiedzialny za tworzenie instancji został przeniesiony do klas potomnych poszczególnych fabryk. Oto przykładowy kod:

interface IShape { }

    class Square : IShape { }
    class Triangle : IShape { }

    abstract class ShapeFactory
    {
        public abstract IShape CreateShape();
    }

    class SquareFactory : ShapeFactory
    {
        public override IShape CreateShape()
        {
            return new Square();
        }
    }

    class TriangleFactory : ShapeFactory
    {
        public override IShape CreateShape()
        {
            return new Triangle();
        }
    }

    static void Main(string[] args)
    {
        ShapeFactory shapeFactory = new SquareFactory();

        IShape square = shapeFactory.CreateShape();
    }

Jak widać w powyższym kodzie klasa fabryki jest teraz klasą abstrakcyjną. Logika umieszczona w switchu prostej fabryki została zastąpiona mechanizmem polimorfizmu. Proces tworzenia instancji klas został wydelegowany do klas potomnych, i to one decydują, jaką instancję zwrócić. Dzięki temu, przy rozbudowie powyższego kodu programista nie musi zmieniać klasy fabryki, co oznacza że spełniona jest zasada open/closed principle. Dopisanie nowego typu polega jedynie na dopisaniu nowej fabryki.

Powyższy przykład jest prosty, aby maksymalnie ukazać koncepcje metody fabrykującej. Dzięki temu, że korzystamy z metody fabrykującej mamy jednak kolejną ważną zaletę, której nie widać w przykładzie. Klasa abstrakcyjna może posiadać metody, które odziedziczą klasy potomne. Dzięki temu mamy większe pole możliwości niż w prostej fabryce, której sednem działania była instrukcja warunkowa switch.

Zalety metody fabrykującej
  • spełnia zasadę SOLID open/closed principle, jest łatwiejsza do późniejszych modyfikacji.
  • główny kod fabryki może być dystrybuowany w postaci osobnej, zamkniętej biblioteki DLL, mimo to programista i tak będzie miał możliwość jej rozbudowy.
  • dodatkowa warstwa abstrakcji, która umożliwia hermetyzację skomplikowanej logiki tworzenia instancji obiektu w klasach pochodnych (w prostej fabryce wszystko byłoby w switchu).
  • poszczególne fabryki pochodne można w łatwy sposób testować jednostkowo, należy tylko wprowadzić dodatkowy interfejs, które będą spełniać (oprócz klasy abstrakcyjnej). Prostej fabryki natomiast testować się nie da.

Fabryka abstrakcyjna (abstract factory)

Fabryka abstrakcyjna jako ostatnia z odmian wzorca fabryk sprawia zawsze najwięcej kłopotów. Główna zasada mówi, że wzorzec fabryki abstrakcyjnej ma dostarczyć interfejs do tworzenia rodziny obiektów, konkretniej oddzielić interfejs od implementacji. Najważniejszym słowem kluczowym jest tutaj rodzina obiektów, która nie występuje w przypadku dwóch poprzednich fabryk.

Ale, że o co chodzi? Dwóch poprzednich fabryk, czyli fabryki prostej i metody fabrykującej używamy, gdy chcemy tworzyć obiekty spełniające jeden interfejs. W poprzednich akapitach tworzyliśmy figury geometryczne spełniające interfejs IShape. Nie możemy przedstawić poprzedniego przykładu w formie fabryki abstrakcyjnej, ponieważ figury geometryczne to pojedynczy typ, a nie rodzina obiektów.

Przykład z figurami geometrycznymi nie jest zbyt trafny, ponieważ ciężko na szybko wymyślić jakąś sensowną rodzinę obiektów. Załóżmy jednak, że chcemy tworzyć typy figur spełniające interfejs IShape oraz zbiory liczbowe spełniające interfejs INumber. Obydwa typy tworzą rodzinę obiektów MathTest, czyli naszą fabryką abstrakcyjną będzie fabryka zwracająca test. W momencie gdy zdefiniowaliśmy umowną rodzinę obiektów, możemy zamodelować fabrykę abstrakcyjną. MathTest będzie naszą fabryką abstrakcyjną, a konkretne fabryki abstrakcyjne będą jej różnymi odmianami.

Spójrzmy na przykładowy kod:

// kształty
    interface IShape { }

    class Square : IShape { }
    class Triangle : IShape { }

    // liczby
    interface INumber { }

    class RealNumber : INumber { }
    class ComplexNumber : INumber { }

    // fabryka abstrakcyjna rodziny obiektów
    abstract class MathTestFactory
    {
        public abstract IShape CreateShape();
        public abstract INumber CreateNumber();
    }
        
    // konkretna fabryka rodziny obiektów
    class PrimarySchoolTestFactory : MathTestFactory
    {
        public override IShape CreateShape()
        {
            return new Square();
        }

        public override INumber CreateNumber()
        {
            return new RealNumber();
        }
    }

    // konkretna fabryka rodziny obiektów
    class HighSchoolTestFactory : MathTestFactory
    {
        public override IShape CreateShape()
        {
            return new Triangle();
        }

        public override INumber CreateNumber()
        {
            return new ComplexNumber();
        }
    }

    // klasa klienta (kontekst wykonania fabryki)
    class MathTest
    {
        private MathTestFactory mathTestFactory;

        public MathTest(MathTestFactory mathTestFactory)
        {
            this.mathTestFactory = mathTestFactory;
        }

        public void GenerateTest()
        {
            var shape = this.mathTestFactory.CreateShape();
            var number = this.mathTestFactory.CreateNumber();
            System.Console.WriteLine("Test wygenerowany");
        }
    }

    static void Main(string[] args)
    {
        MathTest mathTest;

        mathTest = new MathTest(new PrimarySchoolTestFactory());
        mathTest.GenerateTest(); // łatwy test przy użyciu PrimarySchoolTestFactory()

        mathTest = new MathTest(new HighSchoolTestFactory());
        mathTest.GenerateTest(); // trudny test przy użyciu PrimarySchoolTestFactory()
    }

Klasa MathTestFactory jest fabryką abstrakcyjną definiującą rodzinę obiektów. Za pomocą kompozycji definiujemy, że obydwa elementy są w jakiś sposób od siebie zależne. Na potrzeby artykułu przyjęliśmy, że tworzą „test”. Następnie tworząc konkretne egzemplarze fabryk, możemy rodzinę obiektów stworzyć na wiele sposób i tak np. PrimarySchoolTestFactory tworzy rodzinę obiektów bardzo prostych (kwadrat i zbiór liczb rzeczywistych). Natomiast HighSchoolTestFactory tworzy rodzinę obiektów trudnych (trójkąt i zbiór liczb zespolonych).

Ostatnim najważniejszym elementem fabryki abstrakcyjnej jest klasa „klienta” (kontekstu wykonania fabryki). Za pomocą kompozycji tworzymy klasę zawierającą odpowiednią fabrykę. Często w klasie klienta istnieje też namiastka wzorca projektowego metody szablonowej, która wywołuje metody fabryki (w kodzie wyżej metoda GenerateTest()). Klasa klienta służy do obsługi logiki wymaganej przy tworzeniu rodziny obiektów, hermetyzuje inicjalizację rodziny obiektów oraz ich konfigurację. Klient nie wie, jaka fabryka zostanie mu dostarczona i wywołana – ważne aby spełniała typ fabryki abstrakcyjnej.

Rola poszczególnych elementów fabryki abstrakcyjnej

  • fabryka abstrakcyjna – definiuje umowną rodzinę obiektów.
  • konkretna fabryka abstrakcyjna – implementuje fabrykę abstrakcyjną, dostarcza implementację jej metodom, zawiera logikę.
  • klient – musi zawierać odniesienie do fabryki abstrakcyjnej (za pomocą kompozycji), zawiera metodę (na wzór metody szablonowej) wywołującą metody dostarczonej fabryki, nie wie z jakiej fabryki będzie korzystał (logika zahermetyzowana w innych warstwach).

Mimo zalet wzorca fabryki abstrakcyjnej, niesie on ze sobą także wady. Jest o wiele trudniejszy do rozbudowy, łamie zasadę open/closed principle. Dodając nowy produkt, trzeba zmodyfikować wiele innych klas np. konkretne fabryki, a dodając nowy typ do rodziny obiektów trzeba zmodyfikować klasę fabryki abstrakcyjnej. Czasem trzeba zmodyfikować także kod klasy klienta.

Podsumowanie wzorca fabryki

Wszelkie odmiany fabryki abstrakcyjnej potrafią przysporzyć wiele problemów nawet zaawansowanym programistom. Osobiście wzorzec fabryki uważam za najbardziej skomplikowany wzorzec gangu czworga (gank of fours – ojcowie wzorców i paradygmatu OOP). Jego niezwykła komplikacja polega na wielu możliwościach implementacji, a niniejszy artykuł pokazał tylko małą jej część. Przykłady zawarte w artykule są ubogie, a poszczególne rodzaje fabryk można znacząco modyfikować i łączyć z innymi wzorcami np. wzorcem strategii i wzorcem polecenia. Mimo to, poniższe wskazówki pomogą Ci ogarnąć temat fabryk:

Prosta fabryka
  • najprostsza możliwość oddzielenia implementacji od interfejsu.
  • brak dziedziczeniabrak kompozycji.
  • brak osobnej klasy klienta.

Prosta fabryka zawsze zwraca obiekt spełniający jeden interfejs. Jest ciężka do testowania i ciężka do rozbudowy. Czasem (niesłusznie) uważana za antywzorzec, bo bywa nadużywana tam gdzie wcale nie powinno być fabryki.

Metoda fabrykująca
  • najbardziej uniwersalna możliwość oddzielenia implementacji od interfejsu (najlepiej spełnia open/closed principle).
  • zawsze występuje mechanizm dziedziczenia.
  • brak kompozycji.
  • brak osobnej klasy klienta.

Metoda fabrykująca zawsze zwraca obiekt spełniający jeden interfejs. Jest bardzo skalowalna, idealna do testowania i rozbudowy. Zawsze korzysta z mechanizmu dziedziczenia i polimorfizmu, przenosząc odpowiedzialność na klasy pochodne (czyli konkretne fabryki).

Fabryka abstrakcyjna
  • umożliwia tworzenie rodzin obiektów w jakiś sposób powiązanych ze sobą.
  • zawsze występuje mechanizm dziedziczenia.
  • zawsze występuje mechanizm kompozycji.
  • zawsze występuje dedykowana klasa klienta.

Minusem fabryki abstrakcyjnej jest mała skalowalność, dodając nowy produkt trzeba przerobić klasę fabryki abstrakcyjnej oraz konkretnych fabryk. Często nazywana jest fabryką fabryk, ponieważ tak naprawdę korzystając z mechanizmu kompozycji zwraca metody fabrykujące. Zawsze występuje mechanizm dziedziczenia i polimorfizm (do konkretnych fabryk) oraz mechanizm kompozycji (do powiązania klienta z określoną fabryką). Jej sednem jest to, że klient wykonuje jakieś operacje, ale nie wie z jakiej fabryk korzysta. Bez osobnej klasy klienta jest po prostu metodą fabrykującą. Bez zwracania rodziny obiektów (tylko pojedynczego typu) jest po prostu metodą fabrykującą.

Mam nadzieję, że ten artykuł choć trochę rozjaśnił Ci rodzaje fabryk oraz różnic, jakie między nimi występują.

]]>
https://www.p-programowanie.pl/wzorce-projektowe/fabryka-abstrakcyjna/feed/ 5
Po co testy jednostkowe? https://www.p-programowanie.pl/paradygmaty-programowania/po-co-testy-jednostkowe/ https://www.p-programowanie.pl/paradygmaty-programowania/po-co-testy-jednostkowe/#comments Sat, 22 Apr 2017 20:11:46 +0000 https://www.p-programowanie.pl/?p=3168 Wielu programistów ma bardzo różne podejście do testów jednostkowych. Niektórzy piszą, bo muszą. Inni nie lubią lub nie rozumieją, trzymają się z daleka. Co tak naprawdę dają nam testy jednostkowe i czy warto zaprzątać sobie nimi głowę?

Czym są testy jednostkowe?

Testy jednostkowe to metoda testowania wytwarzanego oprogramowania, polegająca na pisaniu metod testujących określone, małe fragmenty naszego programu (jednostki). Jednostkami mogą być np. metody lub klasy. Kończąc studia informatyczne lub zaczynając przygodę z programowaniem ciężko dostrzec zalety pisania testów jednostkowych. Wszystko wydaje się niby proste, jednak nasuwa się pytanie: po co testować coś, co sami piszemy? Brzmi to paradoksalnie jak np. sprawdzanie poprawności zadania matematycznego rozwiązanego przez nas samych.

Dodatkowym zniechęceniem do pisania testów mogą okazać się cechy testów jednostkowych takie jak:

  • żadnego oprogramowania nie jesteśmy przetestować całkowicie
  • nigdy nie mamy pewności znalezienia wszystkich błędów, nawet gdyby pokrycie testami wynosiło 100% (co jest nierealne)
  • testy wydłużają czas wytwarzania oprogramowania praktycznie nawet o połowę

Po tych wszystkich rozważaniach nasuwa się pytanie: po co?

KIedy warto testować

Przede wszystkim należy uzmysłowić sobie kiedy warto pisać testy jednostkowe do aplikacji. Jeżeli piszemy prosty programik i mamy pewność, że będziemy rozwijać go samemu, to testy raczej nie będą potrzebne. Nie będą potrzebne także w przypadku programów, które chcemy napisać
„raz i zostawić”, nie nastawiamy się na ich systematyczny rozwój.

Duże projekty i wielu programistów

Całkiem inaczej w dużych projektach pisanych przez kilka osób. Tworzenie dużego projektu niekiedy może przypominać spagetti. Każdy programista dopisuje do projektu swoje własne metody i klasy, rozwija istniejące funkcje i dodaje nowe. W takim przypadku brak testów może w przyszłości nieść wiele negatywnych skutków.

Każdy programista ma swój styl kodowania, który dodatkowo zmienia się pod wpływem czasu i doświadczenia. Jeden kod napisany przed dwóch różnych programistów, działający tak samo, będzie prawdopodobnie wyglądał inaczej. Przykładem może być proste przekazywanie argumentów do funkcji.

Odmienne style pisania kodu

Wyobraź sobie sytuację, w której musisz napisać funkcję wypisującą na ekran informacje o osobie. Jeden programista zadeklaruje ją następująco: void PrintPersonInfo(Person person) a inny tak: void PrintPersonInfo(string name, string lastname, int age). Niby to samo, a jednak nie. Oczywiście poprawnym wyborem jest deklaracja druga. Nie wpływa ona negatywnie na wartość metryki Coupling Between Objects z zestawu metryk CK. Metryka CBO mierzy stopień sprzężenia pomiędzy klasami i powinna być ona jak najmniejsza (typy proste nie zwiększają wartości wskaźnika).

Będąc programistą dużego projektu, musisz wiedzieć, że spotkasz się z sytuacjami takimi jak powyższa. Wynika to z różnego poziomu doświadczenia współpracowników a także choćby z ich języków bazowych z jakich się wywodzą. Uogólnienie parametru funkcji poprzez typ złożony nie będzie niczym dziwnym dla JavaScriptowca, jednak programista języków silnie typowanych np. C# powinien tego unikać.

Kiedy ktoś użyje typów prostych raczej nic się nie wysypie pod wpływem czasu. Jeżeli ktoś użyje typu złożonego takiego jak klasa Person, to nikt tak naprawdę nie wie, jak ta klasa będzie wyglądać za 4 miesiące lub za rok. Jeżeli na dodatek będzie ona wykorzystana jako model warstwy biznesowej można być pewnym bugów.

W takich momentach niezbędne są testy. Współpracując z wieloma osobami nie jesteśmy w stanie wpływać na jakość ich kodu. Testy jednostkowe gwarantują nam to, że nie musimy. W minimalnym stopniu chronią nas one przed zmianami, które zajdą w projekcie za wiele miesięcy lub lat. Mimo, że my o czymś zapomnimy a inny programista nie będzie wiedział, że element systemu, który reużywa jest używany gdzieś indziej, to testy będą pamiętać o tym za nas.

Jakie ma być pokrycie kodu testami?

Pisanie testów wydłuża proces tworzenia oprogramowania. M.in. z tego powodu nie jest realne aby projekt był w 100% pokryty testami. Jest to niepotrzebne, wręcz może prowadzić do błędów. To ile testów napisać zależy od czasu jakim dysponujemy i przede wszystkim od przeczucia. Warto testować fragmenty kodu, które są bardzo newralgiczne i używane w wielu miejscach.

Podobno dobre pokrycie testami to 40%, jednak czy ta informacje coś mówi? Można mieć duże pokrycie testami a żadnych korzyści, lub małe pokrycie testami i wielkie korzyści. Zależy to od jakości testów i elementów, które testujemy.

Testowanie regresyjne

W jednym z projektów (ERP) jaki współtworzyłem nie pisaliśmy testów jednostkowych. Cały projekt w miarę działał dopóki nie pojawiła się magiczna funkcja kolejki zadań. Kolejka była fragmentem systemu działającym dosłownie wszędzie. Każda nowa funkcja, która się pojawiała, w jakiejś części musiała zintegrować się z kolejką.

Pomijając fakt, że kolejka była źle zaprojektowana przez programistę, który już nie pracował, nie dało się jej nawet dotknąć. Jedna zmiana wysypywała stare funkcje systemu przetestowane wiele miesięcy wcześniej. Nie dało się jej nawet dotknąć w znaczeniu jak najbardziej dosłownym. Dopisanie nowego parametru lub dodanie nowego typu zadania, psuło funkcje systemu zaimplementowane 8 miesięcy temu.

Pomyślisz „no dobrze, nie ma testów jednostkowych więc trzeba testować ręcznie?” – otóż nie. Nie da się przetestować wszystkich scenariuszy systemu tworzonego latami przez kilku/kilkunastu programistów. W tak dużym i złożonym systemie dopisanie jednej funkcji „do kolejki” wymagałoby 2 dni testowania aplikacji. Drobna poprawa funkcji? Kolejne dwa dni testowania. Dopisujemy nową funkcję? Kolejne kilka dni testowania. Tak wyglądający proces testowania oprogramowania jest kosmiczny.

Lekarstwem byłyby tutaj testy jednostkowe. Ich pokrycie mogłoby wynosić chociaż 8% projektu. Ważne, aby pokryta była bardzo wrażliwa funkcja systemu, z której korzysta wiele elementów systemu. Zmieniając lub dopisując coś po wielu miesiącach wystarczyłoby odpalić testy, aby przekonać się czy nie zepsuliśmy kogoś pracy.

Testy wymagają pisania przemyślanego kodu

Kolejną zaletą testów jednostkowych jest pisanie mądrego kodu. Na studiach lub świeżo po studiach nic na ten temat nie wiadomo, jednak po kilku miesiącach pracy zawodowej zaczyna się poznawać wiedzę tajemną. Otóż, nie wszystkie fragmenty kodu da się pokryć testami. Aby dało się napisać test jednostkowy, kod programu musi być testowalny.

Przykładem platformy programistycznej, która umożliwia pisanie nietestowalnego kodu jest ASP.NET MVC. Nie wiem jak będzie wyglądać sytuacja w .NET Core, jednak MVC w żaden sposób nie wymusza na programiście eleganckich rozwiązań. Jeżeli nie pomyślisz, lub ktoś w zespole nie pomyśli, i nie wprowadzi do projektu wzorca repozytorium z jakimś mądrym mechanizmem Inversion of Control (czyli odwrócenia zależności np. dependency injection) to wasz kod będzie absolutnie nietestowalny. Jeżeli ktoś zacznie używać klas statycznych, pisać tzw. klasy helperki czyli niby do wszystkiego, niby do niczego, to wasz kod będzie absolutnie nietestowalny.

Każdy obiekt, z którego chcesz skorzystać, powinien zostać w jakiś sposób wstrzyknięty. Powinien spełniać jakiś interfejs i być od niego zależny (zasady SOLID). Dzięki interfejsom można napisać test i zamockować np. obiekt bazy danych, lub warstwę repozytorium.

Miłym zaskoczeniem było dla mnie tutaj zetknięcie się z platformą AngularJS, w której czy chcesz, czy nie chcesz, musisz korzystać z mechanizmu wstrzykiwania zależności. Idąc dalej, praktycznie zawsze jesteś w stanie przetestować swoje serwisy i kontrolery, co np. nie uda się w ASP.NET MVC.

Jeżeli w projekcie macie testy, to programista pisząc bubla szybko zorientuje się, że tak to działać nie może, ponieważ nie da się tego przetestować. Wymusi to na nim konieczność refaktoryzacji lub ponownego zaimplementowania funkcji.

Szkoda czasu na testy? Oj nie..

Jest to najpiękniejszy argument za pisaniem testów jednostkowych, z którym spotkałem się na szkoleniu z TDD, oraz odczułem jego skutki na własnej skórze. Możesz myśleć, że szkoda Ci czasu na pisanie testów, jednak cały ten czas zwróci się z nawiązką.

Zadanie, które możesz napisać w 5h z testami napiszesz bez testów w 3h. Pozornie zaoszczędziłeś 2h pracy, jednak napisałeś nietestowanego/nietestowalnego bubla. Teraz każdy inny programista, który będzie musiał w jakiś sposób skorzystać z Twoich metod, klas, modułów, będzie musiał przeprowadzać testy ręczne. Będzie musiał linijka po linijce starać zrozumieć się, dlaczego w 97 linijce jest jakaś dziwna instrukcja warunkowa, sprawdzająca czy zmienna priceDiff jest równa zero, i dlaczego jak jest równa zero to wyskakujesz z obiektu pętli. Jeżeli po jakimś czasie da radę się domyślić – pół biedy. Jeżeli nie da rady się domyślić a projekt skompiluje się bez błędów, to ktoś i tak zmarnuje czas szukając później błędów w działaniu produkcji.

Brak testów to brak możliwości refaktoryzacji

Niezależnie od poziomu naszej naszej wiedzy oraz od naszego doświadczenia, każdy każdy programista ma świadomość konieczności systematycznej refaktoryzacji pisanego kodu. Refaktoryzacja i optymalizacja są tematami tak obszernymi, że należałoby o nich napisać osobny artykuł.

Warto jednak brać pod uwagę, że refaktoryzacja w dużym projekcie, który nie ma testów, jest po prostu całkowicie niemożliwa. Bez testów programista może sobie pozwolić najwyżej na pozmienianie nazw zmiennych, ponieważ jakiekolwiek próby rozbicia klas na osobne podklasy prawdopodobnie zakończą się powstaniem błędów.

Podsumowanie

W każdym z wypadków wiele czasu da się oszczędzić świadomie pisząc testy jednostkowe dla odpowiednich elementów systemu. Nie chodzi tu o to, aby starać się być z testami w granicach 30-40% pokrycia. To tylko wskaźnik, który może mówić wszystko albo nic. W sztuce pisania testów należy wiedzieć co testować i gdzie testy będą niezbędne, a gdzie okażą się stratą czasu.

Czy pisanie testów wydłuża proces pisania oprogramowania? – zdecydowanie tak, dlatego testy trzeba pisać mądrze, pokrywać testami tylko wrażliwe elementy systemu.

Po co testować coś co sami napisaliśmy? Nie piszemy testu, aby przetestować własny kod. Testy piszemy na zapas, to jak inwestycja na przyszłość. Test nie ma na celu wychwycenia naszych błędów, tylko ewentualne błędy podczas przyszłej refaktoryzacji (szczególnie jeżeli wykona ją ktoś inny niż my). Jeżeli tworzymy jakąś funkcję np. kalkulator walut, zakładamy że jesteśmy jedyną osobą, która ma wiedzieć jak ma ten kalkulator działać. Inni nie wiedzą, dlaczego trzeba pokryć go testami.

Pisanie testów zawsze zaprocentuje w przyszłości, chyba że piszemy testy „sztuka dla sztuki”, pokrywamy testami niepotrzebne elementy.

]]>
https://www.p-programowanie.pl/paradygmaty-programowania/po-co-testy-jednostkowe/feed/ 7
Filtry https://www.p-programowanie.pl/kurs-angular/filtry/ https://www.p-programowanie.pl/kurs-angular/filtry/#respond Sat, 25 Mar 2017 23:11:13 +0000 http://www.p-programowanie.pl/?p=3014 Filtry w Angularze są bardzo przydatnym narzędziem. Pomagają formatować dane i są wygodnym sposobem na reużywalność kodu. Tworząc aplikację w AngularJS mamy dostępnych wiele gotowych filtrów. Jeżeli nie spełnią one naszych oczekiwań, możliwe jest dopisanie swoich. Ich główną zaletą w stosunku do zwykłych funkcji jest to, że możemy ich używać zarówno po stronie kontrolerów jak i w widokach. Sprytne operowanie filtrami potrafi znacznie skrócić kod aplikacji.

Użycie filtrów w widoku

Używanie wbudowanych filtrów jest bardzo proste. Filtr możemy dokleić do dowolnego wyrażenia, oraz niektórych dyrektyw. Przykładowe użycie wygląda następująco:
h
{{ wyrażenie | <nazwa_filtru> }}

Po wywołaniu powyższego kodu podczas wywołania wyrażenia zostanie dla niego zastosowany filtr. Filtry łączymy za pomocą operatora pionowej kreski (tzw. pipe).  Filtry możemy ze sobą łączyć, są one wywoływane w kolejności od lewej do prawej:

{{ wyrażenie | <filtr1> | <filtr2> | <filtr3> }}

Co ciekawe, niektóre filtry mogą przyjmować argumenty. Przekazujemy je do filtra po dwukropku.

{{ wyrażenie | <filtr1> | <filtr2>:<arg1> | <filtr3>:<arg1>:<arg2> }}

Lista dostępnych filtrów

Dla wygody programista AngularJS udostępnia wiele wbudowanych filtrów. Oto ich lista:

Filter Opis
currency Formatuje liczby do postaci waluty
date Formatuje datę według wzorca
filter Filtruje elementy kolekcję
json Wyświetla obiekt jako tekst JSON
limitTo Przycina tablice lub tekst do określonej długości
lowercase Zamienia tekst na małe litery
number Formatuje liczbę według wzorca
orderBy Sortuje po określonym parametrze
uppercase Zamienia tekst na duże litery

Przykład użycia currency

Currency jest bardzo wygodnym filtrem do formatowania walut. Jako jeden z wielu filtrów korzysta z tzw. locale z biblioteki i18n. Jeżeli dołączymy odpowiedni plik locale do projektu (np. z konfiguracją polską) wtedy domyślną walutą filtra będzie polski Złoty. Jeżeli nie dołączymy ustawień lokalizacji domyślną walutą będzie amerykański Dolar. Oto przykładowy kod:

<script>
	var app = angular.module("mainModule", []);
 
	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
		$scope.kwota = 2500.25;
	}
</script>
 
<body ng-app="mainModule" ng-controller="simpleCtrl">
	<p>{{kwota | currency}}</p>
	<p>{{kwota | currency:'PLN'}}</p>
	<p>{{kwota | currency:''}} zł</p>
	<p>{{ 1234 | currency:''}} zł
</body>

Efekt działania jest następujący:

Zauważ jaka duża jest różnorodność osiągniętych wyników. W pierwszym przypadku wykorzystane zostały ustawienia domyślne. W drugim przypadku zmieniliśmy symbol waluty przekazując wprost parametr PLN.

Domyślnie znak dolara wyświetlany jest przed kwotą. Chcąc wyświetlić polską walutę  za liczbą, możemy posłużyć się sztuczką. Przekazujemy pusty parametr waluty a interesującą nas walutę dopisujemy bezpośrednio poza wyrażeniem. Innym lepszym rozwiązaniem byłoby dołączenie odpowiedniej konfiguracji biblioteki i18n przeznaczonej dla Polski.

Przykład użycia filter

Filter o nazwie filter służy do odfiltrowywania elementów kolekcji o określonych parametrach. Łączymy go z dyrektywą ng-repeat. Ma naprawdę sporo możliwości. Z jego pomocą łatwo można zrobić Angularową wyszukiwarkę. Oto przykładowy kod:

<script>
	var app = angular.module("mainModule", []);
 
	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {

		$scope.pracownicy = [
			{id: 1, imie: "Karol", wiek: 24},
			{id: 2, imie: "Kaja", wiek: 51},
			{id: 3, imie: "Tomek", wiek: 22},
			{id: 4, imie: "Justyna", wiek: 39},
			{id: 5, imie: "Kazek", wiek: 8}
		]
	}
</script>
 
<body ng-app="mainModule" ng-controller="simpleCtrl">
	<p>Szukaj: <input type="text" ng-model="szukaj"></p>
	<table>
		<tr ng-repeat="pracownik in pracownicy | filter : szukaj track by pracownik.id">
			<td>{{pracownik.imie}}<td>
			<td>{{pracownik.wiek}} lat</td>
		</tr>
	</table>
</body>

Wynik działania jest następujący:

Pole wyszukiwania zbindowane jest bezpośrednio ze zmienną szukaj. Jej deklaracja nie była potrzebna w $scope ponieważ odbędzie się automatycznie. Do dyrektywy ng-repeat został natomiast dołączony filter. Filtruje on wartości właśnie po zmiennej szukaj. Zwróć uwagę, ponieważ filtry wykonują się od lewej do prawej dlatego track by musiał zostać dołączony za filtrem, aby indeksować elementy przefiltrowane a nie te, które filtr wytnie.

Filtrowanie odbywa się po wszystkich atrybutach obiektów kolekcji pracownicy. Z tego powodu wpisanie wartości 5 wyświetliło nam Kazimierza, który nie ma 5 lat – ma natomiast atrybut id z taką wartością.

Możemy filtrować kolekcję tylko po jednej wartości przesyłając do filter obiekt z nazwą atrybutu:

<tr ng-repeat="pracownik in pracownicy | filter : {imie: szukaj} track by pracownik.id">
	<td>{{pracownik.imie}}<td>
	<td>{{pracownik.wiek}} lat</td>
</tr>

Takie ustawienie spowoduje, że wyszukiwane będą tylko elementy, których atrybut imie dopasowuje się do zmiennej szukaj.

Przykład użycia json

Za pomocą filtra json możemy w bardzo prosty sposób wyświetlić dowolny obiekt JavaScript podpięty do naszego $scope. Przykładowy kod:

<script>
	var app = angular.module("mainModule", []);
 
	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
	
		$scope.osoba = {
			imie: "Karol",
			wiek: 24,
			ulubionePiosenki: ['aaa', 'bbb', 'ccc']
		}
	}
</script>
 
<body ng-app="mainModule" ng-controller="simpleCtrl">
	<pre>{{osoba | json}}</pre>
</body>

Efekt działania:

Przykład użycia orderBy

Filter orderBy jest niezwykle pomocnym filtrem, używanym do budowania zaawansowanych list. Możemy zastosować go do dyrektywy ng-repeat i bardzo dobrze łączy się z on z filter filter. Używając go możemy ustalić po jakim atrybucie przefiltrować kolekcję:

<script>
	var app = angular.module("mainModule", []);
 
	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
	
		$scope.atrybutSortowania = 'id';
	
		$scope.osoby = [
			{id: 1, imie: "Karol", wiek: 24},
			{id: 2, imie: "Zenon", wiek: 12},
			{id: 3, imie: "Maciej", wiek: 33},
			{id: 4, imie: "Beata", wiek: 12}
		];
		
		$scope.sortuj = function(atrybut) {
			$scope.atrybutSortowania = atrybut;
		}
	}
</script>
 
<body ng-app="mainModule" ng-controller="simpleCtrl">
	<table>
		<tr>
			<th ng-click="sortuj('id')">id</th>
			<th ng-click="sortuj('imie')">imie</th>
			<th ng-click="sortuj('wiek')">wiek</th>
		</tr>
		<tr ng-repeat="osoba in osoby | orderBy: atrybutSortowania track by osoba.id">
			<td>{{osoba.id}}</td>
			<td>{{osoba.imie}}</td>
			<td>{{osoba.wiek}}</td>
		</tr>
	</table>
</body>

Efekt działania:

Kod jest rozbudowany, ale bardzo prosty. Filter orderBy przyjmuje jako parametr nazwę atrybutu kolekcji, po której ma sortować. Jeżeli parametr nie zostanie przekazany jako string wtedy szuka on funkcji lub zmiennej podpiętej do $scope o takiej nazwie. W naszym przypadku dodana została funkcja, ustawiająca parametr sortujący na właściwą wartość. Sortowanie odbywa się po parametrze sortującym.

Łącząc filtr orderBy z filter filter można uzyskać zaawansowaną tabelkę z możliwością sortowania kolumn i dynamicznego wyszukiwania. Dokładając do nich filter limitTo można stworzyć paginację wartości np. po 50 elementów w tabelce na stronę.

Użycie filtrów w kontrolerze

Filtrów Angularowych można użyć także w kodzie kontrolera aplikacji. W tym celu należy skorzystać z serwisu o nazwie $filter. Zwraca on instancję filtra o nazwie przekazanej jako argument. Aby móc użyć serwisu należy wstrzyknąć go jako zależność do kontrolera. Przykładowy kod wygląda następująco:

<script>
	var app = angular.module("mainModule", []);
 
	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope, $filter) {
	
		var kwota = 123456.60;

		$scope.kwotaFormatowana = $filter('currency')(kwota);
		
		$scope.kwotaFormatowana2 = $filter('currency')(kwota, '');
		
	}
</script>
 
<body ng-app="mainModule" ng-controller="simpleCtrl">
	<p>{{kwotaFormatowana}}</p>
	<p>{{kwotaFormatowana2}} zł</p>
</body>

Efekt działania:

Oprócz wykorzystania filtru, mamy możliwość przesyłana do niego parametrów co widać w przykładzie wyżej. Wyczyszczony został domyślny symbol waluty a następnie ręcznie w widoku dopisana waluta .

Własne filtry

Platforma AngularJS pozwala na pisanie własnych filtrów. Nie jest problemem napisanie własnej funkcji, która filtruje tablicę po określonej wartości atrybutu, jednak dzięki wprowadzeniu filtrów kod aplikacji Angular jest uporządkowany. Filtry znajdują się w filtrach, kontrolery w kontrolerach a komponenty w komponentach.

Z tego powodu zachęcam Cię, do trzymania funkcji odpowiedzialnych za filtrowanie tablic właśnie w postaci filtrów.

Aby utworzyć własny filter należy skorzystać z funkcji konstruującej wywołanej na instancji modułu. Przykładowo:

<script>
	var app = angular.module("mainModule", []);
 
	app.filter('mojFilter', function() {
		//cialo filtra
	});
</script>

Tak samo jak w przypadku kontrolerów, możemy stworzyć filter jako funkcję anonimową lub normalną:

<script>
	var app = angular.module("mainModule", []);
 
	app.filter('mojFilter', function() {
		//anonimowy
	});
	
	app.filter('mojFilter2');
	function mojFilter2() {
		//normalny
	}
</script>

Przykładowy własny filter

Stwórzmy własny filter Angularowy wyświetlający tylko ludzi wysokich. Ważną cechą filtra musi być jego skalowalność, a więc możliwość dynamicznej edycji wzrostu progowego. Kodem początkowym niech będzie następujący prosty kod:

<script>
	var app = angular.module("mainModule", []);
 
	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope, $filter) {
		$scope.osoby = [
			{id: 1, imie: "Karol", wzrost: 180},
			{id: 2, imie: "Arek", wzrost: 178},
			{id: 3, imie: "Natalia", wzrost: 172},
			{id: 4, imie: "Przemek", wzrost: 192},
		];
	}
	
</script>
 
<body ng-app="mainModule" ng-controller="simpleCtrl">
	<table>
		<tr ng-repeat="osoba in osoby track by osoba.id">
			<td>{{osoba.imie}}</td>
			<td>{{osoba.wzrost}}cm</td>
		</tr>
	</table>
</body>

Aby dodać filter należy:

  • zdefiniować nazwę filtra poprzez funkcję konstruującą <moduł>.filter(<nazwa_filtra>).
  • podpiąć do filtra funkcję filtra, która zwróci anonimową funckję filtrującą
  • anonimowa metoda filtra powinna przyjmować parametry kolekcji wejściowej oraz kryterium filtrowania
  • anonimowa metoda filtra powinna zwrócić przefiltrowaną tablicę wyników

Metoda filtra może w naszym wypadku wyglądać następująco:

function wysocyLudzieFilter() {
	return function(input, wzrostProgowy) {
		var przefiltrowane = [];
		
		//jezeli ktos nie podał parametru to przyjmij 180cm
		if (typeof wzrostProgowy === "undefined") {
			wzrostProgowy = 180;
		}
		
		for (var i = 0; i<input.length; i++) {
			if (input[i].wzrost >= wzrostProgowy) { //jeżeli spełnia warunek wzrostu
				przefiltrowane.push(input[i]) //to dodaj do nowej tablicy
			}
		}
		
		return przefiltrowane; //zwróć nową tablicę
	}
};

Pierwszym parametrem input jest kolekcja wejściowa, zostanie ona przesłana do filtra automatycznie. Kolejne argumenty to dodatkowe parametry filtra, które możemy zdefiniować. W powyższym kodzie jest to wzrostProgowy powyżej którego człowiek jest uznawany za wysokiego. Jeżeli nie zostanie on przekazany do filtra, wtedy przyjmijmy że jego wartość wynosi 180.

Po podpięciu funkcji do instancji filtra należy zastosować go w dyrektywie ng-repeat:

<script>
	var app = angular.module("mainModule", []);
 
	app.controller('simpleCtrl', simpleController);
	app.filter('wysocyLudzieFilter', wysocyLudzieFilter);
	
	
	function simpleController($scope, $filter) {
		$scope.wzrostMin = 180;
		
		$scope.osoby = [
			{id: 1, imie: "Karol", wzrost: 180},
			{id: 2, imie: "Arek", wzrost: 178},
			{id: 3, imie: "Natalia", wzrost: 172},
			{id: 4, imie: "Przemek", wzrost: 192},
		];
	}
	
function wysocyLudzieFilter() {
	return function(input, wzrostProgowy) {
		var przefiltrowane = [];
		
		//jezeli ktos nie podał parametru to przyjmij 180cm
		if (typeof wzrostProgowy === "undefined") {
			wzrostProgowy = 180;
		}
		
		for (var i = 0; i<input.length; i++) {
			if (input[i].wzrost >= wzrostProgowy) { //jeżeli spełnia warunek wzrostu
				przefiltrowane.push(input[i]) //to dodaj do nowej tablicy
			}
		}
		
		return przefiltrowane; //zwróć nową tablicę
	}
};
	
</script>
 
<body ng-app="mainModule" ng-controller="simpleCtrl">
	<input type="number" ng-model="wzrostMin" /> <br><br>
	<table>
		<tr ng-repeat="osoba in osoby | wysocyLudzieFilter:wzrostMin track by osoba.id">
			<td>{{osoba.imie}}</td>
			<td>{{osoba.wzrost}}cm</td>
		</tr>
	</table>
</body>

Efekt działania:

]]>
https://www.p-programowanie.pl/kurs-angular/filtry/feed/ 0
Czy warto iść na studia magisterskie? https://www.p-programowanie.pl/studia-praca/czy-warto-isc-studia-magisterskie/ https://www.p-programowanie.pl/studia-praca/czy-warto-isc-studia-magisterskie/#comments Wed, 22 Mar 2017 19:52:07 +0000 http://www.p-programowanie.pl/?p=3026 Duże zainteresowanie tematem studiów zainspirowało mnie do napisania kolejnego artykułu poświęconego edukacji. Problem, z którym ostatnio się spotkałem był koniecznością podjęcia decyzji: kontynuować studia czy nie? Jestem studentem informatyki dlatego cały artykuł będzie dotyczył właśnie tego kierunku. Kierunek kierunkowi nie równy, dlatego nie wszystkie moje spostrzeżenia mogą mieć odzwierciedlenie gdzie indziej.

Studia dwustopniowe

W mojej skromnej opinii całe zamieszanie ze studiami wynika z wprowadzenia dwustopniowego systemu edukacji wyższej. Każdy absolwent pierwszego stopnia (czy to licencjat, czy to inżynier) musi podjąć decyzję o kontynuowaniu nauki bądź nie. Z jednej strony ogromną pokusą jest oddanie się pracy, bez martwienia się o kolejne kolokwia, zaliczenia, egzaminy. Z drugiej strony rozsądek podpowiada dokończyć sprawę i kilkanaście lat edukacji przypieczętować tytułem magistra.

Nie wiem dlaczego wprowadzono system dwustopniowy na wzór amerykański. Dla mnie osobiście lepiej byłoby gdyby studia były jednolite. Nie stałbym wtedy przed żadnym dylematem. Wielu moich kolegów z którymi dyskutowałem ma podobne wnioski. Na pewno wielu osobom dwustopniowy podział odpowiada. Może okazać się ratunkiem w wielu trudnych życiowych sytuacjach (ciąża, choroba, sprawy osobiste). O wiele łatwiej jest przypieczętować inżyniera/magistra po 3 latach, niż ciągnąć jednolite studia magisterskie trwające 5 lat. Przez 5 lat w życiu może się wiele wydarzyć, więc rozdzielenie tego okresu na dwa mniejsze daje jakąś gwarancję.

Dla mnie niesamowitą wadą obecnego systemu jest zmarnowanie ostatniego semestru na poszczególnych stopniach. Nie jest tajemnicą, że ostatni semestr jest prostszy. Jest w nim mniej zajęć, większą wagę przykłada się do pisania pracy dyplomowej. W systemie dwustopniowym takie semestry są dwa, wiec jest to w sumie rok studiów stracony. Idąc dalej tym tokiem rozumowania, studia magisterskie to rok porządnej nauki. Z trzech semestrów robią się już tylko dwa.

Studiowanie pokrewnego kierunku

Za zaletę dwustopniowego podziału na pewno nie można uznać możliwości studiowania dwóch pokrewnych kierunków studiów. Chociaż brzmi to fajnie, to w życiu rzeczywistym nie ma odwzorowania. Poznałem kilka osób, które wybrały na studia magisterskie inny kierunek studiów niż na pierwszym stopniu. Każda z tych osób na studiach wypadała, powiedzmy, dość słabo. Ktoś kto nie studiował informatyki i ma z nią mało wspólnego, nie ma żadnych szans nadrobić wiadomości z pierwszego stopnia.

Pewnego dnia wdałem się w dyskusję na ten temat z moim profesorem od programowania i też w 100% się ze mną zgodził, wręcz więcej, powiedział że jest to bardzo zły system. Z tym prowadzącym miałem na pierwszym stopniu przedmiot o wzorcach projektowych oraz kolejny przedmiot programowanie obiektowe. Na magisterce z tym samym prowadzącym mieliśmy programowanie obiektowe zaawansowane. Prowadzący zapytał mnie retorycznie: „Jak mam zrealizować z wami zaawansowane programowanie obiektowe, skoro na drugi stopień przyszły osoby bez znajomości podstaw informatyki?”. Taka sytuacja jest niekorzystna zarówno dla prowadzącego, jak i dla studentów. My oczekiwaliśmy szerszej wiedzy, inni oczekiwali wstępu do programowania a prowadzący starał się realizować zajęcia dwóch prędkości.

Magisterka powinna rozszerzyć wiedzę z pierwszego stopnia

Osobiście nigdy nie odważyłbym się na podjęcie studiów drugiego stopnia na kierunku innym niż mój bazowy czyli informatyka. Ukończyłem technikum informatyczne, zdobyłem tytuł inżyniera z zakresu informatyki, skąd nagle miałbym pojawić się na mechanice, matematyce, automatyce lub budownictwie? Zapewne zginąłbym na każdym z tych kierunków ze strzępkami wiedzy jakie posiadam. Istnieje powiedzenie, że jak coś jest do wszystkiego to jest do niczego. Osobiście także uważam, że lepiej być specem w danej, konkretnej dziedzinie (single responsibility – wiadomo podstawa OOP).

Jeżeli ktoś obudził się i stwierdził, że kierunek z pierwszego stopnia nie jest jego powołaniem (wkraczamy tu już na rejony lekko patologiczne), to o wiele lepszym wyborem, byłoby zaczęcie drugiej inżynierki/licencjatu na nowym kierunku, niż kontynuacja magisterki na nowym kierunku.

Dlatego dochodząc do konsensusu, mimo niezaprzeczalnych plusów studiów dwustopniowych dla mnie jest to tandeta. Nikt nie każe mi zmieniać kierunku? – tak zgadzam się. Jednak przez to, że istnieje taka możliwość, poziom studiów magisterskich w moim odczuciu jest zbyt niski. Na drugim stopniu oczekiwałbym rozszerzenia wiadomości z pierwszego stopnia a nie ich powtarzanie i wałkowanie w kółko. Jeżeli ktoś chce podstaw, powinien zacząć kolejne studia pierwszego stopnia (jeśli ma na to czas).

Studia magisterskie zaocznie? Czemu nie

Mimo, że jestem fanem studiów dziennych, jako jedynego słusznego trybu studiowania, to uważam, że studia magisterskie można ukończyć zaocznie. Mam świadomość, że w innym moim artykule prezentuję odmienne podejście. Teraz gdy ujrzałem na oczy problemy opisane w powyższym akapicie stwierdzam, że na magisterkę naprawdę szkoda czasu. Studia magisterskie przesycone są bezużytecznością, niezależnie czy dziennie czy zaocznie. Z tego też powodu bardzo żałuję, że nie było mi pisane studiować na studiach jednolitych. Przypuszczam, że program studiów jednolitych jest po prostu lepiej ułożony, nie sądzę aby w siatce znajdowały się powtórzone przedmioty.

Należy pamiętać, że studia magisterskie wiążą się z koniecznością dłuższego studiowania (4 semestry zamiast 3) oraz koniecznością opłacania czesnego za każdy semestr nauki (co w przypadku informatyków nie powinno być problemem). Plusy i minusy trzeba przekalkulować samemu.

Osobiście ukończyłem studia magisterskie studiując dziennie i pracując na pełny etat, więc mój dzień zaczynał się o 7 rano a kończył o 19 (doliczając siłownię o 22). Nie wiem czy zdecydowałbym się na taki krok drugi raz w życiu.

Dlaczego zdecydować się na magisterkę

Sadzę, że powodów dlaczego warto ukończyć studia magisterskie jest wiele. Jeżeli ukończyłeś studia pierwszego stopnia, oznacza to, że faktyczny proces Twojej edukacji trwa już dobre 15 lat. Naprawdę warto poświęcić ten dodatkowy rok i przypieczętować wszystko magistrem. Dlaczego?

Stopień licencjata lub inżyniera jest pełnoprawnym wykształceniem wyższym w Polsce. We wszystkich małych firmach tyle wystarczy, raczej nikt nigdy nie będzie od Ciebie wymagał magistra. Ważne, abyś znał się na tym co robisz. Sytuacja może wyglądać inaczej w korporacji, gdzie procedury są takie a nie inne. Często w dużych firmach nikogo nie interesują Twoje umiejętności dopóki nie spełnisz określonych kryteriów. Co z tego, że jesteś dobry, skoro zostaniesz odrzucony już na etapie preselekcji przez rekruterów?

Wykształcenie wyższe w postaci tytułu magistra to międzynarodowy tytuł master of science respektowany w bardzo wielu krajach. Natomiast polskie stopnie inżyniera i licencjata respektowane są bardzo różnie. Inaczej patrzą na nie na zachodzie Europy a inaczej w Anglii. Poza tym, zagraniczne firmy przyjmują znaczną uwagę do posiadania tytułu magistra i są mi znane przypadki powrotu z emigracji w celu uzupełnienia wykształcenia (właśnie w zawodzie programisty).

Nikt nie jest w stanie przewidzieć jak potoczy się jego kariera. W zawodzie programisty bardzo częstym zjawiskiem jest zjawisko zawodowego wypalenia się. Dotyka to naprawdę wielu programistów. Jednym ze sposobów przeciwdziałania jest zwyczajnie zmiana stanowiska. Można zostać kierownikiem projektu, architektem systemowym, zająć się menedżerką zamiast klepania kodu. W tym wypadku magister z pewnością okaże się niezbędny. Jest to niejako powiązane z awansem.

Jest to moje spojrzenie na papierek poświadczający posiadanie magistra i właśnie dlatego zdecydowałem się na studia drugiego stopnia.

]]>
https://www.p-programowanie.pl/studia-praca/czy-warto-isc-studia-magisterskie/feed/ 9
Wstrzykiwanie zależności https://www.p-programowanie.pl/kurs-angular/wstrzykiwanie-zaleznosci/ https://www.p-programowanie.pl/kurs-angular/wstrzykiwanie-zaleznosci/#respond Sun, 18 Dec 2016 01:17:00 +0000 http://www.p-programowanie.pl/?p=2990 Rozumienie mechanizmu wstrzykiwania zależności nie jest Ci niezbędne do stworzenia prostej aplikacji AngularJS, jednak na pewno da Ci dobre spojrzenie na działanie całego frameworka. Wstrzykiwanie zależności jest bardzo popularnym wzorcem projektowym, który sprawdza się w aplikacjach modularnych bardzo dobrze. Pisząc aplikację w AngularJS przede wszystkim dzielimy ją na dziesiątki modułów, kontrolerów oraz serwisów dlatego twórcy Angulara nie mogli nie zaimplementować tego mechanizmu.

Idea wstrzykiwania zależności

Rozważmy hipotetyczną sytuację, w której nasz obiekt wywołuję operację wypisywania logów do konsoli. W tym celu w dowolnej funkcji w naszym $scope wywołujemy metodę Console.writeline(). Wypuszczając aplikację produkcyjne logów musimy się pozbyć. Jeżeli nasza aplikacja ma kilka tysięcy linii kodu problematyczne będzie usuwanie wszystkich wywołań tej funkcji w kodzie.

Tutaj z pomocą przychodzi wstrzykiwanie zależności a więc tzw. odwrócenie kontroli. Główna idea wstrzykiwania zależności polega na tym, aby nie wiązać obiektu referencją do innego obiektu z którego korzysta na stałe. Zamiast tego lepiej przekazać tę referencję np. przez konstruktor albo parametr. Dzięki temu zmieniając obiekt wypisywania logów na ekran w jednym miejscu, zmienia się zachowanie całej aplikacji, która korzysta z tego obiektu. Mało tego, możemy wstrzykiwany obiekt podmienić i wstrzyknąć tam coś zupełnie innego.

Wstrzykiwanie zależności do kontrolera

Aby wstrzyknąć zależność do kontrolera w AngularJS wystarczy dopisać nazwę obiektu do parametrów funkcji kontrolera. Właśnie w ten sposób we wszystkich poprzednich wpisach wstrzykiwany był obiekt $scope.:

W kolejnych artykułach do kontrolera trzeba będzie wstrzykiwać zależności do serwisów, aby zwiększyć jego funkcjonalność. W bardziej rozbudowanych aplikacjach często na liście zależności jest ich więcej niż 10.

Adnotowanie wstrzykiwanych obiektów

Wypuszczając aplikację AngularJS produkcyjnie przeważnie minifikuje się jej kod. Po pierwsze aby zmniejszyć rozmiar pliku a po drugie aby kod zaciemnić. Podczas minifikacji nazwy parametrów wszelkich funkcji są zamieniane na jednoliterowe odpowiedniki (można ponieważ JavaScript nie jest silnie typowany). Z tego powodu twórcy AngularJS umożliwili opisywanie wstrzykiwanych obiektów.

Aby adnotować wstrzykiwanie zależności trzeba wypełnić tablicę $inject dostępną na obiekcie funkcji kontrolera. Wygląda to następująco:

var app = angular.module("mainModule", []);

app.controller('simpleCtrl', simpleController);

simpleController.$inject = ['$scope'];
function simpleController($scope) {
	$scope.uzytkownik = {
		login: '',
		haslo: ''
	};
	$scope.logowanie = function() {
		console.log($scope.uzytkownik.login 
					+ ' ' 
					+ $scope.uzytkownik.haslo);
	}
}

Ponieważ wartości tekstowe nie są minifikowane ani zaciemniane (rozwaliłoby to aplikację gdyby było inaczej) adnotacje podawane są jako ciągi znaków. Dzięki temu zamiast korzystać z nazwy $scope w parametrze kontrolera możemy zmienić jej nazwę na dowolnie inną:

var app = angular.module("mainModule", []);

app.controller('simpleCtrl', simpleController);

simpleController.$inject = ['$scope'];
function simpleController(lala) {
	lala.uzytkownik = {
		login: '',
		haslo: ''
	};
	lala.logowanie = function() {
		console.log(lala.uzytkownik.login 
					+ ' ' 
					+ lala.uzytkownik.haslo);
	}
}

Działanie aplikacji zupełnie się nie zmieniło.

Dodawanie adnotacji do wstrzykiwanych zależności jest standardem. Od teraz będzie się pojawiać w każdym kolejnym wpisie o Angularze. Aby kompilator był wstanie dopasować adnotację do zależności pozycje w tablicy adnotacji $inject muszą się pokrywać z kolejnością parametrów funkcji kontrolera. Jest to bardzo ważna zasada.

Notacja inline

W wielu miejscach w internecie można też spotkać się z innym formatem dodawania adnotacji do wstrzykiwanych zależności umieszczonych „w jednej linii”. Aby osiągnąć taki cel, kontrolery muszą być definiowane za pomocą funkcji anonimowej. Wygląda to tak:

// kontroler jako funkcja anonimowa
app.controller('simpleCtrl', function($scope) {
	$scope.uzytkownik = {
		login: '',
		haslo: ''
	};
});

// kontroler jako funkcja anonimowa z adnotacjami
app.controller('simpleCtrl', ['$scope', function($scope) {
	$scope.uzytkownik = {
		login: '',
		haslo: ''
	};
}]);

Być może na pierwszy rzut oka nie jest to dla Ciebie zbyt przejrzysty zapis. Osobiście tez go nie używam. Funkcja anonimowa została poprzedzona nawiasami kwadratowymi i jest ostatnim elementem tablicy adnotacji.

Korzyści płynące ze wstrzykiwania zależności

Mechanizm wstrzykiwania zależności jest typowym wzorcem odwrócenia kontroli (z ang. inversion of control). Przydaje się przede wszystkim podczas pisania testów do kodu. Używając serwisów, których głównym zadaniem jest przejmowanie zaawansowanej logiki aby nie zapychać kontrolerów, możemy wstrzykiwać ich różne instancje bez jakichkolwiek zmian kodu kontrolera.

Przykładowo, dla testów możemy zmienić zależność bazy danych i wstrzyknąć instancję obiektu, który zwraca statyczne dane JSON. Podobną strategię wstrzykiwania zależności realizuje np. framework Ninject na platformie ASP.NET MVC. Korzyści i zasady postępowania są takie same.

Bez stosowania wstrzykiwania zależności kod programu jest kodem nietestowalnym. Nie da się napisać testów dla kontrolera, który pobiera dane z zewnętrznego źródła. Jedyną możliwością jest odpięcie źródła danych (tzn. bazy danych) i zastąpienie ich danymi testowymi (fikcyjnymi).

]]>
https://www.p-programowanie.pl/kurs-angular/wstrzykiwanie-zaleznosci/feed/ 0
Zdarzenia https://www.p-programowanie.pl/kurs-angular/zdarzenia/ https://www.p-programowanie.pl/kurs-angular/zdarzenia/#respond Sun, 18 Dec 2016 00:16:52 +0000 http://www.p-programowanie.pl/?p=2977 Zdarzenia są bardzo ważnym mechanizmem umożliwiającym interakcję użytkownika z naszą aplikację. Oczywiście, w AngularJS zdarzenia zostały obsłużone wyjątkowo dobrze. Czym są zdarzenia? Są to przechwycenia wszelkich operacji jakie może wykonać użytkownik w naszej aplikacji np. kliknięcie myszką, naciśnięcie klawisza.

Podpinanie zdarzeń

Zdarzenia w AngularJS podpinamy poprzez dodanie do dowolnego elementu HTML odpowiedniej dyrektywy reprezentującej zdarzenie. Jako parametr dyrektywy należy podać funkcję zwrotną, która zostanie wykonana w danej sytuacji. Składania zdarzenia:

<element nazwa_zdarzenia="funkcja" />

A więc przykładowo zdarzenie kliknięcia w przycisk wywołujące funkcje $scope.logowanie() wygląda następująco:

<button ng-click="logowanie()">Zaloguj</button>

Pełny kod działającej aplikacji:

<script>
	var app = angular.module("mainModule", []);
 
	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
		$scope.uzytkownik = {
			login: '',
			haslo: ''
		};
		$scope.logowanie = function() {
			console.log('wpisany login: ' + $scope.uzytkownik.login);
			console.log('wpisane haslo: ' + $scope.uzytkownik.haslo);
			console.log('ktos kliknął logowanie!');
		};
	}
</script>
 
<body ng-app="mainModule" ng-controller="simpleCtrl">
	<p>Login: <input ng-model="uzytkownik.login" /></p>
	<p>Hasło: <input ng-model="uzytkownik.haslo" /></p>
	<button ng-click="logowanie()">Zaloguj</button>
</body>

Do przycisku logowania zostało podpięte zdarzenie ng-click wywołujące funkcje logowanie(). Bindując zmienne lub funkcje podpięte do $scope z warstwą widoku zawsze pomijamy nazwę $scope. Efekt działania jest następujący:

W przypadku podania nieistniejącej nazwy funkcji do parametru dyrektywy zdarzenia, niestety nic się nie stanie. W konsoli nie pojawi się żaden błąd. Jeżeli coś nie działa a w konsoli nie ma błędów, warto w pierwszej kolejności sprawdzić czy zdarzenia podpięte są pod istniejące funkcje lub czy nie zapomnieliśmy o nawiasach.

Lista dostępnych zdarzeń

Oto lista dostępnych zdarzeń:

Zdarzenie Wywoływany gdy
ng-blur Dany element traci aktywność (traci focus)
ng-change Zmieni się model ng-model danego elementu
ng-click Kliknięcie myszką
ng-copy Nastąpi skopiowanie wartości elementu
ng-cut Nastąpi wycięcie wartości elementu
ng-dblclick Podwójne kliknięcie myszką
ng-focus Dany element zostaje aktywny (focus)
ng-keydown Naciśnięcie klawisza
ng-keypress Naciśnięcie klawisza (rozróżnia wielkość liter)
ng-keyup Puszczenie klawisza
ng-mousedown Naciśnięcie klawisza myszy
ng-mouseenter Najechanie myszą na element gdziekolwiek
ng-mouseleave Zjechanie myszą z elementu
ng-mousemove Poruszanie myszą po elemencie
ng-mouseover Najechanie myszą na element lub jego dzieci
ng-mouseup Puszczenie klawisza myszy
ng-paste Wklejenie wartości do elementu

Większości wymienionych zdarzeń w codziennej pracy nie używa się. Najważniejsze zdarzenia, które na pewno należy zapamiętać to ng-click oraz ng-change. Za pomocą kliknięcia możemy obsłużyć wszystkie przyciski i formularze w aplikacji.

Z kolei ng-change to wywołuje funkcje zwrotną podczas jakiejkolwiek zmiany modelu przez użytkownika, więc może np. przeliczyć w tle dane. Jest to dobra alternatywa dla zakładania obserwatorów na $scope o czym będzie w innym artykule.

Zdarzenie ng-change

Wcześniej czy później zdasz sobie sprawę, że ng-change jest jednym z najbardziej przydatnych zdarzeń. Aby zdarzenie działało, musi być nałożone na obiekt posiadający model podpięty dyrektywą ng-model. To właśnie zmiana wartości modelu wpływa na wywołanie ng-change. Przykładowe użycie:

<script>
	var app = angular.module("mainModule", []);
 
	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
	
		$scope.check = false;
		$scope.ilosc = 0;
		
		$scope.klikniecie = function() {
			$scope.ilosc++;
		}
	}
</script>
 
<body ng-app="mainModule" ng-controller="simpleCtrl">
	<p>Zaznacz checkbox: <input type="checkbox" ng-model="check" ng-change="klikniecie()" /></p>
	<p>
		Checkbox jest 
		<span ng-if="check" style="color:#00CE00; font-weight:bold;">zaznaczony</span>
		<span ng-if="!check" style="color:#FF0000; font-weight:bold;">odznaczony</span>
	<p>
	<p>Został kliknięty <b>{{ilosc}}</b> razy</p>
</body>

Efekt działania jest przewidywalny:

Przy jakiejkolwiek zmianie modelu podpiętego do pola typu checkbox zostaje wywołana funkcja klikniecie(). Zwiększa ona wartość zmiennej ilosc, który jest następnie wyświetlany za pomocą wyrażenia {{ilosc}}. Dodatkowo zastosowana dyrektywa ng-if sprawdza aktalny stan pola i wyświetla odpowiedni span z tekstem.

]]>
https://www.p-programowanie.pl/kurs-angular/zdarzenia/feed/ 0
Dyrektywa ngIf https://www.p-programowanie.pl/kurs-angular/dyrektywa-ngif/ https://www.p-programowanie.pl/kurs-angular/dyrektywa-ngif/#respond Sat, 17 Dec 2016 20:24:35 +0000 http://www.p-programowanie.pl/?p=2956 Programowanie we wszystkich językach opiera się na budowaniu przepływu programu za pomocą instrukcji warunkowych. W AngularJS po stronie kontrolera używamy dokładnie takich samych konstrukcji jak w języku JavaScript. Oznacza to, że bez żadnych problemów możemy korzystać z ifswitch, operatorów trójargumentowych i wszystkiego co wymyślimy. Operatory warunkowe dostępne w kontrolerach nie są dostępne w warstwie widoku (a więc pliku HTML). Zamiast tego Twórcy AngularJS przygotowali dla programistów specjalną dyrektywę ng-if.

Dyrektywa ngIf

Aby użyć dyrektywy ng-if należy osadzić ją w dowolnym atrybucie HTML. Jej wartością może być dowolne wyrażenie. Przykładowo następujące porównanie nigdy nie zostanie spełnione:

<script>
	var app = angular.module("mainModule", []);

	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
		$scope.imie = "";
	}
</script>

<body ng-app="mainModule" ng-controller="simpleCtrl">
	<div ng-if="1 == 5">
		Niewidoczny element
	</div>
</body>

Podglądając wygenerowany kod można zauważyć, że jeżeli warunek dyrektywy nie jest spełniony to nie zostanie wygenerowane statyczne drzewo DOM dla elementu:

Jest to bardzo ważny wniosek. Używając dyrektywy ng-if należy pamiętać, że za każdym razem przetworzone zostanie statyczne drzewo DOM dokumentu. W przypadku kiedy warunek nie jest spełniony, zostaje wycięta cała treść strony zagnieżdżona w dyrektywie.

Tak samo jak w przypadku dyrektywy ng-repeat elementy zagnieżdżone mają podpięty swój osobny $scope.

Przykład użycia dyrektywy ng-if może wyglądać następująco:

<script>
	var app = angular.module("mainModule", []);
 
	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
		$scope.osoba = {
			imie: "",
			nazwisko: ""
		};
	}
</script>
 
<body ng-app="mainModule" ng-controller="simpleCtrl">
	<p>Imię: <input ng-model="osoba.imie" /></p>
	<p>Nazwisko: <input ng-model="osoba.nazwisko" /></p>
	
	<p><label>Czy pracujesz? <input type="checkbox" ng-model="osoba.czyPracuje" /></label></p>
	<p ng-if="osoba.czyPracuje">
		Podaj nazwę firmy: <input ng-model="osoba.firma" />
	</p>
	
	<br>
	<p>Nazywasz się <b>{{osoba.imie}} {{osoba.nazwisko}}</b>.<p>
	<p ng-if="osoba.czyPracuje && osoba.firma.length">Pracujesz w <b>{{osoba.firma}}</b>.<p>
</body>

Wnioski na temat ng-if są następujące:

  • jeżeli wartość wyrażenia dyrektywy nie jest spełniona, zmodyfikowane zostaje statyczne drzewo DOM dokumentu.
  • dla elementów znajdujących się w dyrektywie zostaje utworzony osobny $scope (zagnieżdżony względem $scope kontrolera)

Nadużywanie ng-if może znacznie spowolnić ładowanie strony. Uzależniając wyświetlanie danych elementów od warunków okazuje się, że nasze $scopy stają się bardzo zagnieżdżone i nieuporządkowane. Z tego powodu powstała dyrektywa ng-show oraz ng-hide.

Dyrektywy ngShow i ngHide

Twórcy AngularJS zauważyli, że nie za każdym razem kiedy zależy nam na ukryciu danego elementu chcemy od razu przebudowywać drzewo DOM co wiąże się ze spadkami wydajności.

Dyrektywy ng-show oraz ng-hide działają w ten sam sposób, tzn. przyjmują jako parametr wyrażenie, jednak ukrywanie elementów odbywa się poprzez dodanie do nich atrybutu CSS o nazwie display: none !important. W większości przypadków lepszym wyborem będzie użycie właśnie wyżej wymienionych dyrektyw. Bardzo dobrze sprawdzają się do budowania elementów typu pokaż/ukryj (z ang. toggle). Tylko nieznacznie obciążają pamięć.

Przykładowy kod:

<script>
	var app = angular.module("mainModule", []);
 
	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
		$scope.toggle = false;
	}
</script>
 
<body ng-app="mainModule" ng-controller="simpleCtrl">
	<h4>Lista zakupów <a href="#" ng-click="toggle = !toggle">[pokaż/ukryj]</a></h4>
	<ul ng-show="toggle">
		<li>3 bułki</li>
		<li>majonez</li>
		<li>czekolada</li>
	</ul>
</body>

W przykładzie użyte zostało zdarzenie ng-click. Nie musisz się nim przejmować, jego jedynym zadaniem jest nadawanie wartości zmiennej toggle na przeciwną względem aktualnej. Zamiast podpięcia ng-click możliwe było skorzystanie np. z pola typu checkbox z podpiętym modelem. Zdarzenia zostaną opisane w kolejnym artykule. Działanie prezentuje się następująco:

Na załączonym gifie widać wyraźnie, że do ukrycia elementu zostaje użyta klasa ng-hide. Drzewo DOM dokumentu pozostaje bez zmian, nie zostają też utworzone nowe $scope. Dyrektywa ng-hide jest przeciwieństwem ng-show i można używać ich zamiennie. Obydwoma uzyskujemy ten sam efekt, ewentualną różnicą jest dodanie negacji przed wyrażeniem.

Kiedy używać ngShow a kiedy ngIf

Dyrektywy ng-show używamy w większości wypadków, szczególnie tam gdzie zależy nam aby na chwilę ukryć pewne fragmentu strony w zależności od stanu aplikacji.

Ng-if przydaje się gdy szacujemy, że fragment drzewa HTML który ukrywa nie zostanie pokazany w dużej ilości przypadków. Nie ma sensu generować 100 linijek kodu trzymanych w pamięci przeglądarki i inicjalizujących swoje modele jeżeli dana funkcjonalność używana jest przez 20% odwiedzających użytkowników. Dodatkowo, ng-if przydaje się podczas walidacji formularzy, co zostanie opisane w innym artykule. Pokrótce chodzi o to, że ukrywając input który jest wymagany, nie zostaje on zaliczony do elementów walidowanego formularza. Ng-show nie daje takich efektów.

]]>
https://www.p-programowanie.pl/kurs-angular/dyrektywa-ngif/feed/ 0
Dyrektywa ngRepeat https://www.p-programowanie.pl/kurs-angular/dyrektywa-ngrepeat/ https://www.p-programowanie.pl/kurs-angular/dyrektywa-ngrepeat/#respond Fri, 16 Dec 2016 22:51:02 +0000 http://www.p-programowanie.pl/?p=2942 W poprzednich artykułach poznaliśmy kilka podstawowych dyrektyw wbudowanych we framework AngularJS. Pierwsza z nich ng-app służy do inicjalizacji Agulara. Kolejna ng-controller służy do podłączenia kontrolera w danym miejscu statycznego drzewa DOM. Kontroler można podłączyć też automatycznie za pomocą konfiguracji routingu jednak o tym innym razem.  Ostatnią poznaną dyrektywą jest ng-model niezbędna aby uruchomić mechanizm dwustronnego bindowania pomiędzy widokiem a kontrolerem.

Dyrektywa ngRepeat

Bardzo użyteczną dyrektywą wbudowaną w AngularJS jest ng-repeat. Dzięki niej możemy iterować wartości dowolnej kolekcji i wyświetlać je po stronie widoku. Oczywiście w Angularze czy też JavaScripcie możemy używać pętli, jednak nie działają one po stronie drzewa DOM dokumentu. Dyrektywa ngRepeat natomiast tak. Podstawowy schemat użycia ng-repeat jest następujący:

ng-repeat="<element> in <kolekcja>"

Na potrzeby testów podepnijmy do $scope kontrolera listę liczb. Możemy ją bardzo prosto wyświetlić w widoku następująco:

<script>
	var app = angular.module("mainModule", []);

	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
		$scope.listaLiczb = [1, 2, 3, 4, 5, 6];
	}
</script>

<body ng-app="mainModule" ng-controller="simpleCtrl">
	<span ng-repeat="liczba in listaLiczb">{{liczba}}, </span>
</body>

Wynikiem działania powyższego kodu jest lista liczb oddzielonych przecinkami. Ng-repeat jest dyrektywą, która musi zostać osadzona w jakimś elemencie HTML. Następnie klonuje ten element tyle razy jaki jest rozmiar przeglądanej kolekcji. Aby wyświetlić aktualnie iterowany element posługujemy się klamrowym operatorem wyrażenia. Oto podgląd kodu strony w narzędziach deweloperskich Chrome:

Oprócz powtórzenia span kilka razy, został on obłożony dodatkowymi komentarzami, które są niezbędne do debugowania platformy AngularJS i nie powinny nas aktualnie interesować. Ciekawym jest fakt, że powyższy kod zostaje dynamicznie wygenerowany dopiero po fazie ładowania i kompilowania Angulara (czyli po załadowaniu głównego modułu, kontrolera i przetworzeniu dyrektyw drzewa DOM). Sprawdzając źródło strony widzimy tyle:

Naszym oczom ukazuje się fragment kodu nieskompilowany. Niestety tak samo widzą go roboty sieciowe, dlatego właśnie AngularJS nie naddaje się do pisania stron, które mają być pozycjonowane. Wszelkie parsery, pająki i roboty nie są wstanie przetworzyć Angulara  i nie widzą treści. (update 2017.03.01 wyjątkiem na dzień dzisiejszy jest Google, który parsuje AngularJS).

Iterowanie kolekcji obiektów

Oczywiście dyrektywa ng-repeat jest znacznie potężniejsza niż wynika z poprzedniego akapitu. Można nią bez problemowo iterować i wyświetlać kolekcje obiektów. Stwórzmy przykładową kolekcję osób i osadźmy ją w tabelce:

<script>
	var app = angular.module("mainModule", []);

	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
		$scope.osoby = [
			{imie: "Karol", nazwisko: "Trybulec", wiek: 24},
			{imie: "Arek", nazwisko: "Kowalski", wiek: 12},
			{imie: "Jarek", nazwisko: "Nowacki", wiek: 51},
			{imie: "Marek", nazwisko: "Zawadzki", wiek: 21},
		];
	}
</script>

<body ng-app="mainModule" ng-controller="simpleCtrl">
	<table border="1" cellpadding="10">
		<tr ng-repeat="osoba in osoby">
			<td>{{osoba.imie}}</td>
			<td>{{osoba.nazwisko}}</td>
			<td>{{osoba.wiek}}</td>
		</tr>
	</table>
</body>

Wyliczenie działa bezproblemowo. Wyświetlone zostały wszystkie elementy i wrzucone do tabelki:

Iterowanie atrybutów obiektu

Dyrektywa ng-repeat została wyposażona w możliwość iterowania atrybutów danego obiektu. W tym celu należy posłużyć się składnią:

ng-repeat="(<klucz>, <wartosc>) in <obiekt>"

Przykładowy kod wygląda następująco:

<script>
	var app = angular.module("mainModule", []);
 
	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
		$scope.osoba = {
			imie: "Karol",
			nazwisko: "Trybulec",
			kraj: "Polska",
			plec: "mężczyzna"
		};
	}
</script>
 
<body ng-app="mainModule" ng-controller="simpleCtrl">
	<table border="1" cellpadding="10">
		<tr ng-repeat="(klucz, wartosc) in osoba">
			<td>{{klucz}}</td>
			<td>{{wartosc}}</td>
		</tr>
	</table>
</body>

Iterowanie zagnieżdżonych kolekcji

Dyrektywę ng-repeat można zagnieżdżać wewnątrz siebie, jeżeli istnieje taka potrzeba. Taka sytuacja może wystąpić np. w przypadku posiadania kolekcji kolekcji, lub listy obiektów które posiadają kolekcję jako atrybut. Przykładowy kod wygląda następująco:

<script>
	var app = angular.module("mainModule", []);
 
	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
		$scope.uczniowie = [
			{
				imie: "Karol",
				przedmioty: [
					{
						nazwa: "matematyka", 
						oceny: [5,4,3]
					},
					{
						nazwa: "fizyka", 
						oceny: [3,2,4,1]
					}
				]
			},
			{
				imie: "Arek",
				przedmioty: [
					{
						nazwa: "j. polski", 
						oceny: [5,1,2,4]
					}
				]
			}
		];
	}
</script>
 
<body ng-app="mainModule" ng-controller="simpleCtrl">
	<table border="1" cellpadding="10">
		<tr ng-repeat="osoba in uczniowie">
			<td>{{osoba.imie}}</td>
			<td>
				<div ng-repeat="przedmiot in osoba.przedmioty">
					przedmiot: {{przedmiot.nazwa}} <br>
					oceny: <span ng-repeat="ocena in przedmiot.oceny">{{ocena}}</span>
					
					<br><br>
				</div>
			</td>
		</tr>
	</table>
</body>

Efekt działania programu jest następujący:

Przy tworzeniu takich konstrukcji trzeba kierować się zdrowym rozsądkiem. W niektórych przypadkach optymalniejsze może okazać się przetworzenie kolekcji w jezyku JavaScript i uproszenie jej, a następnie przesłanie jej do dyrektywy ng-repeat. Operacje na drzewie DOM dokumentu są kosztowne i w przypadku skomplikowanych szablonów mogą spowalniać działanie przeglądarki.

$scope elementów kolekcji

Każda iteracja ng-repeat i wyświetlenie elementu tworzy dodatkowy osobny $scope, który jest dzieckiem $scope aktualnego kontrolera. Dlaczego tak jest wynika wprost z budowy Angularowych dyrektyw i zostanie wytłumaczone w dalszych częściach kursu. Podgląd w ngInspector wygląda następująco:

Każdy $scope pojedynczego elementu kolekcji dostaje swoje unikalne atrybuty, które mogą się nam bardzo przydać. Oto ich lista:

  • $index (number) – indeks aktualnego elementu
  • $first (boolean) – flaga informująca czy jest to pierwszy element (pierwsza iteracja na kolekcji)
  • $middle (boolean) – flaga informująca czy jesteśmy równo w środku kolekcji
  • $last (boolean) – flaga informująca czy jest to ostatni element (ostatnia iteracja na kolekcji)
  • $even (boolean) – flaga informaująca czy jest to element parzysty
  • $odd (boolean) – flaga informująca czy jest to element nieparzysty

Z wymienionych atrybutów możemy korzystać w każdym miejscu struktury HTML znajdującej się wewnątrz ng-repeat. Bardzo przydatny jest atrybtut $index oraz $first i $last. Często wykorzystywane są do tworzenia domknięć pojemników, listingów i tabel (np. gdy ostatni element tabeli musi mieć mocniejsze podkreślenie). Aby to osiągnąć trzeba skorzystać z instrukcji warunkowej o czym w będzie w następnej lekcji.

Optymalizacja kolekcji

Kolekcja wyświetlona za pomocą dyrektywy ng-repeat zostaje osadzona w statycznym drzewie DOM dokumentu. Podmiana referencji lub nawet przeładowanie kolekcji spowoduje usunięcie wszystkich jej elementów i wyświetlenie ich ponownie. W przypadku kolekcji większej niż 500 elementów użytkownik odczuje półsekundowe spowolnienie przeglądarki.

Aby móc poruszać się po kolekcji obiektów Angular dodaje każdemu elementowi kolekcji atrybut $$hashKey. Stanowi on unikalny identyfikator dla każdego elementu w aplikacji. Jeżeli zmieniony zostanie element bez zmiany jego $$hashKeya, wtedy w dokumencie odświeżają się jedynie jego atrybuty (bez usunięcia i utworzenia elementu na nowo). Dzięki temu, zmiana atrybutów pojedynczego elementu kolekcji jest szybka i odbywa się w czasie rzeczywistym (zostaje on zlokalizowany po $$hashKey).

Niestety podmiana referencji kolekcji spowoduje zmianę hashy, dlatego mimo że obiekty są takie same (mają takie same atrybuty) struktura DOM dokumentu i tak zostanie przeładowana. Aby nie identyfikować elementów kolekcji po $$hashKey została do ng-repeat wprowadzona opcja track by. Ma ona następującą strukturę:

ng-repeat="<element> in <kolekcja> track by <unikalny_atrybut>"

Przykładowy kod wygląda następująco:

<script>
	var app = angular.module("mainModule", []);
 
	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
		$scope.przedmioty = [
			{
				id:1, nazwa: "matematyka", 
			},
			{
				id: 2, nazwa: "fizyka", 
			}
		];
	}
</script>
 
<body ng-app="mainModule" ng-controller="simpleCtrl">
	<table border="1" cellpadding="10">
		<tr ng-repeat="przedmiot in przedmioty">
			<td>{{przedmiot.nazwa}}</td>
		</tr>
	</table>
</body>

Gdy podejrzymy $scope aplikacji, zobaczymy że obiekty na liście posiadają unikalny hash doklejony przez Angulara:

 

Ponieważ elementy kolekcji posiadają unikalny atrybut id możemy wykorzystać go zamiast unikalnego hasha. Kod po modyfikacji będzie wyglądał następująco:

<body ng-app="mainModule" ng-controller="simpleCtrl">
	<table border="1" cellpadding="10">
		<tr ng-repeat="przedmiot in przedmioty track by przedmiot.id">
			<td>{{przedmiot.nazwa}}</td>
		</tr>
	</table>
</body>

Podglądając $scope aplikacji przekonamy się, że atrybuty $$hashKey zniknęły. Dzięki temu zabiegowi, przeładowanie kolekcji czy nawet podmiana jej referencji nie spowoduje skasowania wszystkich elementów w strukturze DOM dokumentu. Angular znajdzie elementy, których atrybuty się zmieniły, a następnie zidentyfikuje je po unikalnym identyfikatorze i uaktualni. Korzystanie z opcji track by potrafi znacząco przyśpieszyć działanie aplikacji wykorzystujących kolekcje kilku a nawet kilkunastokrotnie.

Kolekcje typów prostych

Bardzo niebezpieczne jest wyświetlanie kolekcji złożonych z typów prostych. Atrybut $$hashKey może być dodany tylko do obiektu. Iterując kolekcję liczb (np. listę ocen) Angular nie może identyfikować poszczególnych elementów po unikalnym hashu. Z tego powodu, jeżeli kolekcja typów prostych posiada dwie takie same wartości naszym oczom ukarze się błąd:

Error: [ngRepeat:dupes] Duplicates in a repeater are not allowed. Use ‚track by’ expression to specify unique keys. Repeater: ocena in oceny, Duplicate key: number:1, Duplicate value: 1

Przykładowym błędnym kodem jest następująca lista ocen złożona z typów prostych:

<script>
	var app = angular.module("mainModule", []);
 
	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
		$scope.oceny = [1,2,3,4,5,1];
	}
</script>
 
<body ng-app="mainModule" ng-controller="simpleCtrl">
	<table border="1" cellpadding="10">
		<tr ng-repeat="ocena in oceny">
			<td>ocena</td>
		</tr>
	</table>
</body>

Rozwiązaniem tego problemu jest nie wyświetlanie kolekcji typów prostych za pomocą ng-repeat. Wychodząc na przeciw wymaganiom programistów w jednej z wersji Angulara została wprowadzona możliwość indeksowania kolekcji posługując się ich numerem porządkowym w kolekcji (czyli indeksem kolekcji). Kod wykorzystujący indeksowanie po numerze kolekcji wygląda następująco:

<script>
	var app = angular.module("mainModule", []);
 
	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
		$scope.oceny = [1,2,3,4,5,1];
	}
</script>
 
<body ng-app="mainModule" ng-controller="simpleCtrl">
	<table border="1" cellpadding="10">
		<tr ng-repeat="ocena in oceny track by $index">
			<td>ocena</td>
		</tr>
	</table>
</body>

Powyższy kod nie spowoduje wyświetlenia błędu. Mimo tego, lepszym rozwiązaniem jest obudowanie kolekcji typów prostych w obiekt i atrybut (do obiektu możliwe jest doklejenie hasha).

]]>
https://www.p-programowanie.pl/kurs-angular/dyrektywa-ngrepeat/feed/ 0
Scope i wyrażenia https://www.p-programowanie.pl/kurs-angular/scope-i-wyrazenia/ https://www.p-programowanie.pl/kurs-angular/scope-i-wyrazenia/#comments Fri, 16 Dec 2016 20:42:37 +0000 http://www.p-programowanie.pl/?p=2913 Zrozumienie tego artykułu jest kluczowe aby móc tworzyć aplikacje w AngularJS. Do tej pory powinieneś wiedzieć czym jest Angular, umieć utworzyć moduł główny aplikacji i podpiąć do niego kontroler. Zdefiniowany przez nas moduł oraz kontroler zostają załadowane do pamięci w fazie ładowania. Następnie w fazie kompilacji AngularJS analizuje statyczne drzewo DOM dokumentu zbudowane ze znaczników HTML. W momencie natrafienia na dyrektywy ng-application oraz ng-controller zostaje utworzony nowy $scope.

Czym jest $scope

W większości języków programowania możemy definiować własne zasięgi zmiennych, w których są one widoczne. W C++ każda funkcja posiada swój własny zasięg ograniczony przez nawiasy klamrowe. Nic nie stoi na przeszkodzie aby w środku funkcji zagnieździć kolejne nawiasy klamrowe definiujące nowy zakres widoczności.

$scope w AngularJS jest niczym innym jak zasięgiem widoczności danego kontrolera i tworzy on warstwę widok-model wzorca MVVM. Każdy kontroler posiada swój unikalny $scope do którego przypięte są funkcje oraz zmienne widoczne w warstwie widoku. $scope jest jedynym elementem, który jest w stanie wystawić dane dla widoku i komunikować się z widokiem.

W czystym JavaScripcie zasięgi zmiennych tworzone były poprzez funkcje anonimowe, w AngularJS posiadamy kontrolery, które także są funkcjami.

$scope zostaje utworzony automatycznie dla każdego kontrolera, który zostanie zainicjalizowany dyrektywą ng-controller (lecz nie tylko o czym później). Oprócz tego, podczas inicjalizacji aplikacji za pomocą dyrektywy ng-app zostaje utworzony tzw. $rootScope, a więc zasięg bazowy dla wszystkich innych.

W przypadku zagnieżdżonych kontrolerów zasięgi także zostają zagnieżdżone. Każdy $scope posiada referencję do dziecka child-scope oraz do rodzica parent-scope. Rodzicem nadrzętnym jest $rootScope. Nie jest to informacja, która może Ci się przydać na początku kursu, jednak warto o takim fakcie wiedzieć. Rozwieje to Twoje wątpliwości szczególnie podczas przeglądania $scope za pomocą wtyczki ngInspector.

Dwustronne bindowanie danych

Potęgą frameworków JavaScriptowych jest niezaprzeczalnie mechanizm dwustronnego bindowania danych two-way data binding. Jest to element obiektu $scope i zapewnia dynamiczne odświeżanie danych pomiędzy widokiem a view-modelem (czy też upraszczając kontrolerem).

Każdy kto pisał cokolwiek w JavaScript lub jego młodszym bracie jQuery wie jak uciążliwe jest wymienianie danych pomiędzy elementami HTML a skryptem. W AngularJS nie trzeba się o to martwić.

Wyrażenia

Zanim przetestujemy mechanizm dwustronnego bindowania danych trzeba dowiedzieć się czym są wyrażenia. Wyrażenia w AngularJS służą do przekazywania danych pomiędzy kontrolerem a widokiem (w jedną stronę!):

Istnieją dwie metody na przekazanie wyrażenia do widoku:

  • Przekazanie wyrażenia za pomocą nawiasów klamrowych np. {{wyrażenie}}
  • Przekazanie wyrażenia za pomocą wbudowanej dyrektywy np. ng-bind="wyrażenie"

Wyrażenia w AngularJS są takie same jak w JavaScript, mogą zawierać zmienne, literały oraz operatory. W celu przetestowania wyrażeń proponuję podpiąć do $scope zmienną o dowolnej treści. Przykładowo utworzę atrybut imie o wartości „Karol”. Definiowanie atrybutów i funkcji obywa się tak samo jak w przypadku JavaScript.:

<script>
	var app = angular.module("mainModule", []);

	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
		$scope.imie = "Karol"
	}
</script>

<body ng-app="mainModule" ng-controller="simpleCtrl">
	<p>Wynik dodawania 5+2 to {{5+2}}</p>
	<p>Masz na imię {{imie}}<p>
</body>

Po otworzeniu powyższego kodu w przeglądarce Chrome przekonasz się, że zmienne zostały wyświetlone w poprawny sposób. Nawiasy klamrowe można zastąpić dyrektywą ng-bind:

<p>Masz na imię {{imie}}<p>
<p>Masz na imię <span ng-bind="imie"></span><p>

Wynik dwóch powyższych linijek będzie ten sam. Jedyna różnica w przypadku posługiwania się dyrektywą jest to, że musi być ona osadzona w jakimś znaczniku HTML. Z tego powodu konieczne było utworzenie dodatkowego spana wewnątrz akapitu aby można było zbindować do niego wartość zmiennej. Z tego powodu bardziej popularne i wygodniejsze są nawiasy klamrowe.

Spójrzmy jak wygląda struktura zakresów podglądając je za pomocą wtyczki ngInspector:

Wyraźnie widać moduł, podpięty do niego $rootScope oraz jego dziecko czyli $scope naszego kontrolera. Uwaga, aby dodatek ngInspector działał plik jaki badamy musi być serwowany przez serwer np. przez serwer lokalny. Odwołując się do pliku za pomocą ścieżki file:// wtyczka nie uruchomi się. Szybki darmowy serwer HTTP jest wbudowany np. w npm (http-server). Można użyć też XAMPA lub innych podobnych.

Modele

Użycie modelu dostarcza nam 100% prawdziwego mechanizmu dwustronnego bindowania, z którego słyną frameworki JavaScriptowe. W przypadku modelu bindowanie odbywa się w dwie strony, a więc zmiany które zajdą w formularzu są widocznie w kontrolerze oraz na odwrót:

Modele można podpinać wyłącznie do kontrolek HTMLowych czyli np. do elementów input. Jedyną możliwością podpięcia modelu jest użycie wbudowanej dyrektywy ng-model. Przykładowo:

<script>
	var app = angular.module("mainModule", []);

	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
		$scope.imie = "Karol"
	}
</script>

<body ng-app="mainModule" ng-controller="simpleCtrl">
	<p>Masz na imie <input ng-model="imie" /><p>
</body>

Powyższy kod podpina do pola tekstowego model $scope.imie. Po uruchomieniu kodu wartość zmiennej zostanie automatycznie zbindowana do widoku.

Pełny przykład bindowania danych

Sprawdźmy teraz, dlaczego AngularJS jest tak fajny jak o nim mówią. Poznaliśmy podstawowe mechanizmu umożliwiające bindowania danych, spróbujmy je ze sobą połączyć. W tym celu zdefiniujmy model dla zmiennej imie oraz nazwisko a następnie za pomocą wyrażenia wyświetlmy jak nazywa się użytkownik:

<script>
	var app = angular.module("mainModule", []);

	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
		$scope.imie = "";
		$scope.nazwisko = "";
	}
</script>

<body ng-app="mainModule" ng-controller="simpleCtrl">
	<p>Imię: <input ng-model="imie" /></p>
	<p>Nazwisko: <input ng-model="nazwisko" /></p>
	<br>
	<p>Nazywasz się {{imie}} {{nazwisko}}.<p>
</body>

W powyższym przykładzie użytkownik może wpisać do dwóch pól tekstowych wartości tekstowe. Jest dla nich zdefiniowany model. Za pomocą wyrażeń wartość zostaje automatycznie wyświetlona niżej jako zwykły element strony HTML:

Jak widać na załączonym przykładzie AngularJS odświeża dane ze $scope w czasie rzeczywistym. Jest to niewątpliwie jego największa zaleta. Używając czystego JavaScriptu ilość utworzonego kodu byłaby ogromna. Nawet używając jQuery pomógłby on jedynie szybciej załapać uchwyt dla poszczególnego pola tekstowego – nic więcej.

Grupowanie w obiekty i czysty scope

Pisząc aplikacje w AngularJS musimy dbać o porządek w kontrolerze a przede wszystkim o porządek w $scope. Nigdy nie należy podpinać zmiennych bezpośrednio do scope szczególnie jeżeli tworzą jakiś model biznesowy. Powyższy przykład pokazany dla celów naukowych jest błędny, o wiele lepszym rozwiązaniem będzie zgrupowanie danych osobowych do obiektu osoba. Lepsza organizacja powyższego przykładu:

<script>
	var app = angular.module("mainModule", []);

	app.controller('simpleCtrl', simpleController);
	
	function simpleController($scope) {
		$scope.osoba = {
			imie: "",
			nazwisko: ""
		};
	}
</script>

<body ng-app="mainModule" ng-controller="simpleCtrl">
	<p>Imię: <input ng-model="osoba.imie" /></p>
	<p>Nazwisko: <input ng-model="osoba.nazwisko" /></p>
	<br>
	<p>Nazywasz się {{osoba.imie}} {{osoba.nazwisko}}.<p>
</body>

$scope jest obiektem ważnym, tworzy warstwę widoku-modelu i jest odpowiedzialny za bindowanie danych pomiędzy kontrolerem a widokiem. Jego zaśmiecenie powoduje znaczne straty wydajności w działaniu aplikacji. Odpowiednie grupowanie atrybutów i podpinanie ich jako atrybutów obiektów pomaga w utrzymaniu porządku.

W żadnym wypadku nie możemy uszkodzić albo nadpisać zakresu, możemy jedynie dodawać do niego atrybuty i funkcje, które są nam potrzebne. Istnieją też inne sposoby na dbanie o „lekkość” zakresu i zostaną opisane w kolejnych artykułach.

]]>
https://www.p-programowanie.pl/kurs-angular/scope-i-wyrazenia/feed/ 2
Kontrolery i moduły https://www.p-programowanie.pl/kurs-angular/kontrolery-i-moduly/ https://www.p-programowanie.pl/kurs-angular/kontrolery-i-moduly/#comments Thu, 15 Dec 2016 21:32:03 +0000 http://www.p-programowanie.pl/?p=2891 Kontrolery i moduły są podstawowymi bytami w każdej aplikacji AngularJS. Są one odpowiedzialne za podzielenie aplikacji na logiczne warstwy abstrakcji a także za sterowanie jej przepływem. Ucząc się Angulara prawdopodobnie będziesz używał jednego modułu i jednego kontrolera. Ich większa ilość jest potrzebna w momencie tworzenia konkretnej aplikacji, a poprawne zaplanowanie takiej struktury zostanie opisane w kolejnych artykułach.

Główny moduł aplikacji

Moduły aplikacji tworzą abstrakcyjną warstwę aplikacji służącą jedynie do utrzymania porządku w kodzie. Tworząc aplikację AngularJS nie musisz dzielić jej na moduły, jednak wymogiem jest zdefiniowanie przynajmniej jednego tzw. głównego modułu aplikacji, do którego będzie można podpiąć kontrolery.

Do uruchomienia Angulara wystarczy użyć wbudowanej dyrektywy ng-app=”nazwa”. W przypadku podania wartości szuka ona właśnie modułu o takiej nazwie – jest to moduł główny. Na potrzeby kursu będziemy używać tylko jednego modułu, większa ilość przydaje się przy tworzeniu konkretnych aplikacji co zostanie opisane w innym artykule.

Definicja modułu aplikacji sprowadza się do jednej linii kodu:

var app = angular.module("mainModule", []);

Nazwa modułu to mainModule ze względu, że jest to główny moduł aplikacji. Obiekt zostaje przypisany do zmiennej o nazwie app. Oczywiście nazwy możesz dobierać według własnych upodobań. Główny moduł aplikacji opina zasięgiem nasz widok dokładnie w tych atrybutach w jakich został zainicjowany.

Zwróć uwagę, że moduł nie posiada własnego „ciała”. Nie da się zdefiniować w nim zmiennych, funkcji ani umieścić w nim żadnej logiki. Jest tylko abstrakcyjnym elementem, do którego będziemy podpinać nasze kontrolery.

Kontroler aplikacji

Kontroler aplikacji AngularJS jest zwykłą funkcją. Jego instancja zostaje utworzona w momencie napotkania dyrektywy ng-controller=”nazwa” w kodzie HTML. Dyrektywa ng-controller szuka kontrolera o podanej przez nas nazwie, jeżeli go nie znajdzie AngularJS rzuci w konsoli wyjątek.

Definicja przykładowego kontrolera wygląda następująco:

<script>
	var app = angular.module("mainModule", []);

	app.controller('simpleController', function() {
		// ciało kontrolera
	});
</script>

<body ng-app="mainModule" ng-controller="simpleController">
	Angular start {{3+2}}
</body>

W powyższym przykładzie do utworzenia kontrolera użyta została funkcja anonimowa. Jest to rozwiązanie najczęściej spotykane w różnych artykułach i kursach. Wydaje się w miarę proste i logiczne. Mimo to zachęcam Cię, aby trzymać się konwencji, którą zaprezentuję poniżej:

<script>
	var app = angular.module("mainModule", []);

	app.controller('simpleCtrl', simpleController);
	
	function simpleController() {
		//ciało kontrolera
	}
</script>

<body ng-app="mainModule" ng-controller="simpleCtrl">
	Angular start {{3+2}}
</body>

Funkcja kontrolera została zadeklarowana jako normalna nazwana funkcja a nie funkcja anonimowa. Dzięki temu definicje kontrolerów nie przeplatają się z ich deklaracjami co ma ogromne znaczenie w przyszłości, gdy będziesz tworzył średniej wielkości aplikacje. Taka konwencja jest też bardziej czytelna w momencie kiedy zaczniemy korzystać z mechanizmu wstrzykiwania zależności (opisany w innym artykule).

Kontroler aplikacji jest warstwą, która zawiera w sobie $scope (z ang. przestrzeń/zasięg). Każdy kontroler ma swój własny $scope, do którego przypinamy różne atrybuty oraz metody widoczne później w widoku. $scope tworzy warstwę widok-model wzorca MVVM i moim zdaniem jest nieprzetłumaczalna na język polski. Z tego względu do końca serii będę posługiwał się tym pojęciem.

Oto przykładowa struktura aplikacji:

Gdy w kolejnych lekcjach do kontrolerów a konkretniej do $scope danego kontrolera będziemy podpinać zmienne i metody, ich zasięg będzie taki jak na rysunku wyżej. Kontroler zainicjalizowany dyrektywą ng-controller ma dostęp tylko do danych widoku elementów drzewa DOM które są mu podrzędne. Kontrolery nie mogą wymieniać między sobą danych ani wpływać na swój stan (przynajmniej bez zastosowania dodatkowych mechanizmów).

Oczywiście możliwe jest podpięcie jednego kontrolera na całą gałąź atrybutu body a nawet html. Wtedy moduł i kontroler opinałby swoim zasięgiem całą stronę.

]]>
https://www.p-programowanie.pl/kurs-angular/kontrolery-i-moduly/feed/ 1
Szkielet aplikacji https://www.p-programowanie.pl/kurs-angular/szkielet-aplikacji/ https://www.p-programowanie.pl/kurs-angular/szkielet-aplikacji/#respond Thu, 15 Dec 2016 18:38:41 +0000 http://www.p-programowanie.pl/?p=2881 AngularJS to platforma programistyczna w całości napisana w języku JavaScript. Aby móc tworzyć aplikację opartą o AngularJS niezbędne jest zaimportowanie pliku źródłowego Angulara. Tak jak większość plików JavaScriptowych można go doczytywać lokalnie lub dynamicznie z zewnętrznego serwera. Informacje o wszystkich wersjach znajdziesz wchodząc na stronę www.angularjs.org.

Wersje AngularJS

Na dzień dzisiejszy wersje Angular prezentują się następująco:

  • wersja 1.6 – testowa
  • wersja 1.5 – stabilna produkcyjna
  • wersja 1.2 – stara wersja wspierająca m.in. IE8, nie polecana do zastosowań produkcyjnych

Wersja 1.2 jest kopią starej wersji AngularJS, która jeszcze daje radę integrować się ze starymi przeglądarkami takimi jak IE8. Są na nią nanoszone wszelkie poprawki bezpieczeństwa bez żadnych dodatkowych ulepszeń.

Tworząc aplikację komercyjną/produkcyjną używanie wersji testowej jest ryzykowane, ponieważ nie ma pewności czy zmiany jakie w niej zachodzą zostaną ostatecznie włączone do głównej stabilnej wersji frameworka. W kursie nie musimy się tym martwić.

Pobieranie AngularJS

Pisząc projekt AngularJS z prawdziwego zdarzenia powinniśmy posługiwać się narzędziami służącymi do automatyzacji pracy takimi jak NodeJS czy Grunt. Są to narzędzia znane każdemu programiście front-endu. Pomagają one przebudowywać projekt, instalować i usuwać dodatkowe biblioteki takie jak AngularJS, dbają o posiadanie najnowszych wersji bibliotek a także zaciemniają kod (obfuskacja) i minifikują go. Na ten temat pojawi się na pewno osobny wpis.

W niniejszym kursie będę używał AngularJS wczytywanego dynamicznie z zewnętrznego serwera z pod adresu, jednak nic nie stoi na przeszkodzie aby pobrać AngularJS poprzez narzędzie npm dołączone do NodeJS.

https://ajax.googleapis.com/ajax/libs/angularjs/1.6.0/angular.js

Niezbędne narzędzia dla programisty AngularJS

Aby zacząć tworzyć aplikacje oparte o platformę AngularJS potrzebujemy trzech podstawowych rzeczy:

  • przeglądarki Google Chrome
  • wtyczki ngInspector
  • pliku *.html z podstawowym szkieletem strony z dołączonym skryptem AngularJS (link wyżej)

Przez długi czas programowałem w AngularJS używając przeglądarki Firefox, jednak jest to niewygodne. Google niesamowicie rozbudował możliwości Chrome szczególnie jeśli chodzi o narzędzia dla programistów. Chrome posiada lepsze możliwości debugowania kodu oraz wbudowany klient RESTa.

Momentem przełomowym kiedy przestałem używać przeglądarki Firefox do programowania było wprowadzenie przez Mozillę zabezpieczenia blokującego używanie nieautoryzowanych wtyczek. Przez to, nie byłem wstanie używać dodatku ngInspector dla Firefoxa, a jest to narzędzie nieocenione.

NgInspector jest prostym narzędziem pozwalającym przeglądać modele kontrolerów aplikacji AngularJS. Co prawda, wartości zmiennych można wyświetlić w konsoli deweloperskiej jednak jest to operacja nieznacznie dłuższa, a programista musi znać nazwę i lokalizację zmiennej, którą chce wyświetlić. Dzięki używaniu ngInspectora mamy podgląd na całą strukturę modelu ze wszystkimi zagnieżdżeniami i wartościami.

Podstawowy szkielet aplikacji

Oto treść pliku, która może być używana przez Ciebie jako szkielet do pisania pierwszych aplikacji w AngularJS:

<!DOCTYPE html>
<html lang="pl-PL">

<head>
	<meta http-equiv="content-type" content="text/html; charset=utf-8" />
	<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.0/angular.js"></script>
</head>

<body ng-app>
	Angular start {{3+2}}
</body>
	
</html>

Po uruchomieniu pliku w przeglądarce Chrome na ekranie powinien pojawić się napis „Angular start 5”. Jeżeli tak się nie stało, informacji o błędach szukaj w konsoli deweloperskiej. Wyświetlają się tam wszystkie błędy aplikacji.

Do tagu <body> została dołączona dyrektywa ng-app. AngularJS parsując drzewo DOM dokumentu szuka jej ponieważ jest to dyrektywa startowa dla aplikacji angularowych. W momencie jej znalezienia zaczyna działać Angular. Podwójne nawiasy klamrowe są tzw. wyrażeniem i zostają automatycznie obliczone, dlatego zwracają wynik dodawania.

W przypadku braku dyrektywy ng-app nawiasy klamrowe nie zostaną potraktowane jako wyrażenie AngularJS i wyświetlą się jako zwykł tekst.

]]>
https://www.p-programowanie.pl/kurs-angular/szkielet-aplikacji/feed/ 0