Все потоки
Поиск
Написать публикацию
Обновить
29
3

Пользователь

Отправить сообщение

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

Интересно, гоняют ли их по теории как разработчиков при найме. Хотя нас вот гоняют, а потом видишь O(n^4), где можно O(n), и копи-пасту спагетти-кода у людей с опытом 10+ лет.

Парно кодить это не только сложение компетенций

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

Ещё не обязательно иметь одинаковый грейд

Возможно, я окуклился в энтерпрайзе, но уже лет как семь или больше джунов видел только на собеседованиях, безуспешно пытающимися выдать себя за миддлов. Один работодатель гордился корпаративными стажировками, но тех стажеров или их кода ни в одной продуктовой команде в глаза не видели. Работают люди уровня миддл и выше. Процентов 90-95 примерно одного уровня. Другие просто не проходят все этапы найма. Так что предпосылка одинаковых разработчиков продиктована не только стремлением к упрощению, но и личным опытом.

Думаю, что есть проблема в самой модели

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

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

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

В жизни все исполнители всегда обладают всеми знаниями хотя бы на уровне "где-то что-то слышал, сейчас поисковик/ИИ спрошу". И далеко не каждый проскочивший найм и испытательный срок признается, что местами не дотягивает. Как объективно определять нужного исполнителя? Что делать с командой, если на все задачи нужным окажется один и тот же человек?
Кстати, про декомпозицию в статье тоже можно найти.

Про выделение памяти полезно ещё в Diagnostic Tools глянуть на выходе из метода.

static void Main()
{
    var count = 1000000;
    var collection = new ArrayList(count);
    for (var i = 0; i < count; i++)
        collection.Add(i);
}

Выше приведенный код даст помимо ArrayList миллион упакованных Int32.

ArrayList здесь ссылается на Object[], который ссылается на Int32.

Использование List<int> дает другую картину.

static void Main()
{
    var count = 1000000;
    var collection = new List<int>(count);
    for (var i = 0; i < count; i++)
        collection.Add(i);
}

Никаких упакованных Int32. List<Int32> ссылается на Int32[]. Использование памяти значительно сокращено.

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

public class Subject
{
    private int value;
  
    public Subject(int value) => this.value = value;
}

А нужен тривиальный тест, проверяющий присвоение значения полю в конструкторе. Тогда поле делаем видимым для наследника, а в тестопригодном наследнике выставляем наружу:

public class Subject
{
    private int value;
  
    // exposing a private field to subclasses
    // is less harm than making it public
    protected int Value => value;
  
    public Subject(int value) => this.value = value;
}

public class TestableSubject : Subject
{
    // transitively grant client code (i.e.test)
    // access to the private field
    public new int Value => base.Value;
    
    public TestableSubject(int value) : base(value) { }
}

И тестируем спокойно:

[Test]
public void NormalTest()
{
    // Arrange
    int value = 42;

    // Act
    var sut = new TestableSubject(value);

    // Assert
    Assert.AreEqual(value, sut.Value);
}

Правда, такой фокус потом любой пользователь библиотеки повторить сможет.

У .NET есть InternalsVisibleToAttribute и вообще рефлексия:

public class Subject
{
    private int value;

    public Subject(int value) => this.value = value;
}

public class TestableSubject : Subject
{
    public int Value => (int)typeof(Subject).GetField("value",
        BindingFlags.NonPublic | BindingFlags.Instance)
        .GetValue(this);

    public TestableSubject(int value) : base(value) { }
}

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

Тесты, о которых Вы пишете - то, к чему надо стремиться. В рафинированном виде такие встречаются только в обучающих материалах по TDD. В жизни обычно приходишь на новый проект, смотришь на юнит тесты и ужасаешься. В некоторых компаниях их до сих пор вообще не применяют. А даже там, где юнит тестирование организованно более или менее прилично, могут быть иерархии наследования тестовых классов с переопределениями вспомогательных методов типа SetUp(), TearDown() или CreateSut(), чтобы не было дублирующегося кода. Если впадать в крайности пуризма, то такие методы даже без наследования тестовых классов надо объявлять ересью.

может ли посторонний человек посмотреть на код теста...

