이 글은 주로 PHP 코드 재구성 방법을 소개하고, PHP 코드 재구성의 개념, 원리, 관련 구현 기술 및 주의 사항을 예제 형식으로 자세히 분석합니다. 필요한 친구가 참고할 수 있습니다.
이 글은 PHP 예제를 분석합니다. 코드 리팩토링 방법. 다음과 같이 참조할 수 있도록 모든 사람과 공유하세요.
PHP가 단순한 스크립팅 언어에서 성숙한 프로그래밍 언어로 변화함에 따라 일반적인 PHP 애플리케이션의 코드 기반의 복잡성도 증가합니다. 이러한 애플리케이션의 지원 및 유지 관리를 제어하기 위해 다양한 테스트 도구를 사용하여 프로세스를 자동화할 수 있습니다. 그 중 하나는 단위 테스트로, 작성한 코드의 정확성을 직접 테스트할 수 있습니다. 그러나 레거시 코드 기반은 이러한 종류의 테스트에 적합하지 않은 경우가 많습니다. 이 기사에서는 인기 있는 단위 테스트 도구를 사용한 테스트 프로세스를 단순화하는 동시에 코드 기반 개선에 대한 종속성을 줄이기 위해 일반적인 문제가 포함된 PHP 코드에 대한 리팩토링 전략을 설명합니다.
소개
PHP의 개발 역사를 되돌아보면 당시 인기 있는 CGI 스크립트를 대체하는 데 사용되었던 단순한 동적 스크립트 언어에서 성숙한 현대 프로그래밍 언어로 변모한 것을 알 수 있습니다. 코드 기반이 커짐에 따라 수동 테스트는 불가능한 작업이 되었으며 모든 코드 변경은 크든 작든 전체 애플리케이션에 영향을 미칩니다. 이러한 영향은 특정 페이지의 로딩이나 양식 저장에만 영향을 미치는 정도로 작을 수도 있고, 감지하기 어려운 문제를 생성할 수도 있고, 특정 조건에서만 발생하는 오류를 생성할 수도 있습니다. 심지어 이전에 수정된 문제가 애플리케이션에 다시 나타날 수도 있습니다. 이러한 문제를 해결하기 위해 많은 테스트 도구가 개발되었습니다.
널리 사용되는 방법 중 하나는 일반적인 사용자 상호 작용을 통해 애플리케이션을 테스트하는 소위 기능 또는 승인 테스트입니다. 이는 애플리케이션의 개별 프로세스를 테스트하는 좋은 방법이지만 테스트 프로세스는 매우 느릴 수 있으며 일반적으로 기본 클래스와 메서드가 예상대로 작동하는지 테스트하지 못합니다. 이번에는 또 다른 테스트 방법인 단위 테스트를 사용해야 합니다. 단위 테스트의 목표는 애플리케이션의 기본 코드 기능을 테스트하고 실행 후 올바른 결과를 생성하는지 확인하는 것입니다. 종종 이러한 "성장하는" 웹 애플리케이션은 시간이 지남에 따라 테스트하기가 점점 더 어려워지는 레거시 코드를 서서히 도입하여 개발 팀이 애플리케이션 테스트 범위를 보장하기 어렵게 만듭니다. 이를 종종 "테스트할 수 없는 코드"라고 합니다. 이제 애플리케이션에서 테스트할 수 없는 코드를 식별하는 방법과 이를 수정하는 방법을 살펴보겠습니다.
테스트할 수 없는 코드 식별
코드 베이스의 테스트 불가능성과 관련된 문제 영역은 코드를 작성할 때 명확하지 않은 경우가 많습니다. PHP 애플리케이션 코드를 작성할 때 사람들은 웹 요청의 흐름을 따르도록 코드를 작성하는 경향이 있습니다. 이는 일반적으로 애플리케이션 디자인에 대해 보다 프로세스 지향적인 접근 방식을 취한다는 것을 의미합니다. 서둘러 프로젝트를 완료하거나 애플리케이션을 신속하게 수정하려는 경우 개발자는 코딩을 신속하게 완료하기 위해 "모서리를 제거"해야 할 수 있습니다. 이전에는 잘못 작성되거나 혼란스러운 코드로 인해 애플리케이션의 테스트 불가능 문제가 악화될 수 있었습니다. 후속 지원 문제가 발생할 수 있더라도 개발자가 가장 위험하지 않은 수정을 하는 경우가 많았기 때문입니다. 이러한 문제 영역은 일반적인 단위 테스트를 통해 발견할 수 없습니다.
전역 상태에 의존하는 함수
전역 변수는 PHP 애플리케이션에서 편리합니다. 이를 통해 애플리케이션에서 일부 변수나 개체를 초기화한 다음 이를 애플리케이션의 다른 곳에서 사용할 수 있습니다. 그러나 이러한 유연성에는 대가가 따르며 전역 변수의 남용은 테스트할 수 없는 코드에서 흔히 발생하는 문제입니다. Listing 1에서 이런 일이 일어나는 것을 볼 수 있다.
목록 1. 전역 상태에 의존하는 함수
<?php function formatNumber($number) { global $decimal_precision, $decimal_separator, $thousands_separator; if ( !isset($decimal_precision) ) $decimal_precision = 2; if ( !isset($decimal_separator) ) $decimal_separator = '.'; if ( !isset($thousands_separator) ) $thousands_separator = ','; return number_format($number, $decimal_precision, $decimal_separator, $thousands_separator); }
이러한 전역 변수는 두 가지 다른 문제를 가져옵니다. 첫 번째 문제는 테스트에서 이러한 전역 변수를 모두 고려하여 함수에 허용되는 유효한 값으로 설정되었는지 확인해야 한다는 것입니다. 두 번째이자 더 심각한 문제는 후속 테스트의 상태를 수정하거나 결과를 무효화할 수 없다는 점입니다. 전역 상태가 테스트가 실행되기 전의 상태로 재설정되었는지 확인해야 합니다. PHPUnit에는 전역 변수를 백업하고 테스트 실행 후 해당 값을 복원하는 데 도움이 되는 도구가 있어 이 문제를 해결하는 데 도움이 될 수 있습니다. 그러나 더 나은 접근 방식은 테스트 클래스가 이러한 전역 변수의 값을 메서드에 직접 전달할 수 있도록 하는 것입니다. Listing 2는 이 접근법의 예를 보여준다.
목록 2. 전역 변수 재정의를 지원하도록 이 함수를 수정하세요
<?php function formatNumber($number, $decimal_precision = null, $decimal_separator = null, $thousands_separator = null) { if ( is_null($decimal_precision) ) global $decimal_precision; if ( is_null($decimal_separator) ) global $decimal_separator; if ( is_null($thousands_separator) ) global $thousands_separator; if ( !isset($decimal_precision) ) $decimal_precision = 2; if ( !isset($decimal_separator) ) $decimal_separator = '.'; if ( !isset($thousands_separator) ) $thousands_separator = ','; return number_format($number, $decimal_precision, $decimal_separator, $thousands_separator); }
이렇게 하면 코드를 더 쉽게 테스트할 수 있을 뿐만 아니라 메서드의 전역 변수에 대한 의존도도 낮아집니다. 이를 통해 더 이상 전역 변수를 사용하지 않도록 코드를 리팩터링할 수 있었습니다.
재설정할 수 없는 단일 인스턴스
单一实例指的是旨在让应用程序中一次只存在一个实例的类。它们是应用程序中用于全局对象的一种常见模式,如数据库连接和配置设置。它们通常被认为是应用程序的禁忌, 因为许多开发人员认为创建一个总是可用的对象用处不大,因此他们并不太注意这一点。这个问题主要源于单一实例的过度使用,因为它会造成大量不可扩展的所谓 god objects 的出现。但是从测试的角度看,最大的问题是它们通常是不可更改的。清单 3就是这样一个例子。
清单 3. 我们要测试的 Singleton 对象
<?php class Singleton { private static $instance; protected function __construct() { } private final function __clone() {} public static function getInstance() { if ( !isset(self::$instance) ) { self::$instance = new Singleton; } return self::$instance; } }
您可以看到,当单一实例首次实例化之后,每次调用 getInstance() 方法实际上返回的都是同一个对象,它不会创建新的对象,如果我们对这个对象进行修改,那么就可能造成很严重的问题。最简单的解决方案就是给对象增加一个 reset 方法。清单 4 显示的就是这样一个例子。
清单 4. 增加了 reset 方法的 Singleton 对象
<?php class Singleton { private static $instance; protected function __construct() { } private final function __clone() {} public static function getInstance() { if ( !isset(self::$instance) ) { self::$instance = new Singleton; } return self::$instance; } public static function reset() { self::$instance = null; } }
现在,我们可以在每次测试之前调用 reset 方法,保证我们在每次测试过程中都会先执行 singleton 对象的初始化代码。总之,在应用程序中增加这个方法是很有用的,因为我们现在可以轻松地修改单一实例。
使用类构造函数
进行单元测试的一个良好做法是只测试需要测试的代码,避免创建不必要的对象和变量。您创建的每一个对象和变量都需要在测试之后删除。这对于文件和数据库表等 麻烦的项目来说成为一个问题,因为在这些情况下,如果您需要修改状态,那么您必须更小心地在测试完成之后进行一些清理操作。坚持这一规则的最大障碍在于对 象本身的构造函数,它执行的所有操作都是与测试无关的。清单 5 就是这样一个例子。
清单 5. 具有一个大 singleton 方法的类
<?php class MyClass { protected $results; public function __construct() { $dbconn = new DatabaseConnection('localhost','user','password'); $this->results = $dbconn->query('select name from mytable'); } public function getFirstResult() { return $this->results[0]; } }
在这里,为了测试对象的 fdfdfd 方法,我们最终需要建立一个数据库连接,给表添加一些记录,然后在测试之后清除所有这些资源。如果测试 fdfdfd完全不需要这些东西,那么这个过程可能太过于复杂。因此,我们要修改 清单 6所示的构造函数。
清单 6. 为忽略所有不必要的初始化逻辑而修改的类
<?php class MyClass { protected $results; public function __construct($init = true) { if ( $init ) $this->init(); } public function init() { $dbconn = new DatabaseConnection('localhost','user','password'); $this->results = $dbconn->query('select name from mytable'); } public function getFirstResult() { return $this->results[0]; } }
我们重构了构造函数中大量的代码,将它们移到一个 init() 方法中,这个方法默认情况下仍然会被构造函数调用,以避免破坏现有代码的逻辑。然而,现在我们在测试过程中只能够传递一个布尔值 false 给构造函数,以避免调用 init()方法和所有不必要的初始化逻辑。类的这种重构也会改进代码,因为我们将初始化逻辑从对象的构造函数分离出来了。
经硬编码的类依赖性
正如我们在前一节介绍的,造成测试困难的大量类设计问题都集中在初始化各种不需要测试的对象上。在前面,我们知道繁重的初始化逻 辑可能会给测试的编写造成很大的负担(特别是当测试完全不需要这些对象时),但是如果我们在测试的类方法中直接创建这些对象,又可能造成另一个问题。清单 7显示的就是可能造成这个问题的示例代码。
清单 7. 在一个方法中直接初始化另一个对象的类
<?php class MyUserClass { public function getUserList() { $dbconn = new DatabaseConnection('localhost','user','password'); $results = $dbconn->query('select name from user'); sort($results); return $results; } }
假设我们正在测试上面的 getUserList方法,但是我们的测试关注点是保证返回的 用户清单是按字母顺序正确排序的。在这种情况下,我们的问题不在于是否能够从数据库获取这些记录,因为我们想要测试的是我们是否能够对返回的记录进行排 序。问题是,由于我们是在这个方法中直接实例化一个数据库连接对象,所以我们需要执行所有这些繁琐的操作才能够完成方法的测试。因此,我们要对方法进行修 改,使这个对象可以在中间插入,如 清单 8所示。
清单 8. 这个类有一个方法会直接实例化另一个对象,但是也提供了一种重写的方法
<?php class MyUserClass { public function getUserList($dbconn = null) { if ( !isset($dbconn) || !( $dbconn instanceOf DatabaseConnection ) ) { $dbconn = new DatabaseConnection('localhost','user','password'); } $results = $dbconn->query('select name from user'); sort($results); return $results; } }
이제 예상 데이터베이스 연결 개체와 호환되는 개체를 직접 전달하고 새 개체를 생성하는 대신 이 개체를 직접 사용할 수 있습니다. 시뮬레이션 개체를 전달할 수도 있습니다. 즉, 하드 코딩된 방식으로 일부 호출 메서드에서 원하는 값을 직접 반환할 수 있습니다. 여기서는 데이터베이스 연결 개체의 쿼리 메서드를 시뮬레이션할 수 있으므로 실제로 데이터베이스를 쿼리하지 않고 결과만 반환하면 됩니다. 이와 같은 리팩터링을 사용하면 애플리케이션이 지정된 기본 데이터베이스 연결에 바인딩하는 대신 필요할 때 다른 데이터베이스 연결을 연결할 수 있으므로 이 접근 방식도 향상될 수 있습니다.
테스트 가능한 코드의 이점
분명히 테스트 가능한 코드를 더 많이 작성하면 PHP 애플리케이션의 단위 테스트를 확실히 단순화할 수 있지만(이 기사에 표시된 예에서 볼 수 있듯이) 이 과정에서 다음과 같은 작업도 수행할 수 있습니다. 애플리케이션의 디자인, 모듈성 및 안정성을 향상시킵니다. 우리 모두는 PHP 애플리케이션의 주요 프로세스 중 하나에 많은 비즈니스 및 프리젠테이션 로직이 포함된 "스파게티" 코드를 본 적이 있습니다. 이는 의심할 여지 없이 애플리케이션을 사용하는 사람들에게 심각한 지원 문제를 일으킬 것입니다. 코드를 더욱 테스트 가능하게 만드는 과정에서 이전에 문제가 있었던 코드 중 일부를 리팩토링했습니다. 이러한 코드는 디자인 측면에서 문제가 있을 뿐만 아니라 기능적으로도 문제가 있었습니다. 우리는 이러한 함수와 클래스를 더욱 다양하게 만들고 하드 코딩된 종속성을 제거하여 애플리케이션의 다른 부분에서 더 쉽게 재사용할 수 있도록 함으로써 코드의 재사용성을 향상시킵니다. 또한, 코드 베이스에 대한 향후 지원을 단순화하기 위해 잘못 작성된 코드를 더 나은 품질의 코드로 대체하고 있습니다.
결론
이 기사에서는 PHP 애플리케이션에서 테스트할 수 없는 코드의 몇 가지 일반적인 예를 통해 PHP 코드의 테스트 가능성을 향상시키는 방법을 살펴보았습니다. 또한 애플리케이션에서 이러한 상황이 어떻게 발생할 수 있는지 설명하고 테스트를 용이하게 하기 위해 문제가 있는 코드를 적절하게 수정하는 방법도 설명합니다. 우리는 또한 이러한 코드 수정이 코드의 테스트 가능성을 향상시킬 수 있을 뿐만 아니라 일반적으로 코드의 품질을 향상시키고 리팩터링된 코드의 재사용성을 향상시킬 수 있다는 것을 배웠습니다.
관련 권장 사항:
위 내용은 PHP 코드 리팩토링 방법에 대한 토론의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!