Javascript. Visitor pattern

Паттерн Посетитель (Visitor pattern) определяет операцию, выполняемую на каждом элементе некоторой структуры. Позволяет, не изменяя классы этих объектов, добавлять в них новые операции. Как известно, различные и несвязные операции выполняются над узловыми
объектами некоторой гетерогенной совокупной структуры. Чтобы избежать "загрязнения" классов этих узлов такими операциями (то есть избежать добавления соответствующих методов в эти классы), можно воспользоваться паттерном Посетитель. Также в реализации этого паттерна играют роль типы объектов, что позволяет делать типизированный выбор действий. Так как мы имеем дело с Javascript, то здесь это делается немного иначе: через условные конструкции. Данный паттерн относится к паттернам поведения. Ниже редставлена диаграмма классов реализации данного паттерна.

Также ниже представлен пример реализации данного паттерна.

/* internal iterator */

Array.prototype.iterator = function () {
    
    var self = this, position = 0, undefined;
    
    return {
        
        next: function () {
            return self[position++];
        },
        
        hasNext: function () {
            if (position >= self.length) {
                return false;
            } 
            else {
                return true;
            }
        },
        
        remove: function () {
            if (position <= 0) {
                throw new Error("You can`t an item until you`ve done at least one next()");
            }
            if (self[position - 1] !== undefined) {
                for (var i = position - 1; i < (self.length - 1); i++) {
                    self[i] = self[i + 1];
                }
                self[self.length - 1] = undefined;
            }
        }
        
    };
};

/* interface Element */

var ComputerPart = function () {
this.accept = function (computerPartVisitor) {
throw new Error("accept method is not implemented");
};
};

/* ConcreteElement */

var Keyboard = function () {
this.accept = function (computerPartVisitor) {
computerPartVisitor.visit(this);
};
};

Keyboard.prototype = new ComputerPart();
Keyboard.prototype.constructor = Keyboard;

/* ConcreteElement */

var Monitor = function () {
this.accept = function (computerPartVisitor) {
computerPartVisitor.visit(this);
};
};

Monitor.prototype = new ComputerPart();
Monitor.prototype.constructor = Monitor;

/* ConcreteElement */

var Mouse = function () {
this.accept = function (computerPartVisitor) {
computerPartVisitor.visit(this);
};
};

Mouse.prototype = new ComputerPart();
Mouse.prototype.constructor = Mouse;

/* ConcreteElement */

var Computer = function () {

var parts = [new Mouse(), new Keyboard(), new Monitor()];

this.accept = function (computerPartVisitor) {
for (var it = parts.iterator(); it.hasNext();) {
(it.next()).accept(computerPartVisitor);
}
computerPartVisitor.visit(this);
};
};

Computer.prototype = new ComputerPart();
Computer.prototype.constructor = Computer;

/* interface Visitor */

var computerPartVisitor = function () {
this.visit = function (computerPart) {
throw new Error("visit method is not implemented");
};
};

/* ConcreteVisitor */

var computerPartDisplayVisitor = function () {
this.visit = function (computerPart) {
if (computerPart.constructor === Computer) {
console.log("Displaying Computer.");
}
if (computerPart.constructor === Mouse) {
console.log("Displaying Mouse.");
}
if (computerPart.constructor === Keyboard) {
console.log("Displaying Keyboard.");
}
if (computerPart.constructor === Monitor) {
console.log("Displaying Monitor.");
}
};
};

computerPartDisplayVisitor.prototype = new computerPartVisitor();
computerPartDisplayVisitor.constructor = computerPartDisplayVisitor;

/* testing... */

var Application = function () {
this.run = function () {
var computer = new Computer();
computer.accept(new computerPartDisplayVisitor());
};
};

var app = new Application();
app.run();

В итоге получаем:

Displaying Mouse.
Displaying Keyboard.
Displaying Monitor.
Displaying Computer.

Всем успехов.

Javascript. Proxy pattern

Паттерн Заместитель (Proxy pattern) предоставляет суррогатный объект, управляющий доступом к другому объекту. Выделяют несколько вариантов управления доступом в заместителях:
- удаленный заместитель управляет доступом к удаленному объекту;
- виртуальный заместитель управляет доступом к ресурсу, создание которого требует больших затрат ресурсов;
- защитный заместитель контролирует доступ к ресурсу в соответствии с системой привилегий.
Данный паттерн относится к структурным паттернам. Основная реализация основана на отложенной инициализации объекта. Данный паттерн полезно использовать там, где приходится работать с дорогими ресурсами, например, пусть создание реального объекта или его использование занимает много ресурсов, тогда прокси-объект может взять на себя часть обязанностей данного объекта с целью избегания лишних обращений к нему, например, прокси может иметь кэш, который можно отдавать клиентам на их запросы, вместо повторного обращения к ресурсам. Вот примерчик сказанного.

