Created
July 6, 2013 22:23
-
-
Save ralfw/5941505 to your computer and use it in GitHub Desktop.
Tic Tac Toe mit JavaScript und Eventstore
Inspiriert durch Mike Bilds Vorlage: https://gist.github.com/MikeBild/5926056
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| var BlackBox = function() { | |
| var self = this; | |
| self._events = []; | |
| self.Record = function(event) { | |
| self._events.push(event); | |
| self.Recorded(event); | |
| }; | |
| self.Replay = function() { | |
| return self._events; | |
| }; | |
| self.Recorded = function(event) {}; | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| var Controller = function() { | |
| var self = this; | |
| Eventhandler_an_View_binden(); | |
| self.Anzeigen = function(spiel) { | |
| spiel.Zuege.forEach(function(zug) { | |
| document.getElementById(zug.Spielfeldkoordinate).innerText = zug.Spieler; | |
| }); | |
| document.getElementById("situation").innerText = spiel.Spielstand; | |
| document.getElementById("player").innerText = spiel.Spieler; | |
| document.getElementById("round").innerText = spiel.Runde; | |
| }; | |
| self.Gezogen = null; | |
| function Eventhandler_an_View_binden() { | |
| var spielbrett = Array.prototype.slice.call(document.getElementsByTagName("td")); | |
| spielbrett.forEach(function (spielfeld) { | |
| spielfeld.addEventListener('click', function () { | |
| self.Gezogen(this.id); | |
| }); | |
| }); | |
| } | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| var Domain = function(blackbox) { | |
| var self = this; | |
| self._blackbox = blackbox; | |
| blackbox.Record({Name:"Spielerwechsel", Spieler:"X"}); | |
| self.Ziehen = function(spielfeldkoordinate) { | |
| self.Validieren(spielfeldkoordinate, function() { | |
| self.Zug_ausfuehren(spielfeldkoordinate); | |
| self.Spieler_wechseln(); | |
| self.Spielstand_ermitteln(); | |
| self.Zug_ausgefuehrt(); | |
| }); | |
| }; | |
| self.Validieren = function(spielfeldkoordinate, weiter_bei_validem_Zug) { | |
| var events = self._blackbox.Replay(); | |
| if (events.length > 0 && events[events.length-1].Name == "Spielende") | |
| return; | |
| if (linq.from(events).any(function(e){return e.Name=="Zug" && e.Spielfeldkoordinate==spielfeldkoordinate})) | |
| return; | |
| weiter_bei_validem_Zug(); | |
| }; | |
| self.Zug_ausfuehren = function(spielfeldkoordinate) { | |
| var events = self._blackbox.Replay(); | |
| var aktueller_Spieler = events[events.length-1].Spieler; | |
| self._blackbox.Record({Name:"Zug", Spielfeldkoordinate:spielfeldkoordinate, Spieler:aktueller_Spieler}); | |
| }; | |
| self.Spieler_wechseln = function() { | |
| var events = self._blackbox.Replay(); | |
| var aktueller_Spieler = events[events.length-1].Spieler == "X" ? "O" : "X"; | |
| self._blackbox.Record({Name:"Spielerwechsel", Spieler:aktueller_Spieler}); | |
| }; | |
| self.Spielstand_ermitteln = function() { | |
| var events = self._blackbox.Replay(); | |
| var zuege = linq.from(events).where(function(e){return e.Name=="Zug"}).list; | |
| if (Pruefen_auf_Gewinn("X")) return; | |
| if (Pruefen_auf_Gewinn("O")) return; | |
| if (zuege.length == 9) | |
| self._blackbox.Record({Name:"Spielende", Spielstand:"unentschieden"}); | |
| function Pruefen_auf_Gewinn(spieler) { | |
| var solutions = [[11,12,13], [21,22,23], [31,32,33], | |
| [11,22,33], [13,22,31], [11,21,31], | |
| [12,22,32], [13,23,33]]; | |
| var spielerzuege = linq.from(zuege) | |
| .where(function (e) {return e.Spieler == spieler}) | |
| .select(function (e) {return e.Spielfeldkoordinate}) | |
| .list; | |
| for(var i=0; i<solutions.length; i++) { | |
| if (linq.from(spielerzuege) | |
| .intersect(solutions[i]) | |
| .list | |
| .length == 3) { | |
| self._blackbox.Record({Name:"Spielende", Spielstand:spieler+" gewinnt"}); | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| }; | |
| self.Zug_ausgefuehrt = null; | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <style> | |
| table { | |
| border: 4px solid black; | |
| } | |
| table tr td{ | |
| width: 50px; | |
| height: 50px; | |
| border: 1px solid black; | |
| text-align: center; | |
| vertical-align: middle; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="board"> | |
| <table> | |
| <tr> | |
| <td id="31"></td> | |
| <td id="32"></td> | |
| <td id="33"></td> | |
| </tr> | |
| <tr> | |
| <td id="21"></td> | |
| <td id="22"></td> | |
| <td id="23"></td> | |
| </tr> | |
| <tr> | |
| <td id="11"></td> | |
| <td id="12"></td> | |
| <td id="13"></td> | |
| </tr> | |
| </table> | |
| </div> | |
| <div> | |
| Round: | |
| <div id="round">1</div> | |
| Player: | |
| <div id="player">X</div> | |
| Situation: | |
| <div id="situation"></div> | |
| </div> | |
| <script src="js/Linq.js"></script> | |
| <script src="js/BlackBox.js"></script> | |
| <script src="js/Controller.js"></script> | |
| <script src="js/Domain.js"></script> | |
| <script src="js/ReadModel.js"></script> | |
| <script> | |
| var blackbox = new BlackBox(); | |
| var controller = new Controller(); | |
| var domain = new Domain(blackbox); | |
| var readModel = new ReadModel(blackbox); | |
| controller.Gezogen = domain.Ziehen; | |
| domain.Zug_ausgefuehrt = readModel.Generieren; | |
| readModel.Spiel = controller.Anzeigen; | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| var Linq = function() { | |
| var self = this; | |
| self.from = function(list) { | |
| return { | |
| select: function(selector) { | |
| var newList = new Array(); | |
| for(var i=0; i<list.length; i++) | |
| newList.push(selector(list[i], i)); | |
| return self.from(newList); | |
| }, | |
| where: function(predicate) { | |
| var newList = new Array(); | |
| for(var i=0; i<list.length; i++) | |
| if (predicate(list[i], i)) | |
| newList.push(list[i]); | |
| return self.from(newList); | |
| }, | |
| any: function(predicate) { | |
| for(var i=0; i<list.length; i++) | |
| if (predicate(list[i], i)) | |
| return true; | |
| return false; | |
| }, | |
| intersect: function(list2) { | |
| var intersection = new Array(); | |
| for(var i=0; i<list.length; i++) | |
| for(var j=0; j<list2.length; j++) | |
| if (list[i]==list2[j]) | |
| intersection.push(list[i]); | |
| return self.from(intersection); | |
| }, | |
| list: list | |
| } | |
| } | |
| } | |
| var linq = new Linq(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| var ReadModel = function(blackbox) { | |
| var self = this; | |
| self._blackbox = blackbox; | |
| self.Generieren = function() { | |
| var events = self._blackbox.Replay(); | |
| var zuege = linq.from(events) | |
| .where(function(e){return e.Name=="Zug"}) | |
| .select(function(e, i){return { | |
| Spielfeldkoordinate: e.Spielfeldkoordinate, | |
| Spieler: i % 2 == 0 ? "X" : "O" | |
| }}) | |
| .list; | |
| var aktueller_spieler = events[events.length-1].Name == "Spielerwechsel" | |
| ? events[events.length-1].Spieler | |
| : "-"; | |
| var spielstand = events[events.length-1].Name == "Spielende" | |
| ? events[events.length-1].Spielstand | |
| : "-"; | |
| self.Spiel({Zuege: zuege, Spielstand: spielstand, Spieler: aktueller_spieler, Runde: zuege.length+1}); | |
| }; | |
| self.Spiel = null; | |
| }; |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Struktur: Klar bin ich "außen" (Membran der Softwarezelle) technisch und innen fachlich. So sollte es sein, würde ich sagen. Die Domain ("innen") repräsentiert ja die Fachlichkeit. Dafür wird Software gemacht :-) Ubiquitous language lässt grüßen.
JavaScript: Ich bin da ja kein Profi. Ob man sich <Script> sparen kann oder so, weiß ich nicht. Dito bei "private" Methoden. Da war ich mir unsicher, wie das am besten ausgedrückt wird. Ich will in denen ja auch auf den Zustand einer Instanz zugreifen.
Projektionen: Das ist mir wichtig zu zeigen. Jeder kann den einen Event-Strom so projizieren/analysieren, wie er mag. Damit sind die einzelnen Teile maximal unabhängig. Zustand bzw. gemeinsame Datenstrukturen sind Optimierungen. Die kann man später einführen, wenn man mag.
Um die Projektionen unabhängiger zu halten, habe ich sogar die Events gegenüber der C#-Variante im Blog "aufgeblasen". Es gibt verschiedene und mit mehr Infos. An jedem Zug hängt jetzt z.B. der Spieler. Man könnte zwar alles aus den Zügen immer wieder ableiten - aber sprechender finde ich es, wenn auch "Schlüsse" (z.B. ein Spielerwechsel oder Spielende) mit protokolliert werden.
Damit schützt man die App gegen Veränderungen in der Domänenlogik. Denn die könnten in der Zukunft zu Neuinterpretationen alter Events führen. Das wäre nicht gut. Ein EventStore speichert also nicht nur Veränderungen, sondern auch Entscheidungen. Sozusagen geronnene Domänenlogik.
Das bisschen Linq hatte ich mal Lust zu basteln, um mich an JS zu versuchen und den Code self-contained zu halten. Aber es gibt natürlich auch "offizielle" Libs dazu, z.B. http://linqjs.codeplex.com/