Avoiding coupling
- by Seralize
It is also true that a system may become so coupled, where each class
is dependent on other classes that depend on other classes, that it is
no longer possible to make a change in one place without having a
ripple effect and having to make subsequent changes in many places.[1]
This is why using an interface or an abstract class can be valuable in
any object-oriented software project.
Quote from Wikipedia
Starting from scratch
I'm starting from scratch with a project that I recently finished because I found the code to be too tightly coupled and hard to refactor, even when using MVC. I will be using MVC on my new project aswell but want to try and avoid the pitfalls this time, hopefully with your help.
Project summary
My issue is that I really wish to keep the Controller as clean as possible, but it seems like I can't do this. The basic idea of the program is that the user picks wordlists which is sent to the game engine. It will pick random words from the lists until there are none left.
Problem at hand
My main problem is that the game will have 'modes', and need to check the input in different ways through a method called checkWord(), but exactly where to put this and how to abstract it properly is a challenge to me. I'm new to design patterns, so not sure whether there exist any might fit my problem.
My own attempt at abstraction
Here is what I've gotten so far after hours of 'refactoring' the design plans, and I know it's long, but it's the best I could do to try and give you an overview (Note: As this is the sketch, anything is subject to change, all help and advice is very welcome. Also note the marked coupling points):
Wordlist
class Wordlist {
// Basic CRUD etc. here!
// Other sample methods:
public function wordlistCount($user_id) {} // Returns count of how many wordlists a user has
public function getAll($user_id) {} // Returns all wordlists of a user
}
Word
class Word {
// Basic CRUD etc. here!
// Other sample methods:
public function wordCount($wordlist_id) {} // Returns count of words in a wordlist
public function getAll($wordlist_id) {} // Returns all words from a wordlist
public function getWordInfo($word_id) {} // Returns information about a word
}
Wordpicker
class Wordpicker {
// The class needs to know which words and wordlists to exclude
protected $_used_words = array();
protected $_used_wordlists = array();
// Wordlists to pick words from
protected $_wordlists = array();
/* Public Methods */
public function setWordlists($wordlists = array()) {}
public function setUsedWords($used_words = array()) {}
public function setUsedWordlists($used_wordlists = array()) {}
public function getRandomWord() {} // COUPLING POINT! Will most likely need to communicate with both the Wordlist and Word classes
/* Protected Methods */
protected function _checkAvailableWordlists() {} // COUPLING POINT! Might need to check if wordlists are deleted etc.
protected function _checkAvailableWords() {} // COUPLING POINT! Method needs to get all words in a wordlist from the Word class
}
Game
class Game {
protected $_session_id; // The ID of a game session which gets stored in the database along with game details
protected $_game_info = array();
// Game instantiation
public function __construct($user_id) {
if (! $this->_session_id = $this->_gameExists($user_id)) {
// New game
} else {
// Resume game
}
}
// This is the method I tried to make flexible by using abstract classes etc.
// Does it even belong in this class at all?
public function checkWord($answer, $native_word, $translation) {} // This method checks the answer against the native word / translation word, depending on game mode
public function getGameInfo() {} // Returns information about a game session, or creates it if it does not exist
public function deleteSession($session_id) {} // Deletes a game session from the database
// Methods dealing with game session information
protected function _gameExists($user_id) {}
protected function _getProgress($session_id) {}
protected function _updateProgress($game_info = array()) {}
}
The Game
/* CONTROLLER */
/* "Guess the word" page */
// User input
$game_type = $_POST['game_type']; // Chosen with radio buttons etc.
$wordlists = $_POST['wordlists']; // Chosen with checkboxes etc.
// Starts a new game or resumes one from the database
$game = new Game($_SESSION['user_id']);
$game_info = $game->getGameInfo();
// Instantiates a new Wordpicker
$wordpicker = new Wordpicker();
$wordpicker->setWordlists((isset($game_info['wordlists'])) ? $game_info['wordlists'] : $wordlists);
$wordpicker->setUsedWordlists((isset($game_info['used_wordlists'])) ? $game_info['used_wordlists'] : NULL);
$wordpicker->setUsedWords((isset($game_info['used_words'])) ? $game_info['used_words'] : NULL);
// Fetches an available word
if (! $word_id = $wordpicker->getRandomWord()) {
// No more words left - game over!
$game->deleteSession($game_info['id']);
redirect();
} else {
// Presents word details to the user
$word = new Word();
$word_info = $word->getWordInfo($word_id);
}
The Bit to Finish
/* CONTROLLER */
/* "Check the answer" page */
// ??????????????????
( http://pastebin.com/cc6MtLTR )
Make sure you toggle the 'Layout Width' to the right for a better view. Thanks in advance.
Questions
To which extent should objects be loosely coupled? If object A needs info from object B, how is it supposed to get this without losing too much cohesion?
As suggested in the comments, models should hold all business logic. However, as objects should be independent, where to glue them together? Should the model contain some sort of "index" or "client" area which connects the dots?
Edit:
So basically what I should do for a start is to make a new model which I can more easily call with oneliners such as $model->doAction(); // Lots of code in here which uses classes!
How about the method for checking words? Should it be it's own object? I'm not sure where I should put it as it's pretty much part of the 'game'. But on another hand, I could just leave out the 'abstraction and OOPness' and make it a method of the 'client model' which will be encapsulated from the controller anyway. Very unsure about this.