Перейти к содержимому

SOLID и принципы

Пять принципов SOLID и три эвристики DRY, KISS и YAGNI: что они на самом деле утверждают, как выглядят нарушения на C# и в самом .NET и где принципы перестают помогать. Перечислить буквы умеет каждый, а на уровне Senior спрашивают цену каждого принципа и его границы.

Перед этой темой: ООП.

Junior+

Что означают принцип единственной ответственности (SRP) и принцип открытости/закрытости (OCP)?

Заголовок раздела «Что означают принцип единственной ответственности (SRP) и принцип открытости/закрытости (OCP)?»
Ответ

Что это

  • SRP, Single Responsibility Principle. У класса одна причина для изменения: им управляет один источник требований.
  • OCP, Open/Closed Principle. Класс открыт для расширения и закрыт для изменения: новое поведение добавляют, не переписывая готовый код.

Зачем

Правка одной функции не ломает соседнюю, а проверенный код не трогают ради новой фичи.

Как пользоваться

  1. Найдите в классе части, которые меняются по разным причинам: расчёт, формат, сохранение. Вынесите их в отдельные классы.
  2. Растущий switch по типу или формату замените интерфейсом и отдельной реализацией на каждый вариант.

Частая ошибка

Понимать SRP как «класс делает одно действие» и дробить код на классы с одним методом. Принцип про причины изменений, а не про число методов.

Как устроено

  • SRP по Мартину. В «Clean Architecture» модуль отвечает перед одним актором — группой людей, которые просят изменений. Пример Мартина: Employee с методами CalculatePay (бухгалтерия), ReportHours (операционный отдел) и Save (технический отдел). Если первые два делят вспомогательный метод, правка для бухгалтерии тихо меняет отчёт.
  • OCP. Формулировку дал Бертран Мейер в 1988 году. Мейер расширял модуль наследованием реализации, Мартин — через абстракции: клиент зависит от интерфейса, а новое поведение — новая реализация.
  • Стратегия. Типичный способ соблюсти OCP: алгоритм, который меняется, выносится в интерфейс, и класс получает нужную реализацию снаружи.

Ловушки

  • Один и тот же switch по типу в нескольких местах — главный признак нарушения OCP: новый вариант требует правок во всех.
  • SRP не означает «один метод». Класс с десятью методами вокруг одного правила нормален.
  • Исправление ошибки OCP не нарушает: принцип про добавление поведения.

Правило

Разделяйте код по тем, кто просит изменений. Точку расширения вводите, когда изменение по этой оси уже случилось, а не «на всякий случай».

Под капотом

  • Мартин пересказывает SRP так: собирайте вместе то, что меняется по одним причинам, и разделяйте то, что меняется по разным. Это связность, сформулированная через людей и организацию.
  • «Функция должна делать одно дело» — отдельный принцип для функций. Мартин прямо оговаривает, что это не SRP.
  • Закрытость не бывает полной: модуль закрыт только от предвиденных изменений. Мартин называет это стратегической закрытостью — автор выбирает, от чего защищаться.

Цена

  • Каждая точка расширения — интерфейс, лишние типы, косвенный вызов и лишний переход при чтении кода.
  • Ось, угаданная неверно, хуже, чем никакая. Изменение приходит поперёк абстракции и правит её вместе со всеми реализациями.

Компромиссы

  • Первое изменение вносят прямо в код, на втором похожем выделяют абстракцию: ось уже видна по истории.
  • На уровне архитектуры OCP — это плагины. Зависимости направлены к ядру, а новый модуль подключается без правки ядра.

Как проверить в проде

  • git log по файлу: класс, который правят в каждом втором PR по несвязанным задачам, и конфликты слияния разных команд в одном файле указывают на нарушение SRP.
  • switch по одному и тому же признаку в нескольких местах — кандидат на полиморфизм.

Итог: SRP и OCP защищают от каскада изменений, а ось для них выбирают по реальной истории изменений, а не угадывают.

