Привет, Хабр! Меня зовут Ник, мне 25, и я уже несколько лет работаю в сфере разработки. Недавно я столкнулся с интересной задачей, которую хотел бы обсудить с вами. Я получил её как тестовое задание — компании Оборот.ру, которая специализируется на автоматизации процессов. Задача заключалась в том, чтобы написать прототип сборщика фруктов в саду, реализовав его в парадигме объектно-ориентированного программирования (ООП) на PHP. В этой статье я расскажу, как я подошел к её решению.

Описание задачи

Представьте себе фруктовый сад, в котором растут яблони и груши. У нас есть 10 яблонь и 15 груш, с каждой из которых можно собрать разное количество плодов. Сборщик фруктов должен проехать по саду, собрать все плоды, а затем отсортировать их по типу и определить вес. Основные условия задачи:

  • С одной яблони можно собрать от 40 до 50 яблок, каждое весит от 150 до 180 грамм.

  • С одной груши можно собрать от 0 до 20 груш, каждая весит от 130 до 170 грамм.

  • У каждого дерева есть уникальный регистрационный номер.

Система должна:

  1. Добавлять деревья в сад.

  2. Собирать плоды со всех деревьев.

  3. Подсчитывать общее количество собранных плодов для каждого типа деревьев.

  4. Считать общий вес собранных фруктов каждого вида.

  5. Выдавать самое тяжелое яблоко и ID дерева, с которого оно было собрано.

Мой подход

Как только я ознакомился с задачей, сразу понял, что ООП здесь будет идеальным решением. Задача отлично ложится на разделение на классы и объекты. Прежде чем приступить к коду, я выделил основные сущности: дерево, фрукт, сад и сборщик.

1. Классы для деревьев и фруктов
Класс Tree будет абстрактным, так как яблони и груши имеют свои уникальные особенности, например, количество плодов и их вес. Поэтому я создал два подкласса — AppleTree и PearTree. У каждого дерева есть метод harvestFruits(), который возвращает массив плодов.

Для фруктов я также сделал абстрактный класс Fruit, который будет наследоваться в Apple и Pear. У каждого фрукта есть вес и номер дерева, с которого он был сорван.

2. Класс для сада
Класс Garden содержит массив деревьев и методы для добавления деревьев и получения их списка. Это простой класс, который представляет собой контейнер для наших деревьев.

3. Класс для сборщика фруктов
Сборщик, представленный в виде класса Harvester, содержит методы для сбора плодов с деревьев и подсчета общего количества и веса фруктов. Он также может определить самое тяжелое яблоко и дерево, с которого оно было сорвано.

Реализация

Теперь перейдем к реализации. Ниже приведены основные моменты:

Класс Tree и его подклассы:

<?php
abstract class Tree {
    protected $id;

    public function __construct($id) {
        $this->id = $id;
    }

    abstract public function harvestFruits();

    public function getId() {
        return $this->id;
    }
}

class AppleTree extends Tree {
    public function harvestFruits() {
        $apples = [];
        $count = rand(40, 50);
        for ($i = 0; $i < $count; $i++) {
            $apples[] = new Apple($this->id, rand(150, 180));
        }
        return $apples;
    }
}

class PearTree extends Tree {
    public function harvestFruits() {
        $pears = [];
        $count = rand(0, 20);
        for ($i = 0; $i < $count; $i++) {
            $pears[] = new Pear($this->id, rand(130, 170));
        }
        return $pears;
    }
}
?>

