Dużo praktyki mało teorii
Do czego służą wzorce projektowe i po co nam one
Duży projekt
Warto poświęcić chwilę żeby się zastanowić nim siądzie się do kodu. Nawet z kartką i długopisem w ręku.
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();
}
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
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)
Czasem nawet najładniej napisany kod nie obędzie się bez wzorców.
Krótkie przypomnienie obiektowości w ES6
Kilka metod tworzenia obiektów
{}; // Object {}
new Object(); // Object {}
Object.create(null); // Object {} bez prototypu
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);
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);
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();
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.
Czysty singleton w ES6.
class EmptySingleton {
constructor() {
if(EmptySingleton.singletonInstance) {
return EmptySingleton.singletonInstance;
}
EmptySingleton.singletonInstance = this;
// TOTO: Zaimplementuj mnie ;)
}
}
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);
}
}
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];
}
}
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.
Jak zaimplementować mediator
class EmptyMediator {
constructor() {
}
}
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();
}
}
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;
Wwzorzec projektowy należący do grupy wzorców czynnościowych. Używany jest do powiadamiania zainteresowanych obiektów o zmianie stanu pewnego innego obiektu.
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
}
}
Implementacja obserwatora za pomocą eventów. SimpleEventer
class ComponentTimeout extends SimpleEventer {
constructor() {
super();
}
}
// component1.on(...) po stronie obserwatora
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
});
}
}
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();
}
(...)
}
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');
});
}
}
(...)