До: две причины для изменения и switch по формату
var report = new SalesReport();
Console.WriteLine(report.Export([120, 80], "csv")); // total;200
public class SalesReport
{
public string Export(int[] sales, string format)
{
int total = sales.Sum(); // правило расчёта меняет бухгалтерия
return format switch // формат меняют потребители отчёта
{
"csv" => $"total;{total}",
"json" => $$"""{"total":{{total}}}""",
_ => throw new NotSupportedException(format)
}; // новый формат — правка этого метода
}
}
После: расчёт отдельно, формат — стратегия
var report = new SalesReport(new SalesCalculator());
Console.WriteLine(report.Export([120, 80], new CsvFormatter())); // total;200
Console.WriteLine(report.Export([120, 80], new JsonFormatter())); // {"total":200}
// Одна причина для изменения: правила расчёта
public class SalesCalculator
{
public int Total(int[] sales) => sales.Sum();
}
public interface IReportFormatter { string Format(int total); }
// Новый формат — новый класс, SalesReport не меняется
public class CsvFormatter : IReportFormatter
{
public string Format(int total) => $"total;{total}";
}
public class JsonFormatter : IReportFormatter
{
public string Format(int total) => $$"""{"total":{{total}}}""";
}
public class SalesReport(SalesCalculator calculator)
{
public string Export(int[] sales, IReportFormatter formatter) =>
formatter.Format(calculator.Total(sales));
}
Junior+

Что такое DRY, KISS и YAGNI и как они соотносятся?

Заголовок раздела «Что такое DRY, KISS и YAGNI и как они соотносятся?»
Ответ

Что это

Принцип Смысл
DRY, Don’t Repeat Yourself у каждого знания одно место в системе
KISS, Keep It Simple, Stupid самое простое решение, которое работает
YAGNI, You Aren’t Gonna Need It не писать то, что пока не нужно

Как пользоваться

  • DRY. Ставка налога или правило валидации — в одном месте, иначе одну из копий забудут обновить.
  • KISS. Обычный if вместо паттерна, пока простое решение справляется.
  • YAGNI. «Вдруг понадобится второй провайдер оплаты» — не повод писать его поддержку сейчас.

Как они соотносятся

DRY подталкивает объединять, KISS и YAGNI — не усложнять заранее. Компромисс — правило трёх: два похожих фрагмента терпят, на третьем выносят общее.

Частая ошибка

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

Как устроено

  • DRY. Формулировка Ханта и Томаса (1999): каждое знание должно иметь единственное, однозначное, авторитетное представление в системе. Во втором издании (2019) авторы уточнили, что DRY — про дублирование знания и намерения, а не про одинаковые строки.
  • Знание дублируется и в разной форме. Правило в коде и в ограничении БД, контракт в DTO и в документации — тоже нарушение DRY.
  • Случайное дублирование. Проверки «имя до 50 символов» и «не больше 50 штук в заказе» похожи, но меняются по разным причинам.
  • YAGNI пришёл из экстремального программирования (Кент Бек). Он запрещает функциональность впрок, но не рефакторинг и тесты.

Когда что выбирать

Ситуация Что делать
Одно правило в двух местах вынести в один источник
Похожий код, разные причины изменений оставить дубль
Третье повторение одной логики выделить общее
«Пригодится потом» не писать

Ловушки

  • DRY через общий базовый класс или Utils склеивает модули, которые иначе друг от друга не зависели бы.
  • YAGNI не оправдывает отсутствие тестов: они нужны уже сейчас.

Правило

Сначала спросите «это одно знание?», а не «это похожий код?».

Цена

  • Неверная абстракция дороже дубля. Сэнди Метц описывает типичный путь: общий метод обрастает параметрами и if под каждого нового вызывающего. Её совет — встроить код обратно в места вызова и выделить абстракцию заново.
  • Фаулер раскладывает цену преждевременной фичи на четыре части: построить, задержать ценное, нести сложность (cost of carry) и переделать, если угадали неверно. Даже угаданная фича несёт сложность до того дня, когда понадобится.

Компромиссы

  • DRY между сервисами опаснее, чем внутри одного: общая библиотека моделей связывает релизы. Между ограниченными контекстами дублирование часто правильно.
  • Знание, которое обязано меняться синхронно, держат в одном месте даже ценой связности: схему сообщения генерируют из одного контракта.
  • YAGNI работает, только когда изменение дёшево: есть тесты и частые релизы. Публичный API или формат хранения так не переделать, их проектируют заранее.

