Pull to refresh

Comments 33

Думаю, что всё хорошо в меру. Любая идея, доведённая до абсурда, перестаёт быть хорошей и правильной. По типу - болезнь легче предотвратить, чем лечить и всякие ранние диагностики, чекапы, etc. это хорошо. Но ходить проверяться каждый день, кажется, что перебор.

Дополню ваш комментарий цитатой Роберта Мартина, на счет того, как не довести своё желание следовать принципам "до абсурда":

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

По моему мнению, лучшие способ не доводить идею до абсурда, это понимать что лежит в основе этой идеи, и какую проблему она решает. И оставаться приземленным, используя идею как полезный инструмент, а не как истину в последней инстанции. В принципе, об этом "приземлении" отчасти и была эта статья.

используя идею как полезный инструмент

Именно это вы и не продемонстрировали. Вы пытались применить все принципы к коду, который весьма вероятно не требовал применения ни одного. А если и требовал - то вы этого не показали, а сразу начали "применять". Если вы именно это хотели показать - у вас получилось так себе.

какую проблему она решает

Вот именно. Я допускаю, что я невнимательно что-то читал, если так - ткните пальцем, где вы описали, какие проблемы были в вашем примере кода до применения принципов?

Ваши примеры пытаются походить на solid. Но вы создаёте кучу бессмысленных абстракций

Абстракции создаются на уровне доменов (ddd) и уровне взаимодействия с внешним миром (базы данных, межпроцессное взаимодействие, http контроллеры и прочее)

Я бы советовал дополнительно изучить значение каждой из букв. Они хорошо описано в книге Мартина Чистый код

Если под "бессмысленными абстракциями" вы подразумеваете структуру кода "после применения SOLID", то эти абстракции не мои. Я взял их из курса Роберта Мартина по SOLID. Вот кадр из урока (кусочек из статьи слева-сверху):

Чтобы и контроллеры и UseCase'ы соответствовали принципам OCP и DIP действительно нужно столько дополнительных классов. Можно заменить Factories для интерактора используя инъекцию зависимостей и передавать UseCaseActivator как параметр в Controller. Но сильно нашу схему это не упростит, и мой посыл остаётся актуален: "SOLID улучшает coupling, но ухудшает читабельность"

Эта схема хороша для показа на мастер классе, но для реального использования на проекте - это полный ужас.

Как я и написал в статье:

Будьте уверены, что увидев эту структуру у себя на проекте, вы подумаете, что это шутка по типу FizzBuzzEnterprise.

Однако обратите внимание. Концептуально это действительно единственный способ добиться той низкой связности, которую ставит своей задачей SOLID. При таком дизайне все компоненты защищены от изменений по "частоте", "важности компонентов" и "стоимости".

Но в ваших проектах, скорее всего, и такая сильная защита не нужна. А значит вы можете не делать его 100% SOLID. Об этом утверждении и была моя статья.

Мне кажется вы во многом правы, и это связано с более глубоким вопросом - как и зачем декомпозировать, что такое модуль и в каких целях он используется, а так же зачем нам, собственно SOLID и правила, какие у них границы применимости.

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

Вторая важная деталь - за архитектуру нужно платить. Создавая архитектурный шов "про запас", например с расчетом иметь возможность сменить реализацию источника данных позднее, мы должны заплатить за это - дополнительными интерфейсами и слоями, более сложными объектами.
Цена может быть невелика, особенно по сравнению с болью решаемых проблем, но ее нужно заплатить все равно.

Как итог, помимо хороших рекомендаций, как делать что-то, зачастую нужны не менее хорошие рекомендации, как чего-то не делать, или в каком месте первые рекомендации не применимы.


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

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

Вы абсолютно правы. Как говорил Эдсгера Дейкстра: "Цель абстракции состоит не в том, чтобы добавить неопределенности, а в том, чтобы создать новый семантический уровень, используя который мы можем быть абсолютно точны".

SOLID не работает с абстракциями, и не добавляет "новый семантический уровень". Как описывает их Роберт Мартин: "SOLID - это принципы менеджмента зависимостей".

Посыл статьи как раз в этом. В большинстве проектов хорошие абстракции, имена и транзакционная целостность (а все вместе - читабельность) важнее, чем менеджмент зависимостей.

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

Давайте применим к этой структуре все принципы SOLID.

Вот после этого места уже можно не читать. Давайте применим... зачем? Вы даже не соизволили сформулировать цель применения.

SOLID не самоцель, это инструмент. Инструмент для улучшения некоторых показателей кода. У вас показатели были плохие? Какие именно?

А если вы не знаете, чего хотели - вот ровно это непонятно что вы и получили в итоге.

Я исхожу из того, что вы применяли принципы просто "чтоб было". С поправкой на то, что это учебная статья, вы разумеется можете показать игрушечный код, но "давайте применим все принципы" - это все равно за пределами разумного. И кстати, достижение low coupling тоже не самоцель, связность кода - это просто показатель, который полезно измерять, и который в текущем (исходном состоянии) вас вполне может устраивать.

