«Статические методы - смерть для тестируемости» - альтернативы альтернативным конструкторам? - PullRequest
11 голосов
/ 23 января 2012

Говорят, что "статические методы - смерть для проверяемости" .Если это так, каков жизнеспособный альтернативный шаблон для приведенного ниже?

class User {

    private $phone,
            $status = 'default',
            $created,
            $modified;

    public function __construct($phone) {
        $this->phone    = $phone;
        $this->created  = new DateTime;
        $this->modified = new DateTime;
    }

    public static function getByPhone(PDO $pdo, $phone) {
        $stmt = $pdo->prepare('SELECT * FROM `users` WHERE `phone` = :phone');
        $stmt->execute(compact('phone'));
        if (!$stmt->rowCount()) {
            return false;
        }

        $record         = $stmt->fetch(PDO::FETCH_ASSOC);
        $user           = new self($record['phone']);
        $user->status   = $record['status'];
        $user->created  = new DateTime($record['created']);
        $user->modified = new DateTime($record['modified']);
        return $user;
    }

    public function save(PDO $pdo) {
        $stmt = $pdo->prepare(
            'INSERT INTO `users` (`phone`, `status`, `created`, `modified`)
                  VALUES         (:phone,  :status,  :created,  :modified)
             ON DUPLICATE KEY UPDATE `status`   = :status,
                                     `modified` = :modified');

        $data = array(
            'phone'    => $this->phone,
            'status'   => $this->status,
            'created'  => $this->created->format('Y-m-d H:i:s'),
            'modified' => date('Y-m-d H:i:s')
        );

        return $stmt->execute($data);
    }

    ...

}

Это всего лишь сокращенный пример.У класса есть еще несколько методов и свойств и больше проверки при записи в базу данных и т. Д. Руководящий принцип проектирования этого класса заключается в том, что он моделирует пользователя как объект.Некоторые свойства объекта не могут быть изменены после его создания, например номер телефона (который выступает в качестве основного идентификатора), дата создания пользователя и т. Д.Другие свойства могут быть изменены только в соответствии со строгими бизнес-правилами, которые все имеют строго проверяющие сеттеры и геттеры.

Объект не представляет запись базы данных как таковую, база данных видится только как one возможна форма постоянного хранения.Таким образом, соединитель базы данных не сохраняется в объекте, а должен вводиться каждый раз, когда объект должен взаимодействовать с базой данных.

Когда создается новый пользователь, это выглядит следующим образом:

$user = new User('+123456789');

Когда существующий пользователь восстанавливается из постоянного хранилища, это выглядит следующим образом:

$pdo  = new PDO('...');
$user = User::getByPhone($pdo, '+123456789');

Если бы я всерьез отнесся к строке «смерть для тестируемости», это, предположительно, плохо.Я вполне могу протестировать этот объект, так как он полностью внедрен в зависимости и методы static не имеют состояния.Как я могу сделать это иначе и избежать использования static методов?Или, скорее, что именно в этом случае противоречит static?Что делает такое конкретное использование static методов настолько сложным для тестирования?

Ответы [ 6 ]

3 голосов
/ 23 января 2012

Это в основном сводка (моя точка зрения) чата, который завязался между мной и @ zerkms :

Суть спора на самом деле такова:

public function doSomething($id) {
    $user = User::getByPhone($this->pdo, $id);

    // do something with user

    return $someData;
}

Это затрудняет тестирование doSomething, так как он жестко кодирует класс User, который может иметь или не иметь много зависимостей.Но на самом деле это то же самое, что создание экземпляра объекта с использованием нестатического метода:

public function doSomething($id) {
    $user = new User;
    $user->initializeFromDb($this->pdo, $id);

    // do something with user

    return $someData;
}

Мы не используем статический метод, но он все еще не может быть смоделирован.На самом деле, все стало еще хуже.
Ответ заключается в использовании фабрики:

public function doSomething($id) {
    $user = $this->UserFactory->byPhone($id);

    // do something with user

    return $someData;
}

Теперь фабрику можно вводить и высмеивать в зависимости, и класс User больше не жестко закодирован.Вы можете или не можете подумать, что это избыточное убийство, но это, безусловно, улучшает способность к игре.

Это не меняет того факта, что эта фабрика вполне может создать экземпляр реального пользовательского объекта с использованием статического метода:

public function byPhone($id) {
    return User::getByPhone($this->db, $id);
}

Здесь нет разницы между использованием статического метода или обычного конструктора.

$user = new User($db, $id);
$user = User::getByPhone($db, $id);

Оба выражения возвращают экземпляр User и оба "жестких кода" класса User.Что в любом случае просто должно произойти в какой-то момент.

Для моего варианта использования метод конструктора static имеет наибольшее значение для объекта.И, как было показано, методы static не являются проблемой.Куда звонить , это точка разногласия, а не то, что они вообще существуют.И мне еще предстоит увидеть убедительный аргумент в пользу того, чтобы не использовать статические конструкторы, поскольку они могут быть упакованы на фабрике, что устраняет любую проблему с поддающимся воспроизведению, так же, как и для обычной реализации объекта.

2 голосов
/ 23 января 2012

Статические методы - это только «смерть для тестирования», если они зависят от состояния. Если вы избегаете писать такие методы для начала (что следует), то эта проблема просто исчезнет.

Данный пример Math.abs() является одним из хороших использования статического метода. Он не зависит от состояния, поэтому он очень легко тестируется.

Сказано, считаете ли вы, что статические методы следует использовать - это другая история. Некоторым людям не нравится их на первый взгляд процедурный характер. Я согласен с теми, кто говорит, что ООП - это инструмент, а не цель. Если написание «правильного» ОО-кода не имеет смысла для конкретной ситуации (например, Math.abs()), тогда не делайте этого. Я обещаю, что бандит не будет есть ваше приложение только потому, что вы использовал статический метод. : -)

