Can I decouple Classes by adding logic to the Controller in MVC? - javascript

I have hopefully explained my problem below by writing a short simplified example with the same conceptual question as my actual larger application. My question is centred around ways in which I can de-couple classes from each other. Whilst I accept no application can be free of coupling completely and modules have to interact with each other, I am trying to establish best ways in which I can keep each class as a separate unit so that if changes are made in one class it won't affect another. I want to keep changes simple.
Below I have outlined an example to the regular problem I face. As an example I have centred this example around a simple game where we can add a player named 'Kevin' everytime we press enter. Then everytime we click a button we add a score of 10 to every players running score and print the updated score of the current player to the screen.
Like most things I have used the MVC pattern to achieve this small example game. The Model has a Player class with data and methods. One of the methods adds 10 to the players score.
I also have a PlayerCircle class in the Model which returns things like the current player as-well as changing which players turn it is.
Lastly in the Model I have a Game Class which imports PlayerCircle. PlayerCircle is then a property of the Game class. Its the only property at the moment but as the game grows, further classes will be added e.g. Dice class
The controller module calls methods from the Model and passes things into view much like how an MVC pattern should work.
This is my PlayerCircle class
export class PlayersCircle {
constructor() {
this.players = [];
this.playersTurn = 0;
}
addPlayer(name) {
const player = new Player(name);
this.players.push(player);
}
changePlayer() {
this.playersTurn++;
}
getPlayer() {
return this.players[this.playersTurn];
}
increaseAllScores() {
this.players.forEach((player) => {
player.add10();
});
}
getAllPlayers() {
return this.players;
}
}
This is my player class
export class Player {
constructor(name) {
this.name = name;
this.score = 0;
}
add10() {
this.score += 10;
}
getScore() {
return this.score;
}
}
This is my game class
import { PlayersCircle } from "./PlayersCircle.js";
export class Game {
constructor() {
this.playersCircle = new PlayersCircle();
}
}
Now I have set up the classes for this very simple game, I now write this very simple function from the controller which adds a player to the game.
import { Game } from "./model/Game.js";
import { viewShowScore } from "./view/viewShowScore.js";
const game = new Game();
const addPlayer = () => {
game.playersCircle.addPlayer("kevin");
};
document.querySelector("body").addEventListener("keypress", addPlayer);
Ok so now I want to add 10 points to all the players in the game. Lets say I have fired the keypress 5 times and have 5 players. This is where I often struggle to establish the best Architectural Plan going forward with an Application.
The two options I normally have are
Call the add10 method on each player from the playerCircle class :-
This will enable me to use my controller just to call methods which change data or get data to the render the state into the view. This keeps my controller short and concise.However it means that the PlayersCircle class is now tightly coupled with the Player Class as one of the PlayerCircle methods uses a method of the Player class ( the add10 method) which means any changes in the Player Class on the add10 method will now break the PlayerCircle class.
My function from the controller to add 10 point to each player looks like this:
const increaseAllScores = () => {
game.playersCircle.increaseAllScores();
const currentPlayer = game.playersCircle.getPlayer();
viewShowScore(currentPlayer.getScore()); // function from view prints 10
};
document.querySelector("button").addEventListener("click", increaseAllScores);
Return all the players to the controller and then from inside the controller module run a loop on the returned array which holds the players-:
This will enable my PlayersCircle class to be completely independent from the Player class and just control the state of who'es turn it is, return all the players at once or separately. It won't have to concern itself with the inner methods of the Player class. The Player class as the game grows will have many methods. If I want to call a method on more than one player at a time then (like the add10 method) then the playersCircle class is very tightly coupled and changes from both classes will almost have to simultaneously occur.
However this would mean that my controller module isn't now just calling methods from the Model and passing results into the view but is now containing loops. Whilst I prefer this way whenever I see examples of MVC the controller is full of simple methods calls from the Model to the view.
The function inside the controller now looks like this:
const increaseAllScores = () => {
const allPlayers = game.playersCircle.getAllPlayers();
allPlayers.forEach((player) => {
player.add10();
});
const currentPlayer = game.playersCircle.getPlayer();
viewShowScore(currentPlayer.getScore()); // function from view prints 10
};
Both Pieces of code give the same result. However for long term code management when the game increases in complexity and may have new features in the future, which one is better than the other and why - option 1 which calls the add10 from the playersCircle targeting every player or option 2 which runs a loop from the controller targeting every player which is returned from the playerCircle class ?