Это решается конвенционально. Также как, например, организации тестов по принципу AAA или создание sut только через CreateSut(). Просто вся команда знает (и в проекте даже соответствующая инструкция лежит), что если тест помечен DeclarativeCaseAttribute<TValidator, TScript, TCheck>, то для

[TestFixture]
public class DemoTests : IDeclarativeTest
{
    public void Test<TValidator, TScript, TCheck>(bool expected)
        where TValidator : IValidator, new()
        where TScript : IScript, new()
        where TCheck : ICheck, new() =>
        DefaultDeclarativeTest.Test<TValidator, TScript, TCheck>(expected);

    [DeclarativeCase<OrdinaryValidator, AdvancedAttack, AttackDetected>(false)]
    [DeclarativeCase<AdvancedValidator, AdvancedAttack, AttackDetected>(true)]
    public void TestAdvancedAttack() { }
}

это означает, что фактически тест содержится в void Test<TValidator, TScript, TCheck>(bool) тестового класса, а для

[TestFixture, Declarative]
public class DemoTests
{
    [DeclarativeCase<OrdinaryValidator, AdvancedAttack, AttackDetected>(false)]
    [DeclarativeCase<AdvancedValidator, AdvancedAttack, AttackDetected>(true)] 
    public void TestAdvancedAttack() { }
}

подразумевается, что непосредственно код теста лежит в void Test() аттрибута (пара кликов/хот-кеев - и перед Вами фактическая реализация теста). Если так делать постоянно, то оно начнет восприниматься, как само собой разумеющееся. Многие ли задумываются о том, что происходит под капотом, когда для добавления теста просто пишут [Test], а потом в Test Explorer видят результат его выполнения?

Вообще статья является продолжением другой публикации с пометкой "ненормальное программирование". Я необоснованно предположил, что в сознании читающего этот аттрибут неявно наследуется. Сейчас исправлю. В обеих описывается не как нужно делать, а как можно. Вот пришла человеку в голову занимательная идея, он ее воплотить попытался, глубже в инструменты погрузился, столкнулся с ограничениями, но нашел workaround'ы, а свой опыт здесь описал. Наверное, с названием "Как хакнуть NUnit, чтобы заставить делать странное" воспринималось бы иначе.

Или даже так:

using System;

class Program
{
    static void Main()
    {
        Console.WriteLine(
            from @default in (object)default
            join _ in (object)default
            on default equals default
            where default
            group default
            by default
            into @default
            orderby default
            select default);
    }
}

static class Extensions
{
    public static object Join(
        this object outer,
        object inner,
        Func<object, object> outerKeySelector,
        Func<object, object> innerKeySelector,
        Func<object, object, object> resultSelector)
        => default;

    public static object Where(
        this object source,
        Func<object, bool> predicate)
        => default;

    public static object GroupBy(
        this object source,
        Func<object, object> keySelector,
        Func<object, object> elementSelector)
        => default;

    public static object OrderBy(
        this object source,
        Func<object, object> keySelector)
        => default;

    public static string Select(
        this object source,
        Func<object, object> selector)
        => "Hello, World!";
}

Вот автор вопроса на System.dll не ссылается, но если ему понадобятся LinkedList<T>, ObservableCollection<T> или Timer оттуда, то он её добавит. И тогда можно будет использовать Process из этой библиотеки:

using (var process = Process.Start(
       new ProcessStartInfo(
           "cmd.exe",
           "/c echo WASTED> <file>")
       {
           CreateNoWindow = true,
           UseShellExecute = false,
       }))
    process.WaitForExit();

Кроме Activator'а, там еще почти отсутствует защита от рефлексии. И вместо:

var type = Type.GetType("System.IO.StreamWriter");
using (var sw = (IDisposable)Activator.CreateInstance(type, new object[] { "<file>" }))
    type.GetMethod("Write", new[] { typeof(string) }).Invoke(sw, new object[] { "WASTED" });

можно, например:

Type.GetType("System.IO.File")
    .GetMethod("WriteAllText", new[] { typeof(string), typeof(string) })
    .Invoke(null, new object[] { "<file>", "WASTED" });

И вполне возможно, что на этом список уязвимостей не исчерпывается.

