Как стать автором
Поиск
Написать публикацию
Обновить

Проектируем рейтинговое оценивание

Время на прочтение5 мин
Количество просмотров1.3K
Часто требуется реализовать возможность рейтингового оценивания того или иного объекта (заметки, комментария, цитаты, фотограммы, видеоролика и т. д.) посетителями сайта. Как это запрографировать?

Прежде всего мы имеем объект оценивания и субъект оценивания. Последним могут быть, например, зарегистрированные пользователи, незарегистрированные пользователи (гости) и пр.

Для того, чтобы обеспечить слабую связанность конкретных сущностей предметной области, к которым мы привязываем возможность рейтингового голосования, с модулем, реализующим нашу задачу, мы выделяем отдельные классы для объекта (Rating_Object) и субъекта (Rating_Subject). Оба эти класса — конкретные и реализованы как active record. Чтобы иметь возможность привязывать всяческие статьи и фотограммы к экземплярам Rating_Object, мы предусматриваем интерфейс Rating_Ratable:

interface Rating_Ratable {
    /**
     * @return Rating_Object
     */
    public function asRatingObject();
}


Этот интерфейс теперь может быть реализован в классах Article и т. д., например, так:

class Article extends ActiveRecord implements Rating_Ratable {
    public function setTableDefinition() {
        $this->hasReferenceColumn("rating_object_id");
    }

    public function setUp() {
        $this->hasOne("Rating_Object as rating_object", array("local" => "rating_object_id", "foreign" => "id"));
    }

    public function asRatingObject() {
        return $this->rating_object;
    }
}


Аналогично поступаем с субъектами оценивания. Необходимо лишь, чтобы мы хранили идентификатор субъекта для каждой конкретной сущности, которая может голосовать. Поскольку практически всегда у нас есть active record для текущего посетителя сайта (гостя можно и нужно идентифицировать по комплексу сведений и, соответственно, заводить для него запись в базе данных, — если вы это ещё не делаете, самое время начать), достаточно добавить в соответствующий класс поле rating_subject_id.

Если в будущем у нас появится необходимость принимать голоса за рейтинг от сущностей, не являющихся посетителями сайта (например, брать данные из сторонних рейтингов), мы так же легко сможем привязать к каждому из экземпляров таких сущностей отдельный экземпляр Rating_Subject.

Итак, у нас есть те, кто могут оценивать, и то, что можно оценивать. Нужно их связать. Для этого вводим класс Rating_Vote, — active record с полями object_id, subject_id, opinion. Последнее поле представляет собой конкретный выбор на шкале рейтинга — конкретную оценку, которую этот субъект поставил этому объекту.

Здесь необходимо отметить, что спектры возможных оценок могут быть разными. Можно ограничиться двумя оценками «хорошо» и «плохо» (мой личный выбор), можно предлагать поставить от одной до пяти звёзд (а можно — от одной до десяти) и т. д. Естественно, конкретная шкала оценивания определяется для конкретного объекта: например, цитаты и комментарии оцениваем бинарно, а статьи и фотограммы — по шкале 1…5.

Вместе с тем необходимо определить и способ вычисления итогового рейтинга. Скажем, для бинарного выбора «плюс-минус» можно брать за итоговую оценку разницу между количеством плюсов или количеством минусов. Но можно — отношение количества плюсов к общему количеству голосов (мой личный выбор). Для шкалы из нескольких вариантов можно брать среднее арифметическое, среднее гармоническое, моду или медиану. Опять же, для конкретного объекта задаём конкретный способ.

Класс, определяющий, с одной стороны, шкалу оценок, а с другой стороны, метод вычисления итогового рейтинга, назовём стратегией оценивания. Введём интерфейс Rating_Strategy:

interface Rating_Strategy {
    /**
     * @return array of float
     */
    public function getRatingOptions();

    /**
     * @param Rating_Vote_Collection $votes
     * @return float
     */
    public function getAggregatedOpinion(Rating_Vote_Collection $votes);
}


Почему для отражения конкретной возможной оценки выбран float? Это позволит нам отразить как дискретные шкалы оценивания (сопоставив варианты целым числам, каковые суть подмножество вещественных), так и плавающую шкалу, если нам такая понадобится. Итоговая же оценка может быть дробной даже для целочисленной шкалы (например, оценивать по шкале от 1 до 10 и брать среднее арифметическое оценок). Выбор float даёт нам возможность покрыть практически все варианты. Поле opinion в классе Rating_Vote, соответственно, тоже float.

