Избегать выхода из-под контроля количества функций в классе (не размера функций) - PullRequest
0 голосов
/ 14 марта 2012

У меня есть класс Calendar, который упаковывает кучу классов Day, чтобы представлять, что происходит с точки зрения событий, исключений и т. Д. Для наших приложений, связанных с расписанием.

Проблема заключается в том, что список функций ползетв, и классы используются все больше и больше, API для класса растет из-под контроля.Сами функции невелики, поскольку большинство алгоритмов делегируются другим классам, но чтобы API был простым и не заставлял пользователя знать базовую структуру класса, я заканчиваю тем, что много функций заключаю в функции.По сути, это набор функций, вызывающих один или два метода в других классах.

Единственный способ решить эту проблему - расширить класс Calendar для каждого основного типа использования (например, для обнаружения конфликтов).

Из-за запроса на более конкретный код здесь это выглядит так:

class Calendar
{
    public $start_date;
    public $end_date;

    /**
     * @var Hours_UnitDefault[]
     */
    public $unit_defaults = array();

    /**
     * @var Hours_HoursException[]
     */
    public $exceptions = array();

    /**
     * @var Event[]
     */
    public $events = array();


    /**
     * The underlining data structure of this class.
     * In the form of ['date'] => CalendarDay obj
     * Why hash table?
     *  -Convinience/Speed of storage retreaval and organization.
     * @var CalendarDay[]
     *
     */
    public $calendarHashTable = array();

    public function __construct()
    {

    }

    public function fetchAll(
        $start_date,
        $end_date,
        array $room_ids,
        array $ignore_events = array(),
        array $ignore_recurrences = array()
    )
    {
        $this->setDateRange($start_date, $end_date);
        $unit_ids = $this->fetchUnitIDs($room_ids);
        $this->fetchUnitDefaults($unit_ids);
        $semester_ids = array_unique(array_from_key('semester_id', $this->unit_defaults));
        $this->fetchExceptions($unit_ids, $semester_ids);
        $this->fetchEvents($room_ids, $ignore_events, $ignore_recurrences);


    }

    /**
     * @param Event[] $pending_events
     * checks collisions against each pending event and stores the conflicts
     */
    public function checkPendingEventsForConflicts(array $pending_events)
    {
        $this->initHashTableByPendingEvents($pending_events);
        $this->hashAllDbData();
        $this->selectCorrectUnitDefaults();
        $this->calculateCollisionsForPendingEvents($pending_events);
    }


    /**
     * goes through each CalendarDay and makes sure the UnitDefault stored is the one it should use
     */
    public function selectCorrectUnitDefaults()
    {
        foreach ($this->calendarHashTable as $key => $day) {
            $day->selectCorrectUnitDefault();
        }
    }

    public function fetchUnitIDs($room_ids)
    {
        return array_unique(array_from_key('unit_id', RoomUnit::getRoomUnits(array('room_ids' => $room_ids)))); // Directory_));
    }

    public function fetchUnitDefaults($unit_ids)
    {
        $this->unit_defaults = Hours_UnitDefault::getUnitDefaults(array('date_range' => array($this->start_date, $this->end_date), 'library_unit_ids' => $unit_ids));
    }

    public function fetchExceptions($unit_ids, $semester_ids)
    {
        $this->exceptions = Hours_HoursException::getExceptions(
            array(
                'with_unit_defaults' => true,
                'date_range' => (array(
                    $this->start_date,
                    $this->end_date
                )),
                'semester_unit_id' => $semester_ids[0],
                'library_unit_ids' => $unit_ids)
        );
    }

    public function fetchEvents($room_ids, $ignore_events = array(), $ignore_recurrences = array())
    {
        $this->events = Event::getEvents(array(
            'start_date_after' => $this->start_date,
            'end_date_before' => $this->end_date . ' 23:59:59',
            'room_ids' => $room_ids,
            'event_status' => array('approved', 'blocked'),
            'not_events' => $ignore_events,
            'not_recurrences' => $ignore_recurrences));
    }

    public function setDateRange($start_date, $end_date)
    {
        $this->start_date = date("Y-m-j", strtotime($start_date));
        $this->end_date = date("Y-m-j", strtotime($end_date));
    }

