Skip to content

Kapitel 7: Refactoring

Kevin Wiesner edited this page Jun 7, 2024 · 15 revisions

Code Smells

Jeweils ein Code-Beispiel zu zwei Code Smells aus der Vorlesung, jeweils Code-Beispiel und einen möglichen Lösungsweg bzw. den genommen Lösungsweg beschreiben (inkl. (Pseudo-)Code)

Beispiel 1: Duplicated Code

Die Spiellogik der beiden Spielmodi Training und LocalMultiplayer ist, wie in den Code-Beispielen zu sehen, nahezu identisch. Der einzige Unterschied ist die Anzahl der Spieler. Während der LocalMultiplayer dazu auf das UserRepository zugreift und alle Benutzer in das Spiel aufnimmt, wird der Trainingsmodus immer nur mit einem Spieler gespielt. An dieser Stelle kämen zwei mögliche Lösungswege in Frage. Da mittlerweile eine Benutzerverwaltung implementiert wurde, könnte der Trainingsmodus entfernt werden. Der lokale Spielmodus könnte dann entsprechend der aktuell eingestellten Benutzer mit einer beliebigen Anzahl an Spielern gespielt werden. Dies hätte den Nachteil, dass für eine Trainingsrunde alle Benutzer entfernt werden müssten. Dem könnte eine Auswahl der Benutzer für das Spiel vor Spielbeginn entgegenwirken. Eine andere Möglichkeit wäre es, die duplizierte Logik in eine Methode auszulagern, die mit der Anzahl der Spieler aufgerufen wird. Dann würden weiterhin zwei separate Spielmodi im Menü angeboten werden, die allerdings die gleiche Logik verwenden. Eine Entscheidung steht noch aus.

TrainingScreenProvider.java

public void execute() {
  while (!quizGame.isFinished()) {
    if (quizGame.isSelectingCategory()) {
      String playerName = quizGame.getCurrentPlayer().getPlayerName();
      OptionScreen optionScreen = screenFactory.createOptionScreen(playerName + ", select category", quizGame.getRemainingGameCategories());
      optionScreen.render();
      int actionValue = optionScreen.getOptionInput().getActionValue();
      quizGame.setCurrentCategory(actionValue - 1);
    } else {
      Question currentQuestion = quizGame.getCurrentQuestion();
      OptionScreen optionScreen = screenFactory.createOptionScreen("Question: " + currentQuestion.getQuestion(), List.of(currentQuestion.getQuestionOptions()));
      optionScreen.render();
      int actionValue = optionScreen.getOptionInput().getActionValue();
      quizGame.submitQuestionAnswer(actionValue - 1);
    }
  }
  List<String> lines = new ArrayList<>();
  Player trainingPlayer = quizGame.getPlayers()[0];
  lines.add(String.format("%s - %d points", trainingPlayer.getPlayerName(), trainingPlayer.getCurrentScore().getIntScore()));
  screenFactory.createInformationScreen("Result", lines).render();
}

LocalMultiplayerScreenProvider.java

public void execute() {
  while (!quizGame.isFinished()) {
    if (quizGame.isSelectingCategory()) {
      String playerName = quizGame.getCurrentPlayer().getPlayerName();
      OptionScreen optionScreen = screenFactory.createOptionScreen(playerName + ", select category", quizGame.getRemainingGameCategories());
      optionScreen.render();
      int action = optionScreen.getOptionInput().getActionValue();
      quizGame.setCurrentCategory(action - 1);
    } else {
      Question currentQuestion = quizGame.getCurrentQuestion();
      OptionScreen optionScreen = screenFactory.createOptionScreen("Question: " + currentQuestion.getQuestion(), List.of(currentQuestion.getQuestionOptions()));
      optionScreen.render();
      int action = optionScreen.getOptionInput().getActionValue();
      quizGame.submitQuestionAnswer(action - 1);
    }
  }
  List<String> lines = new ArrayList<>();
  Player[] players = quizGame.getPlayers();
  for (Player player : players) {
    lines.add(String.format("%s - %d points", player.getPlayerName(), player.getCurrentScore().getIntScore()));
  }
  screenFactory.createInformationScreen("Result", lines).render();
}