Как проверить в проде

  • Одна и та же ошибка, исправленная в нескольких местах, — пропущенный DRY.
  • Метод с флагами-параметрами и ветками под каждого вызывающего — ложный DRY.
  • Код без вызовов и без покрытия тестами — след фич «на будущее».

Итог: выбор между дублем и абстракцией — ставка на то, меняются ли фрагменты вместе; при сомнении дешевле подождать.

Пример
Console.WriteLine(UserRules.IsValidName("Ann")); // True
Console.WriteLine(OrderRules.IsValidQuantity(3)); // True
// Одно знание — одно место: валидация и схема БД
// (HasMaxLength(UserRules.MaxNameLength)) берут лимит отсюда
public static class UserRules
{
public const int MaxNameLength = 50;
public static bool IsValidName(string name) =>
name.Length is > 0 and <= MaxNameLength;
}
// Похоже на дубль, но это другое знание: лимит заказа меняет бизнес.
// Общий метод IsInRange(x, 1, 50) связал бы два независимых правила.
public static class OrderRules
{
public const int MaxQuantity = 50;
public static bool IsValidQuantity(int quantity) =>
quantity is > 0 and <= MaxQuantity;
}
Middle+

Что такое принцип подстановки Лисков (LSP) и принцип разделения интерфейсов (ISP)?

Заголовок раздела «Что такое принцип подстановки Лисков (LSP) и принцип разделения интерфейсов (ISP)?»
Ответ

Что это

  • LSP, принцип подстановки Лисков. Код, который работает с базовым типом, правильно работает и с любым наследником, не зная о нём.
  • ISP, принцип разделения интерфейсов. Клиент зависит только от методов, которые вызывает.

Зачем

  • LSP позволяет полагаться на полиморфизм. Проверка if (shape is Square) в клиентском коде — признак нарушения.
  • ISP уменьшает связь: изменение чужого метода клиента не затрагивает, а заглушку для теста писать проще.

Как пользоваться

  • Наследуйтесь, только если наследник выполняет всё, что обещает базовый тип.
  • Если реализация вынуждена бросать NotImplementedException, интерфейс слишком широк: разделите его.

Частая ошибка

Наследовать по принципу «является» из реальной жизни. В геометрии квадрат — прямоугольник, но изменяемый Square не подтип изменяемого Rectangle: установка ширины меняет высоту.

Как устроено

LSP — про контракт метода: предусловия (что верно до вызова), постусловия (что гарантировано после) и инварианты (что верно всегда). Подтип обязан:

  1. не усиливать предусловия — не отвергать аргументы, которые базовый метод принимает;
  2. не ослаблять постусловия — не обещать меньше базового;
  3. сохранять инварианты базового типа;
  4. не бросать исключения, которых клиент базового типа не ждёт.

Сигнатуры проверяет компилятор, поведение — нет.

Классические нарушения

  • Square : Rectangle с изменяемыми сторонами: ломается постусловие «сеттер ширины не меняет высоту».
  • NotImplementedException в реализации интерфейса.
  • ReadOnlyCollection<T>, переданная как ICollection<T>: Add бросает NotSupportedException.

ISP

«Толстый» интерфейс, например IRepository с двадцатью методами, заставляет клиентов зависеть от лишнего. Решение — ролевые интерфейсы по нуждам клиентов: IOrderReader и IOrderWriter. Один класс может реализовать оба.

Правило

Принимайте самый узкий интерфейс, который нужен методу: IEnumerable<T> или IReadOnlyList<T>, а не List<T>.

Под капотом

  • Неформально LSP прозвучал в докладе Барбары Лисков «Data Abstraction and Hierarchy» (1987). Строго — в статье Лисков и Винг «A Behavioral Notion of Subtyping» (1994): если свойство доказуемо для объектов типа T, оно должно выполняться для объектов подтипа S.
  • Статья добавляет ограничение истории: подтип не допускает изменений состояния, которых не допускает базовый тип. Поэтому изменяемая точка не подтип неизменяемой, даже если все методы совпадают.
  • Мартин в «Clean Architecture» применяет LSP шире наследования: к реализациям интерфейсов и даже к REST-сервисам с общим контрактом.