    /**
     * Gets an array of dates that are relevant to an event type to use as hash keys.
     * @static
     * @param EventInterface $event
     * @return string[] dates
     */
    public function getHashDateKeys(EventInterface $event)
    {

        return ConflictUtility::getDatesBetween2Dates($event->getStartDate(), $event->getEndDate());

    }

    /**
     * Hashes any event type(Event, Exception, UnitDefault) into the hash table.
     * @param EventInterface $event
     */
    public function hashEvent(EventInterface $event)
    {
        $keys = $this->getHashDateKeys($event);

        foreach ($keys as $key) {

            if (!$this->isHashKeySet($key)) {
                continue;
            }

            $day = $this->getCalendarDayByKey($key);
            $day->assignEvent($event);
        }

    }

    public function splitEvent(EventInterface $event)
    {
        //we need to split multi-day spanning days into single days

        $split_events = array();
        $dates = $this->getHashDateKeys($event);
        $dates_count = count($dates);

        if ($dates_count > 1) {

            foreach ($dates as $date) {
                array_push($split_events , clone $event);
            }

            for ($i = 0; $i setStartDate($dates[$i] . " " . date('g:i:s A', $split_events[$i]->getStartTimeStamp()));
                $split_events[$i]->setEndDate($dates[$i] . " " . date('g:i:s A', $split_events[$i]->getEndTimeStamp()));
            }

        } else { array_push($split_events, $event); }

        return $split_events;

    }

    /**
     * Hashes events, exceptions and events into the hash table.
     * Assumes the hash table was initialized.
     */
    public function hashAllDbData()
    {

        //we need to split multi-day spanning exceptions into single days
        foreach ($this->exceptions as $exception) {

            $split_exceptions = $this->splitEvent($exception);

            foreach($split_exceptions as $single_exception){
                $this->hashEvent($single_exception);
            }

        }

        foreach ($this->events as $event) {

            $split_events = $this->splitEvent($event);
            foreach($split_events as $single_event){
                $this->hashEvent($single_event);
            }
        }

        //splitting unit defaults into days (requires different approach then previous once)
        foreach ($this->unit_defaults as $unit_default) {
            $unit_default_semester_adapter = new Hours_UnitDefaultEventInterfaceAdopter($unit_default);
            $unit_default_semester_adapter->treatAsSemester();
            $dates = $this->getHashDateKeys($unit_default_semester_adapter);

            foreach ($dates as $date) {
                $unit_default_adapter = new Hours_UnitDefaultEventInterfaceAdopter($unit_default);
                $unit_default_adapter->treatAsEvent();
                $unit_default_adapter->setDate($date);
                $this->hashEvent($unit_default_adapter);
            }
        }
    }


    /**
     * @return CalendarDay
     * @param string $key date
     */
    public function getCalendarDayByKey($key)
    {
        return $this->calendarHashTable[$key];
    }

    /**
     *
     * @param string $key date
     * @return bool
     */
    public function isHashKeySet($key)
    {
        if (isset($this->calendarHashTable[$key])) return true;
        else                        return false;
    }

    /**
     * Inits a hash table key.
     * @param string $key date
     */
    public function initHashKey($key)
    {
        $calendar_day = new CalendarDay();
        $calendar_day->setDate($key);
        $this->calendarHashTable[$key] = $calendar_day;
    }

    /**
     * Sets up hash table based on a date range
     * @param string $date_start
     * @param string $date_end
     */
    public function initHashTableByDateRange($date_start, $date_end)
    {
        $keys = ConflictUtility::getDatesBetween2Dates($date_start, $date_end);

        foreach ($keys as $key) {
            $this->initHashKey($key);
        }
    }

    /**
     * Sets up hash table based on array of dates
     * @param string[] $dates
     */
    public function initHashTableByDateArray(array $dates = array())
    {
        foreach ($dates as $key) {
            $this->initHashKey($key);
        }
    }

    /**
     * Sets up hash table based on pending events.
     * @param Event[] $pending_events
     */
    public function initHashTableByPendingEvents(array $pending_events = array())
    {
        foreach ($pending_events as $pending_event) {
            $key = self::getHashDateKey($pending_event);
            $this->initHashKey($key);
            $calendar_day = $this->getCalendarDayByKey($key);
            $calendar_day->assignPendingEvent($pending_event);
        }
    }