/* real object */

var http = {
makeRequest: function (handler) {
$.ajax({
url: 'javascript.js.php',
async: false, // синхронный режим
success: handler
});
}
};

/* proxy object */

var proxy = {
cache: null,
makeRequest: function (handler) {
console.log(this.cache);
if (this.cache != null) {
handler(this.cache);
}
else {
http.makeRequest(function (data) {
proxy.cache = data;
handler(data);
});
}
}
};

var handler = function (data) {
// some actions
};

// первоначальное обращение к реальному объекту http
proxy.makeRequest(handler);
// выдача кэша
proxy.makeRequest(handler); 

Ниже приведена диаграмма классов данного паттерна


Стоит отметить, что на Javascript реализовать удаленного заместителя в силу ее природы невозможно, хотя есть идеи, высказываемые некоторыми разработчиками, например см. книгу - Pro JavaScript Design Pattens. Ross Harmes. Dustan Diaz - о работе через ajax с удаленными веб сервисами или объектами. Но это только некоторого рода приближение. Также затруднительно реализовать защитного заместителя, в силу слабой типизации языка. Хотя на мой взгляд, все зависит от смысла вкладываемого к слово защитный :) Ниже приведу еще один пример реализации данного паттерна.


/* interface Subject - Image */

var Image = function () {
this.display = function () {};
};

/* RealSubject - RealImage */

var RealImage = function (fileName) {
var fileName = fileName;

this.display = function () {
console.log("Displaying " + fileName);
};

this.loadFromDisk = function () {
console.log("Loading " + fileName);
};

this.loadFromDisk(fileName);
};

RealImage.prototype = new Image;
RealImage.prototype.constructor = RealImage;

/* Proxy - ProxyImage */

var ProxyImage = function (fileName) {
var fileName = fileName,
realImage = null;

this.display = function () {
if (realImage === null) {
realImage = new RealImage(fileName);
}
realImage.display();
};
};

ProxyImage.prototype = new Image;
ProxyImage.prototype.constructor = ProxyImage;

/* testing... */

var Application = function () {
this.run = function () {
var image = new ProxyImage("test_10mb.jpg");
//image will be loaded from disk
image.display();
//image will not be loaded from disk
image.display();
};
};

var app = new Application();
app.run();

На выходе получаем следующий лог

Loading test_10mb.jpg
Displaying test_10mb.jpg
Displaying test_10mb.jpg

Javascript. Mediator pattern

Паттерн Посредник (Mediator pattern) используется для централизации сложных взаимодействий и управляющих операций между объектами. Использование данного паттерна позволяет снизить количество зависимостей между объектами в системе (так называемые - коллеги), а взаимодействие между ними осуществляет непосредственно специальный объект (посредник или медиатор). Это улучшает сопровождение кода, так как мы всегда можем реализовать посредник с требуемыми критериями. Данный паттерн относится к поведенческим паттернам. В самом простом виде диаграмма классов данного паттерна выглядит следующим образом.


Заметим, что коллеги знают только о посреднике, в котором инкапсулирована логика взаимодействия. При возрастании объектов в системе данная логика может усложниться, что является одним из минусов паттерна. Рассмотрим ряд пример реализации данного паттерна. Пример первый. В системе имеются пользователи, необходимо наладить между ними общение, например в виде чата. Одна из реализаций.

/* Colleagues */

var User = function (mediator, name) {
this.mediator = mediator;
this.name = name;
};

User.prototype.send = function (msg) {
console.log(this.name + ": sending message: " + msg);
this.mediator.sendMessage(msg, this);
};

User.prototype.receive = function (msg) {
console.log(this.name + ": received message: " + msg);
};

/* ConcreteMediator */

var chatMediator = {
users: [],
addUser: function (user) {
this.users.push(user);
},
sendMessage: function (msg, user) {
for (var i = 0; i < this.users.length; i++) {
if (this.users[i] !== user) {
this.users[i].receive(msg);
}
}
}
};

var Adam = new User(chatMediator, "Adam");
var Poll = new User(chatMediator, "Poll");
var Jon = new User(chatMediator, "Jon");
var Allan = new User(chatMediator, "Allan");

chatMediator.addUser(Adam);
chatMediator.addUser(Poll);
chatMediator.addUser(Jon);
chatMediator.addUser(Allan);

Adam.send("Hi All");

На выходе:

