Refactoring Patterns #3 Extract Class

Refactoring Patterns - #3 Extract Class

Mit dem Extract Class Refactoring Pattern kann die Komplexität einer Klasse verringert werden. Dabei wird ein Teil, der in einer Klasse logisch zusammengehört extrahiert.
Die alte Klasse nutz die neue Klasse meistens als Abhängigkeit (dependency).

Als Beispiel für das Extract Class Pattern können wir das Beispiel vom Extract Method Post weiter optimieren:

class UserRegistrationService{

   protected $connection;

   /**
   * Creates a database connection.
   */
   protected function connectToDatabase()
   {
        $this->connection = new mysqli('localhost', 'dbuser', 'dbpassword', 'users');
        if ($this->connection->connect_error) {
                throw new NoDatabaseConnectionException("Connection failed: " . $this->connection->connect_error);
        }
   }

   /**
   * Checks if the user allready exists.
   *
   * @param string $username
   * @return bool
   */
   protected function getUserExists($username) 
   {
	$this->connectToDatabase();
        $query = "select * from users where username='" . $this->connection->real_escape_string($username) . "'";
        $result = $this->connection->query($query);
        return $result->num_rows > 0;  
   }

   /**
   * Stores a user in the database.
   *
   * @param string $username
   * @param string $password
   *
   * @return bool
   */
   protected function storeUser($username, $password)
   {
        $this->connectToDatabase();
        $escapedUser = $this->connection->real_escape_string($username);
        $escapedPassword = hash('sha512', $this->connection->real_escape_string($password));
        $query = "insert into users (username, password) values ('" . $escapedUser  ."', '" . $escapedPassword . "');";

        $result = $this->connection->query($query);
        if($result !== true) {
                throw new Exception("Error during user creation");
        }

	return true;
   }

   /**
   * Validates the user data and throws an exception in the case when the user data is invalid
   *
   * @param string $username
   * @param string $password1
   * @param string $password2
   */  
   protected function validateUser($username, $password1, $password2)
   {
        if ($this->getUserExists($username)) {
                throw new UserAllreadyExistsException("The user allready exists");
        }

        if ($password1 !== $password2) {
                throw new PasswordsDoNotMatchException("Password1 and Password2 are not same");
        }

        if (strlen($password1) < 8) {
                throw new PasswordToShortException("The password was to short");
        }
   }

   /**
   * Saves a user registation to the database when the user does not exists and the passwords
   * meet several validation contraints.
   *
   *
   * @param string $username
   * @param string $password1
   * @param string $password2
   *
   * @throws NoDatabaseConnectionException
   * @throws UserAllreadyExistsException
   * @throws PasswordsDoNotMatchException
   * @throws PasswordToShortException
   *
   * @return boolean
   */
   public function saveRegistration($username, $password1, $password2)
   {
	$this->validateUser($username, $password1, $password2);

	// user is valid, we can store it
	return $this->storeUser($username, $password1);
    }
}

Extract Class anwenden

Bei lesen des Codes können die Methoden von UserRegistrationService in zwei Gruppen unterteilt werden:

  • Die Validierung der Benutzerdaten in „validateUser“
  • Das Speichern und Prüfen ob ein Benutzer existiert („storeUser“ und „getUserExists“)

Ein Kandidat für „Extract Class“ ist die Extraktion von „storeUser“ und „getUserExists“ in eine neue Klasse „UserRepository“. Die Klasse „UserRepository“ ist dafür verantwortlich Benutzerdaten aus der Datenbank zu holen und in die Datenbank zu schreiben.
Die vorhandene Klasse „UserRegistrationService“ kann das „UserRepository“ verwenden und die Funktionalität ist so klarer getrennt.

Das „UserRepository“ sieht dann so aus:

class UserRepository {

    /**
     * @var mysqli
     */
    protected $connection;

    /**
     * Creates a database connection.
     */
    protected function connectToDatabase()
    {
        $this->connection = new mysqli('localhost', 'dbuser', 'dbpassword', 'users');
        if ($this->connection->connect_error) {
            throw new NoDatabaseConnectionException("Connection failed: " . $this->connection->connect_error);
        }
    }

    /**
     * Checks if the user allready exists.
     *
     * @param string $username
     * @return bool
     */
    public function getUserExists($username)
    {
        $this->connectToDatabase();
        $query = "select * from users where username='" . $this->connection->real_escape_string($username) . "'";
        $result = $this->connection->query($query);
        return $result->num_rows > 0;
    }

    /**
     * Stores a user in the database.
     *
     * @param string $username
     * @param string $password
     * @throws Exception
     * @return bool
     */
    public function storeUser($username, $password)
    {
        $this->connectToDatabase();
        $escapedUser = $this->connection->real_escape_string($username);
        $escapedPassword = hash('sha512', $this->connection->real_escape_string($password));
        $query = "insert into users (username, password) values ('" . $escapedUser  ."', '" . $escapedPassword . "');";

        $result = $this->connection->query($query);
        if($result !== true) {
            throw new Exception("Error during user creation");
        }

        return true;
    }
}

Der modifizierte „UserRegistrationService“ bekommt eine Instanz des „UserRepository“ als Argument im Konstruktiv übergeben. In modernen Framework kann das „UserRepository“ per „DependecyInjection“ übergeben werden.

Eine Implementierung des „UserRegistrationService“ das ein „UserRepository“ nutzt, sieht so aus:

class UserRegistrationService {

    /**
     * @var UserRepository
     */
    protected $userRepository;

    /**
     * UserRegistrationService constructor.
     * @param UserRepository $userRepository
     */
    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    /**
     * Validates the user data and throws an exception in the case when the user data is invalid
     *
     * @param string $username
     * @param string $password1
     * @param string $password2
     */
    protected function validateUser($username, $password1, $password2)
    {
        if ($this->userRepository->getUserExists($username)) {
            throw new UserAllreadyExistsException("The user allready exists");
        }

        if ($password1 !== $password2) {
            throw new PasswordsDoNotMatchException("Password1 and Password2 are not same");
        }

        if (strlen($password1) < 8) {
            throw new PasswordToShortException("The password was to short");
        }
    }

    /**
     * Saves a user registation to the database when the user does not exists and the passwords
     * meet several validation contraints.
     *
     *
     * @param string $username
     * @param string $password1
     * @param string $password2
     *
     * @throws NoDatabaseConnectionException
     * @throws UserAllreadyExistsException
     * @throws PasswordsDoNotMatchException
     * @throws PasswordToShortException
     *
     * @return boolean
     */
    public function saveRegistration($username, $password1, $password2)
    {
        $this->validateUser($username, $password1, $password2);

        // user is valid, we can store it
        return $this->userRepository->storeUser($username, $password1);
    }
}

Nutzen kann man die Klassen im Code dann zum Beispiel so:

try {
    $userRepository = new UserRepository();
    $registrationService = new UserRegistrationService($userRepository);
    $registrationService->saveRegistration('test', 'password', 'password');
} catch(UserAllreadyExistsException $e) {
    echo "User allready exists";
}

Anmerkung: Das ist natürlich ein Minimalbeispiel. In einer fertigen Anwendung würden z.b. die Datenbankzugangsdaten ebenfalls von aussen übergeben werden.

 

Woran Kandidaten für "Extract Class" erkennen

Refactoring Kandidaten für kann man typischerweise daran erkennen:

  • Eine Klasse tut viele Dinge aufeinmal
  • Es gibt Methoden die auf den selben Attributen arbeiten, aber andere Teil der Klasse nicht
  • Diese Methoden gehören logisch zusammen

Navigation