• 4 hours
  • Easy

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 2/18/22

"D" for the Dependency Inversion Principle

The dependency inversion principle always sounds more complicated than it is. It says: 

High-level classes shouldn’t have to change because low-level classes change.

High-level classes are the classes that typically drive your system. You want them to remain stable as much as possible.

Why Should You Use Dependency Inversion?

Speaking of driving, let’s take cars as an example. The high-level classes are the items you interact with most: steering wheel, accelerator, and brake pedal. They tell the low-level implementation classes (tires, engine, brakes) what to do.

Let’s see what happens to the high-level classes if you change the engine from gas to electric. Nothing! A driver still steers, accelerates, and brakes using the same functionality. If you had violated this principle, the switch to electric (a low-level class) would force a change to the interface (a high-level class) for the driver. That sounds obvious with a car, but it’s easy to mess it up in code.

This ensures that you put the high-level classes in charge. They define the interface through which they communicate. In the car, you make the engine go faster by stepping on the accelerator pedal. It’s up to the engine to conform to this standard.

How Do We Apply Dependency Inversion to Our Code?

In our game, the view needs to be “driven” by the controller. So the controller defines the interface. Any view must conform to it. Otherwise, every time you changed the interface, the controller would have to be modified to conform to the view (that’s backward).

Let’s add a simple GUI. We know the interface it needs to implement (GameViewable). Adding this new view type should not drive the controller to be changed. It should just plug right in.

Let's create it together!

Here is a straightforward text based implementation of the GUI.  Pay special attention to the comments:

package com.openclassrooms.cardgame.view;

import java.awt.Component;
import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

import com.openclassrooms.cardgame.controller.GameController;

public class GameSwing implements GameViewable {
    GameController controller;
    
    JButton btnAddPlayer;
    JButton btnDealCards;
    JButton btnFindWinner;
    JTextArea textArea;
    char nextPlayerName = 'A';