Такого кодоанализатора, на сколько я знаю, нет. Из коробки есть песочница AppDomain. Но в последних версиях фреймворка от нее осталось одно название. На поддержку AppDomain.PermissionSet, таких, напрмер, как FileIOPermission, подзабили. Еще есть Managed Add-In Framework для работы с плагинами. В новых версия фактически тоже не поддерживается.

С одной стороны, да. А с другой, у автора вопроса может быть свой редактор кода с подсветкой синтаксиса, IntelliSense и прочим, и он хочет как-то визуально обращать внимание пользователя на недопустимые вызовы. Мы не знаем точно.

Да, действительно, это скорее про особенности yield и того, что генерируется под копотом.
И код действительно нелинеен. Но, к счастью, и не параллелен, и поэтому наглядно пробегается в дебагере. Выглядит занятно. Особенное если это какое-нибудь нагромождение IEnumerable(IEnumerable(IEnumerable)) типа цепочек LINQ.

И про

Да, вот как раз в этом и дело. По-моему, просто пока не будет первого MoveNext() выполнение метода с yield вообще даже не начнется.

верно подмечено.

Поэтому, кстати, рекомендуют отделять проверки аргументов от yield. Иначе можно получить исключение в несовсем ожидаемом месте.

Пример:

Пусть есть такой код:

static object CreateObject(object arg)
{
    if (arg == null)
        throw new ArgumentNullException(nameof(arg));

    return null;
}

static IEnumerable<object> CreateEnumerable(object arg)
{
    if (arg == null)
        throw new ArgumentNullException(nameof(arg));

    yield return null;
}

static void Test(object obj)
{
    if (obj is IEnumerable enumerable)
    {
        var enumerator = enumerable.GetEnumerator();
        Console.WriteLine(enumerator.ToString());
        Console.WriteLine(enumerator.GetType());
        Console.WriteLine(enumerator.GetHashCode());
        Console.WriteLine(enumerator.Equals(null));
        Console.WriteLine(enumerator.Current ?? "NULL");
        enumerator.MoveNext();
    }
}

Тогда для Test(CreateObject(null)) вывод будет:

Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'arg')
   at TryFinally.Program.CreateObject(Object arg) in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 19
   at TryFinally.Program.Main(String[] args) in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 11

Вызов метода Test не произошел.

А для Test(CreateEnumerable(null)):

TryFinally.Program+<CreateEnumerable>d__2
TryFinally.Program+<CreateEnumerable>d__2
58225482
False
NULL
Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'arg')
   at TryFinally.Program.CreateEnumerable(Object arg)+MoveNext() in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 27
   at TryFinally.Program.Test(Object obj) in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 40
   at TryFinally.Program.Main(String[] args) in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 11

Метод Test не только был вызван, но и спокойно обращался к членам enumerator до вызова enumerator.MoveNext().

Кстати, try-finally с foreach имеет свои особенности.

Допустим, есть метод:

IEnumerable<int> CountDown(int count, int? stopAt = null, int? failAt = null)
{
    try
    {
        while (count > -1)
        {
            yield return count;

            if (count == stopAt)
                yield break;

            if (count == failAt)
                throw new Exception($"Inner failure at {nameof(count)} = {count}");

            count--;
        }
    }
    finally
    {
        Console.WriteLine("Finally!!!");
    }
}

Тогда

foreach (var i in CountDown(3))
    Console.WriteLine(i);

Даст

3
2
1
0
Finally!!!

CountDown(3, stopAt: 1) выведет

3
2
1
Finally!!!

А CountDown(3, failAt: 1)

3
2
1
Unhandled exception. System.Exception: Inner failure at count = 1
   at TryFinally.Program.CountDown(Int32 count, Nullable`1 stopAt, Nullable`1 failAt)+MoveNext() in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 63
   at TryFinally.Program.Main(String[] args) in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 10
Finally!!!

Ранний break finally не помеха:

Код
foreach (var i in CountDown(3, failAt: 1))
{
    Console.WriteLine(i);
    break;
}
3
Finally!!!

Грубо говоря

foreach (var i in CountDown(3))
    Console.WriteLine(i);

развернется в

Код
var enumerator = CountDown(3).GetEnumerator();
try
{
    int i;
    while (enumerator.MoveNext())
    {
        i = enumerator.Current;
        Console.WriteLine(i);
    }
}
finally
{
    enumerator?.Dispose();
}