Adam: sending message: Hi All
Poll: received message: Hi All
Jon: received message: Hi All
Allan: received message: Hi All

Пример второй. Ниже будет представлена реализация простой игры - два игрока в течении 30 секунд должны нажимать свою кнопку (1 или 2), по окончании времени побеждает тот, кто успел нажать как можно больше раз. В реализации игры будут участвовать объекты-коллеги и посредник, которые представлены на следующем рисунке.


/* Colleagues */

var Player = function (name) {
this.points = 0;
this.name = name;
};
Player.prototype.play = function () {
this.points += 1;
mediator.played();
};

/* ConcreteColleague */

var scoreboard = {

// element of HTML, which should be updated
element: document.getElementById('results'),

// updates the score on the screen
update: function (score) {
var i, msg = '';
for (i in score) {
if (score.hasOwnProperty(i)) {
msg += '<p><strong>' + i + '<\/strong>';
msg += score[i];
msg += '<\/p>';
}
}
this.element.innerHTML = msg;
}
};

/* ConcreteMediator */

var mediator = {

// all players
players: {},

// initialization
setup: function () {
var players = this.players;
players.home = new Player('Home');
players.guest = new Player('Guest');
},

// updates the score, if someone of the players made the move
played: function () {
var players = this.players,
score = {
Home:  players.home.points,
Guest: players.guest.points
};

scoreboard.update(score);
},

// handler of user actions
keypress: function (e) {
e = e || window.event;   // IE
if (e.which === 49) { // key "1"
mediator.players.home.play();
return;
}
if (e.which === 48) { // key "0"
mediator.players.guest.play();
return;
}
}
};

/* testing */

var Application = function () {
this.run = function () {
// Start!
mediator.setup();
window.onkeypress = mediator.keypress;

// The game ends after 30 seconds
setTimeout(function () {
window.onkeypress = null;
alert('Game over!');
}, 30000);
};
};

var game = new Application;
game.run();

Приятной Вам игры :)

Javascript. State pattern

Паттерн Состояние (State pattern) управляет изменением поведения объекта при изменении его внутреннего состояния. Внешне это выглядит так, словно объект меняет свой класс. Паттерн инкапсулирует состояние в отдельных классах и делегирует операции объекту, представляющему текущее состояние, поэтому поведение объекта изменяется вместе с внутренним состоянием. Данный паттерн относится к поведенческим шаблонам проектирования. Ниже представлена диаграмма классов данного паттерна.



Данный паттерн по диаграмме эквивалентен паттерну Стратегия, но отличие заключается в цели: вместо алгоритмов в нашем случае варианты поведения инкапсулируются в объектах состояния. Поэтому данный паттерн может рассматриваться как замена многочисленных условных конструкций в коде контекста: если поведение инкапсулировано в объектах состояния, для изменения поведения контекста достаточно выбрать другой объект состояния.

Рассмотрим сначала пример реализации работы автомата, выдающего шарики за монетку без использования паттерна Состояние. Итак, наш автомат может находиться в определенных состояниях, например - ожидание, пуст и прочее, что зависит от ряда условий. Между состояниями существуют переходы, например - выдыча шарика или прием монетки. И вот здесь нужно использовать различные условные конструкции. Для начала рассмотрим модель нашего автомата на следующем рисунке.



Как видно, кружки - состояния автомата, а стрелки - переходы между состояниями. Вот реализация без паттерна.

/* GumballMachine */

