Wzorce projektowe

w praktyce


npm, github

Agenda

  • O czym nie jest
  • Po co?
  • Obiektowość w praktyce
  • Singleton
  • Mediator
  • Obserwator
  • Żeby nie było zbyt pięknie
  • Podsumowanie

O czym nie jest

Dużo praktyki mało teorii

  • Obiektowość - paradygmat programowania w informatyce
  • Klasa - definicja obiektu
  • Instancja - pojedynczy obiekt pewnej klasy
  • Dziedziczenie - współdzielenie funkcjonalności
  • Hermetyacja - ukrywanie składowych
  • Wzorce - gotowe rozwiązania

Po co?

Do czego służą wzorce projektowe i po co nam one

  • Jak to z Toolboxem było
  • Najpierw myślimy potem robimy
  • Poziomy abstrakcji
  • Przykład z jajecznicą
  • I właśnie po to są wzorce

Toolbox

Duży projekt

Najpierw myślimy potem robimy

Warto poświęcić chwilę żeby się zastanowić nim siądzie się do kodu. Nawet z kartką i długopisem w ręku.

Poziomy abstrakcji

Dodatkowy czinnik wpływający na jakość kodu.


initUi() {
  this.initEvaluationInfo();
  this.initCostSummary();
  this.initEvaluationVariant();
  this.initCosts();
  this.initTemplateSelect();
  this.initDuplicateForm();
  this.initShortcuts();
  this.initHelpMessage();
  this.initStartProject();
}

Przepis na jajecznicę

  • Rozpuścić masło na małej patelni
  • Wrzucić na stopione masełko szynkę pokrojoną w małe kwadraciki oraz drobno posiekany szczypiorek
  • rozbić ostrym nożem skorupki jajek i zawartość wylewać na patelnię
  • Dodać trochę soli oraz pieprzu i mieszać, aż do momentu ścięcia się jajek

Kod przepisu na jajecznicę

Jak NIE powinien wyglądać kod tworzenia jajecznicy


  dodaj(maslo)

  while(maslo.nieJestStopione){
    for( atom in atomyWProbceMasla){
      atom.dostarczEnergii
    }
  }

  dodaj(pokrojona Szynka)

  while(szczypiorek.jestCaly){
    oddziaływuj nożem na sieć krystaliczną szczypiorku
  }

  dodaj(szczypiorek)

  noż.dodajEnerigiiPotencjalnej
  noz.zamieńEnergięPotencjalnąNaKinetyczną
  noż.uderzW(jajko)

  dodaj(zawartośćJajka)

  dodaj(sól)

  for(ziarnkoPieprzu in szczyptaPipeprzu){
    dodaj(ziarnkoPieprzu)
  }

  i mieszać, aż do momentu ścięcia się jajek

Kod przepisu na jajecznicę

Jak powinien wyglądać kod tworzenia jajecznicy


  patelnia.dodaj(maslo)
  patelnia.podgrzejDoRostopieniaMasla()
  patelnia.dodaj(pokroj(szynka))
  patelnia.dodaj(pokroj(szczypiorek))
  patelnia.dodaj(rozbij(jajko))
  patelnia.dodaj(sól)
  patelnia.dodaj(pieprz)
  mieszajDoMomentuScieciaSieJajek(patelnia)

I właśnie po to są wzorce

Czasem nawet najładniej napisany kod nie obędzie się bez wzorców.

Obiektowość w praktyce

Krótkie przypomnienie obiektowości w ES6

  • Tworzenie obiektów
  • Definicja klasy
  • Dziedziczenie
  • Przesłanianie

Tworzenie obiektów

Kilka metod tworzenia obiektów


{}; // Object {}
new Object(); // Object {}
Object.create(null); // Object {} bez prototypu
            

Definicja klasy

Przypomnienie definiowanie klasy w ES6


// Przykład klasy definiującej kształt
class Shape {
  constructor() {
    this.x = 0;
    this.y = 0;
  }
  move(x, y) {
    this.x += x;
    this.y += y;
    console.info('Shape moved.');
  }
}

// Tworzenie instancji klasy Shape
let shape1 = new Shape();

// Wywołanie metody klasy Shape na obiekcie shape1
shape1.move(10, 10);
            

Dziedziczenie

Ale ja chcę kwadrat!