и результат будет идентичен foreach и для CountDown(3).GetEnumerator(), и для CountDown(3, stopAt: 1).GetEnumerator(), и для CountDown(3, failAt: 1).GetEnumerator(), и для break; после Console.WriteLine(i);.

Если закомментировать enumerator.Dispose();

Код
var enumerator = CountDown(3, stopAt: 1).GetEnumerator();
try
{
    int i;
    while (enumerator.MoveNext())
    {
        i = enumerator.Current;
        Console.WriteLine(i);
        break;
    }
}
finally
{
    // enumerator.Dispose();
}

то в выводе получим просто 3, и можно предположить, что код блока finally метода CountDown вызывается в enumerator.Dispose().

Но если также закомментировать и break;, то вывод будет:

3
2
1
Finally!!!

Если открыть сборку с методом с помощью ILSpy, метод примерно выглядит так:

Код
IEnumerable<int> CountDown(int count, int? stopAt = null, int? failAt = null)
{
    try
    {
        while (true)
        {
            if (count > -1)
            {
                yield return count;
                if (count != stopAt)
                {
                    if (count == failAt)
                    {
                        break;
                    }
                    count--;
                    continue;
                }
                yield break; // <- triggers 'finally'
            }
            yield break; // <- triggers 'finally'
        }
        throw new Exception(string.Format("Inner failure at {0} = {1}", "count", count));
    }
    finally
    {
        Console.WriteLine("Finally!!!");
    }
}

И можно заметить, что код блока finally метода CountDown вызывается после yield break;. Один из них используется явно, а второй добавлен автоматически в конец метода компилятором.

А теперь выполним

var enumerator = CountDown(3, failAt: 1).GetEnumerator();
try
{
    int i;
    while (enumerator.MoveNext())
    {
        i = enumerator.Current;
        Console.WriteLine(i);
    }
}
finally
{
    // enumerator.Dispose();
}

и получим

3
2
1
Unhandled exception. System.Exception: Inner failure at count = 1
   at TryFinally.Program.CountDown(Int32 count, Nullable`1 stopAt, Nullable`1 failAt)+MoveNext() in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 66
   at TryFinally.Program.Main(String[] args) in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 40
Finally!!!

То есть исключение тоже запустило выполнение блока finally.

Таким образом, при использовании try-finally с yield return, блок finally выполняется:

  • в enumerator.Dispose();

  • после исключения

  • и после yield break;

И не выполняется вовсе при невыполнении ни одного из этих условий.

Кстати

var enumerator = CountDown(3, failAt: 1).GetEnumerator();
enumerator.Dispose();

Все равно не запустит finally. Нужен хотябы один вызов enumerator.MoveNext().

Спасибо, добрый человек. Прошел на одном дыхании. Но что-то подсказывает, что она вполне реиграбельна.
Иногда замечаю странную запятую по типу a, b, and c в английских фразах в Duolingo. Всегда думал, что их программисты криво сделали составление предложений по шаблонам. (После примеров вроде «Мальчик ест мороженое с горчицей» или «Женщина разговаривает с конём» можно предположить, что они создаются скриптом.) А это, оказывается, на оксфордский манер.
Ясно. Спасибо, что поделились. Для меня тема с викториной по промо-ролику — тревожный звонок. А по кодингу фидбэк сразу или «мы перезвоним»?
Когда я учился, дневники были бумажные. И там была суббота, которая почти всегда пустовала. Вот живо представил, как администрация школы могла продавать эти рекламные площади местным ИП, а учителя бы объявления вклеивали. И как только они не додумались? Хоть сейчас предпринимательская мысль вперед шагнула.
Для конечного результата не принципиально, т.к. имеем дело с коммутативностью.
Нет. Пример из реального кода. Типы назывались иначе и имели более сложное поведение и критерий слияния. От лишних деталей я абстрагировался для краткости, и чтобы не отвлекать внимание от главной цели (и NDA ненароком не нарушить). Вот и вышло так игрушечно.
Исходный вариант метода — это, грубо говоря, наивная реализация. Чтобы такое написать, необязательно знать толк в извращениях.
1

Информация

В рейтинге
1 128-й
Зарегистрирован
Активность