Посетитель
Также известен как: Visitor
Посетитель — это поведенческий паттерн проектирования, который позволяет добавлять в программу новые операции, не изменяя классы объектов, над которыми эти операции могут выполняться.

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

Экспорт геоузлов в XML.
Ваша задача — сделать экспорт этого графа в XML. Дело было бы плёвым, если бы вы могли редактировать классы узлов. Достаточно было бы добавить метод экспорта в каждый тип узла, а затем, перебирая узлы графа, вызывать этот метод для каждого узла. Благодаря полиморфизму, решение получилось бы изящным, так как вам не пришлось бы привязываться к конкретным классам узлов.
Но, к сожалению, классы узлов вам изменить не удалось. Системный архитектор сослался на то, что код классов узлов сейчас очень стабилен, и от него многое зависит, поэтому он не хочет рисковать и позволять кому-либо его трогать.

Код XML-экспорта придётся добавить во все классы узлов, а это слишком накладно.
К тому же он сомневался в том, что экспорт в XML вообще уместен в рамках этих классов. Их основная задача была связана с геоданными, а экспорт выглядит в рамках этих классов чужеродно.
Была и ещё одна причина запрета. Если на следующей неделе вам бы понадобился экспорт в какой-то другой формат данных, то эти классы снова пришлось бы менять.
Решение
Паттерн Посетитель предлагает разместить новое поведение в отдельном классе, вместо того чтобы множить его сразу в нескольких классах. Объекты, с которыми должно было быть связано поведение, не будут выполнять его самостоятельно. Вместо этого вы будете передавать эти объекты в методы посетителя.
Код поведения, скорее всего, должен отличаться для объектов разных классов, поэтому и методов у посетителя должно быть несколько. Названия и принцип действия этих методов будут схожи, но основное отличие будет в типе принимаемого в параметрах объекта, например:
Здесь возникает вопрос: как подавать узлы в объект-посетитель? Так как все методы имеют отличающуюся сигнатуру, использовать полиморфизм при переборе узлов не получится. Придётся проверять тип узлов для того, чтобы выбрать соответствующий метод посетителя.
Тут не поможет даже механизм перегрузки методов (доступный в Java и C#). Если назвать все методы одинаково, то неопределённость реального типа узла всё равно не даст вызвать правильный метод. Механизм перегрузки всё время будет вызывать метод посетителя, соответствующий типу Node, а не реального класса поданного узла.
Но паттерн Посетитель решает и эту проблему, используя механизм двойной диспетчеризации. Вместо того, чтобы самим искать нужный метод, мы можем поручить это объектам, которые передаём в параметрах посетителю. А они уже вызовут правильный метод посетителя.
Как видите, изменить классы узлов всё-таки придётся. Но это простое изменение позволит применять к объектам узлов и другие поведения, ведь классы узлов будут привязаны не к конкретному классу посетителей, а к их общему интерфейсу. Поэтому если придётся добавить в программу новое поведение, вы создадите новый класс посетителей и будете передавать его в методы узлов.
Аналогия из жизни

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

- Посетитель описывает общий интерфейс для всех типов посетителей. Он объявляет набор методов, отличающихся типом входящего параметра, которые нужны для запуска операции для всех типов конкретных элементов. В языках, поддерживающих перегрузку методов, эти методы могут иметь одинаковые имена, но типы их параметров должны отличаться.
- Конкретные посетители реализуют какое-то особенное поведение для всех типов элементов, которые можно подать через методы интерфейса посетителя.
- Элемент описывает метод принятия посетителя. Этот метод должен иметь единственный параметр, объявленный с типом интерфейса посетителя.
- Конкретные элементы реализуют методы принятия посетителя. Цель этого метода — вызвать тот метод посещения, который соответствует типу этого элемента. Так посетитель узнает, с каким именно элементом он работает.
- Клиентом зачастую выступает коллекция или сложный составной объект, например, дерево Компоновщика. Зачастую клиент не привязан к конкретным классам элементов, работая с ними через общий интерфейс элементов.
Псевдокод
В этом примере Посетитель добавляет в существующую иерархию классов геометрических фигур возможность экспорта в XML.

Пример организации экспорта объектов в XML через отдельный класс-посетитель.
accept — это лишнее звено? Если так, то ещё раз рекомендую вам ознакомиться с проблемой раннего и позднего связывания в статье Посетитель и Double Dispatch.
Применимость
Когда вам нужно выполнить какую-то операцию над всеми элементами сложной структуры объектов, например, деревом.
Посетитель позволяет применять одну и ту же операцию к объектам различных классов.
Когда над объектами сложной структуры объектов надо выполнять некоторые не связанные между собой операции, но вы не хотите «засорять» классы такими операциями.
Посетитель позволяет извлечь родственные операции из классов, составляющих структуру объектов, поместив их в один класс-посетитель. Если структура объектов является общей для нескольких приложений, то паттерн позволит в каждое приложение включить только нужные операции.
Когда новое поведение имеет смысл только для некоторых классов из существующей иерархии.
Посетитель позволяет определить поведение только для этих классов, оставив его пустым для всех остальных.
Шаги реализации
- Создайте интерфейс посетителя и объявите в нём методы «посещения» для каждого класса элемента, который существует в программе.
- Опишите интерфейс элементов. Если вы работаете с уже существующими классами, то объявите абстрактный метод принятия посетителей в базовом классе иерархии элементов.
- Реализуйте методы принятия во всех конкретных элементах. Они должны переадресовывать вызовы тому методу посетителя, в котором тип параметра совпадает с текущим классом элемента.
- Иерархия элементов должна знать только о базовом интерфейсе посетителей. С другой стороны, посетители будут знать обо всех классах элементов.
- Для каждого нового поведения создайте конкретный класс посетителя. Приспособьте это поведение для работы со всеми типами элементов, реализовав все методы интерфейса посетителей.
Вы можете столкнуться с ситуацией, когда посетителю нужен будет доступ к приватным полям элементов. В этом случае вы можете либо раскрыть доступ к этим полям, нарушив инкапсуляцию элементов, либо сделать класс посетителя вложенным в класс элемента, если вам повезло писать на языке, который поддерживает вложенность классов. - Клиент будет создавать объекты посетителей, а затем передавать их элементам, используя метод принятия.
Преимущества и недостатки
Преимущества
- Упрощает добавление операций, работающих со сложными структурами объектов.
- Объединяет родственные операции в одном классе.
- Посетитель может накапливать состояние при обходе структуры элементов.
Недостатки
- Паттерн не оправдан, если иерархия элементов часто меняется.
- Может привести к нарушению инкапсуляции элементов.
Примеры реализации паттерна
Python
- Сложность: 3/3
- Популярность: 1/3
- Применимость: Посетитель нечасто встречается в Python-коде из-за своей сложности и нюансов реализазации.
Этот пример показывает структуру паттерна Посетитель, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | |
The client code works with all visitors via the base Visitor
interface:
A + ConcreteVisitor1
B + ConcreteVisitor1
It allows the same client code to work with different types of
visitors:
A + ConcreteVisitor2
B + ConcreteVisitor2
PHP
- Сложность: 3/3
- Популярность: 1/3
- Применимость: Посетитель нечасто встречается в PHP-коде из-за своей сложности и нюансов реализазации.
Этот пример показывает структуру паттерна Посетитель, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | |
=== "Пример из реальной жизни">
В этом примере паттерн **Посетитель** помогает внедрить функцию отчётности в существующую иерархию классов:
`Компания > Отдел > Сотрудник`
После реализации **Посетителя** вы можете легко добавлять в приложение другие подобные поведения без изменения существующих классов.
```php linenums="1"
<?php
/**
* Интерфейс Компонента объявляет метод принятия объектов-посетителей.
*
* В этом методе Конкретный Компонент вызывает конкретный метод Посетителя, с
* тем же типом параметра, что и у компонента.
*/
interface Entity
{
public function accept(Visitor $visitor): string;
}
/**
* Конкретный Компонент Компании.
*/
class Company implements Entity
{
private $name;
/**
* @var Department[]
*/
private $departments;
public function __construct(string $name, array $departments)
{
$this->name = $name;
$this->departments = $departments;
}
public function getName(): string
{
return $this->name;
}
public function getDepartments(): array
{
return $this->departments;
}
// ...
public function accept(Visitor $visitor): string
{
// Смотрите, Компонент Компании должен вызвать метод visitCompany. Тот
// же принцип применяется ко всем компонентам.
return $visitor->visitCompany($this);
}
}
/**
* Конкретный Компонент Отдела.
*/
class Department implements Entity
{
private $name;
/**
* @var Employee[]
*/
private $employees;
public function __construct(string $name, array $employees)
{
$this->name = $name;
$this->employees = $employees;
}
public function getName(): string
{
return $this->name;
}
public function getEmployees(): array
{
return $this->employees;
}
public function getCost(): int
{
$cost = 0;
foreach ($this->employees as $employee) {
$cost += $employee->getSalary();
}
return $cost;
}
// ...
public function accept(Visitor $visitor): string
{
return $visitor->visitDepartment($this);
}
}
/**
* Конкретный Компонент Сотрудника.
*/
class Employee implements Entity
{
private $name;
private $position;
private $salary;
public function __construct(string $name, string $position, int $salary)
{
$this->name = $name;
$this->position = $position;
$this->salary = $salary;
}
public function getName(): string
{
return $this->name;
}
public function getPosition(): string
{
return $this->position;
}
public function getSalary(): int
{
return $this->salary;
}
// ...
public function accept(Visitor $visitor): string
{
return $visitor->visitEmployee($this);
}
}
/**
* Интерфейс Посетителя объявляет набор методов посещения для каждого класса
* Конкретного Компонента.
*/
interface Visitor
{
public function visitCompany(Company $company): string;
public function visitDepartment(Department $department): string;
public function visitEmployee(Employee $employee): string;
}
/**
* Конкретный Посетитель должен предоставить реализации для каждого из классов
* Конкретных Компонентов.
*/
class SalaryReport implements Visitor
{
public function visitCompany(Company $company): string
{
$output = "";
$total = 0;
foreach ($company->getDepartments() as $department) {
$total += $department->getCost();
$output .= "\n--" . $this->visitDepartment($department);
}
$output = $company->getName() .
" (" . money_format("%i", $total) . ")\n" . $output;
return $output;
}
public function visitDepartment(Department $department): string
{
$output = "";
foreach ($department->getEmployees() as $employee) {
$output .= " " . $this->visitEmployee($employee);
}
$output = $department->getName() .
" (" . money_format("%i", $department->getCost()) . ")\n\n" .
$output;
return $output;
}
public function visitEmployee(Employee $employee): string
{
return money_format("%#6n", $employee->getSalary()) .
" " . $employee->getName() .
" (" . $employee->getPosition() . ")\n";
}
}
/**
* Клиентский код.
*/
$mobileDev = new Department("Mobile Development", [
new Employee("Albert Falmore", "designer", 100000),
new Employee("Ali Halabay", "programmer", 100000),
new Employee("Sarah Konor", "programmer", 90000),
new Employee("Monica Ronaldino", "QA engineer", 31000),
new Employee("James Smith", "QA engineer", 30000),
]);
$techSupport = new Department("Tech Support", [
new Employee("Larry Ulbrecht", "supervisor", 70000),
new Employee("Elton Pale", "operator", 30000),
new Employee("Rajeet Kumar", "operator", 30000),
new Employee("John Burnovsky", "operator", 34000),
new Employee("Sergey Korolev", "operator", 35000),
]);
$company = new Company("SuperStarDevelopment", [$mobileDev, $techSupport]);
setlocale(LC_MONETARY, 'en_US');
$report = new SalaryReport();
echo "Client: I can print a report for a whole company:\n\n";
echo $company->accept($report);
echo "\nClient: ...or for different entities " .
"such as an employee, a department, or the whole company:\n\n";
$someEmployee = new Employee("Some employee", "operator", 35000);
$differentEntities = [$someEmployee, $techSupport, $company];
foreach ($differentEntities as $entity) {
echo $entity->accept($report) . "\r\n";
}
// $export = new JSONExport();
// echo $company->accept($export);
```
```bash
Client: I can print a report for a whole company:
SuperStarDevelopment (USD550,000.00)
--Mobile Development (USD351,000.00)
$100,000.00 Albert Falmore (designer)
$100,000.00 Ali Halabay (programmer)
$ 90,000.00 Sarah Konor (programmer)
$ 31,000.00 Monica Ronaldino (QA engineer)
$ 30,000.00 James Smith (QA engineer)
--Tech Support (USD199,000.00)
$ 70,000.00 Larry Ulbrecht (supervisor)
$ 30,000.00 Elton Pale (operator)
$ 30,000.00 Rajeet Kumar (operator)
$ 34,000.00 John Burnovsky (operator)
$ 35,000.00 Sergey Korolev (operator)
Client: ...or for different entities such as an employee,
a department, or the whole company:
35000 Some employee (operator)
Tech Support (199000)
70000 Larry Ulbrecht (supervisor)
30000 Elton Pale (operator)
30000 Rajeet Kumar (operator)
34000 John Burnovsky (operator)
35000 Sergey Korolev (operator)
SuperStarDevelopment (550000)
--Mobile Development (351000)
100000 Albert Falmore (designer)
100000 Ali Halabay (programmer)
90000 Sarah Konor (programmer)
31000 Monica Ronaldino (QA engineer)
30000 James Smith (QA engineer)
--Tech Support (199000)
70000 Larry Ulbrecht (supervisor)
30000 Elton Pale (operator)
30000 Rajeet Kumar (operator)
34000 John Burnovsky (operator)
35000 Sergey Korolev (operator)
```
Задачи
Задача 1. Экспорт AST в разные форматы
Реализуйте абстрактное синтаксическое дерево (AST) для арифметических выражений. Узлы: NumberNode, BinaryOpNode (+, -, *, /), UnaryOpNode (-), VariableNode. Создайте посетителей: EvalVisitor (вычислить результат), PrintVisitor (вывести выражение в инфиксной записи), RpnVisitor (обратная польская запись).
from __future__ import annotations
from abc import ABC, abstractmethod
class ASTVisitor(ABC):
@abstractmethod
def visit_number(self, node: "NumberNode"): ...
@abstractmethod
def visit_binary_op(self, node: "BinaryOpNode"): ...
@abstractmethod
def visit_unary_op(self, node: "UnaryOpNode"): ...
@abstractmethod
def visit_variable(self, node: "VariableNode"): ...
class ASTNode(ABC):
@abstractmethod
def accept(self, visitor: ASTVisitor): ...
class NumberNode(ASTNode):
def __init__(self, value: float) -> None:
self.value = value
def accept(self, visitor: ASTVisitor):
return visitor.visit_number(self)
class BinaryOpNode(ASTNode):
def __init__(self, op: str, left: ASTNode,
right: ASTNode) -> None:
self.op = op # "+", "-", "*", "/"
self.left = left
self.right = right
def accept(self, visitor: ASTVisitor):
return visitor.visit_binary_op(self)
class UnaryOpNode(ASTNode):
def __init__(self, op: str, operand: ASTNode) -> None:
self.op = op # "-"
self.operand = operand
def accept(self, visitor: ASTVisitor):
return visitor.visit_unary_op(self)
class VariableNode(ASTNode):
def __init__(self, name: str) -> None:
self.name = name
def accept(self, visitor: ASTVisitor):
return visitor.visit_variable(self)
class EvalVisitor(ASTVisitor):
def __init__(self, variables: dict[str, float] | None = None) -> None:
self.variables = variables or {}
def visit_number(self, node: NumberNode) -> float:
return node.value
def visit_binary_op(self, node: BinaryOpNode) -> float:
# TODO: вычислить левый и правый операнды, применить op
pass
def visit_unary_op(self, node: UnaryOpNode) -> float:
# TODO: вычислить операнд, применить унарный минус
pass
def visit_variable(self, node: VariableNode) -> float:
# TODO: вернуть значение переменной из self.variables
pass
class PrintVisitor(ASTVisitor):
"""Инфиксная запись с расстановкой скобок."""
def visit_number(self, node: NumberNode) -> str:
return str(node.value)
def visit_binary_op(self, node: BinaryOpNode) -> str:
# TODO: вернуть "(left op right)"
pass
def visit_unary_op(self, node: UnaryOpNode) -> str:
# TODO: вернуть "(-operand)"
pass
def visit_variable(self, node: VariableNode) -> str:
return node.name
class RpnVisitor(ASTVisitor):
"""Обратная польская запись: операнды затем оператор."""
def visit_number(self, node: NumberNode) -> str:
return str(node.value)
def visit_binary_op(self, node: BinaryOpNode) -> str:
# TODO: вернуть "left right op"
pass
def visit_unary_op(self, node: UnaryOpNode) -> str:
# TODO: вернуть "operand NEG"
pass
def visit_variable(self, node: VariableNode) -> str:
return node.name
# Ожидаемый результат:
# # (2 + x) * (y - 3)
# ast = BinaryOpNode("*",
# BinaryOpNode("+", NumberNode(2), VariableNode("x")),
# BinaryOpNode("-", VariableNode("y"), NumberNode(3))
# )
# print(ast.accept(PrintVisitor())) # "((2.0 + x) * (y - 3.0))"
# print(ast.accept(RpnVisitor())) # "2.0 x + y 3.0 - *"
# print(ast.accept(EvalVisitor({"x": 3, "y": 7}))) # 20.0
<?php
interface ASTVisitor
{
public function visitNumber(NumberNode $node): mixed;
public function visitBinaryOp(BinaryOpNode $node): mixed;
public function visitUnaryOp(UnaryOpNode $node): mixed;
public function visitVariable(VariableNode $node): mixed;
}
interface ASTNode
{
public function accept(ASTVisitor $visitor): mixed;
}
class NumberNode implements ASTNode
{
public function __construct(public readonly float $value) {}
public function accept(ASTVisitor $visitor): mixed { return $visitor->visitNumber($this); }
}
class BinaryOpNode implements ASTNode
{
public function __construct(
public readonly string $op,
public readonly ASTNode $left,
public readonly ASTNode $right,
) {}
public function accept(ASTVisitor $visitor): mixed { return $visitor->visitBinaryOp($this); }
}
class UnaryOpNode implements ASTNode
{
public function __construct(
public readonly string $op,
public readonly ASTNode $operand,
) {}
public function accept(ASTVisitor $visitor): mixed { return $visitor->visitUnaryOp($this); }
}
class VariableNode implements ASTNode
{
public function __construct(public readonly string $name) {}
public function accept(ASTVisitor $visitor): mixed { return $visitor->visitVariable($this); }
}
class EvalVisitor implements ASTVisitor
{
public function __construct(private readonly array $variables = []) {}
public function visitNumber(NumberNode $node): float { return $node->value; }
public function visitBinaryOp(BinaryOpNode $node): float
{
// TODO: вычислить оба операнда, применить op
}
public function visitUnaryOp(UnaryOpNode $node): float
{
// TODO: применить унарный минус
}
public function visitVariable(VariableNode $node): float
{
// TODO: вернуть значение из variables
}
}
class PrintVisitor implements ASTVisitor
{
public function visitNumber(NumberNode $node): string { return (string) $node->value; }
public function visitBinaryOp(BinaryOpNode $node): string
{
// TODO: вернуть "(left op right)"
}
public function visitUnaryOp(UnaryOpNode $node): string
{
// TODO: вернуть "(-operand)"
}
public function visitVariable(VariableNode $node): string { return $node->name; }
}
class RpnVisitor implements ASTVisitor
{
public function visitNumber(NumberNode $node): string { return (string) $node->value; }
public function visitBinaryOp(BinaryOpNode $node): string
{
// TODO: вернуть "left right op"
}
public function visitUnaryOp(UnaryOpNode $node): string
{
// TODO: вернуть "operand NEG"
}
public function visitVariable(VariableNode $node): string { return $node->name; }
}
// Ожидаемый результат:
// $ast = new BinaryOpNode("*",
// new BinaryOpNode("+", new NumberNode(2), new VariableNode("x")),
// new BinaryOpNode("-", new VariableNode("y"), new NumberNode(3))
// );
// echo $ast->accept(new PrintVisitor()) . "\n"; // "((2 + x) * (y - 3))"
// echo $ast->accept(new RpnVisitor()) . "\n"; // "2 x + y 3 - *"
// echo $ast->accept(new EvalVisitor(["x"=>3,"y"=>7])) . "\n"; // 20
Задача 2. Система скидок для элементов корзины
Реализуйте иерархию элементов корзины: SimpleProduct, BundleProduct (набор товаров), DigitalProduct (без доставки), GiftCard. Добавьте посетителей без изменения классов: PriceCalculator (итоговая стоимость с учётом типа), ShippingCalculator (стоимость доставки), TaxCalculator (налоги по типу товара).
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
class CartVisitor(ABC):
@abstractmethod
def visit_simple(self, product: "SimpleProduct") -> float: ...
@abstractmethod
def visit_bundle(self, product: "BundleProduct") -> float: ...
@abstractmethod
def visit_digital(self, product: "DigitalProduct") -> float: ...
@abstractmethod
def visit_gift_card(self, card: "GiftCard") -> float: ...
class CartItem(ABC):
@abstractmethod
def accept(self, visitor: CartVisitor) -> float: ...
@property
@abstractmethod
def base_price(self) -> float: ...
@dataclass
class SimpleProduct(CartItem):
name: str
price: float
weight_kg: float = 0.5
tax_rate: float = 0.20 # НДС 20%
@property
def base_price(self) -> float:
return self.price
def accept(self, visitor: CartVisitor) -> float:
return visitor.visit_simple(self)
@dataclass
class BundleProduct(CartItem):
name: str
items: list[CartItem]
discount: float = 0.10 # скидка 10% на набор
@property
def base_price(self) -> float:
return sum(i.base_price for i in self.items)
def accept(self, visitor: CartVisitor) -> float:
return visitor.visit_bundle(self)
@dataclass
class DigitalProduct(CartItem):
name: str
price: float
tax_rate: float = 0.0 # цифровые товары без НДС
@property
def base_price(self) -> float:
return self.price
def accept(self, visitor: CartVisitor) -> float:
return visitor.visit_digital(self)
@dataclass
class GiftCard(CartItem):
nominal: float
recipient: str = ""
@property
def base_price(self) -> float:
return self.nominal
def accept(self, visitor: CartVisitor) -> float:
return visitor.visit_gift_card(self)
class PriceCalculator(CartVisitor):
def visit_simple(self, product: SimpleProduct) -> float:
return product.price
def visit_bundle(self, product: BundleProduct) -> float:
# TODO: сумма всех элементов с учётом скидки набора
pass
def visit_digital(self, product: DigitalProduct) -> float:
return product.price
def visit_gift_card(self, card: GiftCard) -> float:
return card.nominal
def total(self, items: list[CartItem]) -> float:
return sum(item.accept(self) for item in items)
class ShippingCalculator(CartVisitor):
BASE_RATE = 100.0 # 100 руб/кг
def visit_simple(self, product: SimpleProduct) -> float:
# TODO: стоимость по весу (BASE_RATE * weight_kg)
pass
def visit_bundle(self, product: BundleProduct) -> float:
# TODO: сумма доставки всех элементов бандла
pass
def visit_digital(self, product: DigitalProduct) -> float:
return 0.0 # цифровые не требуют доставки
def visit_gift_card(self, card: GiftCard) -> float:
return 0.0 # подарочные карты без доставки
def total(self, items: list[CartItem]) -> float:
return sum(item.accept(self) for item in items)
class TaxCalculator(CartVisitor):
def visit_simple(self, product: SimpleProduct) -> float:
# TODO: price * tax_rate
pass
def visit_bundle(self, product: BundleProduct) -> float:
# TODO: налоги со всех элементов бандла
pass
def visit_digital(self, product: DigitalProduct) -> float:
return product.price * product.tax_rate
def visit_gift_card(self, card: GiftCard) -> float:
return 0.0 # подарочные карты без налога
def total(self, items: list[CartItem]) -> float:
return sum(item.accept(self) for item in items)
# Ожидаемый результат:
# items = [
# SimpleProduct("Книга", 500.0, weight_kg=0.3),
# BundleProduct("Набор", [
# SimpleProduct("Ручка", 100.0, weight_kg=0.1),
# SimpleProduct("Тетрадь", 150.0, weight_kg=0.2),
# ], discount=0.10),
# DigitalProduct("Курс Python", 2000.0),
# GiftCard(1000.0, "Alice"),
# ]
# price = PriceCalculator()
# print(f"Стоимость: {price.total(items):.2f}") # 500+225+2000+1000 = 3725
# ship = ShippingCalculator()
# print(f"Доставка: {ship.total(items):.2f}") # 30+30+0+0 = 60
# tax = TaxCalculator()
# print(f"Налог: {tax.total(items):.2f}") # 100+45+0+0 = 145
<?php
interface CartVisitor
{
public function visitSimple(SimpleProduct $product): float;
public function visitBundle(BundleProduct $product): float;
public function visitDigital(DigitalProduct $product): float;
public function visitGiftCard(GiftCard $card): float;
}
interface CartItem
{
public function accept(CartVisitor $visitor): float;
public function getBasePrice(): float;
}
class SimpleProduct implements CartItem
{
public function __construct(
public readonly string $name,
public readonly float $price,
public readonly float $weightKg = 0.5,
public readonly float $taxRate = 0.20,
) {}
public function getBasePrice(): float { return $this->price; }
public function accept(CartVisitor $visitor): float { return $visitor->visitSimple($this); }
}
class BundleProduct implements CartItem
{
public function __construct(
public readonly string $name,
public readonly array $items,
public readonly float $discount = 0.10,
) {}
public function getBasePrice(): float
{
return array_sum(array_map(fn($i) => $i->getBasePrice(), $this->items));
}
public function accept(CartVisitor $visitor): float { return $visitor->visitBundle($this); }
}
class DigitalProduct implements CartItem
{
public function __construct(
public readonly string $name,
public readonly float $price,
public readonly float $taxRate = 0.0,
) {}
public function getBasePrice(): float { return $this->price; }
public function accept(CartVisitor $visitor): float { return $visitor->visitDigital($this); }
}
class GiftCard implements CartItem
{
public function __construct(
public readonly float $nominal,
public readonly string $recipient = "",
) {}
public function getBasePrice(): float { return $this->nominal; }
public function accept(CartVisitor $visitor): float { return $visitor->visitGiftCard($this); }
}
class PriceCalculator implements CartVisitor
{
public function visitSimple(SimpleProduct $p): float { return $p->price; }
public function visitBundle(BundleProduct $p): float
{
// TODO: сумма элементов с учётом скидки
}
public function visitDigital(DigitalProduct $p): float { return $p->price; }
public function visitGiftCard(GiftCard $c): float { return $c->nominal; }
public function total(array $items): float
{
return array_sum(array_map(fn($i) => $i->accept($this), $items));
}
}
class ShippingCalculator implements CartVisitor
{
private const BASE_RATE = 100.0;
public function visitSimple(SimpleProduct $p): float
{
// TODO: BASE_RATE * weightKg
}
public function visitBundle(BundleProduct $p): float
{
// TODO: сумма доставки всех элементов
}
public function visitDigital(DigitalProduct $p): float { return 0.0; }
public function visitGiftCard(GiftCard $c): float { return 0.0; }
public function total(array $items): float
{
return array_sum(array_map(fn($i) => $i->accept($this), $items));
}
}
class TaxCalculator implements CartVisitor
{
public function visitSimple(SimpleProduct $p): float
{
// TODO: price * taxRate
}
public function visitBundle(BundleProduct $p): float
{
// TODO: налоги всех элементов бандла
}
public function visitDigital(DigitalProduct $p): float { return $p->price * $p->taxRate; }
public function visitGiftCard(GiftCard $c): float { return 0.0; }
public function total(array $items): float
{
return array_sum(array_map(fn($i) => $i->accept($this), $items));
}
}
// Ожидаемый результат:
// $items = [
// new SimpleProduct("Книга", 500.0, weightKg: 0.3),
// new BundleProduct("Набор", [
// new SimpleProduct("Ручка", 100.0, weightKg: 0.1),
// new SimpleProduct("Тетрадь", 150.0, weightKg: 0.2),
// ], discount: 0.10),
// new DigitalProduct("Курс Python", 2000.0),
// new GiftCard(1000.0, "Alice"),
// ];
// $price = new PriceCalculator();
// echo "Стоимость: " . $price->total($items) . "\n"; // 3725
// $ship = new ShippingCalculator();
// echo "Доставка: " . $ship->total($items) . "\n"; // 60
// $tax = new TaxCalculator();
// echo "Налог: " . $tax->total($items) . "\n"; // 145
Задача 3. Обход файловой системы
Реализуйте иерархию файловой системы: File, Directory, SymLink. Добавьте посетителей: SizeCalculator (рекурсивный размер), SearchVisitor (поиск файлов по маске), PermissionsAuditor (поиск файлов с опасными правами), DuplicateFinder (поиск дублей по хешу содержимого).
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
import fnmatch
class FSVisitor(ABC):
@abstractmethod
def visit_file(self, node: "File") -> None: ...
@abstractmethod
def visit_directory(self, node: "Directory") -> None: ...
@abstractmethod
def visit_symlink(self, node: "SymLink") -> None: ...
class FSNode(ABC):
def __init__(self, name: str, permissions: int = 0o644) -> None:
self.name = name
self.permissions = permissions # unix-права, например 0o644
@abstractmethod
def accept(self, visitor: FSVisitor) -> None: ...
@property
@abstractmethod
def path(self) -> str: ...
@dataclass
class File(FSNode):
name: str
size_bytes: int = 0
content_hash: str = ""
permissions: int = field(default=0o644)
@property
def path(self) -> str:
return self.name
def accept(self, visitor: FSVisitor) -> None:
visitor.visit_file(self)
@dataclass
class Directory(FSNode):
name: str
children: list[FSNode] = field(default_factory=list)
permissions: int = field(default=0o755)
@property
def path(self) -> str:
return self.name + "/"
def accept(self, visitor: FSVisitor) -> None:
visitor.visit_directory(self)
def add(self, node: FSNode) -> "Directory":
self.children.append(node)
return self
@dataclass
class SymLink(FSNode):
name: str
target: FSNode | None = None
permissions: int = field(default=0o777)
@property
def path(self) -> str:
return self.name + " -> " + (self.target.name if self.target else "?")
def accept(self, visitor: FSVisitor) -> None:
visitor.visit_symlink(self)
class SizeCalculator(FSVisitor):
def __init__(self) -> None:
self.total_bytes = 0
def visit_file(self, node: File) -> None:
# TODO: добавить размер файла
pass
def visit_directory(self, node: Directory) -> None:
# TODO: рекурсивно обойти дочерние узлы
pass
def visit_symlink(self, node: SymLink) -> None:
pass # симлинки не добавляют размер
class SearchVisitor(FSVisitor):
def __init__(self, pattern: str) -> None:
self.pattern = pattern
self.results: list[FSNode] = []
def visit_file(self, node: File) -> None:
# TODO: проверить fnmatch(node.name, self.pattern)
pass
def visit_directory(self, node: Directory) -> None:
# TODO: рекурсивно обойти дочерние узлы
pass
def visit_symlink(self, node: SymLink) -> None:
# TODO: проверить имя симлинка
pass
class PermissionsAuditor(FSVisitor):
"""Находит файлы с потенциально опасными правами (world-writable)."""
def __init__(self) -> None:
self.dangerous: list[FSNode] = []
def _is_dangerous(self, permissions: int) -> bool:
# TODO: world-writable = (permissions & 0o002) != 0
pass
def visit_file(self, node: File) -> None:
# TODO: добавить в dangerous если права опасные
pass
def visit_directory(self, node: Directory) -> None:
# TODO: проверить директорию и рекурсивно обойти дочерние
pass
def visit_symlink(self, node: SymLink) -> None:
if self._is_dangerous(node.permissions):
self.dangerous.append(node)
class DuplicateFinder(FSVisitor):
"""Находит файлы с одинаковым content_hash."""
def __init__(self) -> None:
self._hashes: dict[str, list[File]] = {}
def visit_file(self, node: File) -> None:
# TODO: группировать файлы по content_hash
pass
def visit_directory(self, node: Directory) -> None:
# TODO: рекурсивно обойти дочерние
pass
def visit_symlink(self, node: SymLink) -> None:
pass
@property
def duplicates(self) -> dict[str, list[File]]:
# TODO: вернуть только группы с len > 1
pass
# Ожидаемый результат:
# root = Directory("root").add(
# Directory("src").add(
# File("main.py", 1024, "abc123")
# ).add(
# File("utils.py", 512, "def456")
# )
# ).add(
# Directory("backup").add(
# File("main_copy.py", 1024, "abc123") # дубль
# )
# ).add(
# File("readme.txt", 256, "xyz999", permissions=0o666)
# ).add(
# File("secret.sh", 128, "sec001", permissions=0o777) # опасный
# )
# calc = SizeCalculator(); root.accept(calc)
# print(calc.total_bytes) # 2944
# search = SearchVisitor("*.py"); root.accept(search)
# print([f.name for f in search.results]) # ["main.py","utils.py","main_copy.py"]
# dupes = DuplicateFinder(); root.accept(dupes)
# print(dupes.duplicates) # {"abc123": [main.py, main_copy.py]}
<?php
interface FSVisitor
{
public function visitFile(FSFile $node): void;
public function visitDirectory(FSDirectory $node): void;
public function visitSymLink(FSSymLink $node): void;
}
abstract class FSNode
{
public function __construct(
public readonly string $name,
public int $permissions = 0644,
) {}
abstract public function accept(FSVisitor $visitor): void;
abstract public function getPath(): string;
}
class FSFile extends FSNode
{
public function __construct(
string $name,
public readonly int $sizeBytes = 0,
public readonly string $contentHash = "",
int $permissions = 0644,
) {
parent::__construct($name, $permissions);
}
public function getPath(): string { return $this->name; }
public function accept(FSVisitor $visitor): void { $visitor->visitFile($this); }
}
class FSDirectory extends FSNode
{
private array $children = [];
public function __construct(string $name, int $permissions = 0755)
{
parent::__construct($name, $permissions);
}
public function add(FSNode $node): static { $this->children[] = $node; return $this; }
public function getChildren(): array { return $this->children; }
public function getPath(): string { return $this->name . "/"; }
public function accept(FSVisitor $visitor): void { $visitor->visitDirectory($this); }
}
class FSSymLink extends FSNode
{
public function __construct(
string $name,
public readonly ?FSNode $target = null,
int $permissions = 0777,
) {
parent::__construct($name, $permissions);
}
public function getPath(): string { return $this->name . " -> " . ($this->target?->name ?? "?"); }
public function accept(FSVisitor $visitor): void { $visitor->visitSymLink($this); }
}
class SizeCalculator implements FSVisitor
{
public int $totalBytes = 0;
public function visitFile(FSFile $node): void
{
// TODO: добавить размер файла
}
public function visitDirectory(FSDirectory $node): void
{
// TODO: рекурсивно обойти дочерние
}
public function visitSymLink(FSSymLink $node): void {}
}
class SearchVisitor implements FSVisitor
{
public array $results = [];
public function __construct(private readonly string $pattern) {}
public function visitFile(FSFile $node): void
{
// TODO: fnmatch($pattern, $node->name) → добавить в results
}
public function visitDirectory(FSDirectory $node): void
{
// TODO: рекурсивно обойти дочерние
}
public function visitSymLink(FSSymLink $node): void
{
// TODO: проверить имя симлинка
}
}
class PermissionsAuditor implements FSVisitor
{
public array $dangerous = [];
private function isDangerous(int $permissions): bool
{
// TODO: world-writable: ($permissions & 0002) !== 0
}
public function visitFile(FSFile $node): void
{
// TODO: добавить в dangerous если опасные права
}
public function visitDirectory(FSDirectory $node): void
{
// TODO: проверить и рекурсивно обойти
}
public function visitSymLink(FSSymLink $node): void
{
if ($this->isDangerous($node->permissions)) $this->dangerous[] = $node;
}
}
class DuplicateFinder implements FSVisitor
{
private array $hashes = [];
public function visitFile(FSFile $node): void
{
// TODO: группировать по contentHash
}
public function visitDirectory(FSDirectory $node): void
{
// TODO: рекурсивно обойти
}
public function visitSymLink(FSSymLink $node): void {}
public function getDuplicates(): array
{
// TODO: вернуть только группы с count > 1
}
}
// Ожидаемый результат:
// $root = (new FSDirectory("root"))
// ->add((new FSDirectory("src"))
// ->add(new FSFile("main.py", 1024, "abc123"))
// ->add(new FSFile("utils.py", 512, "def456")))
// ->add((new FSDirectory("backup"))
// ->add(new FSFile("main_copy.py", 1024, "abc123")))
// ->add(new FSFile("readme.txt", 256, "xyz999", 0666))
// ->add(new FSFile("secret.sh", 128, "sec001", 0777));
// $calc = new SizeCalculator(); $root->accept($calc);
// echo $calc->totalBytes . "\n"; // 2944
// $search = new SearchVisitor("*.py"); $root->accept($search);
// echo implode(",", array_map(fn($f) => $f->name, $search->results)) . "\n";
// $dupes = new DuplicateFinder(); $root->accept($dupes);
// print_r($dupes->getDuplicates()); // ["abc123" => [main.py, main_copy.py]]
Задача 4. Анализатор HTML-документа
Реализуйте упрощённое DOM-дерево HTML с узлами: TextNode, ElementNode (тег с атрибутами и дочерними элементами), CommentNode. Добавьте посетителей: HtmlRenderer (генерация HTML-строки), TextExtractor (извлечение текстового содержимого), LinkCollector (сбор всех ссылок href), WordCounter (подсчёт слов).
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
class DOMVisitor(ABC):
@abstractmethod
def visit_text(self, node: "TextNode") -> None: ...
@abstractmethod
def visit_element(self, node: "ElementNode") -> None: ...
@abstractmethod
def visit_comment(self, node: "CommentNode") -> None: ...
class DOMNode(ABC):
@abstractmethod
def accept(self, visitor: DOMVisitor) -> None: ...
@dataclass
class TextNode(DOMNode):
content: str
def accept(self, visitor: DOMVisitor) -> None:
visitor.visit_text(self)
@dataclass
class CommentNode(DOMNode):
content: str
def accept(self, visitor: DOMVisitor) -> None:
visitor.visit_comment(self)
@dataclass
class ElementNode(DOMNode):
tag: str
attributes: dict[str, str] = field(default_factory=dict)
children: list[DOMNode] = field(default_factory=list)
self_closing: bool = False # <br/>, <img/>
def accept(self, visitor: DOMVisitor) -> None:
visitor.visit_element(self)
def add(self, child: DOMNode) -> "ElementNode":
self.children.append(child)
return self
class HtmlRenderer(DOMVisitor):
def __init__(self) -> None:
self._parts: list[str] = []
def visit_text(self, node: TextNode) -> None:
self._parts.append(node.content)
def visit_comment(self, node: CommentNode) -> None:
self._parts.append(f"<!-- {node.content} -->")
def visit_element(self, node: ElementNode) -> None:
# TODO: собрать открывающий тег с атрибутами,
# рекурсивно обойти children, добавить закрывающий тег
pass
def render(self) -> str:
return "".join(self._parts)
class TextExtractor(DOMVisitor):
def __init__(self) -> None:
self._texts: list[str] = []
def visit_text(self, node: TextNode) -> None:
# TODO: добавить текст (strip пробелов)
pass
def visit_comment(self, node: CommentNode) -> None:
pass # комментарии игнорируем
def visit_element(self, node: ElementNode) -> None:
# TODO: рекурсивно обойти children
pass
@property
def text(self) -> str:
return " ".join(t for t in self._texts if t)
class LinkCollector(DOMVisitor):
def __init__(self) -> None:
self.links: list[dict[str, str]] = []
def visit_text(self, node: TextNode) -> None:
pass
def visit_comment(self, node: CommentNode) -> None:
pass
def visit_element(self, node: ElementNode) -> None:
# TODO: если тег "a" и есть атрибут "href" — добавить в links
# {"href": ..., "text": текстовое содержимое ссылки}
# рекурсивно обойти children
pass
class WordCounter(DOMVisitor):
def __init__(self) -> None:
self.count = 0
def visit_text(self, node: TextNode) -> None:
# TODO: посчитать слова в тексте (split по пробелам)
pass
def visit_comment(self, node: CommentNode) -> None:
pass
def visit_element(self, node: ElementNode) -> None:
# TODO: рекурсивно обойти children
pass
# Ожидаемый результат:
# doc = ElementNode("html").add(
# ElementNode("head").add(
# ElementNode("title").add(TextNode("Моя страница"))
# )
# ).add(
# ElementNode("body").add(
# CommentNode("Основной контент")
# ).add(
# ElementNode("h1").add(TextNode("Привет, мир!"))
# ).add(
# ElementNode("p").add(TextNode("Это ")).add(
# ElementNode("a", {"href": "https://example.com"}).add(TextNode("ссылка"))
# ).add(TextNode(" на сайт."))
# )
# )
# renderer = HtmlRenderer(); doc.accept(renderer)
# print(renderer.render())
# extractor = TextExtractor(); doc.accept(extractor)
# print(extractor.text) # "Моя страница Привет, мир! Это ссылка на сайт."
# links = LinkCollector(); doc.accept(links)
# print(links.links) # [{"href": "https://example.com", "text": "ссылка"}]
# wc = WordCounter(); doc.accept(wc)
# print(wc.count) # 7
<?php
interface DOMVisitor
{
public function visitText(TextNode $node): void;
public function visitElement(ElementNode $node): void;
public function visitComment(CommentNode $node): void;
}
interface DOMNode
{
public function accept(DOMVisitor $visitor): void;
}
class TextNode implements DOMNode
{
public function __construct(public readonly string $content) {}
public function accept(DOMVisitor $visitor): void { $visitor->visitText($this); }
}
class CommentNode implements DOMNode
{
public function __construct(public readonly string $content) {}
public function accept(DOMVisitor $visitor): void { $visitor->visitComment($this); }
}
class ElementNode implements DOMNode
{
private array $children = [];
public function __construct(
public readonly string $tag,
public readonly array $attributes = [],
public readonly bool $selfClosing = false,
) {}
public function add(DOMNode $child): static { $this->children[] = $child; return $this; }
public function getChildren(): array { return $this->children; }
public function accept(DOMVisitor $visitor): void { $visitor->visitElement($this); }
}
class HtmlRenderer implements DOMVisitor
{
private string $output = "";
public function visitText(TextNode $node): void { $this->output .= $node->content; }
public function visitComment(CommentNode $node): void { $this->output .= "<!-- {$node->content} -->"; }
public function visitElement(ElementNode $node): void
{
// TODO: открывающий тег + атрибуты, обойти children, закрывающий тег
}
public function render(): string { return $this->output; }
}
class TextExtractor implements DOMVisitor
{
private array $texts = [];
public function visitText(TextNode $node): void
{
// TODO: добавить текст (trim)
}
public function visitComment(CommentNode $node): void {}
public function visitElement(ElementNode $node): void
{
// TODO: рекурсивно обойти children
}
public function getText(): string
{
return implode(" ", array_filter(array_map('trim', $this->texts)));
}
}
class LinkCollector implements DOMVisitor
{
public array $links = [];
public function visitText(TextNode $node): void {}
public function visitComment(CommentNode $node): void {}
public function visitElement(ElementNode $node): void
{
// TODO: если tag="a" и есть href — добавить ["href" => ..., "text" => ...]
// рекурсивно обойти children
}
}
class WordCounter implements DOMVisitor
{
public int $count = 0;
public function visitText(TextNode $node): void
{
// TODO: посчитать слова через str_word_count или explode
}
public function visitComment(CommentNode $node): void {}
public function visitElement(ElementNode $node): void
{
// TODO: рекурсивно обойти children
}
}
// Ожидаемый результат:
// $doc = (new ElementNode("html"))
// ->add((new ElementNode("head"))
// ->add((new ElementNode("title"))->add(new TextNode("Моя страница"))))
// ->add((new ElementNode("body"))
// ->add(new CommentNode("Основной контент"))
// ->add((new ElementNode("h1"))->add(new TextNode("Привет, мир!")))
// ->add((new ElementNode("p"))
// ->add(new TextNode("Это "))
// ->add((new ElementNode("a", ["href" => "https://example.com"]))->add(new TextNode("ссылка")))
// ->add(new TextNode(" на сайт."))));
// $renderer = new HtmlRenderer(); $doc->accept($renderer);
// echo $renderer->render() . "\n";
// $extractor = new TextExtractor(); $doc->accept($extractor);
// echo $extractor->getText() . "\n"; // "Моя страница Привет, мир! Это ссылка на сайт."
// $links = new LinkCollector(); $doc->accept($links);
// print_r($links->links); // [["href"=>"https://example.com","text"=>"ссылка"]]
Задача 5. Статический анализатор кода
Реализуйте упрощённое AST для Python-подобного языка с узлами: FunctionDef, ClassDef, Assignment, IfStatement, ForLoop, ReturnStatement, FunctionCall. Создайте посетителей: ComplexityAnalyzer (цикломатическая сложность), DependencyCollector (список вызываемых функций), DocstringChecker (проверка наличия документации).
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
class CodeVisitor(ABC):
@abstractmethod
def visit_function(self, node: "FunctionDef") -> None: ...
@abstractmethod
def visit_class(self, node: "ClassDef") -> None: ...
@abstractmethod
def visit_assignment(self, node: "Assignment") -> None: ...
@abstractmethod
def visit_if(self, node: "IfStatement") -> None: ...
@abstractmethod
def visit_for(self, node: "ForLoop") -> None: ...
@abstractmethod
def visit_return(self, node: "ReturnStatement") -> None: ...
@abstractmethod
def visit_call(self, node: "FunctionCall") -> None: ...
class CodeNode(ABC):
@abstractmethod
def accept(self, visitor: CodeVisitor) -> None: ...
@dataclass
class FunctionDef(CodeNode):
name: str
params: list[str]
body: list[CodeNode] = field(default_factory=list)
docstring: str = ""
def accept(self, v: CodeVisitor) -> None: v.visit_function(self)
@dataclass
class ClassDef(CodeNode):
name: str
methods: list[FunctionDef] = field(default_factory=list)
docstring: str = ""
def accept(self, v: CodeVisitor) -> None: v.visit_class(self)
@dataclass
class Assignment(CodeNode):
target: str
value: str
def accept(self, v: CodeVisitor) -> None: v.visit_assignment(self)
@dataclass
class IfStatement(CodeNode):
condition: str
then_body: list[CodeNode] = field(default_factory=list)
else_body: list[CodeNode] = field(default_factory=list)
def accept(self, v: CodeVisitor) -> None: v.visit_if(self)
@dataclass
class ForLoop(CodeNode):
variable: str
iterable: str
body: list[CodeNode] = field(default_factory=list)
def accept(self, v: CodeVisitor) -> None: v.visit_for(self)
@dataclass
class ReturnStatement(CodeNode):
value: str = ""
def accept(self, v: CodeVisitor) -> None: v.visit_return(self)
@dataclass
class FunctionCall(CodeNode):
name: str
args: list[str] = field(default_factory=list)
def accept(self, v: CodeVisitor) -> None: v.visit_call(self)
class ComplexityAnalyzer(CodeVisitor):
"""Считает цикломатическую сложность: 1 + if + for + каждая ветка."""
def __init__(self) -> None:
self.complexity: dict[str, int] = {}
self._current_func: str | None = None
self._current_complexity = 1
def visit_function(self, node: FunctionDef) -> None:
# TODO: начать подсчёт для функции, обойти body, сохранить результат
pass
def visit_class(self, node: ClassDef) -> None:
# TODO: обойти все методы класса
pass
def visit_if(self, node: IfStatement) -> None:
# TODO: +1 за if, +1 за каждую else ветку, обойти оба тела
pass
def visit_for(self, node: ForLoop) -> None:
# TODO: +1 за цикл, обойти тело
pass
def visit_assignment(self, node: Assignment) -> None: pass
def visit_return(self, node: ReturnStatement) -> None: pass
def visit_call(self, node: FunctionCall) -> None: pass
class DependencyCollector(CodeVisitor):
"""Собирает все вызываемые функции."""
def __init__(self) -> None:
self.calls: dict[str, set[str]] = {}
self._current_func: str | None = None
def visit_function(self, node: FunctionDef) -> None:
# TODO: инициализировать set для функции, обойти body
pass
def visit_class(self, node: ClassDef) -> None:
for method in node.methods:
method.accept(self)
def visit_call(self, node: FunctionCall) -> None:
# TODO: добавить вызов в set текущей функции
pass
def visit_if(self, node: IfStatement) -> None:
for n in node.then_body + node.else_body: n.accept(self)
def visit_for(self, node: ForLoop) -> None:
for n in node.body: n.accept(self)
def visit_assignment(self, node: Assignment) -> None: pass
def visit_return(self, node: ReturnStatement) -> None: pass
class DocstringChecker(CodeVisitor):
"""Проверяет наличие документации у функций и классов."""
def __init__(self) -> None:
self.missing: list[str] = []
self.documented: list[str] = []
def visit_function(self, node: FunctionDef) -> None:
# TODO: проверить node.docstring, добавить в missing или documented
pass
def visit_class(self, node: ClassDef) -> None:
# TODO: проверить docstring класса, обойти методы
pass
def visit_if(self, node: IfStatement) -> None:
for n in node.then_body + node.else_body: n.accept(self)
def visit_for(self, node: ForLoop) -> None:
for n in node.body: n.accept(self)
def visit_assignment(self, node: Assignment) -> None: pass
def visit_return(self, node: ReturnStatement) -> None: pass
def visit_call(self, node: FunctionCall) -> None: pass
# Ожидаемый результат:
# code = ClassDef("Calculator", docstring="Калькулятор", methods=[
# FunctionDef("add", ["a", "b"], [ReturnStatement("a + b")], docstring="Сложение"),
# FunctionDef("divide", ["a", "b"], [
# IfStatement("b == 0", [ReturnStatement("None")], [ReturnStatement("a / b")])
# ]), # нет docstring
# ])
# complexity = ComplexityAnalyzer(); code.accept(complexity)
# print(complexity.complexity) # {"add": 1, "divide": 2}
# deps = DependencyCollector(); code.accept(deps)
# checker = DocstringChecker(); code.accept(checker)
# print(checker.missing) # ["divide"]
# print(checker.documented) # ["Calculator", "add"]
<?php
interface CodeVisitor
{
public function visitFunction(FunctionDef $node): void;
public function visitClass(ClassDef $node): void;
public function visitAssignment(Assignment $node): void;
public function visitIf(IfStatement $node): void;
public function visitFor(ForLoop $node): void;
public function visitReturn(ReturnStatement $node): void;
public function visitCall(FunctionCall $node): void;
}
interface CodeNode
{
public function accept(CodeVisitor $visitor): void;
}
class FunctionDef implements CodeNode
{
public function __construct(
public readonly string $name,
public readonly array $params,
public readonly array $body = [],
public readonly string $docstring = "",
) {}
public function accept(CodeVisitor $v): void { $v->visitFunction($this); }
}
class ClassDef implements CodeNode
{
public function __construct(
public readonly string $name,
public readonly array $methods = [],
public readonly string $docstring = "",
) {}
public function accept(CodeVisitor $v): void { $v->visitClass($this); }
}
class Assignment implements CodeNode
{
public function __construct(public readonly string $target, public readonly string $value) {}
public function accept(CodeVisitor $v): void { $v->visitAssignment($this); }
}
class IfStatement implements CodeNode
{
public function __construct(
public readonly string $condition,
public readonly array $thenBody = [],
public readonly array $elseBody = [],
) {}
public function accept(CodeVisitor $v): void { $v->visitIf($this); }
}
class ForLoop implements CodeNode
{
public function __construct(
public readonly string $variable,
public readonly string $iterable,
public readonly array $body = [],
) {}
public function accept(CodeVisitor $v): void { $v->visitFor($this); }
}
class ReturnStatement implements CodeNode
{
public function __construct(public readonly string $value = "") {}
public function accept(CodeVisitor $v): void { $v->visitReturn($this); }
}
class FunctionCall implements CodeNode
{
public function __construct(public readonly string $name, public readonly array $args = []) {}
public function accept(CodeVisitor $v): void { $v->visitCall($this); }
}
class ComplexityAnalyzer implements CodeVisitor
{
public array $complexity = [];
private ?string $currentFunc = null;
private int $currentComplexity = 1;
public function visitFunction(FunctionDef $node): void
{
// TODO: начать подсчёт, обойти body, сохранить
}
public function visitClass(ClassDef $node): void
{
// TODO: обойти методы
}
public function visitIf(IfStatement $node): void
{
// TODO: +1 за if, +1 за else, обойти оба тела
}
public function visitFor(ForLoop $node): void
{
// TODO: +1 за цикл, обойти body
}
public function visitAssignment(Assignment $node): void {}
public function visitReturn(ReturnStatement $node): void {}
public function visitCall(FunctionCall $node): void {}
}
class DependencyCollector implements CodeVisitor
{
public array $calls = [];
private ?string $currentFunc = null;
public function visitFunction(FunctionDef $node): void
{
// TODO: инициализировать [], обойти body
}
public function visitClass(ClassDef $node): void
{
foreach ($node->methods as $m) $m->accept($this);
}
public function visitCall(FunctionCall $node): void
{
// TODO: добавить вызов в calls[currentFunc]
}
public function visitIf(IfStatement $node): void
{
foreach (array_merge($node->thenBody, $node->elseBody) as $n) $n->accept($this);
}
public function visitFor(ForLoop $node): void
{
foreach ($node->body as $n) $n->accept($this);
}
public function visitAssignment(Assignment $node): void {}
public function visitReturn(ReturnStatement $node): void {}
}
class DocstringChecker implements CodeVisitor
{
public array $missing = [];
public array $documented = [];
public function visitFunction(FunctionDef $node): void
{
// TODO: проверить docstring
}
public function visitClass(ClassDef $node): void
{
// TODO: проверить docstring класса, обойти методы
}
public function visitIf(IfStatement $node): void
{
foreach (array_merge($node->thenBody, $node->elseBody) as $n) $n->accept($this);
}
public function visitFor(ForLoop $node): void
{
foreach ($node->body as $n) $n->accept($this);
}
public function visitAssignment(Assignment $node): void {}
public function visitReturn(ReturnStatement $node): void {}
public function visitCall(FunctionCall $node): void {}
}
// Ожидаемый результат:
// $code = new ClassDef("Calculator", [
// new FunctionDef("add", ["a", "b"], [new ReturnStatement("a + b")], "Сложение"),
// new FunctionDef("divide", ["a", "b"], [
// new IfStatement("b == 0", [new ReturnStatement("None")], [new ReturnStatement("a / b")])
// ]), // нет docstring
// ], "Калькулятор");
// $complexity = new ComplexityAnalyzer(); $code->accept($complexity);
// print_r($complexity->complexity); // ["add" => 1, "divide" => 2]
// $checker = new DocstringChecker(); $code->accept($checker);
// print_r($checker->missing); // ["divide"]
// print_r($checker->documented); // ["Calculator", "add"]