// Przykład klasy definiującej kwadrat dziedziczącej po kształcie
class Square extends Shape {
  constructor() {
    super(); // Wywołanie konstruktora z Shape - musi być przed thisem

    this.size = 10; // Dodane pole określające wielkość
  }

  // Dodana metoda do zmiany wielkośći kwadratu
  resize(size) {
    this.size = size;
  }
}

// Tworzenie kwadratu
let square1 = new Square();

// Wywołanie metody move na obiekcie square1
square1.move(20, 20);

// Wywołanie metody resize na obiekcie square1
square1.resize(20);
            

Przesłanianie

A co jeśli chcemy narysować Shape i Square. Dwie różne metody?


// Przykład klasy definiującej kształt
class Shape {
  constructor() {
    this.x = 0;
    this.y = 0;
  }
  move(x, y) {
    this.x += x;
    this.y += y;
    console.info('Shape moved.');
  }
  // Metoda do rysowania kształtu
  draw(ctx) {
     console.log(`${ctx}.fillRect(${this.x}, ${this.y}, 1, 1);`);
  }
}

// Tworzenie instancji klasy Shape
let shape1 = new Shape();

// Wywołanie metody draw klasy Shape na obiekcie shape1
shape1.draw();


// Przykład klasy definiującej kwadrat dziedziczącej po kształcie
class Square extends Shape {
  constructor() {
    super(); // Wywołanie konstruktora z Shape - musi być przed thisem

    this.size = 10; // Dodane pole określające wielkość
  }

  // Dodana metoda do zmiany wielkośći kwadratu
  resize(size) {
    this.size = size;
  }
  // Przesłonięta metoda do rysowania kształtu
  draw(ctx) {
    console.log(`${ctx}.fillRect(${this.x}, ${this.y}, ${this.size}, ${this.size});`);
  }
}

// Tworzenie kwadratu
let square1 = new Square();

// Wywołanie metody draw klasy Square na obiekcie square1
square1.draw();
            

Singleton

Kreacyjny wzorzec projektowy, którego celem jest ograniczenie możliwości tworzenia obiektów danej klasy do jednej instancji oraz zapewnienie globalnego dostępu do stworzonego obiektu.

  • Singleton boilerplate
  • Przykładowa implementacja
  • Przykład zastosowania

Singleton boilerplate

Czysty singleton w ES6.


class EmptySingleton {
  constructor() {
    if(EmptySingleton.singletonInstance) {
      return EmptySingleton.singletonInstance;
    }

    EmptySingleton.singletonInstance = this;

    // TOTO: Zaimplementuj mnie ;)
  }
}
            

Przykładowa implementacja

Implementacja singletona z polami, metodami itp. - zobacz przykład.


class SingletonExample {
  constructor() {
    if(SingletonExample.singletonInstance) {
      return SingletonExample.singletonInstance;
    }

    SingletonExample.singletonInstance = this;

    this.value = 1; // Dodatkowe pole
  }

  // Kilka metod
  setValue(newValue) {
    this.value = newValue;
  }

  getValue() {
    return this.value;
  }

  printValue() {
    console.log(this.value);
  }
}

Przykład zastosowania

Klasa Settings z Toolboxa


let defaults = {
  projectListFilter: false,
  clientListFilter: false
};

export default class Settings {
  constructor() {
    if(Settings.singletonInstance) {
      return Settings.singletonInstance;
    }

    Settings.singletonInstance = this;

    this.load();
  }

  load() {
    this.settings = Object.assign({}, defaults, JSON.parse(localStorage.getItem('settings')));
  }

  save() {
    localStorage.setItem('settings', JSON.stringify(this.settings));
  }

  option(key, value) {
    if(value != null){
      this.settings[key] = value;
      this.save();
    }
    return this.settings[key];
  }

}

Mediator

Wzorzec mediatora umożliwia zmniejszenie liczby powiązań między różnymi klasami, poprzez utworzenie mediatora będącego jedyną klasą, która dokładnie zna metody wszystkich innych klas, którymi zarządza. Nie muszą one nic o sobie wiedzieć, jedynie przekazują polecenia mediatorowi, a ten rozsyła je do odpowiednich obiektów.

  • Mediator boilerplate
  • Przykładowa implementacja
  • Przykład zastosowania

Mediator boilerplate

Jak zaimplementować mediator


  class EmptyMediator {
    constructor() {

    }
  }