Компромиссы

  • Stream с CanSeek и ICollection<T> с IsReadOnly формально LSP соблюдают: базовый контракт сам разрешает NotSupportedException. Цена — каждый клиент проверяет флаг, и проверку легко забыть.
  • IReadOnlyCollection<T> и IReadOnlyList<T> (.NET Framework 4.5) — ISP задним числом: метод, который только читает, больше не требует изменяемой коллекции.
  • Слишком мелкие интерфейсы дробят модель. Фаулер противопоставляет ролевые интерфейсы «заголовочным», которые копируют все публичные методы класса; режут по ролям клиентов, а не по одному интерфейсу на метод.

Как проверить в проде

  • is, as и GetType() для конкретных подтипов в клиентском коде — запах нарушения LSP.
  • NotImplementedException в реализациях находит поиск по коду.
  • Общий абстрактный тестовый класс, который прогоняется для каждой реализации интерфейса, проверяет контракт, а не сигнатуры.

Итог: LSP — поведенческий контракт, который компилятор не проверяет, а ISP удешевляет его соблюдение, сужая контракт до роли клиента.

Нарушение LSP: квадрат и прямоугольник
Rectangle r = new Square();
r.Width = 2;
r.Height = 5;
Console.WriteLine(r.Area); // 25, а клиент Rectangle ждёт 10
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area => Width * Height;
}
// Квадрат добавляет инвариант «ширина == высота» и ломает
// постусловие базового типа: сеттер меняет обе стороны
public class Square : Rectangle
{
public override int Width
{
get => base.Width;
set { base.Width = value; base.Height = value; }
}
public override int Height
{
get => base.Height;
set { base.Width = value; base.Height = value; }
}
}
LSP и ISP в коллекциях .NET
IList<int> list = new List<int> { 1, 2 }.AsReadOnly();
Console.WriteLine(list.IsReadOnly); // True: контракт разрешает отказ
try
{
list.Add(3);
}
catch (NotSupportedException)
{
Console.WriteLine("NotSupportedException");
}
// ISP: методу нужно только читать — он просит узкий интерфейс
Console.WriteLine(Sum(new List<int> { 1, 2 })); // 3
Console.WriteLine(Sum(new[] { 1, 2, 3 })); // 6
static int Sum(IReadOnlyList<int> items)
{
int sum = 0;
for (int i = 0; i < items.Count; i++) sum += items[i];
return sum;
}
Middle+

Что такое принцип инверсии зависимостей (DIP) и чем он отличается от внедрения зависимостей (DI) и IoC?

Заголовок раздела «Что такое принцип инверсии зависимостей (DIP) и чем он отличается от внедрения зависимостей (DI) и IoC?»
Ответ

Что это

DIP — буква D в SOLID. Две части по Мартину:

  1. Модули верхнего уровня не зависят от модулей нижнего уровня. Оба зависят от абстракций.
  2. Абстракции не зависят от деталей. Детали зависят от абстракций.

Верхний уровень — бизнес-правила, например InvoiceService. Нижний — детали: база, почта, платёжный шлюз.

Зачем

  • Бизнес-логику можно протестировать с заглушкой вместо настоящего шлюза.
  • Смена базы или провайдера не трогает правила.

Как пользоваться

  1. Объявите интерфейс рядом с бизнес-логикой: IPaymentGateway.
  2. Пусть класс зависит от интерфейса, а не от клиента конкретного провайдера.
  3. Реализацию передайте снаружи — через конструктор.

Частая ошибка

Считать DIP и DI одним и тем же. DIP — про направление зависимостей, DI — про способ передать объект.

Как устроено

Без DIP зависимость в коде совпадает с потоком вызовов: InvoiceService → CardGatewayClient. С DIP InvoiceService вызывает IPaymentGateway, а CardGateway его реализует. Вызов идёт как раньше, но зависимость исходного кода направлена от детали к бизнес-правилам.

Кто владеет абстракцией

Интерфейс принадлежит клиенту: лежит в его модуле и назван в его терминах — Charge(invoiceId, amount), а не PostJson(path, body). Если интерфейс лежит в сборке инфраструктуры, бизнес-модуль по-прежнему ссылается на неё, и инверсии нет.

DIP, DI и IoC

Понятие Что это
DIP принцип: куда направлены зависимости
DI техника: зависимость приходит снаружи
IoC принцип: управление у фреймворка, а не у вашего кода