var GumballMachine = function (count) {
var SOLD_OUT = 0,
NO_QUARTER = 1,
HAS_QUARTER = 2,
SOLD = 3;
var state = SOLD_OUT,
count = count || 0;
  
if (count > 0) {
state = NO_QUARTER;
}
  
this.insertQuarter = function () {
if (state == HAS_QUARTER) {
console.log("You can't insert another quarter");
} else if (state == NO_QUARTER) {
state = HAS_QUARTER;
console.log("You inserted a quarter");
} else if (state == SOLD_OUT) {
console.log("You can't insert a quarter, the machine is sold out");
} else if (state == SOLD) {
        console.log("Please wait, we're already giving you a gumball");
}
};

this.ejectQuarter = function () {
if (state == HAS_QUARTER) {
console.log("Quarter returned");
state = NO_QUARTER;
} else if (state == NO_QUARTER) {
console.log("You haven't inserted a quarter");
} else if (state == SOLD) {
console.log("Sorry, you already turned the crank");
} else if (state == SOLD_OUT) {
        console.log("You can't eject, you haven't inserted a quarter yet");
}
};
this.turnCrank = function () {
if (state == SOLD) {
console.log("Turning twice doesn't get you another gumball!");
} else if (state == NO_QUARTER) {
console.log("You turned but there's no quarter");
} else if (state == SOLD_OUT) {
console.log("You turned, but there are no gumballs");
} else if (state == HAS_QUARTER) {
console.log("You turned...");
state = SOLD;
this.dispense();
}
};
this.dispense = function () {
if (state == SOLD) {
console.log("A gumball comes rolling out the slot");
count = count - 1;
if (count == 0) {
console.log("Oops, out of gumballs!");
state = SOLD_OUT;
} else {
state = NO_QUARTER;
}
} else if (state == NO_QUARTER) {
console.log("You need to pay first");
} else if (state == SOLD_OUT) {
console.log("No gumball dispensed");
} else if (state == HAS_QUARTER) {
console.log("No gumball dispensed");
}
};
this.refill = function (numGumBalls) {
count = numGumBalls;
state = NO_QUARTER;
};

this.toString = function () {
var result = "";
result += "\nMighty Gumball, Inc.";
result += "\nJavascript-enabled Standing Gumball Model #2004\n";
result += "Inventory: " + count + " gumball";
if (count != 1) {
result += "s";
}
result += "\nMachine is ";
if (state == SOLD_OUT) {
result += "sold out";
} else if (state == NO_QUARTER) {
result += "waiting for quarter";
} else if (state == HAS_QUARTER) {
result += "waiting for turn of crank";
} else if (state == SOLD) {
result += "delivering a gumball";
}
result += "\n";
return result;
};
};

/* testing... */

var Application = function () {
this.run = function () {
var gumballMachine = new GumballMachine(5);

console.log(gumballMachine.toString());

gumballMachine.insertQuarter();
gumballMachine.turnCrank();

console.log(gumballMachine.toString());

gumballMachine.insertQuarter();
gumballMachine.ejectQuarter();
gumballMachine.turnCrank();

console.log(gumballMachine.toString());

gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.ejectQuarter();

console.log(gumballMachine.toString());

gumballMachine.insertQuarter();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();

console.log(gumballMachine.toString());
};
}; 

var application = new Application();
application.run();

На выходе получаем:

Mighty Gumball, Inc.
Javascript-enabled Standing Gumball Model #2004
Inventory: 5 gumballs
Machine is waiting for quarter

You inserted a quarter
You turned...
A gumball comes rolling out the slot

Mighty Gumball, Inc.
Javascript-enabled Standing Gumball Model #2004
Inventory: 4 gumballs
Machine is waiting for quarter

You inserted a quarter
Quarter returned
You turned but there's no quarter

Mighty Gumball, Inc.
Javascript-enabled Standing Gumball Model #2004
Inventory: 4 gumballs
Machine is waiting for quarter

You inserted a quarter
You turned...
A gumball comes rolling out the slot
You inserted a quarter
You turned...
A gumball comes rolling out the slot
You haven't inserted a quarter

Mighty Gumball, Inc.
Javascript-enabled Standing Gumball Model #2004
Inventory: 2 gumballs
Machine is waiting for quarter

You inserted a quarter
You can't insert another quarter
You turned...
A gumball comes rolling out the slot
You inserted a quarter
You turned...
A gumball comes rolling out the slot
Oops, out of gumballs!
You can't insert a quarter, the machine is sold out
You turned, but there are no gumballs

Mighty Gumball, Inc.
Javascript-enabled Standing Gumball Model #2004
Inventory: 0 gumballs
Machine is sold out

А теперь реализуем данный автомат с помощью паттерна Состояние. 

/* interface State */

var State = function () {
  this.insertQuarter = function () {};
this.ejectQuarter = function () {};
this.turnCrank = function () {};
this.dispense = function () {};
};

/* ConcreteState - HasQuarterState */

var HasQuarterState = function (gumballMachine) {
var gumballMachine = gumballMachine;
  
this.insertQuarter = function () {
console.log("You can't insert another quarter");
};
this.ejectQuarter = function () {
console.log("Quarter returned");
gumballMachine.setState(gumballMachine.getNoQuarterState());
};
this.turnCrank = function () {
console.log("You turned...");
gumballMachine.setState(gumballMachine.getSoldState());
};

    this.dispense = function () {
        console.log("No gumball dispensed");
    };
this.toString = function () {
return "waiting for turn of crank";
};
};

HasQuarterState.prototype = new State();
HasQuarterState.prototype.constructor = HasQuarterState;

/* ConcreteState - NoQuarterState */