Beispiel 2: Large Class

Mit dem GameRoom gab es eine Klasse mit sehr vielen Methoden, wie im Code-Beispiel zu sehen. Dies ist meist ein Hinweis darauf, dass eine Klasse zu viel Verantwortung in sich vereint und eine Unterteilung sinnvoll sein könnte.

public abstract class GameRoom {

  private final User user;

  private final String code;

  public GameRoom(User user, String code) {
    this.user = user;
    this.code = code;
  }

  public String getNameOfPlayingUser() {
    return user.getName();
  }

  public String getCode() {
    return code;
  }

  public abstract void waitForGameToFinish();

  public abstract void addListener(GameRoomListener gameRoomListener);

  public abstract String getRoomName();

  public abstract List<User> getPlayers();

  public abstract CategoryRepository getRoomCategoryRepository();

  public abstract void dispatchRoomJoin();

  public abstract void dispatchPlayerUpdate();

  public abstract void dispatchGameStart();

  public abstract void dispatchGameTurnAction();

  public abstract void dispatchGameTurnListen();

  public abstract void dispatchGameTurnListenCategorySubmission(int selectedCategoryId);

  public abstract void dispatchGameTurnListenQuestionOptionSubmission(int selectedQuestionOptionIndex);

  public abstract void sendSelectedCategoryId(int categoryId);

  public abstract void sendSelectedQuestionOptionIndex(int questionOptionIndex);
}

In diesem Commit wurden die Methoden der Klasse daher auf mehrere Interfaces verteilt. Im folgenden Beispiel wird nur exemplarisch das Interface GameRoomActionDispatcher nach dieser Unterteilung gezeigt.

public abstract class GameRoom implements GameRoomActionDispatcher, GameRoomActionSender, GameRoomLifetimeDispatcher {

  private final User user;
  private final String code;

  public GameRoom(User user, String code) {
    this.user = user;
    this.code = code;
  }

  public String getNameOfPlayingUser() {
    return user.getName();
  }

  public String getCode() {
    return code;
  }

  public abstract String getRoomName();

  public abstract List<User> getPlayers();

  public abstract CategoryRepository getRoomCategoryRepository();
}
public interface GameRoomActionDispatcher {

  void dispatchGameTurnAction();

  void dispatchGameTurnListen();

  void dispatchGameTurnListenCategorySubmission(int selectedCategoryId);

  void dispatchGameTurnListenQuestionOptionSubmission(int selectedQuestionOptionIndex);
}

Zwei Refactorings

Zwei unterschiedliche Refactorings aus der Vorlesung anwenden, begründen, sowie UML vorher/nachher liefern, jeweils auf die Commits verweisen

Beispiel 1: Extract Method

An vielen Stellen in der Anwendung werden Menüs mit einer Liste an Optionen angezeigt. Beispielsweise ist dies beim Wählen des Spielmodi oder beim Ändern von Spieleinstellungen der Fall. Implementiert werden diese mit einem sogenannten OptionScreen, der diese Funktionalität anbietet. Das Erzeugen des OptionScreen sowie die Entgegennahme und Überprüfung der Rückgabewerte wurde dann im jeweiligen ScreenProvider erledigt. Dies hat in vielen Fällen zu sehr ähnlichen Strukturen geführt, die mit dem MenuCreator in eine eigene Klasse extrahiert werden konnten. Nach der Einführung des MenuCreators konnten diese Strukturen in weiteren ScreenProvidern ersetzt werden. Diese Deduplizierung vereinfacht die Logik der verschiedenen ScreenProvider und macht den Code dadurch verständlicher.

Vorher