    public void createAndShowGUI() {
    	// create the main display area, with enough initial space
        JFrame frame = new JFrame("MVC-SOLID-Game");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(500, 500);

        // all of the controls will appear vertically
        Container contentPane = frame.getContentPane();
        contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.Y_AXIS));

        addAddPlayerButton(contentPane);
        addDealCardsButton(contentPane);
        addFindWinnerButton(contentPane);
        
        addControllerCommandTracker(contentPane);

        frame.setVisible(true);
    }

    // when clicked, tell controller to add a player of the given name
    // this SHOULD bring up a dialog box to allow the user to enter a name
    // but that is beyond the scope of what we need to learn
    // for simplicity, we'll just tell it to add "Player A", "Player B", etc.
    private void addAddPlayerButton (Container contentPane) {
        btnAddPlayer = new JButton("Add A Player");
        addCenteredComponent (btnAddPlayer, contentPane);
        btnAddPlayer.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                textArea.append("Current players:\n");
                controller.addPlayer("Player " + nextPlayerName++);
            }
        });
    }
    
    // when clicked, tell controller to deal cards to players
    private void addDealCardsButton (Container contentPane) {
        btnDealCards = new JButton("Deal Cards");
        addCenteredComponent(btnDealCards, contentPane);
        btnDealCards.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                textArea.append("Cards dealt:\n");
                controller.startGame();
            }
        });
    }
    
    // when clicked, tell controller to flip cards and determine who won
    private void addFindWinnerButton(Container contentPane) {
	    btnFindWinner = new JButton("Find Winner");
	    addCenteredComponent(btnFindWinner, contentPane);
	    btnFindWinner.addActionListener(new ActionListener() {
	        public void actionPerformed(ActionEvent e) {
	            textArea.append("Show your cards:\n");
	            controller.flipCards();
	        }
	    });
    }
    
    // a simple place to display what the controller is telling us
    // very similar to our command line version    
    private void addControllerCommandTracker(Container contentPane) {
	    textArea = new JTextArea("Game Status\n", 100, 1);
	    JScrollPane scrollPane = new JScrollPane(textArea);
	    addCenteredComponent(scrollPane, contentPane);
	    textArea.setSize(500, 500);
    }
	
	// make sure that every time something is added to the text area,
	// scroll down to the bottom so that it is visible
	// textArea control does not have an auto-scroll option
	// so we have to do it ourselves
	private void appendText (String text) {
		textArea.append(text + "\n");
		textArea.setCaretPosition(textArea.getDocument().getLength());
	}
    
    // all controls are added so they are centered horizontally in the area
    private void addCenteredComponent(JComponent component, Container contentPane) {
        component.setAlignmentX(Component.CENTER_ALIGNMENT);
        contentPane.add(component);
    }
	
    // GameViewable interface implementations
	public void setController(GameController controller) {
		this.controller = controller;
	}

	// same implementation as our CommandLineView
	// just show what the controller is telling us
    @Override
    public void showPlayerName(int playerIndex, String playerName) {
    	appendText("[" + playerIndex + "][" + playerName +"]");
    }

    @Override
    public void showCardForPlayer(int playerIndex, String playerName, String cardRank, String cardSuit) {
    	appendText("[" + playerName + "][" + cardRank + ":" + cardSuit + "]");
    }

    @Override
    public void showWinner(String winnerName) {
        appendText("Winner!\n" + winnerName);
    }

	@Override
	public void showFaceDownCardForPlayer(int playerIndex, String name) {
		appendText("[" + name + "][][]");
	}

	
	@Override
	public void promptForPlayerName() {
		System.out.println("Enter Player Name:");
		// it's ok to add names
	    btnAddPlayer.setEnabled(true);
	    
	    // it's ok to start, one a player has been added
	    btnDealCards.setEnabled(nextPlayerName > 'A');
	    
	    // but not to ask to find a winner
	    btnFindWinner.setEnabled(false);
	}

	@Override
	public void promptForFlip() {
		System.out.println("Press enter to reveal cards");
		
		// past the time to add names
		btnAddPlayer.setEnabled(false);
		
		// or deal cards
		btnDealCards.setEnabled(false);
		
		// but you can ask to find a winner
		btnFindWinner.setEnabled(true);
	}

	@Override
	public void promptForNewGame() {
		System.out.println("Press enter to deal again");
		
		// past the time to add names
		btnAddPlayer.setEnabled(false);
		
		// ok to deal cards for the next game
		btnDealCards.setEnabled(true);
		
		// can't find a winner yet
		btnFindWinner.setEnabled(false);
	}
}

The easiest way to violate this principle is by knowing too much about actual implementations. If you know the details of how something else works, it’s tempting to code to that specific implementation.

It’s a good thing you have been following the principles. But what if you hadn’t? One place to introduce unnecessary coupling is between the controller and the view. We created an interface for the controller to call, and the view to implement. But what if the controller directly modified the view’s graphical components? You’d have some GameController code that looked like this:

public void addPlayerName(String playerName) {
   // this knows too much about the view's implementation
   view.textArea.append("[" + playerIndex + "][" + playerName +"]\n");
}

Let’s say you want to change the user interface to look better. Replace the JTextArea control, with a set of lined up JLabel controls. Since the controller knows about the specific implementation the view is using; it has to change its code as well to manipulate the new controls. Which means the specific implementation (GUI/low-level implementation), is now driving the controller’s high-level implementation. And you’ve seen that going back and rewriting already working code is a bad idea.

Let's Recap! 

  • Dependency inversion says high-level concepts should communicate through high-level abstractions. In other words: high-level classes shouldn’t have to change because low-level classes change.

  • Be aware of knowing too much about other classes’ implementation.

In the next chapter, we will look at several bad ideas or easy traps when coding and how to avoid them. 

Example of certificate of achievement
Example of certificate of achievement