Разжуём Яблоки от А до Я

  • Абстрактный класс: Tree — это абстрактный класс, который служит шаблоном для создания других классов. Абстрактные классы не могут быть использованы для создания объектов напрямую; они предназначены для того, чтобы быть расширенными другими классами.

  • Свойство $id: Это защищенное свойство, которое содержит уникальный идентификатор дерева. Оно задается через конструктор и доступно в дочерних классах.

  • Конструктор __construct: Этот метод автоматически вызывается при создании нового объекта класса. В данном случае он присваивает значение идентификатору дерева.

  • Абстрактный метод harvestFruits: Абстрактный метод, который должен быть определен в каждом классе-наследнике. Этот метод будет отвечать за сбор фруктов с дерева.

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

  • Класс AppleTree: Это класс, который наследует абстрактный класс Tree. Он представляет собой конкретный тип дерева — яблоню.

  • Метод harvestFruits: Этот метод переопределяет абстрактный метод из класса Tree. Он собирает яблоки с дерева.

    • $apples = []: Инициализируется пустой массив для хранения собранных яблок.

    • $count = rand(40, 50): Случайным образом выбирается количество яблок, которые будут собраны с этого дерева (от 40 до 50).

    • Цикл for: В цикле создаются новые объекты класса Apple, каждому из которых присваивается ID дерева и случайный вес (от 150 до 180 грамм). Эти объекты добавляются в массив $apples.

    • return $apples: Возвращается массив собранных яблок.

  • Класс PearTree: Этот класс также наследует абстрактный класс Tree и представляет собой конкретный тип дерева — грушу.

  • Метод harvestFruits: Метод, аналогичный методу в классе AppleTree, но для груш.

    • $pears = []: Инициализируется пустой массив для хранения собранных груш.

    • $count = rand(0, 20): Случайным образом выбирается количество груш, которые будут собраны с этого дерева (от 0 до 20).

    • Цикл for: В цикле создаются новые объекты класса Pear, каждому из которых присваивается ID дерева и случайный вес (от 130 до 170 грамм). Эти объекты добавляются в массив $pears.

    • return $pears: Возвращается массив собранных груш.

Классы для фруктов:

<?php
abstract class Fruit {
    protected $treeId;
    protected $weight;

    public function __construct($treeId, $weight) {
        $this->treeId = $treeId;
        $this->weight = $weight;
    }

    public function getWeight() {
        return $this->weight;
    }

    public function getTreeId() {
        return $this->treeId;
    }
}

class Apple extends Fruit {
}

class Pear extends Fruit {
}

?>
  • Абстрактный класс Fruit:
    Это абстрактный класс, который служит шаблоном для конкретных типов фруктов. Абстрактный класс сам по себе не может быть использован для создания объектов, но он может быть расширен другими классами, которые будут его конкретными реализациями.

  • Свойства treeId и weight:

    • $treeId: Защищенное свойство, которое хранит идентификатор дерева, с которого был собран фрукт. Это позволяет отследить происхождение фрукта.

    • $weight: Защищенное свойство, которое хранит вес фрукта.

  • Конструктор __construct:
    Этот метод вызывается автоматически при создании нового объекта класса. Он инициализирует свойства $treeId и $weight значениями, переданными при создании объекта. Таким образом, каждый фрукт будет знать, с какого дерева он был собран, и сколько он весит.

  • Методы getWeight() и getTreeId():

    • getWeight(): Этот метод возвращает значение свойства $weight, то есть вес фрукта.

    • getTreeId(): Этот метод возвращает значение свойства $treeId, то есть идентификатор дерева, с которого фрукт был собран.

  • Класс Apple:
    Это конкретный класс, который представляет яблоко. Он наследует все свойства и методы класса Fruit. Таким образом, объект класса Apple имеет идентификатор дерева и вес, а также может использовать методы getWeight() и getTreeId().

  • Класс Pear:
    Аналогично классу Apple, этот класс представляет грушу и наследует все свойства и методы класса Fruit. Объект класса Pear также будет иметь идентификатор дерева и вес, а также доступ к методам getWeight() и getTreeId().

Класс Garden:

<?php
class Garden {
    private $trees = [];

    public function addTree(Tree $tree) {
        $this->trees[] = $tree;
    }

    public function getTrees() {
        return $this->trees;
    }
}

?>