public void execute() {
  int selection = 0;
  while (selection <= 0 || selection > 4) {
    OptionScreen optionScreen = screenFactory.createOptionScreen("Select Game Mode", List.of(GameModeEnum.values()));
    optionScreen.render();
    selection = optionScreen.getOptionInput().getActionValue();
  }
  switch (selection) {
    case 1 -> this.nextScreenProviderType = ScreenProviderType.TRAINING;
    case 2 -> this.nextScreenProviderType = ScreenProviderType.LOCAL_MULTIPLAYER;
    case 3 -> this.nextScreenProviderType = ScreenProviderType.ONLINE_MULTIPLAYER;
  }
}
classDiagram
direction BT
class GameModeScreenProvider {
  + GameModeScreenProvider(Repository, ScreenFactory) 
  - ScreenProviderType nextScreenProviderType
  - ScreenFactory screenFactory
  + execute() void
  + getNextScreenProviderType() ScreenProviderType
}
class GameSettingsScreenProvider {
  + GameSettingsScreenProvider(Repository, ScreenFactory) 
  - ScreenProviderType nextScreenProviderType
  - ScreenFactory screenFactory
  + execute() void
  + getNextScreenProviderType() ScreenProviderType
}
class MenuScreenProvider {
  + MenuScreenProvider(Repository, ScreenFactory) 
  - ScreenFactory screenFactory
  - Repository repository
  - ScreenProviderType nextScreenProviderType
  + execute() void
  + getNextScreenProviderType() ScreenProviderType
}
class OnlineMultiplayerScreenProvider {
  + OnlineMultiplayerScreenProvider(Repository, ScreenFactory, GameRoomManager) 
  - GameRoom gameRoom
  - ScreenFactory screenFactory
  - Repository repository
  - GameRoomManager gameRoomManager
  - QuizGame onlineQuizGame
  - User playingUser
  + onJoinedRoom() void
  + onGameTurnListen() void
  + execute() void
  + onGameTurnListenQuestionOptionSubmission(int) void
  + onGameStart() void
  + onGameTurnListenCategorySubmission(int) void
  + getNextScreenProviderType() ScreenProviderType
  - displayCategorySelection(boolean) void
  + onGameTurnAction() void
  - joinRoom() void
  - createNewRoom() void
  + onUpdatePlayers() void
}
class RemoveUserScreenProvider {
  + RemoveUserScreenProvider(Repository, ScreenFactory) 
  - ScreenProviderType nextScreenProviderType
  - Repository repository
  - ScreenFactory screenFactory
  + execute() void
  + getNextScreenProviderType() ScreenProviderType
}
class ScreenProvider {
<<Interface>>
  + getNextScreenProviderType() ScreenProviderType
  + execute() void
}

GameModeScreenProvider  ..>  ScreenProvider 
GameSettingsScreenProvider  ..>  ScreenProvider 
MenuScreenProvider  ..>  ScreenProvider 
OnlineMultiplayerScreenProvider  ..>  ScreenProvider 
RemoveUserScreenProvider  ..>  ScreenProvider
Loading

Nachher