Przykładowa implementacja

Przykładowy mediator - zobacz przykład.


  import Component1 from '../components/component1';
  import Component2 from '../components/component2';

  class MediatorExample {
    constructor() {

      this.component1 = new Component1();
      this.component2 = new Component2();

      this.functionality1();
      this.functionality2();
      this.functionality3();
    }

    functionality1() {
      this.component1.do1(() => {
        this.component2.do2();
      });
    }

    functionality2() {
      this.component2.do2(() => {
        this.component1.do1();
      });
    }

    functionality3() {
      this.component1.do1();
      this.component2.do2();
    }
  }

Przykład zastosowania

Klasa EstimationEdit z Toolboxa


import Service from 'common/js/service';
import EstimationInfo from 'src/js/widgets/estimationInfo';
import SummaryCosts from 'src/js/widgets/summaryCosts';
import EvaluationVariant from 'src/js/widgets/evaluationVariant';
import Costs from 'src/js/widgets/costs';
import ClientProjectSelector from 'common/js/components/clientProjectSelector';
import ModalForm from 'common/js/components/modalForm';
import AvailableActions from 'common/shared/available-actions/availableActions';
import YesNoModalBox from 'common/js/components/yesNoModalBox';
import {toastrInfo} from 'common/js/plugins/toastr';
import {sweetAlertWarning} from 'common/js/plugins/sweetalert';

let $ = window.jQuery; // global import;

class EstimationEdit extends Service {
  constructor($context, options = {}) {
    super(options);

    this.$context = $context;
    this.$form = this.$context.find('form');

    this.estimationInfo = null;
    this.summaryCosts = null;
    this.evaluationVariant = null;
    this.costs = null;
    this.shortcuts = null;

    this.stage = this.$context.attr('data-stage');

    this.changed = false;

    this.initUi();
  }

  initUi() {
    this.initEvaluationInfo();
    this.initCostSummary();
    this.initEvaluationVariant();
    this.initCosts();
    this.initTemplateSelect();
    this.initDuplicateForm();
    this.initShortcuts();
    this.initHelpMessage();
    this.initStartProject();
  }

  initShortcuts() {
    let $shortcuts = $(document.getElementById('estimationActions')).find('.js-available-actions');
    if ($shortcuts.length === 1) {
      this.shortcuts = new AvailableActions($shortcuts);
      new YesNoModalBox($shortcuts.find('.available-actions__button__delete-btn'), {
        title: 'Czy jesteś pewien?',
        text: 'Odzyskanie elementu nie będzie możliwe!!!'
      });
      new YesNoModalBox($shortcuts.find('.available-actions__button__back-btn'), {
        title: 'Czy jesteś pewien, że chcesz opuścić stronę?'
      });
      let save = new YesNoModalBox($shortcuts.find('.available-actions__button__submit-btn'), {
        title: 'Czy jesteś pewien?',
        text: 'Czy chcesz zapisać?',
        action: 'event'
      });
      save.on('afterYesClick', () => {
        this.$form.trigger('submit');
      });
    }
  }

  initEvaluationInfo() {
    let $estimationInfo = $(document.getElementById('estimationInfo'));
    if($estimationInfo.length > 0) {
      this.estimationInfo = new EstimationInfo($estimationInfo);
    }
  }

  initCostSummary() {
    this.summaryCosts = new SummaryCosts($(document.getElementById('costSummary')));
  }

  initEvaluationVariant() {
    this.evaluationVariant = new EvaluationVariant($(document.getElementById('variants')));
    this.evaluationVariant.on('overheadchange', () => {
      this.costs.setOverhead(this.evaluationVariant.getOverhead());
      this.costs.recalculateAll();
      this.changed = true;
    });
  }

  initCosts() {
    this.costs = new Costs($(document.getElementById('costs')), {
      stage: this.stage
    });
    this.costs.on('change', (event) => {
      this.summaryCosts.updateSummaryCosts(event.target.internalCost, event.target.externalCost);
      this.evaluationVariant.setCosts(event.target.internalCost, event.target.externalCost);
      if(this.estimationInfo !== null) {
        this.estimationInfo.setHours(event.target.hours);
      }
      this.changed = true;
    });
    this.costs.setOverhead(this.evaluationVariant.getOverhead());
    this.costs.recalculateAll();
  }