    /**
     * @param Event[] $modify_pending_events
     * @param Event[] $skip_pending_events
     *
     * deletes pending events that are skipped from the calendar
     * substitutes pendings events that are modified from the calendar
     */
    public function modifyPendingEvents(array $modify_pending_events, array $skip_pending_events)
    {

        //replace old one with new one
        foreach ($modify_pending_events as $modify_pending_event) {
            $key = $this->getHashDateKey($modify_pending_event);
            $day = $this->getCalendarDayByKey($key);
            $day->emptyPendingEvent();
            $day->assignPendingEvent($modify_pending_event);
        }

        //delete pending events that were skipped
        foreach ($skip_pending_events as $skip_pending_event) {
            $key = $this->getHashDateKey($skip_pending_event);
            $day = $this->getCalendarDayByKey($key);
            $day->emptyPendingEvent();
        }
    }


    /**
     * @param EventInterface $event
     * @return string date
     */
    public function getHashDateKey(EventInterface $event)
    {
        $keys = self::getHashDateKeys($event);
        $key = $keys[0];
        return $key;
    }

    /**
     * For each pending event, checks even there are collisions in the appropriate date key.
     * Behind the scenes it populates the conlficts EventContainer.
     * @param Event[] $pending_events
     */
    public function calculateCollisionsForPendingEvents(array $pending_events)
    {
        foreach ($pending_events as $pending_event) {
            $key = self::getHashDateKey($pending_event);
            $day = $this->getCalendarDayByKey($key);
            $day->calculateCollisions();
        }
    }

    /**
     * Removes date keys without conflicts.
     * Returns a hash table with dates that have conflicts.
     * @return array HashTable
     */
    public function getConflictingDays()
    {

        $conflicting_days = array();
        foreach ($this->calendarHashTable as $date => $day) {
            if ($day->hasConflicts()) {
                array_push($conflicting_days, $day);
            }
        }

        return $conflicting_days;
    }

    public function getConflictFreeDays()
    {
        $conflict_free_days = array();
        foreach ($this->calendarHashTable as $date => $day) {
            if (!$day->hasConflicts()) {
                array_push($conflict_free_days, $day);
            }
        }
        return $conflict_free_days;
    }

    /**
     * @return Event[]
     * returns all of the pending events that are stored on the calendar
     */
    public function getPendingEvents()
    {
        $pending_events = array();
        foreach ($this->calendarHashTable as $date => $day) {
            if (!$day->isEmptyPendingEvent()) {
                array_push($pending_events, $day->getPendingEvent());
            }
        }

        return $pending_events;
    }
}

Идея в том, что все хэшируется в [date] => CalendarDay, так что все повторения и т.д ...только один деньЕсли они охватывают много дней, они разбиваются на каждый охватываемый день.

class CalendarDay
{
    /**
     * @var EventContainer
     */
    public $schedule;

    /**
     * @var EventContainer
     */
    public $conflicts;

    public $pending_event;

    public $date;

    public function __construct()
    {
        $this->conflicts = new EventContainer();
        $this->schedule = new EventContainer();
        $this->pending_event = new EventContainer();
    }

    /**
     * Checks whether there are any conflicts in the conflicts container.
     * Only relevant if a check for conflicts was performed and conflicts populated.
     * @return bool
     */
    public function hasConflicts()
    {
        if ($this->conflicts->isEmpty())
            return false;
        else
            return true;
    }

    /**
     * Figures out if there are collisions between each event type and
     * the pending event.  In case there is a conflict it appends it to the
     * EventContainer conlflicts.
     */
    public function calculateCollisions()
    {
        //if there is no pending events, do nothing.
        if ($this->pending_event->isEmptyEvents()) return;

        $pending_event = $this->pending_event->getEvent();

        if (!$this->schedule->isEmptyEvents()) {
            foreach ($this->schedule->getEvents() as $event) {
                if ($event->hasConflict($pending_event)) {
                    $this->conflicts->assignEvent($event);
                }
            }
        }

        if (!$this->schedule->isEmptyUnitDefault()) {
            $default_hours = $this->schedule->getUnitDefault();
            if ($default_hours->hasConflict($pending_event)) {
                $this->conflicts->assignEvent($default_hours);
            }
        }

        if (!$this->schedule->isEmptyException()) {
            $exception = $this->schedule->getException();
            if ($exception->hasConflict($pending_event)) {
                $this->conflicts->assignEvent($exception);
            }
        }
    }