Класс Harvester:

<?php
class Harvester {
    public function harvest(Garden $garden) {
        $apples = [];
        $pears = [];

        foreach ($garden->getTrees() as $tree) {
            $fruits = $tree->harvestFruits();
            foreach ($fruits as $fruit) {
                if ($fruit instanceof Apple) {
                    $apples[] = $fruit;
                } elseif ($fruit instanceof Pear) {
                    $pears[] = $fruit;
                }
            }
        }

        return ['apples' => $apples, 'pears' => $pears];
    }

    public function getTotalWeight($fruits) {
        return array_sum(array_map(function($fruit) {
            return $fruit->getWeight();
        }, $fruits));
    }

    public function getHeaviestApple($apples) {
        usort($apples, function($a, $b) {
            return $b->getWeight() <=> $a->getWeight();
        });

        return $apples[0];
    }
}

?>
  • Задача метода:
    Метод harvest() отвечает за сбор всех фруктов с деревьев в саду. Он проходит по всем деревьям, собирает плоды и сортирует их по типу (яблоки и груши).

  • Параметр Garden $garden:
    Метод принимает объект класса Garden, который содержит коллекцию деревьев.

  • Процесс сбора фруктов:

    • foreach ($garden->getTrees() as $tree): Проход по каждому дереву в саду.

    • $fruits = $tree->harvestFruits(): Сбор фруктов с текущего дерева, используя метод harvestFruits(), который определен в классах деревьев (AppleTree, PearTree).

    • Вложенный foreach: Для каждого собранного фрукта проверяется его тип:

      • Если фрукт является экземпляром класса Apple, он добавляется в массив $apples.

      • Если фрукт является экземпляром класса Pear, он добавляется в массив $pears.

  • Возвращаемое значение:
    Метод возвращает ассоциативный массив с двумя элементами: apples (массив собранных яблок) и pears (массив собранных груш)

  • Задача метода:
    Метод getTotalWeight() вычисляет общий вес фруктов, переданных ему в виде массива.

  • Параметр $fruits:
    Метод принимает массив объектов фруктов (Apple или Pear).

  • Процесс подсчета веса:

    • array_map(): Функция array_map() применяется к каждому элементу массива $fruits. Она вызывает анонимную функцию, которая для каждого фрукта вызывает метод getWeight() и возвращает его вес.

    • array_sum(): После того, как массив весов фруктов создан, функция array_sum() суммирует все эти значения и возвращает общий вес.

  • Задача метода:
    Метод getHeaviestApple() находит самое тяжелое яблоко в массиве яблок.

  • Параметр $apples:
    Метод принимает массив объектов Apple.

  • Процесс поиска самого тяжелого яблока:

    • usort(): Функция usort() сортирует массив яблок по убыванию веса. Сравнение выполняется с помощью анонимной функции, которая использует оператор <=> (космический корабль) для сравнения весов двух яблок.

    • Возвращаемое значение:После сортировки самое тяжелое яблоко оказывается первым элементом массива, и метод возвращает его.

Скрипт main.php:

<?php
$garden = new Garden();

for ($i = 1; $i <= 10; $i++) {
    $garden->addTree(new AppleTree($i));
}

for ($i = 11; $i <= 25; $i++) {
    $garden->addTree(new PearTree($i));
}

$harvester = new Harvester();
$fruits = $harvester->harvest($garden);

$totalApples = count($fruits['apples']);
$totalPears = count($fruits['pears']);
$totalAppleWeight = $harvester->getTotalWeight($fruits['apples']);
$totalPearWeight = $harvester->getTotalWeight($fruits['pears']);
$heaviestApple = $harvester->getHeaviestApple($fruits['apples']);

echo "Total apples: $totalApples\n";
echo "Total pears: $totalPears\n";
echo "Total apple weight: $totalAppleWeight g\n";
echo "Total pear weight: $totalPearWeight g\n";
echo "Heaviest apple weight: " . $heaviestApple->getWeight() . " g from tree ID: " . $heaviestApple->getTreeId() . "\n";