Иными словами - если код уже достаточно хорош, сопровождается и развивается сравнительно легко, понимается (текущей командой проекта) без проблем - нахрена его ломать и пытаться "улучшить" непонятно что?

Да. Но есть нюанс. Кроме знаний немалую роль в поведении людей играет вера.
Принципы SOLID, как и прочие результаты теоретизирования, вполне могут быть учением, предметом веры. И адепты у этой веры находятся: либо их личный склад ума, либо их личный опыт не позволяют смотреть на эти принципы критически. И адепты несут эту веру в массы, не допуская сомнений.
И если такой адепт начнет влиять на руководство (или, хуже, сам станет руководителем), то эти принципы, да ещё и в максимально примитивном варианте (ибо чем примитивнее, тем проще верить), могут внезапно стать ключевыми показателями, по которым будет определяться кому сколько денег платить, кого повышать, кого вообще брать на работу.

Я не буду разбирать, почему структура именно такая, и как она соотносится с принципами SOLID.

А было бы интересно, может я чего не понимаю, но выглядит как усложнение на пустом месте. Например, надо обязательно делать именно фабрики, хотя черт знает сколько времени есть DI из коробки? Вот если написано примерно вот так

Services.AddTransient<UseCase>();

public class Controller
{
   private readonly IUseCase _useCase; 
   public Repository(IUseCase useCase) 
   { 
     _useCase = useCase; 
   }
}

то это уже плохо, это не solid?

хотя черт знает сколько времени есть DI из коробки

DI именно "из коробки", т.е., в самом языке и его стандартной библиотеке, обычно нет. В C#, в частности - нет. DI, если говорить конкретно, например, про ASP.NET Core, есть только в некоторых фреймворках, причем - в специально выденных местах. И во всех таких местах тамошний DI - это Service Locator в каком-нибудь другом месте внутри фрейймворка. То есть - та самая фабрика, просто ее замели под коверубрали под капот, чтобы не отсвечивала и джунов не смущала.

DI именно "из коробки", т.е., в самом языке и его стандартной библиотеке, обычно нет. 

Наверное про "из коробки" я преувеличил (в моем любимом ASP.NET Core есть), но в любом случае DI контейнеров готовых уйма.

То есть - та самая фабрика, просто ее замели под коверубрали под капот

Я к тому и веду, что если у нас есть DI контейнер, который делает за нас кучу работы, зачем делать свои фабрики? Это не будет соответствовать принципам солида?

Я вот смотрю на "кадр из урока", это же какая-то жесть.

моем любимом ASP.NET Core есть

Он там где есть, а где нет.
В шаблонах Web Host и Generic Host (базовых до версии ASP.NET 6) он при конфигурировании конвейера обработчиков (там это делалось в методе Configure Startup-класса) DI был, а в WebApplication он больше не с нами (хотя WebApplication - это тот же Generic Host, только хорошо запрятанный).
В методах конфигурирования конвейра обработчиков для базовых методах расширения IApplicationBuilder - Use, Run и Map - его нет ни на этапе конфигурирования, ни на этапе вызова обработчика, а в UseMiddlware, например, и на этапе конфигурирвания, и на этапе вызова обработчика он есть. В подсистеме маршрутизации (которая конфигурируется через IEndpointRouteBuilder) при вызове обработчика точки назначения маршрута в базовом варианте (где обработчик обязательно имеет тип RequestDelegate) DI, очевидно, не используется (ибо список параметров делегированного метода фиксированный), а в более позднем расширенном варианте Minimal API, где обработчиком может быть любой делегат, использование DI возможно, и так далее.

Я к тому и веду, что если у нас есть DI контейнер, который делает за нас кучу работы, зачем делать свои фабрики?

Иногда приходится. Например, потому что время существования объекта, получаемого через DI, не годится для выполнения задачи. Например - в EF. Объект DbContext в контейнере там обычно конфигурируется с временем жизни запроса (т.е., он - один тот же самый на всё время обработки запрос), а параллельное обращение к себе из нескольких задач этот объект не допускает. Поэтому при попытке распараллелить обработку с использованием EF приходится получать через DI объект фабрики, получать экземпляры DbContext от фабри ки и самостоятельно следить за тем, какой использовать и за их временем жизни. Впрочем, MS там о нас позаботилась, и такую фабрику написала.
А очевидная альтернатива фабрике для такого случая - создавать DbContext напрямую - она явно не SOLID.

Я вот смотрю на "кадр из урока", это же какая-то жесть.

Мартин, к сожалению, не познал вовремя мудрость, содержащуюся в ASP.NET Core ;-) Но у него есть уважительная причина: AFAIK ASP.NET Core тогда просто ещё не было.

SOLID вообще не требует DI, btw

"Спасибо, Кэп"(с). Но предыдущий комментатор завел речь именно о DI как средстве реализации принципа инверсии зависимости, и я отвечал именно ему.