2 голосов
/ 23 января 2012

Пока ОП спрашивал об общей проблеме, а не спрашивал, как улучшить его конкретный код - я постараюсь ответить, используя некоторые абстрактные и крошечные классы:

Что ж, тестирование не сложнеесами статические методы, но труднее протестировать методы, использующие статические методы.

Давайте рассмотрим разницу на небольшом примере.

Допустим, у нас есть класс

class A
{
    public static function weird()
    {
        return 'some things that depends on 3rd party resource, like Facebook API';
    }
}

Он выполняет некоторую работу, которая требует настройки дополнительной среды (в данном случае с указанием ключей API) и подключения к Интернету для служб API FB.Тестирование этого метода займет некоторое время (только из-за задержек в сети и API), но его достаточно просто протестировать.

Теперь мы реализуем класс, который использует метод A::weird():

class TestMe
{
    public function methodYouNeedToTest()
    {
        $data = A::weird();

        return 'do something with $data and return';
    }
}

Пока - мы не можем протестировать TestMe::methodYouNeedToTest() без дополнительных шагов, необходимых для работы A::weird().Да, вместо тестирования methodYouNeedToTest нам также нужно делать вещи, которые не связаны напрямую с этим классом, а с другим.

Если мы пошли другим путем с самого начала:

class B implements IDataSource
{
    public function weird()
    {
        return 'some things that depends on 3rd party resource, like Facebook API';
    }
}

вы видите - ключевое отличие здесь в том, что мы реализовали интерфейс IDataSource и сделали метод нормальным, а не статическим.Пока что мы могли бы переписать наш код выше таким образом:

class TestMe
{
    public function methodYouNeedToTest(IDataSource $ds)
    {
        $data = $ds->weird();

        return 'do something with $data and return';
    }
}

И теперь мы не полагаемся на конкретную реализацию, а делаем на интерфейс.И теперь мы можем легко смоделировать источник данных.

Такие абстракции помогают сосредоточить наши тесты больше на самом тестировании, а не на создании необходимой среды.

Такие шаги помогают нам иметь наш блоктесты быстро.Хотя у нас все еще могут быть приемочные, нагрузочные и функциональные тесты (но это уже другая история), которые проверяют, что наше приложение работает как положено

1 голос
/ 23 января 2012

Я думаю, что цитата, которую вы даете, имеет хорошую точку, но занимает слишком жесткую строку.

Ваш статический метод - это то, что он называет «листовым» методом.В этом случае я думаю, что у вас все в порядке, если у вашего статического метода нет внешних зависимостей.

Альтернативой является преобразователь данных, объект, который знает о взаимосвязи между User и тем, какон хранится в базе данных.Пример:

class UserDBMapper {
    protected $pdo;
    protected $userclass;
    function __construct(PDO $pdo, $userclass) {
        $this->db = $db;
        // Note we can even dependency-inject a User class name if it obeys the interface that UserMapper expects.
        // You can formalize this requirement with instanceof, interface_exists() etc if you are really keen...
        $this->userclass = $userclass;  
    }

    function getByPhone($phone) {
        // fetches users from $pdo
        $stmt = $this->db->query(...);
        $userinfo = $stmt->fetch()....
        // creates an intermediary structure that can be used to create a User object
        // could even just be an array with all the data types converted, e.g. your DateTimes.
        $userargs = array(
            'name' => $userinfo['name'],
            'created' => $userinfo['created'],
            // etc
        );

        // Now pass this structure to the $userclass, which should know how to create itself from $userargs
        return new $this->userclass($userargs);
    }

    function save($userobj) {
        // save method goes in the Mapper, too. The mapper knows how to "serialize" a User to the DB.
        // User objects should not have find/save methods, instead do:
        // $usermapper->save($userobj);
    }   
}