?>

Что получилось в итоге

Запустив этот скрипт, мы получаем подробную информацию о собранных фруктах в саду:

  1. Общее количество фруктов: сколько яблок и груш было собрано со всех деревьев.

  2. Общий вес фруктов: сумма веса всех собранных яблок и груш.

  3. Самое тяжелое яблоко: его вес и ID дерева, с которого оно было сорвано.

Юнит-тесты

Для проверки работы программы я также написал несколько юнит-тестов. Вот пример теста, который проверяет сбор фруктов с одного дерева:

<?php
use PHPUnit\Framework\TestCase;

class GardenTest extends TestCase {
    public function testHarvesting() {
        $tree = new AppleTree(1);
        $fruits = $tree->harvestFruits();

        $this->assertGreaterThanOrEqual(40, count($fruits));
        $this->assertLessThanOrEqual(50, count($fruits));

        foreach ($fruits as $fruit) {
            $this->assertInstanceOf(Apple::class, $fruit);
            $this->assertGreaterThanOrEqual(150, $fruit->getWeight());
            $this->assertLessThanOrEqual(180, $fruit->getWeight());
        }
    }
}

?>

Тест проверяет, что количество собранных яблок находится в пределах от 40 до 50 штук, и что вес каждого яблока соответствует указанным условиям.

Выводы

Задача от Оборот.ру оказалась не только интересной, но и полезной с точки зрения отработки навыков ООП в PHP. Проект продемонстрировал, как можно эффективно организовать код, используя объекты для представления реальных сущностей и процессов.

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

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

Заключение

Надеюсь, эта статья была полезной и вдохновит вас на создание своих проектов с использованием ООП. Если у вас есть идеи, как улучшить этот прототип, или возникли вопросы — пишите в комментариях, с удовольствием обсужу!

До встречи на Хабре!

Комментарии (3)


  1. Kahelman
    04.09.2024 18:23
    +4

    Посмотрел ваш подход. Вы его реализовали «влоб» и ООП тут прибито гвоздями.

    1. У вас дерево «заполняется» фруктами при вызове метода harvest. Соответственно протестировать код будет невозможно. Лучше выделить в отельный метод: growUp или просто initialize. Медли должен принимать коллекцию фруктов.

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

    3. Вам нужен класс HarvestPlan, который определяет порядок обхода деревьев в саду и правила сборки фруктов- какого размера собирать, как отличать зрелые от зелёных и т.д.

    4. harvestPlan должен назначаться сборщику. При этом надо решить- будь ли у сборщика только один HarvestPlan или их может быть несколько.

    5. HarvestPlan скорее всего должен отвечать за сбор только одного типа фруктов - или яблок или груш. Так как в реальности вы не собираетесь все фрукты в одну кучу.

    6. Скорее всего у вас должен быть класс Basket- куда собираются фрукты. На классе Basket можно гонять статистику и аналитику - сколько собрали чего и т.д.

    7. Класс Fruit - скорее всего должен иметь ID дерева, который назначается в момент когда вы «выращиваете» фрукт на дереве. Считаем что у нас супер продвинутое хозяйство и мы маркируем каждое яблоко :) Или можно считать что с каждого дерева мы собираем в одну корзину/Basket, но тогда надо будет переписать класс аналитики.

    Это примерно минимум декомпозиции задачи. Как именно и на каком языке это реализовать - роли не играет.

    Если задача для собеседования то можете блеснуть идеей dubble Dispatch, чтобы ваши HarvestPlan прилагались только к правильным деревьям.


    1. tkovacs
      04.09.2024 18:23

      При решении задачи главное не перестараться


  1. Cels
    04.09.2024 18:23
    +1

    По мне, так вполне норм, только количество и вес фруктов, я бы добавлял при создании дерева.