DI помогает соблюдать DIP, но не гарантирует его. Как это делает контейнер ASP.NET Core, разобрано в теме Внедрение зависимостей.

Ловушки

  • Интерфейс ради интерфейса. IOrderService с одной реализацией, повторяющий все её методы и лежащий рядом с ней. Он ничего не инвертирует, только добавляет переход.
  • Протекающая абстракция. IRepository, который возвращает IQueryable<T> или DbSet<T>, привязывает клиента к EF Core.

Правило

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

Под капотом

  • Поток вызовов и зависимости исходного кода — разные графы. Интерфейс позволяет развернуть любую стрелку зависимости, не меняя поток вызовов. Мартин в «Clean Architecture» называет это главной силой ООП.
  • Мартин подчёркивает: инвертируется не только зависимость, но и владение интерфейсом. Клиент объявляет, что ему нужно, а сервер подстраивается.
  • На этом стоят чистая архитектура и порты и адаптеры: ядро объявляет порты, инфраструктура их реализует, а связывает всё composition root — место сборки графа объектов при старте.

Цена

  • Каждый шов — интерфейс, регистрация и лишний переход при навигации и отладке.
  • Абстракция, выделенная по одной реализации, повторяет её форму, и вторая реализация в неё не ложится.

Компромиссы

  • Стабильное против изменчивого. От string, List<T> и остальной BCL зависеть нормально. Инвертировать нужно зависимость от того, что меняется или недетерминировано: БД, внешних API, фреймворков, времени.
  • Внутри модуля конкретные классы допустимы. DIP ставят на границе: домен и инфраструктура, модуль и модуль.

Как проверить в проде

  • Тесты архитектуры (NetArchTest, ArchUnitNET) фиксируют правило «домен не ссылается на инфраструктуру».
  • Ссылки проектов в .csproj: если доменный проект ссылается на EF Core или на проект инфраструктуры, инверсии нет.

Итог: DIP — инструмент для границ модулей, а не для каждого класса: интерфейс у клиента, детали снаружи.

До: бизнес-правила зависят от детали
var service = new InvoiceService();
Console.WriteLine(service.Pay(42)); // Invoice 42 paid
public class InvoiceService
{
// Политика создаёт и знает HTTP-клиент конкретного провайдера
private readonly CardGatewayClient _client = new();
public string Pay(int invoiceId) =>
_client.PostJson("/charges", $$"""{"invoice":{{invoiceId}}}""") == 200
? $"Invoice {invoiceId} paid"
: "Payment declined";
}
public class CardGatewayClient
{
public int PostJson(string path, string body) => 200; // HTTP-вызов провайдера
}
После: интерфейс принадлежит бизнес-правилам
using Billing;
using Infrastructure;
// Composition root — единственное место, которое знает обе стороны
var service = new InvoiceService(new CardGateway());
Console.WriteLine(service.Pay(42)); // Invoice 42 paid
namespace Billing
{
// Абстракция лежит у клиента и названа в его терминах
public interface IPaymentGateway
{
bool Charge(int invoiceId, decimal amount);
}
public class InvoiceService(IPaymentGateway gateway)
{
public string Pay(int invoiceId) =>
gateway.Charge(invoiceId, 100m) ? $"Invoice {invoiceId} paid" : "Payment declined";
}
}
namespace Infrastructure
{
// Деталь зависит от абстракции бизнес-правил, а не наоборот
public class CardGateway : Billing.IPaymentGateway
{
public bool Charge(int invoiceId, decimal amount) => true; // HTTP-вызов провайдера
}
}
Senior+

Когда следование SOLID вредит и как принимать решения о проектировании?

Заголовок раздела «Когда следование SOLID вредит и как принимать решения о проектировании?»
Ответ

Что это

SOLID — советы, как сделать код удобным для изменений. Они не бесплатны: каждый интерфейс и новый класс — ещё одно место, которое надо прочитать.

Когда SOLID вредит

  • Интерфейс на каждый класс, хотя реализация одна и подменять её не нужно.
  • Класс разбит на пять мелких, и логику одной операции приходится собирать по пяти файлам.
  • «Расширяемость» под требования, которых нет.

Как пользоваться

  1. Напишите простое решение.
  2. Покройте его тестами.
  3. Когда появилась вторая реализация или правки стали болезненными, выделите абстракцию.

