In der TDD-Community gibt es einen Tipp, der besagt, dass
wir keine Scheinobjekte für Typen verwenden sollten, die wir nicht besitzen . Ich denke, das ist ein guter Rat, und ich versuche, ihm zu folgen. Natürlich gibt es Leute, die sagen, wir sollten überhaupt keine Scheinobjekte verwenden. Unabhängig davon, welche Meinung Sie vertreten, enthält der Ratschlag „Nicht nachahmen, was nicht Ihre ist“ auch eine verborgene Bedeutung. Die Leute gehen oft an ihm vorbei, sehen das Wort "verspotten" und geraten in Wut.
Diese versteckte Bedeutung ist, dass Sie Schnittstellen, Clients, Bridges und Adapter zwischen unserer Anwendung und dem von uns verwendeten Code von Drittanbietern erstellen sollten. Ob wir in unseren Tests Scheinobjekte dieser Schnittstellen erstellen, ist nicht einmal so wichtig. Wichtig ist, dass wir Schnittstellen erstellen und verwenden, die unseren Code besser vom Code von Drittanbietern trennen. Ein klassisches Beispiel hierfür in der PHP-Welt ist das Erstellen und Verwenden eines HTTP-Clients in unserer Anwendung, der den
Guzzle-HTTP-Client verwendet , anstatt Guzzle direkt zu verwenden.
Warum? Für den Anfang hat Guzzle eine viel leistungsfähigere API als die, die Ihre Anwendung benötigt (in den meisten Fällen). Durch das Erstellen eines eigenen HTTP-Clients, der nur die erforderlichen Guzzle-APIs bereitstellt, können Anwendungsentwickler nur noch mit diesem Client arbeiten. Wenn sich die Guzzle-API in Zukunft ändert, müssen wir Änderungen an einer Stelle vornehmen, anstatt ihre Aufrufe in der gesamten Anwendung zu korrigieren, in der Hoffnung, dass nichts kaputt geht. Zwei sehr gute Gründe, und ich habe nicht einmal Scheinobjekte erwähnt!
Ich denke nicht, dass dies schwer zu erreichen ist. Code von Drittanbietern befindet sich normalerweise in einem separaten Ordner unserer Anwendung, häufig ist es
vendor/
oder
library/
. Es befindet sich auch in einem anderen Namespace und hat eine andere Namenskonvention als die in unserer Anwendung verwendete. Code von Drittanbietern ist relativ einfach zu identifizieren, und mit ein wenig Disziplin können wir unseren Anwendungscode weniger von Teilen von Drittanbietern abhängig machen.
Was ist, wenn wir dieselben Regeln auf Legacy-Code anwenden?
Was ist, wenn wir uns unseren Legacy-Code sowie den von Drittanbietern ansehen? Dies kann schwierig oder sogar kontraproduktiv sein, wenn veralteter Code ausschließlich im Support-Modus verwendet wird, wenn wir nur Fehler beheben und kleine Teile davon ein wenig anpassen. Wenn wir jedoch neuen Code schreiben, der (wieder) veralteten Code verwendet, lohnt es sich meines Erachtens, ihn genauso zu betrachten wie Code von Drittanbietern. Zumindest aus Sicht des neuen Codes.
Wenn möglich, sollte sich veralteter und neuer Code in verschiedenen Ordnern und Namespaces befinden. Es ist lange her, seit ich das System das letzte Mal
ohne Start gesehen habe, also ist dies durchaus machbar. Aber anstatt blind alten Code im neuen Code zu verwenden, was ist, wenn wir Schnittstellen dafür erstellen und diese verwenden?
Veralteter Code ist oft voll von „göttlichen“ Objekten, die zu viele Dinge tun. Sie verwenden einen globalen Status, verfügen über öffentliche Eigenschaften oder magische Methoden, die den Zugriff auf private Eigenschaften ermöglichen, als wären sie öffentlich, verfügen über statische Methoden, mit denen jeder und überall aufgerufen werden kann. Diese Bequemlichkeit hat uns zu der Situation geführt, in der wir uns befinden.
Ein weiteres, vielleicht noch schwerwiegenderes Problem mit veraltetem Code ist, dass wir bereit sind, ihn zu ändern, zu beheben, zu knacken, da wir ihn nicht als Code von Drittanbietern betrachten. Was tun wir, wenn wir einen Fehler sehen oder dem Code von Drittanbietern eine neue Funktion hinzufügen möchten? Wir beschreiben das Problem und / oder erstellen eine Pull-Anfrage. Was wir
nicht tun, ist nicht zum
vendor/
Ordner zu gehen und den Code dort nicht zu bearbeiten. Warum machen wir das mit Legacy-Code? Und dann drücken wir die Daumen und hoffen, dass nichts gebrochen ist.
Anstatt veralteten Code blind im neuen Code zu verwenden, versuchen wir, Schnittstellen zu schreiben, die nur die erforderliche Teilmenge der API des alten „göttlichen“ Objekts enthalten. Angenommen, wir haben ein
User
im Legacy-Code, das alles über alles weiß. Er weiß, wie man E-Mails und Passwörter ändert, wie man Forumbenutzer zu Moderatoren aktualisiert, wie man öffentliche Benutzerprofile aktualisiert, Benachrichtigungseinstellungen festlegt, sich in der Datenbank speichert und vieles mehr.
src / Legacy / User.php <?php namespace Legacy; class User { public $email; public $password; public $role; public $name; public function promote($newRole) { $this->role = $newRole; } public function save() { db_layer::save($this); } }
Dies ist ein grobes Beispiel, spiegelt jedoch das Problem wider: Jede Eigenschaft ist öffentlich und kann leicht in einen beliebigen Wert geändert werden. Wir müssen daran denken, die
save
nach jeder Änderung zum Speichern usw. explizit aufzurufen.
Beschränken wir uns und verbieten den Zugriff auf diese öffentlichen Objekte und versuchen zu erraten, wie ein veraltetes System funktioniert, während die Benutzerrechte erhöht werden:
src / LegacyBridge / Promoter.php <?php namespace LegacyBridge; interface Promoter { public function promoteTo(Role $role); }
src / LegacyBridge / LegacyUserPromoter.php <?php namespace LegacyBridge; class LegacyUserPromoter implements Promoter { private $legacyUser; public function __construct(Legacy\User $user) { $this->legacyUser = $user; } public function promoteTo(Role $newRole) { $newRole = (string) $newRole;
Wenn wir jetzt die
User
für den neuen Code erhöhen möchten, verwenden wir die Schnittstelle
LegacyBridge\Promoter
, die alle Feinheiten der Benutzererhöhung in einem veralteten System behandelt.
Ändern Sie die Sprache des Erbes
Die Schnittstelle für veralteten Code gibt uns die Möglichkeit, das Design des Systems zu verbessern, und kann uns vor möglichen Namensfehlern bewahren, die vor langer Zeit gemacht wurden. Der Prozess des Wechsels der Rolle eines Benutzers von einem Moderator zu einem Teilnehmer ist nicht "Beförderung" (Erhöhung), sondern "Herabstufung" (Verringerung). Niemand hindert uns daran, zwei Schnittstellen für diese verschiedenen Dinge zu erstellen, selbst wenn der Legacy-Code gleich aussieht.
src / LegacyBridge / Promoter.php <?php namespace LegacyBridge; interface Promoter { public function promoteTo(Role $role); }
src / LegacyBridge / LegacyUserPromoter.php <?php namespace LegacyBridge; class LegacyUserPromoter implements Promoter { private $legacyUser; public function __construct(Legacy\User $user) { $this->legacyUser = $user; } public function promoteTo(Role $newRole) { if ($newRole->isMember()) { throw new \Exception("Can't promote to a member."); } $legacyMemberRole = 2; $this->legacyUser->promote($legacyMemberRole); $this->legacyUser->save(); } }
src / LegacyBridge / Demoter.php <?php namespace LegacyBridge; interface Demoter { public function demoteTo(Role $role); }
src / LegacyBridge / LegacyUserDemoter.php <?php namespace LegacyBridge; class LegacyUserDemoter implements Demoter { private $legacyUser; public function __construct(Legacy\User $user) { $this->legacyUser = $user; } public function demoteTo(Role $newRole) { if ($newRole->isModerator()) { throw new \Exception("Can't demote to a moderator."); } $legacyModeratorRole = 1; $this->legacyUser->promote($legacyModeratorRole); $this->legacyUser->save(); } }
Keine so große Änderung, aber der Zweck des Codes ist viel klarer geworden.
Wenn Sie das nächste Mal einige Methoden aus veraltetem Code aufrufen müssen, versuchen Sie, eine Schnittstelle für sie zu erstellen. Es kann unmöglich sein, es kann zu teuer sein. Ich weiß, dass die statische Methode dieses "göttlichen" Objekts wirklich sehr einfach zu verwenden ist und mit seiner Hilfe Sie die Arbeit viel schneller erledigen können, aber zumindest diese Option in Betracht ziehen. Sie können das Design des neuen Systems, das Sie erstellen, nur geringfügig verbessern.