public void execute() {
  MenuCreator menuCreator = new MenuCreator("Select Game Mode", GameModeEnum.values(),
      screenFactory);
  int selection = menuCreator.displayAndGetSelection();
  switch (selection) {
    case 1 -> this.nextScreenProviderType = ScreenProviderType.TRAINING;
    case 2 -> this.nextScreenProviderType = ScreenProviderType.LOCAL_MULTIPLAYER;
    case 3 -> this.nextScreenProviderType = ScreenProviderType.ONLINE_MULTIPLAYER;
  }
}
classDiagram
direction BT
class GameModeScreenProvider {
  + GameModeScreenProvider(Repository, ScreenFactory) 
  - ScreenProviderType nextScreenProviderType
  - ScreenFactory screenFactory
  + execute() void
  + getNextScreenProviderType() ScreenProviderType
}
class GameSettingsScreenProvider {
  + GameSettingsScreenProvider(Repository, ScreenFactory) 
  - ScreenProviderType nextScreenProviderType
  - ScreenFactory screenFactory
  + execute() void
  + getNextScreenProviderType() ScreenProviderType
}
class MenuCreator {
  + MenuCreator(String, Displayable[], ScreenFactory) 
  - String menuTitle
  - Displayable[] menuItems
  - ScreenFactory screenFactory
  + displayAndGetSelection() int
}
class MenuScreenProvider {
  + MenuScreenProvider(Repository, ScreenFactory) 
  - ScreenFactory screenFactory
  - Repository repository
  - ScreenProviderType nextScreenProviderType
  + execute() void
  + getNextScreenProviderType() ScreenProviderType
}
class OnlineMultiplayerScreenProvider {
  + OnlineMultiplayerScreenProvider(Repository, ScreenFactory, GameRoomManager) 
  - GameRoom gameRoom
  - ScreenFactory screenFactory
  - Repository repository
  - GameRoomManager gameRoomManager
  - QuizGame onlineQuizGame
  - User playingUser
  + onJoinedRoom() void
  + onGameTurnListen() void
  + execute() void
  + onGameTurnListenQuestionOptionSubmission(int) void
  + onGameStart() void
  + onGameTurnListenCategorySubmission(int) void
  + getNextScreenProviderType() ScreenProviderType
  - displayCategorySelection(boolean) void
  + onGameTurnAction() void
  - joinRoom() void
  - createNewRoom() void
  + onUpdatePlayers() void
}
class RemoveUserScreenProvider {
  + RemoveUserScreenProvider(Repository, ScreenFactory) 
  - ScreenProviderType nextScreenProviderType
  - Repository repository
  - ScreenFactory screenFactory
  + execute() void
  + getNextScreenProviderType() ScreenProviderType
}
class ScreenProvider {
<<Interface>>
  + getNextScreenProviderType() ScreenProviderType
  + execute() void
}

GameModeScreenProvider  ..>  MenuCreator : «create»
GameModeScreenProvider  ..>  ScreenProvider 
GameSettingsScreenProvider  ..>  MenuCreator : «create»
GameSettingsScreenProvider  ..>  ScreenProvider 
MenuScreenProvider  ..>  MenuCreator : «create»
MenuScreenProvider  ..>  ScreenProvider 
OnlineMultiplayerScreenProvider  ..>  MenuCreator : «create»
OnlineMultiplayerScreenProvider  ..>  ScreenProvider 
RemoveUserScreenProvider  ..>  MenuCreator : «create»
RemoveUserScreenProvider  ..>  ScreenProvider
Loading

Beispiel 2: Replace Conditional with Polymorphism

In einer ursprünglichen Implementierung des Screen Handlings (siehe Codestand bei folgendem Commit) wurde der Kontrollfluss der Anwendung von einer Klasse in der Plugin-Schicht gesteuert (QuizClashCLI). Diese hat sich einen ScreenProviderManager instanziiert, welcher eine Referenz auf den derzeitigen ScreenProvider hat und durchgeführte Aktionen in der CLI in den derzeitigen ScreenProvider weiterreichen konnte. Dies widerspricht vielen Prinzipien der clean architecture Methodologie, da die Steuerung des Kontrollflusses nicht in der Plugin-Schicht liegen sollte. Weiterhin bestand das Problem, dass die render Methode von QuizClashCLI einen Screen als Eingabe erhält, welcher vom ScreenProviderManager durchgegeben wurde. Es war somit notwendig, die Instanz des Screens zu überprüfen, um eine korrekte Darstellung je nach Typ zu ermöglichen (TextInputScreen, etc.). Dies wurde über mehrere instanceof Aufrufe geregelt.

Vorher