Класс Rating_Vote_Collection отражает, как нетрудно догадаться, коллекцию объектов Rating_Vote — то есть набор полученных голосов за конкретный объект. Зачем нужен этот класс, почему не обойтись просто массивом Rating_Vote? Голосов может быть очень много, несколько тысяч, — загружать их все из базы данных в оперативную память расходно, да и незачем. Почти всегда для получения итоговой оценки необходимо и достаточно иметь на руках сведения об агрегатном количестве голосов за каждый из вариантов. Поэтому в классе Rating_Vote_Collection мы сделаем соответствующий метод getOpinionCounts, который вернёт массив вида: [+1 → 100, −1 → 50] (100 «хорошо», 50 «плохо»). Но предусмотрим также lazy-загрузку на тот случай, если захотим реализовать нетривиальную стратегию (наделяющую, например, голоса весами в зависимости от кармы голосовавших).

Несколько примеров стратегий оценивания:

abstract class Rating_Strategy_Binary implements Rating_Strategy {
    const GOOD  = +1;
    const BAD   = -1;

    public function getRatingOptions() {
        return array(self::GOOD, self::BAD);
    }
}

class Rating_Strategy_Binary_Subtraction extends Rating_Strategy_Binary {
    public function getAggregatedOpinion(Rating_Vote_Collection $votes) {
        $counts = $votes->getOpinionCounts();
        $good   = isset($counts[self::GOOD]) ? $counts[self::GOOD] : 0;
        $bad    = isset($counts[self::BAD]) ? $counts[self::BAD] : 0;
        return $good - $bad;
    }
}

class Rating_Strategy_Binary_Rational extends Rating_Strategy_Binary {
    public function getAggregatedOpinion(Rating_Vote_Collection $votes) {
        $counts = $votes->getOpinionCounts();
        $good   = isset($counts[self::GOOD]) ? $counts[self::GOOD] : 0;
        $bad    = isset($counts[self::BAD]) ? $counts[self::BAD] : 0;
        $total  = $good + $bad;
        return ($total > 0) ? ($good / $total) : 0;
    }
}

abstract class Rating_Strategy_Range implements Rating_Strategy {
    private $min, $max;

    public function __construct($min, $max) {
        $this->min = $min;
        $this->max = $max;
    }

    public function getRatingOptions() {
        return range($this->min, $this->max);
    }
}

class Rating_Strategy_Range_Arithmetic extends Rating_Strategy_Range {
    public function getAggregatedOpinion(Rating_Vote_Collection $votes) {
        $counts = $votes->getOpinionCounts();
        $sum = 0;
        $total_count = 0;
        foreach ($counts as $value => $count) {
            $sum += $value * $count;
            $total_count += $count;
        }
        return ($total_count > 0) ? ($sum / $total_count) : 0;
    }
}


Осталось добавить в интерфейс Rating_Ratable метод getRatingStrategy:

interface Rating_Ratable {
    /**
     * @return Rating_Object
     */
    public function asRatingObject();

    /**
     * @return Rating_Strategy
     */
    public function getRatingStrategy();
}

class Article extends ActiveRecord implements Rating_Ratable {
    public function setTableDefinition() {
        $this->hasReferenceColumn("rating_object_id");
    }

    public function setUp() {
        $this->hasOne("Rating_Object as rating_object", array("local" => "rating_object_id", "foreign" => "id"));
    }

    public function asRatingObject() {
        return $this->rating_object;
    }

    public function getRatingStrategy() {
        return new Rating_Strategy_Range_Arithmetic(1, 5);
    }
}


А в классе Rating_Object предусмотреть методы для оперирования связанными Rating_Vote.

Теперь мы легко можем в нужных местах произвести добавление голоса или узнать текущий рейтинг:

$article->asRatingObject()->addVote($user->asRatingSubject(), 5);

echo $article->getRatingStrategy()->getAggregatedOpinion($article->asRatingObject()->getVotes());
Теги:
Хабы:
Всего голосов 58: ↑44 и ↓14+30
Комментарии60

Публикации

Ближайшие события