Dependency Injection это внедрение зависимостей. Никакого отношения к Dependency Inversion не имеет. Используя Injection, ничего не мешает реализовать прямой порядок зависимостей. И Inversion можно реализовывать без Injection.

Dependency Injection это скорее способ IoC.

Заниматься схоластикой и спорить за абстрактные принципы я даже не буду. Хочу лишь отметить, что автор исходного комментраия упоминал DI именно в контексте принципов SOLID.

Да, можно использовать DI. Мой ответ из другого комментария:

Чтобы и контроллеры и UseCase'ы соответствовали принципам OCP и DIP действительно нужно столько дополнительных классов. Можно заменить Factories для интерактора используя инъекцию зависимостей и передавать UseCaseActivator как параметр в Controller. Но сильно нашу схему это не упростит, и мой посыл остаётся актуален: "SOLID улучшает coupling, но ухудшает читабельность"

В данном примере предполагается, что Factory будет отдавать нам не только UseCaseActivator , но и RequestBuilder , так как их реализации используются одновременно. На схеме у автора это не показано (и у меня как следствие), что косяк. Но в видео-уроке это проговаривается в контексте.

Так что Factory можно заменить на инъекцию UseCaseActivator и RequestBuilder . И все ключевые зависимости останутся без изменений

Читабельность - насколько легко программисту понять, что делает программа.

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

К примеру, увидев такой кусок кода -

  int i,j;
  //... assign smth. to i and to j
  i^=j;
  j^=i;
  i^=j;

- человек, не знающий эту идиому, остановится и задумается - "что это было?", а знающий - отметит на автомате, что здесь были поменяны значения i и j, и продолжит читать дальше.

Так что оценка читабельности - она, в лучшем случае, качественная. Например, один коллега меня тут недавно уверял, что для него код, разбитый на множество мелких функций, раскиданных по всему тексту программы, более читабелен, чем код из одного большого метода, состоящего из последовательности логически слабо связанных блоков, а для меня - строго наоборот (если чо, я много читал исходники того же ASP.NET Core и намаялся раскапывать каждый раз, что там на самом деле делается). Думаю, для натренированного таким образом человека код в стиле SOLID - он, наоборот, оказывается более читабельным.

Я всегда считал и продолжаю считать, что нет такой объективного показателя для текста программы, как читабельность.

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

В вашем примере, если это вынести в функцию и обозвать её swap, то понятно будет и знающему и не знающему.

Вопрос как лучше меня не интересовал. Обмен содержимого через три XOR был приведен как пример идиомы, которую знают не все, и которая, тем самым, делает читаемость субъективной.
PS А по жизни я этот прием первый раз видел в ассемблерной программе (исходный код IBM VM/SP), делать там обмен через вызов процедуры было бы слишком громоздко.

Вторую картинку я бы реализовал на чистом ФП

// Тип для DTO (Объект для передачи данных)
type RequestDTO = { payload: string };

// Создание DTO
const createRequestDTO = (payload: string): RequestDTO => ({ payload });

// Тип для UseCase
type UseCase = (requestDTO: RequestDTO) => Promise<string>;

// Конкретная реализация UseCase
const concreteUseCase: UseCase = async (requestDTO) => {
  const { payload } = requestDTO;
  // Симуляция какой-либо асинхронной операции
  await new Promise((resolve) => setTimeout(() => resolve(), 1000));
  return `Обработана полезная нагрузка: ${payload}`;
};

// Фабрика для создания UseCase (если это может понадобиться)
const createUseCase = (): UseCase => concreteUseCase;

// Чистая функция для обработки запроса
const handleRequest = async (payload: string, useCase: UseCase = createUseCase()): Promise<string> => {
  const requestDTO = createRequestDTO(payload);
  return useCase(requestDTO);
};

// Пример использования
handleRequest('пример полезной нагрузки').then((result) => {
  console.log(result); // Вывод результата: "Обработана полезная нагрузка: пример полезной нагрузки" через секунду
});

Возможность добавления нового обработчика сохранена. Абстракции не текут. Площадь соприкосновения частей кода минимальна.

ООП не умеет делать маленькие стабильные абстракции, он слишком конкретен и постоянно требует абстрактных прокладок между классами.

Обратите внимание, что в вашем примере функция handleRequest ссылается на createRequestDTO , а та в свою очередь ссылается на RequestDTO . Но RequestDTO используется функцией UseCase . А значит единственный способ упаковки, который не вызовет цикличные зависимости будет такой, как на этой схеме:

В таком случае любое изменение внутренней структуры RequestDTO потребует изменение как минимум createRequestDTO , что означает перекомпиляцию и перевыпуск компонента контроллеров (того, что слева от двойных линий). А именно этого мы пытались избежать, добавляя абстракции, связанные с интерфейсом-маркером (красный блок на второй картинке в статье).

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

