Pull to refresh
25
0
Николай@NikolayPyanikov

Программист

Send message

Спасибо за статью, много интересного. Прорекламирую Pure.DI :)

Но ценность таких тестов, где всё замокано - сомнительна.

Потому это и модульный тест. Вы тестируте ваш модуль, а не сторонние библиотеки.

Статья из цикла "Накинь на вентилятор" просто поржать, прибегут миньёны буду комменты писать. Посмотрел только реализацию полиморфизма с "фабриками" и ещё какой... стало грустно. Где этому учат?

Я разделяю вашу точку зрения. DI контейнер можно использовать как ServiceLocator. И чаще всего он так и и используется. В альтернативном, идеальном случае приложение, в самом начале выполнения, должно использовать DI контейнер чтобы получить один инстанс (корень композиции) на все приложение и больше не обращаться к DI.

В Unity - инфраструктура занимается созданием объектов сценариев. В методе инициализации Start() мы вызываем:

Composition.Shared.BuildUp(this);

Этот код передает в метод BuildUp объект конкретного (и даже не базового) типа. Т.е. мы просим инфраструктуру проставить объекту конкретного типа его зависимости. И это точно не ServiceLocator, так как объект не вызывает методов типа T GetServive<T>() и может запросить там любой T.

Это сделано в силу особенностей Unity.

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

Полностью с вами согласен

 не проще ли тогда просто использовать его как сервис локатор

Как сервис локатор работать не будет, Pure.DI - антипод сервис локатору. Только зарегистрированные корни можно получать, так как только для них есть код создания.

То есть отличие от других контейнеров это кодогенерация?

Это отличие порождает множество других. Есть свои плюсы и минусы.

Спасибо за идею! Подумаю как это можно реализовать

Не совсем понял зачем нужен этот метод, если его можно заменить на:

Composition.Shared.BuildUp(obj);

Если вдруг не будет нужной настройки, то не будет метода "BuildUp" и будет ошибка компиляции. Все разумно и безопасно

Да, так будет работать. Но этот код нужно написать вручную

BuildUp(Clock clock) принимает аргументом экземпляр конкретного типа (не базового). И в зависимости от этого типа выполняет внедрение набора зависимостей.

Вы можете использовать несколько Builder<>() что бы создать несколько перегрузок метода BuildUp(...). Но это всегда будет определенный набор, который соответствует набору изBuilder<>(). Это сделано потому что сгенерировать BuildUp() для всех типов объектов на этапе компиляции невозможно. Этим Pure.DI отличается от классическим библиотек DI. Pure.DI на этапе компиляции должен знать какие проверки выполнять и какие методы BuildUp(...)сгенерировать.

Да, так работает тоже

Можно запустить 1 большой интеграционный тест который пройдет по 50% путей логики. Но при этом проверить, например, только то что в результате появился какой-нибудь файл в директории. Но то что все отработало как нужно не проверить. То-же самое и с "модульными" тестами. Можно выполнить модульный тест но не проверить то что нужно.

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

Когда люди пишут тесты им они часто тестирую только базовые сценарии. Но на проде чаще падают НЕ базовые сценарии. Поэтому лучше тестировать все ветки, если это возможно.

Я не говорю про покрытие, я согласен что эта метрика говорит мало о качестве тестирования. Покрытие кода хорошо продавать менеджерам, поэтому его часто мерят в интеграционных тестах, которые покрывают 50% сразу, но проверяют 1 сценарий, хоть и базовый :)

обладает такой же ужасной читаемостью как и лапша из функций в 1000 строк

Я бы с вами не согласился совсем. Большие функции это прямой путь к деградации кода. Тестов на них не напишешь, так как ветвлений логики слишком много, а каждое ветвление это умножение кол-ва тестов на 2. Что с этим делать? Найти ошибку это потратить на отладку много времени. Но что бы отладить нужно настроить среду из 100500 различных условий.

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