  initTemplateSelect() {
    //TODO: Zamknąć ten modal i komunikat zamkniecia w oddzielnym module (SensitiveContent?)
    // Wysylanie templateow
    let $templateChangeModal = $(document.getElementById('templateChangeModal')),
      $templateSelect = $(document.getElementById('templateId'));

    $templateChangeModal.find('[data-dismiss="modal"]').off().on('click', () => {
      $templateSelect.val('');
    });

    $templateChangeModal.find('.btn-primary').off().on('click', () => {
      this.changeTemplate();
    });

    $templateSelect.on('change', () => {
      let value = $templateSelect.val();
      if(typeof value !== 'undefined' && value !== '') {

        if(this.changed) {
          //$templateChangeModal.modal('show');
          sweetAlertWarning({
            title: 'Zamierzasz zmienić szablon wyceny.',
            text: 'Utracisz wszystkie niezapisane w kosztorysie informacje. Kontynuować?'
          },
          (isConfirm) => {
            if (isConfirm) {
              this.changeTemplate();
            }
          });
        }
      }
    });
  }

  changeTemplate() {
    this.$form.trigger('submit');
  }

  initDuplicateForm() {
    let $duplicateEstimation =
      $(document.getElementById('duplicateEstimation'));
    this.duplicateEstimationForm = new ModalForm($duplicateEstimation);
    this.duplicateEstimationForm.on('afterLoad', ()=> {
      new ClientProjectSelector();
    });
  }

  initHelpMessage() {
    if(this.stage === 'typeUnsaved' || this.stage === 'typeDraft') {
      toastrInfo(`W formularzu wyceny możesz stworzyć kosztorys
        mający 4 zagłębienia. Pierwsze to etap, który po uruchomieniu
        projektu będzie posiadał daty. Pozostałe to zadania,
        które posiadają koszt.`, undefined, {
        timeOut: 30000
      });
    } else if(this.stage === 'typeActive') {
      // TODO: sprawdzić czy wszystkie daty są wypełnione i wyświetlić
      // odpowieni komunikat
    }
  }

  initStartProject() {
    if (window.K2.estimation.status_change === 1) {
      toastrInfo(`Podaj daty wszystkich etapów oraz daty wystąpienia kosztów zewnętrznych. Po zapisaniu wyceny status
        projektu zostanie zmieniony na "W produkcji". Projekt zostanie uruchomiony.`, undefined, {
        timeOut: 30000
      });
    }
  }
}

module.exports = EstimationEdit;

Obserwator

Wwzorzec projektowy należący do grupy wzorców czynnościowych. Używany jest do powiadamiania zainteresowanych obiektów o zmianie stanu pewnego innego obiektu.

  • Dlaczego Obserwator
  • Obserwator boilerplate
  • Przykładowa implementacja
  • Przykład zastosowania

Dlaczego Obserwator

  • Luźne powiązania
  • Komunikacja z klasą nadrzędną

class Mediator {
  constructor() {

    this.functionality1();
  }

  functionality1() {
    let componentPopup = new ComponentPopup();
    let componentTimeout = new ComponentTimeout();

    componentPopup.showPopup('ten popup NIE jest po timeoutcie');
    // Co jeśli componentTimeout wykonuje się asynchronicznie a my chcemy
    // poinformować o sukcesie za pomocą componentPopup? Fajnie by było wpiąć się
    // w callbacka ale nie możemy tego zrobić bezpośrednio w componentTimeout
  }
}

Obserwator boilerplate

Implementacja obserwatora za pomocą eventów. SimpleEventer


class ComponentTimeout extends SimpleEventer {
  constructor() {
    super();

  }
}

// component1.on(...) po stronie obserwatora

Przykładowa implementacja

Przykładowy obserwator - zobacz przykład.


class ComponentTimeout extends SimpleEventer {
  constructor() {
    super();

    this.initTimeout();
  }

  initTimeout() {
    setTimeout(() => {
      this.fire('afterTimeout');
    }, 2000);
  }
}

class Mediator {
  constructor() {

  this.componentPopup = new ComponentPopup();
  this.componentTimeout = new ComponentTimeout();

  this.functionality1();
}

  functionality1() {
    this.componentTimeout.on('afterTimeout', () => {
      this.componentPopup.showPopup('po timeoutce'); // po timeoutce
    });
  }
}

Przykład zastosowania

Klasa EstimationEdit z Toolboxa