Related

Arranging JSX according to a certain algorithm

I'm making a simple web app that helps people organize teams by skill level, you enter the player name, skill level ( 0 - 10 ), and the player's avatar.
For example, you add 3 players in team1 and 4 in team2 and then you get them displayed in a way where the sum of skill value of team1 is equal to team2.
I ended up making the app create a new player instance each time the user presses the add button and then push that instance to an array. just like you see here:
class Player {
constructor(name, image, skill) {
this.name = name;
this.image = image;
this.skill = skill;
}
}
const createPlayer = (name, image, skill) => {
// creates a player with given data and pushes it to the playersList array
const playerIns = new Player(name, image, skill);
playersList.push(playerIns);
};
Then the app maps through the playersList array and returns a list element for each player just like a basic todo-app.
I need help with implementing the logic that organizes the players correctly as described above.
I can't really think of a way to make this work so I'd appreciate any help.

Typescript data modeling shared by both front-end and back-end - Classes or interfaces?

I wonder what's the best way of sharing the same data types between the client (React) and the server (Express + Socket.IO).
In my game I have different rooms, each room saves the current status, something like:
class GameRoom {
players: Player[];
started: boolean;
currentPlayerTurn; Player;
dices: [number, number];
constructor({players = [], started = false, currentPlayerTurn = null, dices = [1,1]) {
this.players = players;
this.started = started;
this.currentPlayerTurn = currentPlayerTurn;
this.dices = dices;
}
startGame() {
this.currentPlayerTurn = this.players[0];
this.started = true;
}
// etc..
}
The room is being generated in the server, being sent to the client as JSON, and then rebuilt in the client. I sync the data with socket events, and everything's perfect.
But there's a problem with the React side of the story: changing GameRoom properties won't cause a rerender. That means I have to forceRerender() each time something is edited, or listen to class changes. Both options are a mess and I described it deeply in this question.
This mess made me think maybe classes are not the best way to go. Using interface will solve this problem entirely, but I do lose instance functions like GameRoom.startGame(), that will have to be turned into utility functions, like:
export function startGame(gameRoom: GameRoom) {
gameRoom.currentPlayerTurn = gameRoom.players[0];
gameRoom.started = true;
}
which is another mess, since they're hidden in code, and the developer needs to know they exist, and not edit gameRoom directly.
If you guys have any idea on how to model my data types, I'd be more than happy to hear.
Thanks!
I would go with a functional approach. I'm not a huge fan of classes in JS.
Define a type for your game, but set the properties to readonly. This will tell the developer that they shouldn't mutate the GameRoom.
export type GameRoom = {
readonly players: Player[];
readonly started: boolean;
readonly currentPlayerTurn: Player;
readonly dices: [number, number];
}
Then, you can define all your changes as pure functions, so you define your inputs and create a new object that is your updated GameRoom. This makes things easy to test and track changes.
export function startGame(gameRoom: GameRoom): GameRoom {
return {
...gameRoom,
currentPlayerTurn: gameRoom.players[0],
started: true
}
}
Alternatively, you could use something like Redux and define your changes as a set of actions. (e.g. add-player, start-game, etc) Then, you can dispatch those actions from anywhere in your code.
The great thing about using immutability is every time you make a change to your object, you are returning a new one, so React will always re-render appropriately.

Nested web-components and event handling

I'm writing a memory game in javascript. I have made a web-component for the cards, <memory-card> and a web-component to contain the cards and handle the game state <memory-game>. The <memory-card> class contains its image path for when its turned over, the default image to display as the back of the card, its turned state and an onclick function to handle switching between the states and the images.
The <memory-game> class has a setter that receives an array of images to generate <memory-cards> from. What would be the best method to handle updating the game state in the <memory-game> class? Should I attach an additional event listener to the <memory-card> elements there or is there a better way to solve it? I would like the <memory-card> elements to only handle their own functionality as they do now, ie changing images depending on state when clicked.
memory-game.js
class memoryGame extends HTMLElement {
constructor () {
super()
this.root = this.attachShadow({ mode: 'open' })
this.cards = []
this.turnedCards = 0
}
flipCard () {
if (this.turnedCards < 2) {
this.turnedCards++
} else {
this.turnedCards = 0
this.cards.forEach(card => {
card.flipCard(true)
})
}
}
set images (paths) {
paths.forEach(path => {
const card = document.createElement('memory-card')
card.image = path
this.cards.push(card)
})
}
connectedCallback () {
this.cards.forEach(card => {
this.root.append(card)
})
}
}
customElements.define('memory-game', memoryGame)
memory-card.js
class memoryCard extends HTMLElement {
constructor () {
super()
this.root = this.attachShadow({ mode: 'open' })
// set default states
this.turned = false
this.path = 'image/0.png'
this.root.innerHTML = `<img src="${this.path}"/>`
this.img = this.root.querySelector('img')
}
set image (path) {
this.path = path
}
flipCard (turnToBack = false) {
if (this.turned || turnToBack) {
this.turned = false
this.img.setAttribute('src', 'image/0.png')
} else {
this.turned = true
this.img.setAttribute('src', this.path)
}
}
connectedCallback () {
this.addEventListener('click', this.flipCard())
}
}
customElements.define('memory-card', memoryCard)
implementing the custom event after Supersharp's answer
memory-card.js (extract)
connectedCallback () {
this.addEventListener('click', (e) => {
this.flipCard()
const event = new CustomEvent('flippedCard')
this.dispatchEvent(event)
})
}
memory-game.js (extract)
set images (paths) {
paths.forEach(path => {
const card = document.createElement('memory-card')
card.addEventListener('flippedCard', this.flipCard.bind(this))
card.image = path
this.cards.push(card)
})
}
In the <memory-card>:
Create with CustomEvent() and dispatch a custom event with dispatchEvent()
In the <memory-game>:
Listen to your custom event with addEventListener()
Because the cards are nested in the game, the event will bubble naturally to the container.
This way the 2 custom elements will stay loosley coupled.
Supersharps answer is not 100% correct.
click events bubble up the DOM,
but CustomEvents (inside shadowDOM) do not
Why firing a defined event with dispatchEvent doesn't obey the bubbling behavior of events?
So you have to add the bubbles:true yourself:
[yoursender].dispatchEvent(new CustomEvent([youreventName], {
bubbles: true,
detail: [yourdata]
}));
more: https://javascript.info/dispatch-events
note: detail can be a function: How to communicate between Web Components (native UI)?
For an Eventbased programming challenge
this.cards.forEach(card => {
card.flipCard(true)
})
First of all that this.cards is not required, as all cards are available in [...this.children]
!! Remember, in JavaScript Objects are passed by reference, so your this.cards is pointing to the exact same DOM children
You have a dependency here,
the Game needs to know about the .flipCard method in Card.
► Make your Memory Game send ONE Event which is received by EVERY card
hint: every card needs to 'listen' at Game DOM level to receive a bubbling Event
in my code that whole loop is:
game.emit('allCards','close');
Cards are responsible to listen for the correct EventListener
(attached to card.parentNode)
That way it does not matter how many (or What ever) cards there are in your game
The DOM is your data-structure
If your Game no longer cares about how many or what DOM children it has,
and it doesn't do any bookkeeping of elements it already has,
shuffling becomes a piece of cake:
shuffle() {
console.log('► Shuffle DOM children');
let game = this,
cards = [...game.children],//create Array from a NodeList
idx = cards.length;
while (idx--) game.insertBefore(rand(cards), rand(cards));//swap 2 random DOM elements
}
My global rand function, producing a random value from an Array OR a number
rand = x => Array.isArray(x) ? x[rand(x.length)] : 0 | x * Math.random(),
Extra challenge
If you get your Event based programming right,
then creating a Memory Game with three matching cards is another piece of cake
.. or 4 ... or N matching cards
It would be very helpful to see some of your existing code to know what you have tried. But without it you ca do what #Supersharp has proposed, or you can have the <memory-game> class handle all events.
If you go this way then your code for <memory-card> would listen for click events on the entire field. It would check to see if you clicked on a card that is still face down and, if so, tell the card to flip. (Either through setting a property or an attribute, or through calling a function on the <memory-card> element.)
All of the rest of the logic would exist in the <memory-game> class to determine if the two selected cards are the same and assign points, etc.
If you want the cards to handle the click event then you would have that code generate a new CustomEvent to indicate that the card had flipped. Probably including the coordinates of the card within the grid and the type of card that is being flipped.
The <memory-game> class would then listen for the flipped event and act upon that information.
However you do this isn't really a problem. It is just how you want to code it and how tied together you want the code. If you never plan to use this code in any other games, then it does not matter as much.

How to fit these two classes together?

This is my first attempt at doing OOP and made a simple Pomodoro timer. https://codepen.io/hyrosian/project/editor/XpjOPR
There was an attempt to seperate the countdown timer from the controls. The countdown timer is working but i'm not sure how to get class Counter and class Controls somehow 'fit' together.
const counter = new Counter(DOMnode)
const controls = new Controls()
The plan was to set up the eventlisteners and handlers inside Controls. We get the values e.g. +5mins for changing the session/break length inside of it. But Controls needs access to the state inside Counter.
class Controls extends Time {
constructor() {
super()
this.session_btn = document.querySelectorAll('[data-session]')
this.break_btn = document.querySelectorAll('[data-break]')
}
handleBreak(e) {
console.log(e.target.dataset.break)
}
handleSession(e) {
console.log(e.target.dataset.session)
}
init() {
this.session_btn.forEach(btn => btn.addEventListener('click', this.handleSession))
this.break_btn.forEach(btn => btn.addEventListener('click', this.handleBreak))
}
}
What would be the proper way to fit them together?
Controls should have a reference to the Counter object, but not the other way around.
Controls can access the state by asking for (getTime, getState). If Controls needs to subscribe to some event (timer has run out) then Counter could emit such an event.
The underlying idea would be that Counter is a completely isolated object and just maintains its own state and timers, but Controls is the thing that handles events from the user (and potentially updates the DOM when needed).
This is a pretty common pattern. In this scenario, Counter would often be called the model, and Controls the Controller.
You should first create the Counter, and pass it as a constructor argument to Controls

Preventing Circular Dependencies with Exported Singleton Class

I have a question regarding a scenario I keep running into building HTML5 games resulting in difficult to manage circular dependencies.
I understand completely why the circular dependency is occuring and where it is occurring. However, I can't seem to figure out a convenient way to get around it, so I assume my logic / approach is fundamentally flawed.
Here's a little bit of context.
I have a game that has a single point of entry (compiled with Webpack) called Game.js. I have a basic event manager that allows for two functions on(key, callback) and fire(key, parameters).
The event manager simply creates an object, sets the supplied key of on as a property with an array value populated with any callback functions registered to that key. When the fire method is called that property is retrieved and all of the fuctions defined in it's array value are invoked.
What I'm trying to do
I want to be able to instance the event manager on Game.js and export an instance of Game that other classes can import and subsequently register callbacks to the Game instances event manager.
class Game {
constructor() {
this.events = new EventManager();
window.addEventListener('resize', this.resize.bind(this));
}
resize(event) {
if(window.innerWidth < window.innerHeight) {
this.events.fire('orientation-change', 'vertical');
} else {
this.events.fire('orientation-change', 'horizontal');
}
}
}
export default new Game();
Then for example a Button class may need to respond to an orientation change event fired by the Game. Please note the above is simply an example of a circumstance in which the event manager may fire an event, but this condition could be anything.
import Game from '../core/Game';
class Button {
constructor() {
Game.events.on('orientation-change', this.reorient.bind(this));
}
reorient() {
// ...
}
}
export default Button;
The above class is a UI component called Button that needs to know when the orientation-change event is fired, again please note this event could be anything.
What's the problem?
Nothing looks particularly wrong with the above, however, because Game.js is the entry point, at some point an instance of Button is created whether it be directly in Game.js or through another class which is subsequently instanced via Game.js which of course causes a circular dependency because even if not directly, Game imports Button and Button imports Game.
What I've tried
There are two main solutions that I have found that work (to some degree). The first being simply waiting for the export to be available using an interval check of the value of Game in the constructor of Button, like this:
import Game from '../core/Game';
class Button {
constructor() {
let check = setInterval(() => {
if(Game !== undefined) {
Game.events.on('orientation-change', this.reorient.bind(this));
clearInterval(check);
}
}, 100);
}
reorient() {
// ...
}
}
export default Button;
This will typically resolve in a single iteration.
The second solution being to use dependency injection and pass reference of Game to Button when it's instanced, which again works great, but the prospect of having to repeatedly do this per class seems unintuitive. The interval check works fine too, but seems hacky.
I'm feel like I'm completely missing something and that the solution isn't a difficult as I'm making it.
Thanks for any help regarding this.

Categories

Resources