var NoQuarterState = function (gumballMachine) {
    var gumballMachine = gumballMachine;
this.insertQuarter = function () {
console.log("You inserted a quarter");
gumballMachine.setState(gumballMachine.getHasQuarterState());
};
this.ejectQuarter = function () {
console.log("You haven't inserted a quarter");
};
this.turnCrank = function () {
console.log("You turned, but there's no quarter");
};
this.dispense = function () {
console.log("You need to pay first");
}; 
this.toString = function () {
return "waiting for quarter";
};
}

NoQuarterState.prototype = new State();
NoQuarterState.prototype.constructor = NoQuarterState;

/* ConcreteState - SoldOutState */

var SoldOutState = function (gumballMachine) {
    
var gumballMachine = gumballMachine;
this.insertQuarter = function () {
console.log("You can't insert a quarter, the machine is sold out");
};
this.ejectQuarter = function () {
console.log("You can't eject, you haven't inserted a quarter yet");
};
this.turnCrank = function () {
console.log("You turned, but there are no gumballs");
};
this.dispense = function () {
console.log("No gumball dispensed");
};
this.toString = function () {
return "sold out";
};
};

SoldOutState.prototype = new State();
SoldOutState.prototype.constructor = SoldOutState;

/* ConcreteState - SoldState */

var SoldState = function (gumballMachine) {
var gumballMachine = gumballMachine;
       
this.insertQuarter = function () {
console.log("Please wait, we're already giving you a gumball");
};
this.ejectQuarter = function () {
console.log("Sorry, you already turned the crank");
};
this.turnCrank = function () {
console.log("Turning twice doesn't get you another gumball!");
};
this.dispense = function () {
gumballMachine.releaseBall();
if (gumballMachine.getCount() > 0) {
gumballMachine.setState(gumballMachine.getNoQuarterState());
} else {
console.log("Oops, out of gumballs!");
gumballMachine.setState(gumballMachine.getSoldOutState());
}
};
this.toString = function () {
return "dispensing a gumball";
};
};

SoldState.prototype = new State();
SoldState.prototype.constructor = SoldState;

/* Context */

var GumballMachine = function (numberGumballs) {
var soldOutState = new SoldOutState(this),
noQuarterState = new NoQuarterState(this),
hasQuarterState = new HasQuarterState(this), 
soldState = new SoldState(this);
var state = soldOutState,
count = numberGumballs;
if (numberGumballs > 0) {
state = noQuarterState;
}
this.insertQuarter = function () {
state.insertQuarter();
};
this.ejectQuarter = function () {
state.ejectQuarter();
};
this.turnCrank = function () {
state.turnCrank();
state.dispense();
};

this.setState = function (_state) {
state = _state;
};
this.releaseBall = function () {
console.log("A gumball comes rolling out the slot...");
if (count != 0) {
count = count - 1;
}
};
this.getCount = function () {
return count;
};
this.refill = function (_count) {
count = _count;
state = noQuarterState;
};

    this.getState = function () {
        return state;
    };

    this.getSoldOutState = function () {
        return soldOutState;
    };

    this.getNoQuarterState = function () {
        return noQuarterState;
    };

    this.getHasQuarterState = function () {
        return hasQuarterState;
    };

    this.getSoldState = function () {
        return soldState;
    };
this.toString = function () {
var result = "";
result += "\nMighty Gumball, Inc.";
result += "\nJavascript-enabled Standing Gumball Model #2004";
result += "\nInventory: " + count + " gumball";
if (count != 1) {
result += "s";
}
result += "\n";
result += "Machine is " + state + "\n";
return result;
};
};

/* testing... */

var GumballMachineTestDrive = function () {
this.run = function () {
var gumballMachine = new GumballMachine(5);

console.log(gumballMachine.toString());

gumballMachine.insertQuarter();
gumballMachine.turnCrank();

console.log(gumballMachine.toString());

gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();

console.log(gumballMachine.toString());
};
};

var machine = new GumballMachineTestDrive();
machine.run();

На выходе получаем следующий результат:

Mighty Gumball, Inc.
Javascript-enabled Standing Gumball Model #2004
Inventory: 5 gumballs
Machine is waiting for quarter

You inserted a quarter
You turned...
A gumball comes rolling out the slot...

Mighty Gumball, Inc.
Javascript-enabled Standing Gumball Model #2004
Inventory: 4 gumballs
Machine is waiting for quarter

You inserted a quarter
You turned...
A gumball comes rolling out the slot...
You inserted a quarter
You turned...
A gumball comes rolling out the slot...

Mighty Gumball, Inc.
Javascript-enabled Standing Gumball Model #2004
Inventory: 2 gumballs
Machine is waiting for quarter

Всем удачи в бою :)