    /**
     * Assigns event the to EventContainer schedule
     * @param EventInterface $event
     */
    public function assignEvent(EventInterface $event)
    {
        $this->schedule->assignEvent($event);
    }

    public function assignPendingEvent(EventInterface $event)
    {
        $this->pending_event->assignEvent($event);
    }


    /**
     * Basic idea behind the algorithm:
     * ********************************
     * Open Period(START)
     *          |open -> between opening and first event
     *      Closed Period(START)
     *      Closed Period(END)
     *          |open -> between events
     *      Closed Period(START)
     *      Closed Period(END)
     *          |open -> between closing and last event
     * Open Period(END)
     * ********************************
     *
     * @return TimePeriod[]
     * returns empty array if closed
     */
    public function getAvailableHours() //todo:make sure data is valid before this algorithm happens
    {
        $available_hours = array();

        $open_hours = $this->getOpenTimePeriod();

        //means its closed this day (cases: closed all day exception, closed on that day in unit default)
        if ($open_hours->isEmpty()) return $available_hours; //which is an empty array

        $closed_time_periods = $this->getClosedTimePeriods($open_hours);
        $events_size = count($closed_time_periods);

        $hours_start = $open_hours->getStartTimeStamp();
        $hours_end = $open_hours->getEndTimeStamp();

        //if events are empty, the available hours are the open hours
        if (empty($closed_time_periods)) {
            $available = new TimePeriod();
            $available->setStartTimeStamp($hours_start);
            $available->setEndTimeStamp($hours_end);
            array_push($available_hours, $available);
            return $available_hours;
        } else {
            //takes care of available hours between opening hour and first event
            $first_event = $closed_time_periods[0];

            $first_available = new TimePeriod();
            $first_available->setStartTimeStamp($hours_start);
            $first_available->setEndTimeStamp($first_event->getStartTimeStamp());

            array_push($available_hours, $first_available);

            //takes care of available hours in between events if more then one
            if ($events_size > 1) {
                for ($i = 0; $i setStartTimeStamp($event->getEndTimeStamp());
                    $available->setEndTimeStamp($next_event->getStartTimeStamp());

                    array_push($available_hours, $available);
                }
            }

            //takes care of available hours between closing hour and last event
            $last_event = $closed_time_periods[$events_size - 1];

            $last_available = new TimePeriod();
            $last_available->setStartTimeStamp($last_event->getEndTimeStamp());
            $last_available->setEndTimeStamp($hours_end);

            array_push($available_hours, $last_available);
        }

        //filter out available hours less then 30m //todo: ask slava if its 30m
        foreach ($available_hours as $i => $available) {
            $minutes = ($available->getEndTimeStamp() - $available->getStartTimeStamp()) / 60;
            if ($minutes schedule->isEmptyUnitDefault()) {
            $unit_default = $this->schedule->getUnitDefault();
            $open_hours->setStartTimeStamp($unit_default->getStartTimeStamp());
            $open_hours->setEndTimeStamp($unit_default->getEndTimeStamp());
            $open_hours->setSource($unit_default);
        }

        //if there is an exception for open hours, replace default hours
        else if (!$this->schedule->isEmptyException()) {
            $exception = $this->schedule->getException();
            if ($exception->is_open) {
                $open_hours->setStartTimeStamp($exception->getStartTimeStamp());
                $open_hours->setEndTimeStamp($exception->getEndTimeStamp());
                $open_hours->setSource($exception);
            }
        }

        return $open_hours;
    }

    /**
     * @param TimePeriod $open_hours
     * @return TimePeriod[]
     */
    public function getClosedTimePeriods(TimePeriod $open_hours)
    {
        $closed_hours = array();

        //first take care of events and treat them as closed time periods
        $events = $this->schedule->getEvents();
        foreach ($events as $event) {
            $closed_period = new TimePeriod();
            //add 10 minutes before and after for events //todo:: possibly account for events that go right after each other to avoid 20m gap instead of 10m
            $padding = 0;
            if ($event->event_status != 'blocked') { $padding = 10 * 60; }

            $closed_period->setSource($event);

            //don't need the date
            $closed_period->setStartTimeStamp($event->getStartTimeStamp() - $padding);
            $closed_period->setEndTimeStamp($event->getEndTimeStamp() + $padding);

            array_push($closed_hours, $closed_period);
        }

        //take care of closed exception if any and treat it as closed time period
        if (!$this->schedule->isEmptyException()) {

            $exception = $this->schedule->getException();
            if (!$exception->is_open) {

                $closed_period = new TimePeriod();
                $closed_period->setSource($exception);

                $closed_period_start = $exception->getStartTimeStamp();
                $closed_period_end = $exception->getEndTimeStamp();

                //open hours generally don't have date as part of time stamp so we need to add it for future comparisons with exception
                $open_period_start = $open_hours->getStartTimeStamp();
                $open_period_end = $open_hours->getEndTimeStamp();

                //now we need to take care of cases when closed exception starts before or after opening hours.
                //why? to simplify further algorithms so that all closed time period fall within open time period.

                //if closed exception starts before open -> trunkate
                if ($closed_period_start  $open_period_end) {
                    $closed_period_end = $open_period_end;
                }

                $closed_period->setStartTimeStamp($closed_period_start);
                $closed_period->setEndTimeStamp($closed_period_end);

                array_push($closed_hours, $closed_period);
            }

            if (!function_exists('cmp')) {
                function cmp($a, $b)
                {
                    if ($a->getStartTimeStamp() == $b->getStartTimeStamp()) {
                        return 0;
                    }
                    return ($a->getStartTimeStamp() getStartTimeStamp()) ? -1 : 1;
                }
            }

            usort($closed_hours, 'cmp');
        }

        return $closed_hours;
    }

    public function selectCorrectUnitDefault()
    {

        if (!$this->schedule->isEmptyException()) {
            $exception = $this->schedule->getException();

            if ($exception->is_open) { //if we have exception for open hours, get rid of defaults
                $this->schedule->emptyUnitDefaults();
                return;
            }

            if (!$exception->is_open) { //check for closed exception that last all day //todo:: think about how to do this in available_periods section
                if ($exception->getStartTimeStamp('only_time') == strtotime('12:00:00 AM') && $exception->getEndTimeStamp('only_time') == strtotime('11:59:00 PM')) {
                    $this->schedule->emptyUnitDefaults();
                    return;
                }
            }

        }

        $unit_defaults = $this->schedule->getUnitDefaults();
        //discard library hours if there are multiple unit defaults b/c we are going to use a different one.
        if (count($unit_defaults) > 1) {
            foreach ($unit_defaults as $i => $unit_default) {
                if ($unit_default->getName() == 'Library') unset($unit_defaults[$i]);
            }
            $unit_defaults = array_values($unit_defaults);

            //take the more restrictive unit default
            $last_unit_default = $unit_defaults[0];
            foreach ($unit_defaults as $unit_default) {
                if ($unit_default->getStartTimeStamp() > $last_unit_default->getStartTimeStamp()) $last_unit_default = $unit_default;
            }

            $unit_defaults[0] = $last_unit_default;

        }

        $unit_default = $unit_defaults[0]; //take whatever remains as unit default

        $this->schedule->emptyUnitDefaults();
        $this->schedule->assignEvent($unit_default);
    }

    /**
     * Sets date to correspond to hash table date key for convinience
     * @param string $date
     */
    public function setDate($date)
    {
        $this->date = $date;
        $this->schedule->setDate($date);
        $this->conflicts->setDate($date);
        $this->pending_event->setDate($date);
    }

    public function getDate($format = "Y-m-d")
    {
        return date($format, strtotime($this->date));
    }

    public function getAllConflicts()
    {
        return $this->conflicts->getAllEvents();
    }

    public function getConflictCount()
    {
        return count($this->getAllConflicts());
    }

    public function emptyPendingEvent()
    {
        $this->pending_event->emptyEvents();
    }

    public function emptyConflicts()
    {
        $this->conflicts->emptyAll();
    }

    public function isEmptyPendingEvent()
    {
        return $this->pending_event->isEmptyEvents();
    }

    public function getPendingEvent()
    {
        return $this->pending_event->getEvent();
    }

}

Контейнер CalendarDay использует для хранения.

class EventContainer {