А на счет ООП и ФП. Если в проекте есть полиморфизм, то для меня это уже ООП. Я не считаю, что классы == ООП. В конце-концов функция с замыканием нескольких переменных - это уже объект с одним методом. Если код организован на основе отправки сообщений, где отправитель не знает о внутреннем устройстве получателя - это ООП. Если код организован как конвейер по преобразованию данных - это ФП. Я выбираю парадигму исходя из задачи. Поэтому не могу поддержать вас в недолюбливании ООП, хотя и согласен, что с некоторыми задачами эта парадигма не справляется (как и шуруповерт не справляется с гвоздями)

я видимо дурак, поэтому напишу на хаскель

тех же целей можно достичь проще, если я их верно понял

-- Цель 1: Гибкость в источниках данных (RequestBuilder)

-- Сейчас: У нас есть OrderRequestBuilder, который собирает данные заказа из полей (имя, адрес, товары).
-- Проблема: Что, если данные о заказе приходят не из формы на сайте, а из другого места? Например:
-- Из внешней системы через API (как в ExternalOrderRequestBuilder).
-- Из файла (CSV, JSON, XML).
-- Из базы данных.
-- Из консоли.
-- Из сообщения в очереди (Kafka, RabbitMQ).

-- Цель 2: Гибкость в обработке данных (UseCase)
-- Проблема: Что, если нам нужно добавить новую логику обработки? Например:
-- Расчет скидки.
-- Проверка наличия товара на складе.
-- Создание счета на оплату.
-- Отправка уведомления клиенту.
-- Обработка возвратов (ProcessReturnUseCase).

-- Цель 3: Гибкость в способе выбора UseCase
-- Проблема: Что, если нам нужно изменить логику выбора UseCase? Например:
-- Выбирать UseCase на основе данных в запросе (как в примере с ValidateOrderUseCase и пустым списком товаров).
-- Выбирать UseCase на основе конфигурации (например, включить/выключить определенную обработку).
-- Выбирать UseCase на основе A/B-тестирования.

-- реализуем это на хаскель наиболее просто
-- с примерами гибкости по целям 1, 2, 3