if (screen instanceof OptionScreen optionScreen) {
  List<? extends Displayable> optionList = optionScreen.getScreenOptions();
  Action<Integer> integerAction =  selectFromOptions(optionList);
  screenProviderManager.submitIntegerAction(integerAction);
} else if (screen instanceof TextInputScreen textInputScreen) {
  Action<String> stringAction = enterTextFromRequest(textInputScreen.getInputRequest());
  screenProviderManager.submitStringAction(stringAction);
} else if (screen instanceof NumberInputScreen numberInputScreen) {
  Action<Integer> integerAction = enterNumberFromRequest(numberInputScreen.getInputRequest());
  screenProviderManager.submitIntegerAction(integerAction);
} else if (screen instanceof InformationScreen informationScreen) {
  for (String line : informationScreen.getLines()) {
    cliWindow.println(line);
  }
  cliWindow.moveToActionField();
  cliWindow.waitForEnter();
}
classDiagram
direction BT
class InformationScreen {
  + InformationScreen(String, List~String~) 
  - List~String~ lines
  + getLines() List~String~
}
class NumberInputScreen {
  + NumberInputScreen(String, String) 
  - String inputRequest
  + getInputRequest() String
}
class OptionScreen {
  + OptionScreen(String, List~Displayable~) 
  - List~Displayable~ displayableList
  + getScreenOptions() List~Displayable~
}
class QuizClashCLI {
  + QuizClashCLI(int, int, Repository) 
  - CLIWindowManager cliWindow
  - ScreenProviderManager screenProviderManager
  - enterNumberFromRequest(String) Action~Integer~
  + run() void
  - enterTextFromRequest(String) Action~String~
  + destroy() void
  - selectFromOptions(List~Displayable~) Action~Integer~
  - render(Screen) void
}
class Screen {
  + Screen(String) 
  - String screenName
  + getScreenName() String
}
class ScreenProviderManager {
  + ScreenProviderManager(Repository) 
  - ScreenProvider currentScreenProvider
  - Repository repository
  + getCurrentScreenProvider() ScreenProvider
  + updateScreenProvider() void
  + submitStringAction(Action~String~) void
  + submitIntegerAction(Action~Integer~) void
}
class Starter {
  + Starter() 
  + main(String[]) void
}
class TextInputScreen {
  + TextInputScreen(String, String) 
  - String inputRequest
  + getInputRequest() String
}

InformationScreen  -->  Screen 
NumberInputScreen  -->  Screen 
OptionScreen  -->  Screen 
QuizClashCLI  ..>  ScreenProviderManager : «create»
QuizClashCLI "1" *--> "screenProviderManager 1" ScreenProviderManager 
Starter  ..>  QuizClashCLI : «create»
TextInputScreen  -->  Screen 
Loading

Man sieht, dass durch das instanceof keine vernünftige Kopplung ersichtlich ist und so das Konzept nicht schlüssig ist. Die Klasse QuizClashCLI muss immer dann geändert werden, wenn eine neue Screen Implementierung vorliegt. Daher wurde hier ein Factory Pattern eingeführt (siehe folgender Commit), um einerseits den Kontrollfluss der Anwendung wieder in die Application-Schicht zu bringen und das Rendering als abstrakte Methode in der Screen Basisklasse zu definieren, um dynamische Implementierungen zu ermöglichen. Weiterhin liegt die Erstellung des Objekts vom Typ ScreenProviderManager nicht mehr im Aufgabenbereich von QuizClashCLI. Das resultierende UML Diagramm ist im Folgenden dargestellt.

Nachher

run Methode von ScreenProviderManager

