Часто требуется реализовать возможность рейтингового оценивания того или иного объекта (заметки, комментария, цитаты, фотограммы, видеоролика и т. д.) посетителями сайта. Как это запрографировать?
Прежде всего мы имеем объект оценивания и субъект оценивания. Последним могут быть, например, зарегистрированные пользователи, незарегистрированные пользователи (гости) и пр.
Для того, чтобы обеспечить слабую связанность конкретных сущностей предметной области, к которым мы привязываем возможность рейтингового голосования, с модулем, реализующим нашу задачу, мы выделяем отдельные классы для объекта (Rating_Object) и субъекта (Rating_Subject). Оба эти класса — конкретные и реализованы как active record. Чтобы иметь возможность привязывать всяческие статьи и фотограммы к экземплярам Rating_Object, мы предусматриваем интерфейс Rating_Ratable:
Этот интерфейс теперь может быть реализован в классах Article и т. д., например, так:
Аналогично поступаем с субъектами оценивания. Необходимо лишь, чтобы мы хранили идентификатор субъекта для каждой конкретной сущности, которая может голосовать. Поскольку практически всегда у нас есть active record для текущего посетителя сайта (гостя можно и нужно идентифицировать по комплексу сведений и, соответственно, заводить для него запись в базе данных, — если вы это ещё не делаете, самое время начать), достаточно добавить в соответствующий класс поле rating_subject_id.
Если в будущем у нас появится необходимость принимать голоса за рейтинг от сущностей, не являющихся посетителями сайта (например, брать данные из сторонних рейтингов), мы так же легко сможем привязать к каждому из экземпляров таких сущностей отдельный экземпляр Rating_Subject.
Итак, у нас есть те, кто могут оценивать, и то, что можно оценивать. Нужно их связать. Для этого вводим класс Rating_Vote, — active record с полями object_id, subject_id, opinion. Последнее поле представляет собой конкретный выбор на шкале рейтинга — конкретную оценку, которую этот субъект поставил этому объекту.
Здесь необходимо отметить, что спектры возможных оценок могут быть разными. Можно ограничиться двумя оценками «хорошо» и «плохо» (мой личный выбор), можно предлагать поставить от одной до пяти звёзд (а можно — от одной до десяти) и т. д. Естественно, конкретная шкала оценивания определяется для конкретного объекта: например, цитаты и комментарии оцениваем бинарно, а статьи и фотограммы — по шкале 1…5.
Вместе с тем необходимо определить и способ вычисления итогового рейтинга. Скажем, для бинарного выбора «плюс-минус» можно брать за итоговую оценку разницу между количеством плюсов или количеством минусов. Но можно — отношение количества плюсов к общему количеству голосов (мой личный выбор). Для шкалы из нескольких вариантов можно брать среднее арифметическое, среднее гармоническое, моду или медиану. Опять же, для конкретного объекта задаём конкретный способ.
Класс, определяющий, с одной стороны, шкалу оценок, а с другой стороны, метод вычисления итогового рейтинга, назовём стратегией оценивания. Введём интерфейс Rating_Strategy:
Почему для отражения конкретной возможной оценки выбран 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-загрузку на тот случай, если захотим реализовать нетривиальную стратегию (наделяющую, например, голоса весами в зависимости от кармы голосовавших).
Несколько примеров стратегий оценивания:
Осталось добавить в интерфейс Rating_Ratable метод getRatingStrategy:
А в классе Rating_Object предусмотреть методы для оперирования связанными Rating_Vote.
Теперь мы легко можем в нужных местах произвести добавление голоса или узнать текущий рейтинг:
Прежде всего мы имеем объект оценивания и субъект оценивания. Последним могут быть, например, зарегистрированные пользователи, незарегистрированные пользователи (гости) и пр.
Для того, чтобы обеспечить слабую связанность конкретных сущностей предметной области, к которым мы привязываем возможность рейтингового голосования, с модулем, реализующим нашу задачу, мы выделяем отдельные классы для объекта (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());