避免一个类中的函数数量失控(不是函数的大小)

时间:2012-03-13 22:05:26

标签: php oop design-patterns architecture

我有一个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) . "
"; 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();
}

现在问题是所有的冲突检测等...要求我在CalendarDay中分离组成的EventContainer对象来存储待处理事件等... 随着功能的增长,我发现自己添加了太多东西。

欢迎任何批评。

我在这里学习。这里的任何人都花时间阅读:提前谢谢!

1 个答案:

答案 0 :(得分:0)

免责声明:我实际上没有PHP经验(目前)。

如果我错了,请纠正我,但似乎您的Calendar类将时间间隔划分为CalendarDays,然后在每个CalendarDay中分别处理事件,然后再次在Calendar中重新组合结果。如果是这种情况,那么我认为你应该关注his advice above。你不应该有这个中间步骤,将时间间隔分成几天,并分别在每一天处理事件。相反,您应该在多天内以更通用的方式处理它们并进行数学计算以隐式地将它们映射到几天,而不是将它们明确地分成单独的对象。