class EstimationEdit extends Service {
  constructor($context, options = {}) {
    (...)
  }

  initUi() {
    this.initEvaluationInfo();
    this.initCostSummary();
    this.initEvaluationVariant();
    this.initCosts();
    (...)
  }


  initEvaluationInfo() {
    let $estimationInfo = $(document.getElementById('estimationInfo'));
    if($estimationInfo.length > 0) {
      this.estimationInfo = new EstimationInfo($estimationInfo);
    }
  }

  initCostSummary() {
    this.summaryCosts = new SummaryCosts($(document.getElementById('costSummary')));
  }

  initEvaluationVariant() {
  this.evaluationVariant = new EvaluationVariant($(document.getElementById('variants')));
    this.evaluationVariant.on('overheadchange', () => {
      this.costs.setOverhead(this.evaluationVariant.getOverhead());
      this.costs.recalculateAll();
      this.changed = true;
    });
  }

  initCosts() {
    // Costs też jest mediatorem
    this.costs = new Costs($(document.getElementById('costs')), {
      stage: this.stage
    });
    this.costs.on('change', (event) => {
      this.summaryCosts.updateSummaryCosts(event.target.internalCost, event.target.externalCost);
      this.evaluationVariant.setCosts(event.target.internalCost, event.target.externalCost);
      if(this.estimationInfo !== null) {
        this.estimationInfo.setHours(event.target.hours);
      }
      this.changed = true;
    });
    this.costs.setOverhead(this.evaluationVariant.getOverhead());
    this.costs.recalculateAll();
  }
  (...)
}

Żeby nie było zbyt pięknie

Przykład jak nie korzystać z wzorców


(...)

initProfitability() {
  // Profitability Charts
  let $profitabilityCharts = $(document.getElementById('profitabilityCharts'));
  if($profitabilityCharts.length === 1) {
    this.profitabilityCharts = new ProfitabilityCharts($profitabilityCharts);
  }

  // Profitablility Table
  let $profitabilityTable = $(document.getElementById('profitabilityTable'));
  if($profitabilityTable.length === 1) {
    this.profitabilityTable = new ProfitabilityTable($profitabilityTable);
  }

  // Wzorowy reżim
  if(this.profitabilityCharts && this.profitabilityTable) {
    this.profitabilityCharts.on('enterBudget', () => {
      this.profitabilityTable
        .highlight('.today-plan--header, .today-plan--budget');
    });
    this.profitabilityCharts.on('leaveBudget', () => {
      this.profitabilityTable
        .dehighlight('.today-plan--header, .today-plan--budget');
    });
    this.profitabilityCharts.on('enterInternalCosts', () => {
      this.profitabilityTable
        .highlight('.today-plan--header, .today-plan--internalCosts');
    });
    this.profitabilityCharts.on('leaveInternalCosts', () => {
      this.profitabilityTable
        .dehighlight('.today-plan--header, .today-plan--internalCosts');
    });
    this.profitabilityCharts.on('enterInternalCostsTime', () => {
      this.profitabilityTable
        .highlight('.today-plan--header-internal, ' +
          '.item-summary .today-plan--internalCostsTime');
    });
    this.profitabilityCharts.on('leaveInternalCostsTime', () => {
      this.profitabilityTable
        .dehighlight('.today-plan--header-internal, ' +
          '.item-summary .today-plan--internalCostsTime');
    });
    this.profitabilityCharts.on('enterExternalCosts', () => {
      this.profitabilityTable
        .highlight('.today-plan--header, .today-plan--externalCosts');
    });
    this.profitabilityCharts.on('leaveExternalCosts', () => {
      this.profitabilityTable
        .dehighlight('.today-plan--header, .today-plan--externalCosts');
    });
  }
}

(...)

Podsumowanie

  • Obiektowość jest dla dużych systemów
  • Wzorce są dla jeszcze większych systemów
  • Singleton jest wszędzie tym samym obiektem
  • Mediator odciąża komponenty z niepotrzebnej logiki
  • Obserwator pozwala na elastyczną komunikację z obiektami nadrzędnymi
  • Przejrzysta logika zależności pomiędzy komponentami

Źródło

  • MDN
  • Wzorce projektowe (Wiki)
  • Wzorce projektowe, Elementy oprogramowania obiektowego wielokrotnego użytku, Helion
  • Dzięki