    /**
     * @var Hours_UnitDefaultInterfaceAdopter[]
     */
    private $unit_defaults = array();

    /**
     * @var Hours_HoursException
     */
    private $exception;

    /**
     * @var Event[]
     */
    private $events = array();

    /**
     * Assigns event type to the appropriate variable by checking its class.
     * UnitDefault are saved as UnitDefaultEventInterfaceAdopter for code reuse.
     * @param EventInterface|Hours_UnitDefaultEventInterfaceAdopter|Hours_HoursException|Event $event
     */
    public function assignEvent(EventInterface $event) {
//      echo "assigning: ".get_class($event) . "<br/>";

        switch (get_class($event)) {
            case 'Event':
                array_push($this->events, $event);
                break;
            case 'Hours_HoursException':
                $this->exception = $event;
                break;
            case 'Hours_UnitDefaultEventInterfaceAdopter':
                array_push($this->unit_defaults, $event);
                break;
            default:
                die('Invalid class type argument supplied to the DaySchedule->assign() function');
        }
    }


    /**
     * Checks whether all of the event types arrays are empty.
     * @return bool
     */
    public function isEmpty(){
        if (empty($this->unit_defaults) && empty($this->exception) && empty($this->events)) return true;
        else return false;
    }

    /**
     *
     * @return bool
     */
    public function isEmptyException(){
        return empty($this->exception);
    }