public void run() {
  while (currentScreenProvider != null) {
    currentScreenProvider.execute();
    this.updateScreenProvider();
  }
}
classDiagram
direction BT
class CLIInformationScreen {
  + CLIInformationScreen(String, List~String~, CLIWindowManager) 
  + CLIInformationScreen(String, List~String~, boolean, CLIWindowManager) 
  ~ CLIWindowManager cliWindow
  + render() void
}
class CLINumberInputScreen {
  + CLINumberInputScreen(String, String, CLIWindowManager) 
  - CLIWindowManager cliWindow
  - int userInput
  + render() void
  + getNumberInput() Action~Integer~
}
class CLIOptionScreen {
  + CLIOptionScreen(String, List~Displayable~, CLIWindowManager) 
  - CLIWindowManager cliWindow
  - int userOption
  + getOptionInput() Action~Integer~
  + render() void
  - selectFromOptions(List~Displayable~) int
}
class CLIScreenFactory {
  + CLIScreenFactory(CLIWindowManager) 
  ~ CLIWindowManager cliWindow
  + createOptionScreen(String, List~Displayable~) OptionScreen
  + createNumberInputScreen(String, String) NumberInputScreen
  + createTextInputScreen(String, String) TextInputScreen
  + createInformationScreen(String, List~String~) InformationScreen
  + createInformationScreen(String, List~String~, boolean) InformationScreen
}
class CLITextInputScreen {
  + CLITextInputScreen(String, String, CLIWindowManager) 
  - CLIWindowManager cliWindow
  - String userInput
  + getTextInput() Action~String~
  + render() void
}
class InformationScreen {
  + InformationScreen(String, List~String~, boolean) 
  + InformationScreen(String, List~String~) 
  - boolean isBlocking
  - List~String~ lines
  + getLines() List~String~
  + isBlocking() boolean
}
class NumberInputScreen {
  + NumberInputScreen(String, String) 
  - String inputRequest
  + getInputRequest() String
  + getNumberInput() Action~Integer~
}
class OptionScreen {
  + OptionScreen(String, List~Displayable~) 
  - List~Displayable~ displayableList
  + getScreenOptions() List~Displayable~
  + getOptionInput() Action~Integer~
}
class QuizClashCLI {
  + QuizClashCLI(int, int) 
  - CLIWindowManager cliWindow
  + destroy() void
  + getCLIScreenFactory() CLIScreenFactory
}
class Screen {
  + Screen(String) 
  - String screenName
  + render() void
  + getScreenName() String
}
class ScreenFactory {
<<Interface>>
  + createNumberInputScreen(String, String) NumberInputScreen
  + createTextInputScreen(String, String) TextInputScreen
  + createInformationScreen(String, List~String~, boolean) InformationScreen
  + createOptionScreen(String, List~Displayable~) OptionScreen
  + createInformationScreen(String, List~String~) InformationScreen
}
class ScreenProviderManager {
  + ScreenProviderManager(Repository, ScreenFactory, GameRoomManager) 
  - ScreenFactory screenFactory
  - GameRoomManager gameRoomManager
  - ScreenProvider currentScreenProvider
  - Repository repository
  + run() void
  + updateScreenProvider() void
}
class Starter {
  + Starter() 
  + main(String[]) void
}
class TextInputScreen {
  + TextInputScreen(String, String) 
  - String inputRequest
  + getTextInput() Action~String~
  + getInputRequest() String
}

CLIInformationScreen  -->  InformationScreen 
CLINumberInputScreen  -->  NumberInputScreen 
CLIOptionScreen  -->  OptionScreen 
CLIScreenFactory  ..>  CLIInformationScreen : «create»
CLIScreenFactory  ..>  CLINumberInputScreen : «create»
CLIScreenFactory  ..>  CLIOptionScreen : «create»
CLIScreenFactory  ..>  CLITextInputScreen : «create»
CLIScreenFactory  ..>  ScreenFactory 
CLITextInputScreen  -->  TextInputScreen 
InformationScreen  -->  Screen 
NumberInputScreen  -->  Screen 
OptionScreen  -->  Screen 
QuizClashCLI  ..>  CLIScreenFactory : «create»
ScreenProviderManager "1" *--> "screenFactory 1" ScreenFactory 
Starter  ..>  QuizClashCLI : «create»
Starter  ..>  ScreenProviderManager : «create»
TextInputScreen  -->  Screen 
Loading