Это очень мощный шаблон (например, вам больше не нужно иметь 1-1 тип <-> таблица, соответствие экземпляра <-> строки, как требует шаблон Active Record), и выможет полностью изменить ваш метод сериализации, не изменяя ваши доменные объекты вообще.Также должно быть очевидно, насколько проще тестер.Но во многих случаях этот шаблон также чрезмерно спроектирован и требует больше, чем нужно.В конце концов, большинство веб-сайтов используют гораздо более простой шаблон Active Record.

1 голос
/ 23 января 2012

Как упоминалось в комментариях, я бы реализовал шаблон хранилища для этого случая.

Например, User будет простой моделью со свойствами только для чтения

class User {
    private $phone,
            $status = 'default',
            $created,
            $modified;

    public function __construct($phone) {
        $this->setPhone($phone);
        $this->created  = new DateTime;
        $this->modified = new DateTime;
    }

    private function setPhone($phone) {
        // validate phone here

        $this->phone = $phone;
    }

    public function getPhone() {
        return $this->phone;
    }

    public function getCreated() {
        return $this->created;
    }

    public function getModified() {
        return $this->modified;
    }
}

Ваш интерфейс хранилища может выглядеть следующим образом:

interface UserRepository {

    /**
     * @return User
     */
    public function findByPhone($phone);

    public function save(User $user);
}

Конкретная реализация этого интерфейса может выглядеть примерно таквсе с разными стратегиями сохранения (XML, тестовые данные и т. д.)

0 голосов
/ 23 января 2012

Во-первых, класс DateTime был хорошим (хитрым) классом для выбора, потому что это ужасный класс. Вся его важная работа выполняется в конструкторе, и нет способа установить дату / время после его создания. Это требует, чтобы у нас был objectGenerator, который может построить объект DateTime в нужное время. Тем не менее, мы можем управлять этим, не вызывая new в классе User.

Я держал вещи очень простыми для решения проблемы под рукой, но их можно легко расширять для решения сколь угодно сложной проблемы.

Вот простой объектГенератор для удаления соединения, которое вы получаете с new.

    class ObjectGenerator {
       public function getNew($className) {
          return new $className;
       }
    }

Теперь мы внедряем все зависимости в конструктор. Конструктор не должен выполнять реальную работу, только настраивать объект.

class User {

    private $phone,
            $status = 'default',
            $created,
            $modified,
            $pdo,
            $objectGenerator;

    public function __construct(PDO $pdo, $objectGenerator) {
       $this->pdo = $pdo;
       $this->objectGenerator = $objectGenerator;
       $this->created = $this->objectGenerator->getNew('DateTime');
    }

    public function createNew() {
       $this->phone = '';
       $this->status = 'default';
       $this->created = $this->objectGenerator->getNew('DateTime');
    }

    public function selectByPhone($phone) {
        $stmt = $this->pdo->prepare('SELECT * FROM `users` WHERE `phone` = :phone');
        $stmt->execute(compact('phone'));
        if (!$stmt->rowCount()) {
            return false;
        }

        $record         = $stmt->fetch(PDO::FETCH_ASSOC);
        $this->phone    = $record['phone'];
        $this->status   = $record['status'];
        $this->created  = $record['created'];
        $this->modified = $record['modified'];
    }

    public function setPhone($phone) {
       $this->phone = $phone;
    }

    public function setStatus($status) {
       $this->status = $status;
    }

    public function save() {
        $stmt = $this->pdo->prepare(
            'INSERT INTO `users` (`phone`, `status`, `created`, `modified`)
                  VALUES         (:phone,  :status,  :created,  :modified)
             ON DUPLICATE KEY UPDATE `status`   = :status,
                                     `modified` = :modified');

    $modified = $this->objectGenerator->getNew('DateTime');

    $data = array(
            'phone'    => $this->phone,
            'status'   => $this->status,
            'created'  => $this->created->format('Y-m-d H:i:s'),
            'modified' => $modified->format('Y-m-d H:i:s')
        );

        return $stmt->execute($data);
    }
}

Использование:

$objectGenerator = new ObjectGenerator();

$pdo = new PDO();
// OR
$pdo = $objectGenerator->getNew('PDO');

$user = new User($pdo, $objectGenerator);
$user->setPhone('123456789');
$user->save();

$user->selectByPhone('5555555');
$user->setPhone('5552222');
$user->save();

Итак, нет нового или статического в пользовательском классе . Попробуйте протестировать оба решения. Тестовый код приятно писать без призывов к новым. Все классы, использующие пользователя, также легко тестируются без статических обращений к нему.

Различия в коде теста:

new / static - Требовать заглушку для каждого нового или статического вызова, чтобы устройство не выходило за пределы себя.

внедрение зависимостей - Могут быть введены фиктивные объекты. Это безболезненно.

...