    /**
     *
     * @return bool
     */
    public function isEmptyEvents(){
        return empty($this->events);
    }

    /**
     *
     * @return bool
     */
    public function isEmptyUnitDefault(){
        return empty($this->unit_defaults);
    }

    /**
     * @return Hours_HoursException
     */
    public function getException(){
        return $this->exception;
    }

    /*
     * @return Hours_UnitDefaultInterfaceAdopter
     */
    public function getUnitDefault(){
        return $this->unit_defaults[0];
    }

    /*
    * @return Hours_UnitDefaultInterfaceAdopter[]
    */
    public function getUnitDefaults(){
        return $this->unit_defaults;
    }

    /*
     * @return Event[]
     */
    public function getEvents(){
        return $this->events;
    }

    public function getEvent(){
        return $this->events[0];
    }

    public function setDate($date){
        $this->date = $date;    
    }

    public function getAllEvents(){
        $events = array();
        if(!$this->isEmptyEvents()){
            $events = $this->events;
        }
        if(!$this->isEmptyException()){
            array_push($events, $this->exception);
        }
        if(!$this->isEmptyUnitDefault()){
            array_push($events, $this->getUnitDefault());
        }
        return $events;
    }

    public function emptyEvents(){
       $this->events = array();
    }

    public function emptyUnitDefaults(){
        $this->unit_defaults = array();
    }

    public function emptyException(){
        $this->exception = null;
    }

    public function emptyAll(){
        $this->emptyEvents();
        $this->emptyException();
        $this->emptyUnitDefaults();
    }



}

Интерфейс, который используется везде.

interface EventInterface
{
    public function getStartTimeStamp();
    public function getEndTimeStamp();
    public function getStartDate();
    public function getEndDate();
    public function hasConflict(EventInterface $pending_event);
    public function getName();
    public function getDetails();
}

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

Любая критика приветствуется.

Я здесь, чтобы учиться.Любой, кто нашел время, чтобы прочитать: спасибо заранее!

1 Ответ

0 голосов
/ 14 марта 2012

Отказ от ответственности: у меня фактически нулевой опыт PHP (в настоящее время).

Исправьте меня, если я ошибаюсь, но, похоже, ваш класс Calendar делит временной интервал на CalendarDays, а затем внутри каждого CalendarDay вы имеете дело с событиями отдельноа затем снова объединить результаты в календаре снова.Если это так, то я думаю, что вы должны следовать его совету выше .У вас не должно быть этого промежуточного шага разбивки временного интервала на дни и обработки событий в каждом дне отдельно.Вместо этого вам следует обращаться с ними более общим образом в течение нескольких дней и выполнять математические расчеты, чтобы неявно отображать их в днях, не разделяя их явно на отдельные объекты.

...