Refaktorierung einer Klasse - Teil 3
Geschrieben von Dejan Spasic • Monday, 27. December 2010 • Kategorie: PHP • Kommentare (0)
- Refaktorierung einer Klasse - Teil 1
- Refaktorierung einer Klasse - Teil 2
- Refaktorierung einer Klasse - Teil 3
- Refaktorierung einer Klasse - Teil 4
- Refaktorierung einer Klasse - Teil 5
Im dritten Teil möchte ich die Klasse dahingehend Refaktorieren, dass Betriebsysteme hinzugefügt oder entfernt werden können. Wenn wir uns den derzeitigen Stand der Klasse anschauen, ist dies nur bedingt möglich und führt einfach nur zu schlechtem Code der nichts mit Separation of Concerns zu tun hat. Derzeit kann man die Klasse nämlich nur über eine Kindklasse erweitern. Was meiner Meinung nach unschön und unnötig ist.
Die Refaktorierung die ich einsetzen werde, ist der "Zustandsverändernde Bedingungen durch den Zustand ersetzen (Replace State-Altering Conditionals with State)" am nächsten. Im ersten Schritt erstelle ich ein Interface namens OperatingSystems, dass, wie der Name schon sagt, ein Betriebssystem repräsentieren soll.
Nun erstelle ich die Klasse Windows das die neue Schnittstelle implementiert. Die Methoden werden ebenfalls implementiert, allerdings ohne jede Funktion.
<?php
/**
* (c) 2008 Dejan Spasic <spasic.dejan@yahoo.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Util
* @subpackage UserAgentParser
* @author Dejan Spasic <spasic.dejan@yahoo.de>
* @version GIT: $Id:$
*/
declare(encoding='UTF-8');
namespace Fam\Util\UserAgentParser;
require_once __DIR__ . '/OperatingSystem.php';
/**
* Reproduce a operating system
*
* @package Util
* @subpackage UserAgentParser
* @author Dejan Spasic <spasic.dejan@yahoo.de>
* @version @@PACKAGE_VERSION@@
*/
class Windows implements OperatingSystem
{
/**
* @param string $userAgent
*
* @return boolean
*/
public function match($userAgent)
{
}
/**
* @return string
*/
public function getName()
{
}
/**
* @param string|Fam\Util\UserAgentParser\OperatingSystem $operatingSystem
*
* @return boolean
*
* @throws \InvalidArgumentException
*/
public function equals($operatingSystem)
{
}
}
Nun kommen die Tests. Beim ausführen der Tests schlagen diese logischerweise fehl. Jetzt hauchen ich der Klasse Windows Methode um Methode leben ein. Nach dem alle Tests erfolgreich durchlaufen, kann ich mich der Klasse UserAgentParser widmen.
protected function parseOs()
{
$windows = new UserAgentParser\Windows();
switch (true) {
case $windows->match($this->userAgent):
$this->osClient = self::OS_WIN;;
return;
case preg_match('/Mac_PowerPC/i', $this->userAgent):
case preg_match('/Mac OS X/i', $this->userAgent):
case preg_match('/Macintosh/i', $this->userAgent):
$this->osClient = self::OS_MAC;
return;
case preg_match('/Linux/i', $this->userAgent):
case preg_match('/FreeBSD/i', $this->userAgent):
case preg_match('/NetBSD/i', $this->userAgent):
case preg_match('/OpenBSD/i', $this->userAgent):
case preg_match('/IRIX/i', $this->userAgent):
case preg_match('/SunOS/i', $this->userAgent):
case preg_match('/Unix/i', $this->userAgent):
$this->osClient = self::OS_UNIX;
return;
default:
$this->osClient = self::OS_UNDEFINED;
return;
}
}
Diese Schritte wiederhole ich um die Klassen Unix, Macintosh, UndefinedOperatingSystem sowie die dazugehörigen Tests zu implementieren. Die Klasse UndefinedOperatingSystem nimmt in diesem Sinne die Rolle eines NULL-Objekts ein. Die Methode parseOs mittlerweile wie folgt aus.
protected function parseOs()
{
$windows = new UserAgentParser\Windows();
$macintosh = new UserAgentParser\Macintosh();
$unix = new UserAgentParser\Unix();
$undefined = new UserAgentParser\UndefinedOperatingSystem();
switch (true) {
case $windows->match($this->userAgent):
$this->osClient = self::OS_WIN;;
return;
case $macintosh->match($this->userAgent):
$this->osClient = self::OS_MAC;
return;
case $unix->match($this->userAgent):
$this->osClient = self::OS_UNIX;
return;
default:
$this->osClient = self::OS_UNDEFINED;
return;
}
}
Bevor ich fortfahre, möchte ich noch mal die Klassen des OperatingSystem Packages sauber machen. Hier haben sich nämlich redundante Stellen breit gemacht. Da die Methoden match und equals in jeder Klasse identisch sind, behebe ich das Problem mit einer abstrakten Super-Klasse AbstractOperatingSystem und dazu verwende ich "Extract Superclass".
Jetzt passe ich die Werte der OS_* Konstanten entsprechend der Namen der jeweiligen OperatingSystem Klassen.
const OS_UNDEFINED = 'undefined';
const OS_WIN = 'windows';
const OS_MAC = 'macintosh';
const OS_UNIX = 'unix';
Da die Test weiterhin erfolgreich durchlaufen, fahre ich fort. Der Eigenschaft $osClient wird nicht mehr der Wert der Konstante, sondern die OperatingSystem-Klasse, die auf den UserAgent zutrifft, zugewiesen.
protected function parseOs()
{
$windows = new UserAgentParser\Windows();
$macintosh = new UserAgentParser\Macintosh();
$unix = new UserAgentParser\Unix();
$undefined = new UserAgentParser\UndefinedOperatingSystem();
switch (true) {
case $windows->match($this->userAgent):
$this->osClient = $windows;
return;
case $macintosh->match($this->userAgent):
$this->osClient = $macintosh;
return;
case $unix->match($this->userAgent):
$this->osClient = $unix;
return;
default:
$this->osClient = $undefined;
return;
}
}
Im gleichem Zuge müssen die Methoden os und isOs angepasst werden.
/**
* @return string
*/
public static function os()
{
return self::getInstance()->osClient->getName();
}
/**
* @param string $os The OS to compare with
*
* @return bool
*/
public static function isOs($os)
{
return self::getInstance()->osClient->equals($os);
}
Da die Test weiterhin erfolgreich durchlaufen, entferne ich als nächstes die switch case Anweisung in parseOs und verwende stattdessen ein foreach das alle OperatinsSystem Klassen, bis auf UndefinedOperatinSystem, durchläuft. Vorher wird jedoch noch eine Klassen-Eigenschaft $operatingSystems eingeführt. Die Initialisierung der Eigenschaft findet nun in einer eigene Methode initializeCommonOperatingSystems statt, dass von Konstruktor aufgerufen wird.
/**
* @var array
*/
private $operatingSystems = array();
/**
* @var \Fam\Util\UserAgentParser\OperatingSystem
*/
private $undefinedOperatingSystem;
protected function __construct()
{
$this->initializeCommonOperatingSystems();
$this->parseUserAgent();
}
private function initializeCommonOperatingSystems()
{
$this->operatingSystems = array(
new UserAgentParser\Windows(),
new UserAgentParser\Macintosh(),
new UserAgentParser\Unix(),
);
$this->undefinedOperatingSystem = new UserAgentParser\UndefinedOperatingSystem();
}
//...
protected function parseOs()
{
foreach ($this->operatingSystems as $currentOs) {
if ($currentOs->match($this->userAgent)) {
$this->osClient = $currentOs;
return;
}
}
$this->osClient = $this->undefinedOperatingSystem;
}
Führe wieder die Tests aus, die mal wieder erfolgreich durchlaufen. Nun implementiere ich die Methoden addOperatingSystem, removeOperatingSystem, removeOperatingSystemByClassName sowie getOperatingSystems, die zunächst kein Logik beinhalten. Jetzt schreibe ich erstmal die Tests. Nachdem diese Aufgabe erledigt ist, benötige ich noch eine Möglichkeit das Null-Objekt zu setzen. Also implementiere ich die Methode getUndefinedOperatingSystem und setUndefinedOperatingSystem. Wie immer sind die Methodenrümpfe erstmal leer und ich schreibe erstmal die Tests dazu. Damit auch die ganzen neuen Methoden auch irgendein Effekt haben, muss der Methode parseUserAgent die Sichtbarkeit public zugeordnet werden.
Ich bin nun durch. Die Klasse bietet nun die Möglichkeit weitere Betriebssysteme hinzuzufügen ohne sie über eine Kindklasse erweitern zu müssen. Es sind nur noch die Tests, die zum Teil verschoben werden müssen, da die Klasse UserAgentParser nicht mehr direkt mit dem validieren der UserAgent-Variable zu tun hat. Dafür können wir die Methode parseOs mit Mock-Objekten testen. Das Ganze kann dann auf Github betrachtet werden.


0 Kommentare
Kommentar schreiben