Частая ошибка

Обосновывать усложнение словами «так требует SOLID». Принцип — средство, а не цель: если он не решает конкретную проблему, он её создаёт.

Как устроено

Каждая абстракция добавляет косвенность. Чтобы понять вызов, надо найти реализацию, её регистрацию в контейнере и, возможно, декоратор. Это окупается, только если по этой оси действительно приходят изменения.

Признаки переусердия

  • Интерфейс-копия. IOrderService для OrderService с одной реализацией, повторяющий все его методы.
  • Фрагментация. Одна операция размазана по Handler, Validator, Mapper, Factory и Provider, и чтобы её понять, нужно открыть десять файлов.
  • Спекулятивная общность. Параметры, хуки и базовые классы «на будущее», которыми пользуется один вызывающий. Так этот запах называет Фаулер в «Рефакторинге».

Когда что выбирать

Ситуация Решение
Сеть, время, файлы, внешняя система абстракция сразу — это шов для тестов
Одна реализация внутри модуля конкретный класс
Появилась вторая реализация выделить интерфейс по факту
Абстракция мешает изменениям встроить код обратно и выделить заново

Правило

Сначала просто, рефакторинг — по мере боли. Абстракцию выделяют, когда известны хотя бы два варианта: тогда видно, что у них общего.

Откуда взялись принципы

Мартин собрал эти принципы в статье «Design Principles and Design Patterns» (2000), аббревиатуру SOLID позже предложил Майкл Фэзерс. Они отвечали на боль больших C++- и Java-систем: изменение одного заголовка тянуло пересборку и переразвёртывание половины системы.

Критика

  • Дэн Норт в шуточном докладе «Why Every Element of SOLID is Wrong» назвал SRP «бессмысленно расплывчатым», а для каждого принципа предложил одну альтернативу — «пишите простой код».
  • Позже он предложил CUPID: Composable, Unix philosophy, Predictable, Idiomatic, Domain-based. Это свойства, а не правила: код не «соответствует» им, а приближается к ним.

Цена

  • Случайная сложность, по Фреду Бруксу, — сложность, которую добавили мы сами, а не задача. Интерфейсы, фабрики и слои без второй реализации — её классический источник.
  • Косвенность бьёт по чтению и отладке: стек вызовов через декораторы и прокси, «Go to Implementation» с пятью результатами.

Компромиссы

  • Решение — ставка: вероятность изменения и его цена без абстракции против цены абстракции сейчас. Дорогое для изменения — публичный API, формат данных, границы модулей — проектируют заранее, остальное — по мере необходимости.
  • Команде аргумент дают ценой, а не принципом: «шов здесь, потому что API провайдера нестабилен, а тесты без него ходят в сеть».
  • Подход «рефакторинг по мере боли» работает только с тестами. Без них отложенная абстракция не наступает никогда.

Как проверить в проде

  • Интерфейсы с одной реализацией, которые не подменяются и в тестах, — кандидаты на удаление.
  • Частота изменений файлов в git log показывает реальные оси изменений. Абстракции там, где правок нет, не окупаются.

Итог: принципы — инструменты против конкретной боли; решение принимают по цене и вероятности изменений, а не по букве принципа.

До: четыре типа ради одной формулы
var calculator = new DiscountCalculatorFactory().Create();
Console.WriteLine(calculator.Calculate(new DiscountRequest(200))); // 20
// Одна реализация, никто не подменяет — косвенность без пользы
public interface IDiscountCalculator { int Calculate(DiscountRequest request); }
public interface IDiscountCalculatorFactory { IDiscountCalculator Create(); }
public record DiscountRequest(int Amount);
public class DiscountCalculator : IDiscountCalculator
{
public int Calculate(DiscountRequest request) => request.Amount / 10;
}
public class DiscountCalculatorFactory : IDiscountCalculatorFactory
{
public IDiscountCalculator Create() => new DiscountCalculator();
}
После: сначала просто
Console.WriteLine(Discount.For(200)); // 20
// Чистая функция: тестируется без заглушек и читается сразу.
// Интерфейс появится, когда появится второй способ расчёта.
public static class Discount
{
public static int For(int amount) => amount / 10;
}