Я не понимаю ваших аргументов. Возможно, вам не приходилось работать с хорошо написанным кодом ((

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

Но я бы изначально сделал именно так, например, потому что смогу писать модульные тесты на классы. Например в тестах на Decryptor я не хочу проверять чтение реальных файлов, я хочу проверять лишь работу метода Decrypt . Если объединить функционал классов Decryptor и FileReader , как предложил автор статьи, то у меня будет гораздо больше модульных тестов, они буду сложнее, мне нужно создавать реальные файлы, тесты будут более ломкие и их будет сложнее поддерживать и рефакторинг будет сложнее.

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

Вот репо с кодом примерно в таком стиле

Да конечно. По рукой только Rider, так что вот на C#:

new FileReaderTests().Test();
new DecryptorTests().Test();

var compositionRoot = new Consumer(new FileReader(), new Decryptor());
compositionRoot.Run();

interface IFileReader
{
    Data Read(string filePath);
}

interface IDecryptor
{
    Data Decrypt(Data data);
}

class Data
{
    public Data(string data)
    {
        Val = data;
    }

    public string Val { get; }
}

class FileReader : IFileReader
{
    public Data Read(string filePath)
    {
        return new Data("abc");
    }
}

class Decryptor: IDecryptor
{
    public Data Decrypt(Data data)
    {
        return new Data("decrypted " + data.Val);
    }
}

class Consumer
{
    private readonly IFileReader _fileReader;
    private readonly IDecryptor _decryptor;

    public Consumer(IFileReader fileReader, IDecryptor decryptor)
    {
        _fileReader = fileReader;
        _decryptor = decryptor;
    }

    public void Run()
    {
        var encriptedData = _fileReader.Read("mydatafile.txt");
        var decriptedData = _decryptor.Decrypt(encriptedData);
        Console.WriteLine(decriptedData.Val);
    }
}

class FileReaderTests
{
    public void Test()
    {
        var fileReader = new FileReader();
        var data = fileReader.Read("mydatafile.txt");
        if (data.Val != "abc")
        {
            throw new Exception("test failed");
        }
    }
}

class DecryptorTests
{
    public void Test()
    {
        var decryptor = new Decryptor();
        var data = decryptor.Decrypt(new Data("xyz"));
        if (data.Val != "decrypted " + "xyz")
        {
            throw new Exception("test failed");
        }
    }
}

Я бы предложил заменить на "совет", "рекомендацию" или "предложение". Но мне кажется это и так предполагается

Представил себе интерфейс ОС, где, чтобы и читать и писать файл, потребуется открыть его через два разных интерфейса. Хм, изначальный Паскаль именно так и требовал. И где он сейчас?

Ну так причем тут это ОС? Можно получить файл, а уже для него получить и reader и writer. А с другой стороны множественная реализация интерфейсов почти везде есть, если идти более привычным путем.

Все принципы очень простые и жизненные. Например, ISP довольно просто объяснить так:

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

Да я согласен с вами. Но по моему опыту это не частый сценарий. Чаще всего итератор использую для итерирования и то не напрямую. Ну а что добавили бы еще getCount() удобно ведь, нет count пусть бросит исключение. Это протечка абстракции, которая усложняет жизнь и поставщикам и потребителям. Поставщиков заставляет реализовывать функционал, который может и не нужен да и сложно бывает. Потребителям не понятно как это использовать, т.е. что ожидать: исключения или удаления. Еще и изменяемое состояние, его синхронизация между потоками и т.п. На мой взгляд не удачно получилось

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

Но все же есть вопросы. Если разбить неграмотно, то DIP не позволит? А грамотно это как именно?

Сделает менее вероятным плохой результат

Про "Interface Segregation Principle: Размер имеет значение"

interface Readable {
    String read();
}

interface Writable {
    void write(String data);
}

interface Closeable {
    void close();
}

class FileHandler implements Readable, Writable, Closeable { ... }

ISP поможет уменьшить связанность и значительно упростит рефакторинг. А так же без следования ISP у вас не получится LSP. Например, из какого то источника можно только читать данные. Если не следовать ISP, то вы создадите реализацию для этого источника в котором, на запись будет брошено исключение, или ни чего не произойдет - это костыль, доп. логика обработки исключений или спец сценариев. Хорошо вы делали этот код и знаете куда можно писать а куда нет :), а если придет другой человек ...

ISP часто интерпретируется как "делайте интерфейсы маленькими", что приводит к взрыву количества микроинтерфейсов

альтернатива - говнокод (( вот пример этого :)

public interface Iterator<E>
{
  boolean hasNext();
  E next();
  void remove();
}

remove() в итераторе и живите теперь с этим

1
23 ...

Information

Rating
Does not participate
Location
Санкт-Петербург, Санкт-Петербург и область, Россия
Date of birth
Registered
Activity