{-# LANGUAGE GADTs #-}

module Main where

import System.Random (randomRIO)

-- БАЗОВЫЕ ТИПЫ

data Request a where
    OrderRequest :: { customerName :: String, address :: String, items :: [String] } -> Request OrderRequest
    ReturnRequest :: { orderId :: Int, reason :: String } -> Request ReturnRequest

data ProcessResult 
    = Success String
    | Error String
    deriving Show

-- ЦЕЛЬ 1: ГИБКОСТЬ В ИСТОЧНИКАХ ДАННЫХ
-- Два варианта: Console и File

data DataSource 
    = FromConsole
    | FromFile String
    deriving Show

-- Вариант 1: Чтение из консоли
buildFromConsole :: IO (Request OrderRequest)
buildFromConsole = do
    putStrLn "Enter name:"
    name <- getLine
    putStrLn "Enter address:"
    addr <- getLine
    putStrLn "Enter items:"
    items <- words <$> getLine
    return $ OrderRequest name addr items

-- Вариант 2: Чтение из файла
buildFromFile :: String -> IO (Request OrderRequest)
buildFromFile path = do
    content <- readFile path
    let [name, addr, itemsStr] = lines content
    let items = words itemsStr
    return $ OrderRequest name addr items

-- ЦЕЛЬ 2: ГИБКОСТЬ В ОБРАБОТКЕ
-- Два варианта: Basic и Advanced

data ProcessType 
    = BasicProcess    -- только базовая обработка
    | AdvancedProcess -- с дополнительными проверками
    deriving Show

-- Вариант 1: Базовая обработка
basicProcessOrder :: Request OrderRequest -> IO ProcessResult
basicProcessOrder (OrderRequest name addr items) = do
    putStrLn $ "Basic processing for: " ++ name
    return $ Success "Basic processing complete"

-- Вариант 2: Расширенная обработка
advancedProcessOrder :: Request OrderRequest -> IO ProcessResult
advancedProcessOrder req@(OrderRequest name addr items) = do
    putStrLn $ "Advanced processing for: " ++ name
    -- Проверка наличия
    inStock <- checkInventory items
    if not inStock
        then return $ Error "Items not in stock"
        else do
            -- Расчет скидки
            discount <- calculateDiscount items
            putStrLn $ "Discount: " ++ show discount
            return $ Success "Advanced processing complete"

-- Вспомогательные функции
checkInventory :: [String] -> IO Bool
checkInventory _ = return True

calculateDiscount :: [String] -> IO Double
calculateDiscount items = return $ if length items > 3 then 0.1 else 0.0

-- ЦЕЛЬ 3: ГИБКОСТЬ В ВЫБОРЕ ПРОЦЕССА
-- Два варианта: По конфигурации и по A/B тесту

data ProcessStrategy 
    = ConfigBased Bool  -- на основе конфигурации
    | ABTest           -- на основе A/B теста
    deriving Show

-- Вариант 1: Выбор по конфигурации
selectByConfig :: Bool -> Request a -> ProcessType
selectByConfig useAdvanced _ = 
    if useAdvanced 
        then AdvancedProcess
        else BasicProcess

-- Вариант 2: Выбор по A/B тесту
selectByAB :: IO ProcessType
selectByAB = do
    p <- randomRIO (0.0, 1.0)
    return $ if p > 0.5 
        then AdvancedProcess
        else BasicProcess

-- КОНТРОЛЛЕР

processRequest :: DataSource -> ProcessStrategy -> IO ProcessResult
processRequest source strategy = do
    -- 1. Построение запроса из источника
    request <- case source of
        FromConsole -> buildFromConsole
        FromFile path -> buildFromFile path

    -- 2. Выбор типа обработки
    processType <- case strategy of
        ConfigBased useAdvanced -> return $ selectByConfig useAdvanced request
        ABTest -> selectByAB

    -- 3. Обработка
    case processType of
        BasicProcess -> basicProcessOrder request
        AdvancedProcess -> advancedProcessOrder request

-- ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ

main :: IO ()
main = do
    -- Цель 1: Разные источники данных
    putStrLn "=== Test Different Data Sources ==="
    result1 <- processRequest FromConsole (ConfigBased False)
    print result1
    
    result2 <- processRequest (FromFile "order.txt") (ConfigBased False)
    print result2

    -- Цель 2: Разные способы обработки
    putStrLn "\n=== Test Different Processing Types ==="
    result3 <- processRequest FromConsole (ConfigBased False) -- базовая обработка
    print result3
    
    result4 <- processRequest FromConsole (ConfigBased True)  -- расширенная обработка
    print result4

    -- Цель 3: Разные стратегии выбора
    putStrLn "\n=== Test Different Selection Strategies ==="
    result5 <- processRequest FromConsole (ConfigBased True)  -- по конфигурации
    print result5
    
    result6 <- processRequest FromConsole ABTest             -- по A/B тесту
    print result6

и на C#

  • Гибкость сохраняется, но реализована по-другому:

    • Вместо фабрики - конкретные типы данных

    • Вместо активатора - pattern matching. Активатор это Java паттерн настройки объектов на основе их типа?

    • Вместо DTO - immutable records

    • Вместо downcast или наследования - pattern matching

// БАЗОВЫЕ ТИПЫ
public abstract record Request;
public record OrderRequest(string CustomerName, string Address, List<string> Items) : Request;
public record ReturnRequest(int OrderId, string Reason) : Request;

public abstract record ProcessResult
{
    public record Success(string Message) : ProcessResult;
    public record Error(string Message) : ProcessResult;
}

// ЦЕЛЬ 1: ГИБКОСТЬ В ИСТОЧНИКАХ ДАННЫХ
public abstract record DataSource
{
    public record FromConsole : DataSource;
    public record FromFile(string Path) : DataSource;
}

// Построитель запросов
public static class RequestBuilder
{
    public static async Task<Request> BuildFromConsole()
    {
        Console.WriteLine("Enter name:");
        var name = Console.ReadLine()!;
        Console.WriteLine("Enter address:");
        var addr = Console.ReadLine()!;
        Console.WriteLine("Enter items:");
        var items = Console.ReadLine()!.Split(' ').ToList();
        return new OrderRequest(name, addr, items);
    }

    public static async Task<Request> BuildFromFile(string path)
    {
        var lines = await File.ReadAllLinesAsync(path);
        var items = lines[2].Split(' ').ToList();
        return new OrderRequest(lines[0], lines[1], items);
    }
}

// ЦЕЛЬ 2: ГИБКОСТЬ В ОБРАБОТКЕ
public enum ProcessType
{
    BasicProcess,    // только базовая обработка
    AdvancedProcess  // с дополнительными проверками
}

public static class OrderProcessor
{
    public static async Task<ProcessResult> BasicProcessOrder(OrderRequest request)
    {
        Console.WriteLine($"Basic processing for: {request.CustomerName}");
        return new ProcessResult.Success("Basic processing complete");
    }

    public static async Task<ProcessResult> AdvancedProcessOrder(OrderRequest request)
    {
        Console.WriteLine($"Advanced processing for: {request.CustomerName}");
        
        // Проверка наличия
        var inStock = await CheckInventory(request.Items);
        if (!inStock)
            return new ProcessResult.Error("Items not in stock");

        // Расчет скидки
        var discount = await CalculateDiscount(request.Items);
        Console.WriteLine($"Discount: {discount}");
        return new ProcessResult.Success("Advanced processing complete");
    }

    private static async Task<bool> CheckInventory(List<string> items) => true;

    private static async Task<double> CalculateDiscount(List<string> items) =>
        items.Count > 3 ? 0.1 : 0.0;
}

// ЦЕЛЬ 3: ГИБКОСТЬ В ВЫБОРЕ ПРОЦЕССА
public abstract record ProcessStrategy
{
    public record ConfigBased(bool UseAdvanced) : ProcessStrategy;
    public record ABTest : ProcessStrategy;
}

public static class ProcessSelector
{
    private static readonly Random Random = new();

    public static ProcessType SelectByConfig(bool useAdvanced, Request request) =>
        useAdvanced ? ProcessType.AdvancedProcess : ProcessType.BasicProcess;

    public static ProcessType SelectByAB() =>
        Random.NextDouble() > 0.5 ? ProcessType.AdvancedProcess : ProcessType.BasicProcess;
}

// КОНТРОЛЛЕР
public class Controller
{
    public async Task<ProcessResult> ProcessRequest(DataSource source, ProcessStrategy strategy)
    {
        // 1. Построение запроса из источника
        var request = source switch
        {
            DataSource.FromConsole => await RequestBuilder.BuildFromConsole(),
            DataSource.FromFile file => await RequestBuilder.BuildFromFile(file.Path),
            _ => throw new ArgumentException("Unknown data source")
        };

        // 2. Выбор типа обработки
        var processType = strategy switch
        {
            ProcessStrategy.ConfigBased config => 
                ProcessSelector.SelectByConfig(config.UseAdvanced, request),
            ProcessStrategy.ABTest => 
                ProcessSelector.SelectByAB(),
            _ => throw new ArgumentException("Unknown strategy")
        };

        // 3. Обработка
        if (request is OrderRequest orderRequest)
        {
            return processType switch
            {
                ProcessType.BasicProcess => 
                    await OrderProcessor.BasicProcessOrder(orderRequest),
                ProcessType.AdvancedProcess => 
                    await OrderProcessor.AdvancedProcessOrder(orderRequest),
                _ => throw new ArgumentException("Unknown process type")
            };
        }

        throw new ArgumentException("Unsupported request type");
    }
}

// ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ
public class Program
{
    public static async Task Main()
    {
        var controller = new Controller();

        // Цель 1: Разные источники данных
        Console.WriteLine("=== Test Different Data Sources ===");
        var result1 = await controller.ProcessRequest(
            new DataSource.FromConsole(),
            new ProcessStrategy.ConfigBased(false));
        Console.WriteLine(result1);

        var result2 = await controller.ProcessRequest(
            new DataSource.FromFile("order.txt"),
            new ProcessStrategy.ConfigBased(false));
        Console.WriteLine(result2);

        // Цель 2: Разные способы обработки
        Console.WriteLine("\n=== Test Different Processing Types ===");
        var result3 = await controller.ProcessRequest(
            new DataSource.FromConsole(),
            new ProcessStrategy.ConfigBased(false)); // базовая обработка
        Console.WriteLine(result3);

        var result4 = await controller.ProcessRequest(
            new DataSource.FromConsole(),
            new ProcessStrategy.ConfigBased(true));  // расширенная обработка
        Console.WriteLine(result4);

        // Цель 3: Разные стратегии выбора
        Console.WriteLine("\n=== Test Different Selection Strategies ===");
        var result5 = await controller.ProcessRequest(
            new DataSource.FromConsole(),
            new ProcessStrategy.ConfigBased(true));  // по конфигурации
        Console.WriteLine(result5);

        var result6 = await controller.ProcessRequest(
            new DataSource.FromConsole(),
            new ProcessStrategy.ABTest());           // по A/B тесту (рандомно)
        Console.WriteLine(result6);
    }
}

Сравнение

  • Текущий вариант ПРОЩЕ потому что:

    • Меньше уровней абстракции

    • Явное описание вариантов через типы данных

    • Прямая связь между компонентами

    • Нет необходимости в downcast

    • Типобезопасность на уровне компилятора

  • Недостатки:

    • Бизнес логика (main или контроллер) сама выбирает сочетания Request, DataSource, Process Strategy. Хотя, можно передать этот выбор куда то еще,

и PHP вариант если вам так удобней

<?php

// БАЗОВЫЕ ТИПЫ
abstract class Request {}

class OrderRequest extends Request {
    public function __construct(
        public readonly string $customerName,
        public readonly string $address,
        public readonly array $items
    ) {}
}

class ReturnRequest extends Request {
    public function __construct(
        public readonly int $orderId,
        public readonly string $reason
    ) {}
}

abstract class ProcessResult {}

class SuccessResult extends ProcessResult {
    public function __construct(
        public readonly string $message
    ) {}
}

class ErrorResult extends ProcessResult {
    public function __construct(
        public readonly string $message
    ) {}
}

// ИСТОЧНИКИ ДАННЫХ
abstract class DataSource {}

class ConsoleDataSource extends DataSource {}

class FileDataSource extends DataSource {
    public function __construct(
        public readonly string $path
    ) {}
}

// Построитель запросов
class RequestBuilder {
    public static function buildFromConsole(): Request {
        echo "Enter name:\n";
        $name = trim(fgets(STDIN));
        echo "Enter address:\n";
        $addr = trim(fgets(STDIN));
        echo "Enter items:\n";
        $items = explode(' ', trim(fgets(STDIN)));
        
        return new OrderRequest($name, $addr, $items);
    }

    public static function buildFromFile(string $path): Request {
        $lines = file($path, FILE_IGNORE_NEW_LINES);
        $items = explode(' ', $lines[2]);
        
        return new OrderRequest($lines[0], $lines[1], $items);
    }
}

// ТИПЫ ОБРАБОТКИ
enum ProcessType {
    case BasicProcess;
    case AdvancedProcess;
}

// Обработчик заказов
class OrderProcessor {
    public static function basicProcessOrder(OrderRequest $request): ProcessResult {
        echo "Basic processing for: {$request->customerName}\n";
        return new SuccessResult("Basic processing complete");
    }

    public static function advancedProcessOrder(OrderRequest $request): ProcessResult {
        echo "Advanced processing for: {$request->customerName}\n";
        
        if (!self::checkInventory($request->items)) {
            return new ErrorResult("Items not in stock");
        }

        $discount = self::calculateDiscount($request->items);
        echo "Discount: $discount\n";
        return new SuccessResult("Advanced processing complete");
    }

    private static function checkInventory(array $items): bool {
        return true;
    }

    private static function calculateDiscount(array $items): float {
        return count($items) > 3 ? 0.1 : 0.0;
    }
}

// СТРАТЕГИИ ПРОЦЕССА
abstract class ProcessStrategy {}

class ConfigBasedStrategy extends ProcessStrategy {
    public function __construct(
        public readonly bool $useAdvanced
    ) {}
}

class ABTestStrategy extends ProcessStrategy {}

// Селектор процесса
class ProcessSelector {
    public static function selectByConfig(bool $useAdvanced, Request $request): ProcessType {
        return $useAdvanced ? ProcessType::AdvancedProcess : ProcessType::BasicProcess;
    }

    public static function selectByAB(): ProcessType {
        return rand(0, 1) ? ProcessType::AdvancedProcess : ProcessType::BasicProcess;
    }
}

// КОНТРОЛЛЕР
class Controller {
    public function processRequest(DataSource $source, ProcessStrategy $strategy): ProcessResult {
        // 1. Построение запроса из источника
        $request = match(true) {
            $source instanceof ConsoleDataSource => RequestBuilder::buildFromConsole(),
            $source instanceof FileDataSource => RequestBuilder::buildFromFile($source->path),
            default => throw new \InvalidArgumentException("Unknown data source")
        };

        // 2. Выбор типа обработки
        $processType = match(true) {
            $strategy instanceof ConfigBasedStrategy => 
                ProcessSelector::selectByConfig($strategy->useAdvanced, $request),
            $strategy instanceof ABTestStrategy => 
                ProcessSelector::selectByAB(),
            default => throw new \InvalidArgumentException("Unknown strategy")
        };

        // 3. Обработка
        if ($request instanceof OrderRequest) {
            return match($processType) {
                ProcessType::BasicProcess => OrderProcessor::basicProcessOrder($request),
                ProcessType::AdvancedProcess => OrderProcessor::advancedProcessOrder($request),
            };
        }

        throw new \InvalidArgumentException("Unsupported request type");
    }
}

// ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ
$controller = new Controller();

// Тест разных источников данных
echo "=== Test Different Data Sources ===\n";
$result1 = $controller->processRequest(
    new ConsoleDataSource(),
    new ConfigBasedStrategy(false)
);
var_dump($result1);

$result2 = $controller->processRequest(
    new FileDataSource("order.txt"),
    new ConfigBasedStrategy(false)
);
var_dump($result2);

// Тест разных способов обработки
echo "\n=== Test Different Processing Types ===\n";
$result3 = $controller->processRequest(
    new ConsoleDataSource(),
    new ConfigBasedStrategy(false)
);
var_dump($result3);

$result4 = $controller->processRequest(
    new ConsoleDataSource(),
    new ConfigBasedStrategy(true)
);
var_dump($result4);

// Тест разных стратегий выбора
echo "\n=== Test Different Selection Strategies ===\n";
$result5 = $controller->processRequest(
    new ConsoleDataSource(),
    new ConfigBasedStrategy(true)
);
var_dump($result5);

$result6 = $controller->processRequest(
    new ConsoleDataSource(),
    new ABTestStrategy()
);
var_dump($result6);

Вы сейчас развенчали фанатиков Дяди Боба. Я и сам наблюдал обертки над готовыми абстракциями: efcore, di (mediador) и даже ILogger в Asp.NET. Но красный кружочек по центру зелёного треугольника на вашей схеме как раз опять отрицает все компромиссы. Нет, тем и сложна наша работа, что нужно искать идеальный баланс. И большой сложный монолит должен болтаться где-то посередине сомнительной области.

Это очень интересная тема, и по моему мнению автор этой статьи скорее не прав, в том что SOLID ухудшает читаемость или вообще что-либо.

Первое - не стоит пытаться понять примеры авторов книг про SOLID буквально. В книгах всегда есть две беды: невозможность привести настоящий пример ввиду ограниченности размеров книги и терпения читателя и вместо этого всегда демонстрация концепций на примерах, где они сплошной овер-кил; и вторая беда - чтобы концепция прогремела где-то кроме академических кругов, концепцию в книгах по IT всегда доводят до абсурда с элементами вбрасывания сами знаете чего на вентилятор.

Двигаемся дальше. А что вообще такое SOILD? Отбросим лишнее: "L" - это про правильное наследование, которое всегда лучше неправильного, там не с чем спорить. I - про адекватные задаче интерфейсы, опять таки при разумном использовании там не с чем особенно спорить.

Остается S-O-D, которые в принципе все об одном и том же: программный код должен быть декопозирован таким образом, чтобы он мог развиваться в будущем с наименьшими затратами. И для этого есть по сути только одно средство: сделать код чуть более абстрактным и общим, чем это нужно в текущий момент. Эти 3 буквы поясняют с разных сторон этот принцип избыточной абстрактности:

O - центральное пожелание, чтобы классы были спроектирована так, чтобы их не требовалось переделывать, а только заменять или расширять (как этого достичь здесь не говорится, но в разных источниках используется два подхода к формулированию принципа);

S - одно из средств достижения O: если у нас есть две возможные причины для переделки кода в будущем, то не не должно быть класса, который нужно переделывать и по первой причине и по второй одновременно; если для класса есть такая необходимость - это признак того, что он возможно недостаточно декомпозирован;

D - второе средство достижения O: зависимость на абстракции подразумевает, что границы между классами/модулями должны быть достаточно абстрактными и ясными, что упростит переиспользование классов, замену классов как бонус - тестирование.

И подходим к главному, как это использовать.

Использовать SOLID нужно исключительно по назначению - как средство борьбы со сложностью кода при дальнейшем развитии продукта. А бороться со сложностью нужно конечно же там, где она есть.

Если вам нужно написать сайт с почти однослойной логикой (типа извлекли информацию, показали, изменили, сохранили) в котором есть контроллеры+сервисы+хелперы, то самый лучший способ это реализовать - действовать в рамках традиций и существующих фреймворков. Вся сложность здесь будет на стороне многочисленности требований к продукту а не в сложной архитектуре кода. Высасывать из пальца SOLID в "билдерах DTO" это пустая трата времени.

Но в какой-то момент задача может стать сложнее и тогда SOLID просто неизбежен и в этом случае он никогда не ухудшит читаемость кода или его производительность.

Попробую привести настоящий а не условный пример. Допустим, у нас есть некое хитрое хранилище данных организованное в SQL-ной базе, схему которого пользователь определяет динамически в UI с возможностью добавления хитрых связей между объектами, вычисляемых полей и так далее. Для начала мы пишем API, которое достает эту информацию и отдает в UI. На втором этапе оказалось, что нам нужно паблик REST API для этих же данных, а на третьем - что нужно еще и GraphQL API, а на четвертом мы уходим с SQL-ной базы на NoSQL.

Понятно, что можно написать последовательно три абсолютно разных макаронины кода для каждой из этих целей, а потом менять их все три на NoSLQ и понятно что это будет плохой код.

Выход здесь - уже на этапе когда мы еще не знаем что будет нужен REST API, GraphQL и NoSQL писать переиспользуемый код. Для этого код должен быть попросту несколько более абстрактным чем это нужно на первом этапе и принципы SOLID дают неплохую поддержку в решении этой задачи.

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

Нам нужно читать данные из хранилища, значит нужны классы, которые делают исключительно это.

Принцип хранения может поменяться (например, можно по разному хранить наследуемые сущности), тогда нам придется заменить один или несколько классов.

Может поменяться SQL-ная база - опять нам нужно будет заменить один класс.

Требования API могут поменяться (специализированное API для UI -> REST AP -> GraphQL), но у нас уже есть достаточно абстрактные классы, которые умеют фильтровать данные и извлекать связанные объекты. Значит в будущем мы просто расширяем этот набор классов, и так далее.

Все перечисленные особенности архитектуры не сделают код менее читаемым. Скорее наоборот, он станет проще для понимания, потому что в нем будут отражены более базовые и более очевидные концепции работы с данными.

Забавно автор от SOLID перешёл к DDD. При том что первое это набор принципов организации кода, а второе это стратегия проектирования всей системы. Вещи несравнимые.

SOLID влияет на менеджмент зависимостей. DDD влияет на читабельность. Смысл статьи как раз и был в том, чтобы в небольших проектах сначала думать о читабельности, а уже потом о менеджменте зависимостей. А советы из DDD - это первое, о чем я думаю, говоря про читабельность (clean code уже по умолчанию)

Sign up to leave a comment.

Articles