Хлеб Маркуса и YAGNI

    imageНедавно в нашей новостной ленте появились два Героя, программисты-пекари – Борис и Маркус. Борис – хороший человек и перфекционист, а Маркус – очень скромный и серый программист, не желающий выделяться. Оба стремятся к лучшему и хотят быть полезными. Но кажется, что Маркус не очень старался.
    Это новая ветка – продолжение. Сегодня сюжетная линия коснется только Маркуса. Он – главный герой.
    Итак, история под катом.

    Оригинальный пост: Как два программиста хлеб пекли

    Вступление

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

    Сегодня я попытаюсь реабилитировать положение Маркуса. Пост будет посвящен принципу YAGNI. Это просто пример использования YAGNI и основан на личном опыте. К сожалению, не так много книг, которые показывали бы на примерах, как этот принцип применять. И большинство программистов, кроме чтения книг, рождают эти навыки опытом и трудом. Я считаю, что это именно навыки работы с кодом, а не просто теория. И таким опытом неплохо было бы обмениваться. Буду рад, если узнаю от вас тоже что-то новое. Этот пост посвящен только практике и задача будет рассматриваться на языке C#. Что, к сожалению, может уменьшить потенциальную аудиторию. Но другого выхода я не вижу, потому что теория сама по себе не может быть принята оппонентами, пока они не увидят реальные возможности. UML-диаграммы классов я не люблю. А для Маркуса они, видимо, и не нужны были.

    Прошу, также не очень обращать на оплошности в стиле и коде C#, т.к. я хотел бы просто показать суть подхода, как я понимаю. И не распылять ваше внимание на мелочи. Так же, не буду показывать подход TDD, не буду показывать как писать юнит-тесты, иначе даже для такой простой задачи пост оказался бы очень объемным. Хотя, TDD, конечно, внес бы еще свои коррективы в код. Но нас интересует только – настолько ли плохой код получился бы у Маркуса, если бы он использовал YAGNI, как казалось из оригинального поста.

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

    Начнем. Пройдем по всей цепочке требований. Я — Маркус. Правда, я немного другой Маркус и не в точности веду себя как предыдущий.

    Требование 1
    — Ребята, нам нужно, чтобы делался хлеб

    Анализ

    Чтобы делать хлеб, нужен метод. Что такое хлеб? Это некая сущность. Это не object, иначе его можно спутать с другими объектами. Это не int и не какой-нибудь другой встроенный или созданный тип. Итак, хлеб, это новый отдельный класс. Он имеет состояние или поведение? Лично я знаю, что он состоит из теста (муки, пшеницы или ржи…), что его можно купить в магазине и что его можно есть. Но это мои личные знания. Заказчик пока ни слова не сказал о каком-либо поведении или состоянии. А т.к. я человек ленивый, то хоть я и знаю немного больше, я и кнопку лишний раз не надавлю, без прямого указания на желаемое заказчиком.

    Исходим только из требований: хлеб не имеет состояния и поведения, а также нужен метод для получения хлеба. Сам язык C# к сожалению или к счастью требует еще немного возни, а именно: определить метод в каком-либо классе. Но т.к. заказчик об этом ни слова не сказал, то не заморачиваемся с названием, не заморачиваемся с экземлярами, я пока решил делать статический метод. Если что, всегда успеем переделать. Название выбираю такое, чтобы соответствовало наиболее среднему пониманию требований. Итак, первый код:

    class Bread
    {
    }
    
    class BreadMaker
    {
        public static Bread MakeBread()
        {
            return new Bread();
        }
    }
    


    Требование 2
    — Нам нужно, чтобы хлеб не просто делался, а выпекался в печке

    Анализ

    Иногда заказчики так и норовят указать, как нужно что-то реализовывать, вместо того, чтобы говорить, что они хотят. Желания заказчика о реализации на меня ни физически, ни психологически не действуют. В данном случае это не требование. Заказчик никак не может проверить, где я взял хлеб. И пока не изъявил даже такого желания – проверять. Поэтому – как я получаю хлеб и ему отдаю – не его заказчиковое дело. Но я могу вежливо согласиться с требованием и радоваться, что дальше платят деньги за безделье.

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

    Но всё же сделаем приятное начальству и немного поправим код, чтобы по смыслу соответствовал. А именно: мы уже знаем, что хлеб будет печься в печи, а не покупаться в магазине. Просто переименуем метод получения хлеба:

    class BreadMaker
    {
        public static Bread BakeBread()
        {
            return new Bread();
        }
    }
    


    Требование 3
    — Нам нужно, чтобы газовая печь не могла печь без газа

    Анализ

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

    Попробуем сравнить несколько реализаций.

    Реализация 1.
    enum Oven
    {
        GasOven,
        OtherOven
    }
    
    class BreadMaker
    {
        public static double GasLevel { get; set; }
    
        public static Bread BakeBread(Oven oven)
        {
            return oven == Oven.GasOven && GasLevel == 0 ? null : new Bread();
        }
    }
    


    Сразу бросается в глаза сайд-эффект. Устанавливается уровень газа отдельно от вызова BakeBread(). Раз есть разрыв, то открывается широкое поле возможностей для появления багов. Появление этих багов (жуков) может повредить нашему полю, тогда не будет пшеницы и, следовательно, хлеба.

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

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

    Реализация 2.

    enum Oven
    {
        GasOven,
        OtherOven
    }
    
    class BreadMaker
    {
        public static Bread BakeBread(Oven oven, double gasLevel)
        {
            return oven == Oven.GasOven && gasLevel == 0 ? null : new Bread();
        }
    }
    


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

    Реализация 3. Чтобы согласовать параметры, печи придется сделать кажется всё же классами.
    Причем обычная печет хлеб всегда, а газовая не всегда. Т.е. два класса, виртуальные методы, перегрузка. Но нужно думать, как бы так сделать у них модификаторы доступа, чтобы печи не создавали сами по себе, а пользовались моим методом BakeBread(), иначе, появятся сайд-эффекты.

    И тут меня (Маркуса) осеняет! На данном этапе достаточно сделать так:

    class BreadMaker
    {
        public static Bread BakeBreadByGasOven(double gasLevel)
        {
            return gasLevel == 0 ? null : new Bread();
        }
    
        public static Bread BakeBreadByOtherOven()
        {
            return new Bread();
        }
    }
    


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

    Требование 4
    — Нам нужно, чтобы печки могли выпекать ещё и пирожки (отдельно — с мясом, отдельно — с капустой), и торты.

    Анализ

    Да не вопрос! А если устанавливать в печке уровень температуры, так мы и мороженное сможем в ней выпекать. Шутка. Я, Маркус, стараюсь быть серьезным, — про температуру ни слова. Кто вас, заказчиков знает ))

    Итак, пирожки и торты. Мало того, пирожки двух видов. Но это, мы из жизни знаем, что у пирожка с мясом и у пирожка с капустой больше общего, чем у торта. Но в контексте задачи заказчик об этом не говорил. Он не сказал, что мы будем как-то группировать пирожки отдельно, торты отдельно. Поэтому, пока, исходя из требований – торт ведет себя почти как пирожок с вишнями – все они равноправны. Поведение у них есть? Нет. Состояние есть? Нет. Значит, чтобы их отличать друг от друга, нам вполне достаточно завести перечисление. А забегать наперед, угадывая желания заказчика, которые возникнут завтра, мы принципиально не хотим. Значит – перечисление – самое верное. Скорее всего. Не уверен. Но и не надо. Всегда можно будет переписать, если что.

    Параллельно меняем названия, теперь печем не хлеб, а хлебобулочные изделия.

    public enum BakeryProductType
    {
        Bread,
        MeatPasty,
        CabbagePasty,
        Cake
    }
    
    public class BakeryProduct
    {
        public BakeryProduct(BakeryProductType bakeryProductType)
        {
            this.BakeryProductType = bakeryProductType;
        }
    
        public BakeryProductType BakeryProductType { get; private set; }
    }
    
    class BakeryProductMaker
    {
        public static BakeryProduct BakeByGasOven(BakeryProductType bakeryProductType, double gasLevel)
        {
            return gasLevel == 0 ? null : new BakeryProduct(bakeryProductType);
        }
    
        public static BakeryProduct BakeByOtherOven(BakeryProductType breadType)
        {
            return new BakeryProduct(breadType);
        }
    }
    


    Требование 5
    — Нам нужно, чтобы хлеб, пирожки и торты выпекались по разным рецептам

    Анализ

    Бегло взглянув на код, замечаем, что у нас есть прекрасное перечисление BakeryProductType. Называется оно как-то коряво, как-то по програмистски, не близко к предметной области. Зато, ведет себя как рецепт. Во как бывает! Хлеба и булки пекутся по рецептам, а не по типу. И у нас в конструктор булки попадает все таки, наверное, рецепт. Достаточно переименовать. Единственный непорядок – это свойство-тип «булки». Но я бы смирился. Глядя механически на код и представляя себе предметную область, как некие множества, то я не вижу особой разницы между рецептом и типом. Т.е. рецепт – это прямая причина того, что получится впоследствии. Конечно, в жизни, мы знаем о рецептах немного больше – они не только описывают, что получится. Они так же содержат алгоритм получения. Но кого это волнует? Заказчик говорил об этом? Нет. Значит, в контексте задачи такого и не было. Будет нужен алгоритм – привяжем потом, придумаем что-нибудь.
    Поэтому я мирюсь с тем, что свойство останется типом, а перечисление – рецептом. Не создавать же кучу наследников или другое перечисление из-за свойств нашего разговорного языка. В контексте задачи всё точно. Хотя и не очень красиво. Компромисс?

    public enum Recipe
    {
        Bread,
        MeatPasty,
        CabbagePasty,
        Cake
    }
    
    public class BakeryProduct
    {
        public BakeryProduct(Recipe recipe)
        {
            this.BakeryProductType = recipe;
        }
    
        public Recipe BakeryProductType { get; private set; }
    }
    
    class BakeryProductMaker
    {
        public static BakeryProduct BakeByGasOven(Recipe recipe, double gasLevel)
        {
            return gasLevel == 0 ? null : new BakeryProduct(recipe);
        }
    
        public static BakeryProduct BakeByOtherOven(Recipe recipe)
        {
            return new BakeryProduct(recipe);
        }
    }
    


    Требование 6
    — Нам нужно, чтобы в печи можно было обжигать кирпичи

    Анализ

    Если совсем буквально следовать этому требованию и всем записанным требованиям, то кирпич ничем не отличается от торта или хлеба. Самое смешное, что кирпич у нас сильно больше отличается от пирожка с повидлом, потому что у нас его нет в требованиях, а от пирожка с мясом так себе. Так же, как от хлеба. Поэтому, это требование по YAGNI, сильно утрированно, реализуется всего лишь расширением перечисления рецептов с переименованием всех классов – булки в «продукт печи», коим является и кирпич тоже и т.д. Весь смысл того, как создавать архитектуру классов, заключается в том, как это будут использовать. Именно от того, что считать общим (т.е. состояние и поведение базового класса), а что частным (состояние и поведение наследников). Если ни того, ни другого нет, то можно и перечисление. Не страшно не угадать. Перечисление в класс и наследники легко превращается.

    Кто-то из вас увидел ужас в коде? Может такой код сложно тестировать? Да, вроде, код Бориса значительно сложнее тестировать. Объем больше, больше тестов. Больше функционала, чем требуется? Больше тестов.
    Конечно, по всей видимости, в оригинальном посте подразумевалось, что требования были более подробными и каждая фраза уточнялась подробными объяснениями. Но жанр YAGNI требует не додумывать.

    Давайте дальше поиграем в требования.

    Требование 7
    — Как вы не досмотрели? Не каждая печь может жечь кирпичи. Для этого нужна специальная печь.

    Анализ

    Ну и ладно. Убираем из перечисления (рецептов?) кирпич и возвращаем имена. Создаем отдельный пустой класс Brick. И новый метод:

    public static Brick MakeBrickByFurnace()
    {
        return new Brick();
    }
    


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

    Далее, заказчик решил играть против Маркуса.

    Требование 8
    — Каждый рецепт должен содержать продукты и их количество (вес). Рецепты в требовании прилагаются.

    Анализ

    Первая кровь, которую так ждал Борис.

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

    Продукты для рецепта – очевидно – перечисление. Сам рецепт уже содержит не только название того, что он создаст (или что то же самое, название себя), но и набор продуктов с их количеством. Но при этом, замечаем, что с конкретным рецептом связан строгий набор продуктов и он не меняется. (Еще раз вспоминаем, что пост о YAGNI – никаких «а вдруг заказник захочет, чтобы менялось»! Никаких вдруг, сегодня – это сегодня, а завтра – это завтра).

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

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

    И пару еще мыслей. Т.к. на данный момент, рецепты в коде – всего лишь заданное перечисление и оно задано еще до компиляции, то при не имении других требований, по-видимому, это поведение должно остаться. Из этого, следует, что нам должны быть доступны все рецепты и они заданы прямо в коде. Создать новый, без расширения перечисления нельзя. Отсюда, похоже, нужно делать класс Recipe, предварительно переименовав перечисление с таким именем в RecipeName. Мир такой изменчивый. Теперь перечисление всего лишь указывает на рецепт и позволяет его выбрать, но не характеризирует его в полной мере.

    Чтобы удовлетворить условия выше, достаточно так:

    public enum RecipeName
    {
        Bread,
        MeatPasty,
        CabbagePasty,
        Cake
    }
    
    public enum RecipeProduct
    {
        Salt,
        Sugar,
        Egg,
        Flour
    }
    
    public class Recipe
    {
    
        private Recipe() { }
    
        public RecipeName Name { get; private set; }
    
        public IEnumerable<KeyValuePair<RecipeProduct, double>> Products { get; private set; }
    
        private static Dictionary<RecipeName, Dictionary<RecipeProduct, double>> predefinedRecipes;
        static Recipe()
        {
            predefinedRecipes = new Dictionary<RecipeName, Dictionary<RecipeProduct, double>> 
                { 
                    {
                        RecipeName.Bread, new Dictionary<RecipeProduct, double>
                                            { 
                                                {RecipeProduct.Salt, 0.2},
                                                {RecipeProduct.Sugar, 0.4},
                                                {RecipeProduct.Egg, 2.0},
                                                {RecipeProduct.Flour, 50.0}
                                            }
                    }
    		   ..................
                };
        }
    
        public static Recipe GetRecipe(RecipeName recipeName)
        {
            Recipe recipe = new Recipe();
            recipe.Name = recipeName;
            recipe.Products = predefinedRecipes[recipeName];
    
            return recipe;
        }
    }
    


    Ничего и ломать не приходится. Для создания продукта пока всего лишь достаточно имени рецепта. Там ничего не меняем. Надо будет, передадим и сам рецепт.

    В данном коде мы всего лишь из рецепта-перечисления сделали класс и связали имя рецепта с составляющими его продуктами. Надо будет последовательность действий в рецепте выразить, точно также его можно «прикрутить». Надеюсь, это понятно и никакого спагетти-кода не будет. Появится отдельное поведение у классов – легко класс Recipe становится базовым и появляются наследники. Но мы об этом не думаем. У нас YAGNI, нам такого не говорили делать. Но мы этого и не боимся.

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

    Вступаем с полемику:
    — Как изменяться?
    — Считаем, что повар не знает рецептов и может экспериментировать. Вы сделали рецепты фиксированными? А он хочет добавлять разное количество яиц, сахара и т.д.
    — А что за булку мы в таком случае получим? Мы же должны что-то получить? Хлеб, пирожки или торт? Очевидно, если рецепты будут отличаться, то повар будет печь нечто другое.
    — Я думаю, что торт бывает разный по вкусу, более сладкий, менее сладкий. Также и хлеб. Значит, в каких-то пределах могут рецепты отличаться, но мы получим какой-то продукт из списка.
    — Т.е. чтобы узнать, что мы получим, нам надо искать наиболее близкий рецепт к тому списку продуктов в рецепте повара?
    — Да.


    Анализ

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

    Теперь у нас два конструктора:

    private Recipe() { }
    
    public Recipe(IEnumerable<KeyValuePair<RecipeProduct, double>> products) 
    {
        Dictionary<RecipeProduct, double> copiedProducts = new Dictionary<RecipeProduct, double>();
    
        foreach (KeyValuePair<RecipeProduct, double> pair in products)
        {
            copiedProducts.Add(pair.Key, pair.Value);
        }
    
        this.Products = copiedProducts;
    }
    


    Во втором конструкторе создается копия. Это из-за свойств C# — передавать по умолчанию ссылки. У пользователя класса ссылка останется и если не сделать копию, он сможет позже менять ингредиенты рецепта. Что не входит в наши планы.

    Также, в этом посте, я стараюсь поменьше не использовать лямбды и женерики, оставаясь в рамках стандартного ООП. Чтобы большей аудитории было понятно, что я делаю. Код мог бы быть написан и по-другому и проще. Но моя цель – описать сам принцип YAGNI и какие-то способы оценки кода, а не показывать разные возможности шарпа. Конечно, способы оценки зависят от языка и от его возможностей.

    Второй конструктор, который для пользователей, не устанавливает значение свойства — имени рецепта. Т.к. у нас продукты передаются в конструктор и не могут меняться, то там же и вычислить как-то близость. Точнее, особо «умные» поменять смогут, но не будем страдать паранойей. Считаем, что разработчики адекватные и стоят на позиции созидания, а не разрушения.

    Нужно написать какой-то метод близости. Заказчик не уточнял, поэтому напишем наиболее простой, с методом наименьших квадратов. Учитывая, что каждый ингредиент имеет разный «вес». Пока запишем какие-то веса, которые позже можно настроить.

    Код приблизительно такой:

    private double GetDistance(Recipe recipe)
    {
        Dictionary<RecipeProduct, double> weights = new Dictionary<RecipeProduct, double>();
        weights[RecipeProduct.Salt] = 50;
        weights[RecipeProduct.Sugar] = 20;
        weights[RecipeProduct.Egg] = 5;
        weights[RecipeProduct.Flour] = 0.1;
    
        double sum = 0.0;
        foreach(KeyValuePair<RecipeProduct, double> otherProductAmount in recipe.Products)
        {
            var productAmounts = this.Products.Where(p => p.Key == otherProductAmount.Key);
    
            if (productAmounts.Count() == 1)
            {
                sum += Math.Pow(productAmounts.First().Value - otherProductAmount.Value, 2) 
                                     * weights[otherProductAmount.Key];
            }
            else
            {
                return double.MaxValue;
            }
        }
    
        return sum;
    }
    
    private RecipeName GetRecipeName()
    {
        IEnumerable<Recipe> etalons = ((RecipeName[])Enum.GetValues(typeof(RecipeName)))
            .Select(recipeName => Recipe.GetReceipt(recipeName));
        IEnumerable<KeyValuePair<RecipeName, double>> recipeNamesWithDistances = etalons
            .Select(e => new KeyValuePair<RecipeName, double>(e.Name, GetDistance(e)));                
        double minDistance = recipeNamesWithDistances.Min(rd => rd.Value);
                    
        if (minDistance == double.MaxValue)
        {
            throw new Exception("Подходящий рецепт не найден");
        }
    
        return recipeNamesWithDistances.First(rd => rd.Value == minDistance).Key;
    }            
    


    И в вызов конструктора соответственно, добавляется присвоение имени:

    public Recipe(IEnumerable<KeyValuePair<RecipeProduct, double>> products) 
    {
        Dictionary<RecipeProduct, double> copiedProducts = new Dictionary<RecipeProduct, double>();
    
        foreach (KeyValuePair<RecipeProduct, double> pair in products)
        {
            copiedProducts.Add(pair.Key, pair.Value);
        }
    
        this.Products = copiedProducts;
        this.Name = GetRecipeName();
    }
    


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

    Как видно, совершенно не страшно переделывать код для соответствия такому текущему требованию. Мы особо ничего и не ломали. А просто расширили. Времени на доработку не больше, чем, если бы мы заранее предугадали. Но предугадывать и не угадать – действительно страшно. Представьте себе этот, вроде простой код, по этому требованию, но сделанный раньше. Это просто монстр, требующий лишнего тестирования. А сейчас он написан обосновано.

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

    Я здесь уже завязался на конкретный алгоритм, хотя заказчик именно этот не требовал. YAGNI это или нет? Тут мы разбираемся по ситуации. Возможно, заказчику сразу нужен видимый результат. Чаще так и бывает. Поэтому нужен хоть какой-то алгоритм. Но если позже нам понадобится другой алгоритм, ничего не стоит заменить этот на другой. Или даже написать несколько и выбирать. Или даже у пользователей кода брать делегат, который будет делать сравнение на близость.

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

    public class BakeryProduct
    {
        public BakeryProduct(Recipe recipe)
        {
            this.BakeryProductType = recipe.Name;
        }
    
        public RecipeName BakeryProductType { get; private set; }
    }
    


    И:

    public static BakeryProduct BakeByGasOven(Recipe recipe, double gasLevel)
    {
        return gasLevel == 0 ? null : new BakeryProduct(recipe);
    }
    
    public static BakeryProduct BakeByOtherOven(Recipe recipe)
    {
        return new BakeryProduct(recipe);
    }
    


    Здесь рефакторинг совсем не сверхнапряжный.

    Требование 10
    Коварный заказчик изучил каким-то образом наш код и ищет самое больное место, к чему наш код совершенно не готов. Ведь у нас должно уже 9 раз получиться нечитаемое спагетти. Мы ж писали без плана и наш код страшно не гибкий. Видимо Борис вступил с ним в сговор и они ищут слабые места.

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

    Анализ

    До этого, у нас печи изготавливали иногда не связанные вещи. Кирпич, например. Он не имеет общего предка с булками. И верно, откуда мы могли знать, что у них общего? Нет, мы конечно, в жизни знаем и про кирпичи и про хлеб. Но мы не могли полагать, что заказчик захочет рассматривать их позже именно как товар. Потом, у нас три разных метода, которые не очень пересекаются. Нет одного метода, который бы выпускал любую вещь. А разве должен был? Предка не было. Не должен.

    Потом, иерархия классов – это скорее зло. Как и любая лишняя строчка кода. То, что у нас на данный момент до этого требования были отдельные методы, возвращающие конкретные классы – лучше, безопаснее. Представим, что мы бы сразу сделали печь, которая делает нечто через единый метод. Как делал Борис. И вот она выпускала бы Product. Что есть Product? Это базовый класс. Который имеет базовое поведение и состояние. Но нам, допустим, в пользовательском коде понадобилось сделать именно пирожок. Вот, печь, изготавливает продукт. Не пирожок. Т.е. это пирожок на самом деле, только когда мы из печи достаем, он называется продуктом. И вдруг нам понадобилось узнать, сколько в нем мяса. А это поведение в наследнике, а именно – в пирожке с мясом. Что нам в таком случае делать? Приводить ссылку на продукт к ссылке на его настоящий тип.

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

    Т.е. преждевременная гибкость – это не только не полезно, это вредно.

    Но сейчас нам это уже понадобилось по-настоящему.
    Итак, сами себе формулируем сжато требования, которые сложились на данный момент: «Булки готовятся по рецепту, кирпичи нет. Кирпичи могут изготавливаться только в специальной печи, причем в ней могут изготавливаться только кирпичи (не булки). Нужен единый механизм загрузки сырья и получения товара. Булки и кирпичи являются товаром, который имеет цену.»

    Последнее реализуется просто. У булок и кирпичей появился общий предок – товар.

    public abstract class Article
    {
        public double Price { get; private set; }
        public Article(double price)
        {
            this.Price = price;
        }
    }
    


    Наследуем классы:

    public class BakeryProduct : Article
    {
        public BakeryProduct(Recipe recipe, double price): base(price)
        {
            this.BakeryProductType = recipe.Name;
        }
    
        public RecipeName BakeryProductType { get; private set; }
    }
    
    public class Brick: Article
    {
        public Brick(double price) : base(price) { }
    }
    


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

    Приблизительно так:

    public static BakeryProduct BakeByOtherOven(Recipe recipe, double price)
    {
        return new BakeryProduct(recipe, price);
    }
    


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

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

    Как видим, нет никаких сложностей преобразовывать архитектуру на ходу, по мере поступления требований. Нет и никаких сложностей покрывать это тестами. Методы не большие. Мало того, такой код легче покрывать тестами, потому что его меньше. Код без предугадываний всегда находится в более или менее гибком состоянии во всех направлениях. При этом он достаточно жесткий. Код Бориса сполз с дистанции еще на трети пути. А этот можно преобразовывать до бесконечности. Чтобы преобразования были возможны, нужно всегда проводить рефакторинг. Принцип YAGNI говорит только о том, что нужно реализовывать только минимальный функционал. Но ни в коем случае он не говорит, что если код работает, то его не трогать. На рефакторинг и юнит-тесты принцип YAGNI не распространяется. Только тогда такая технология разработки работает.

    Естественно, код в посте не идеален. Цель была только показать принцип. Я уверен, что со многими частностями в том, как я делал анализ требований, и сторонники YAGNI не будут согласны. У каждого свой личный опыт. У каждого свои методы и приемы. И это еще значительно зависит от языка программирования, потому что он — средство выражения мыслей.
    Поделиться публикацией

    Похожие публикации

    Комментарии 46
      +5
      Спасибо. Было бы интересно почитать такой анализ для большого проекта, где пятисотстраничный талмуд-ТЗ дают вместо первого требования, а апдейты к нему (шестисотстраничные) вместо остальных. (Ответ типа «ничего не меняется» тоже сгодится. :))
        +3
        Где-то то же самое. Так, как в посте, возможно начать проект. Далее он где-то так и развивается. Я не люблю UML, как и многие, кто следует такому YAGNI. Но общие черты, алгоритмы, документируются. После кодирования. Для проектирования не особо нужно.
        А для передачи знаний — сгодится. Но лучше всего должны документироваться требования. Т.е. как-то собираться, структурироваться. И они связаны с автоматическими тестами.

        Тогда, имея минимальное понимание всего, что хотят пользователи и приблизительное о коде, можно подхватить эстафету и дальше так развивать продукт.
        +8
        никогда бы не подумал, что мою статью можно воспринять так всерьёз! я даже не знаю, что это за чувство такое, открываешь Хабр — а тут целая огромная статья по мотивам твоей
          +1
          Вам спасибо за ту первую статью. Только в первом примере класс менеджер лишний. Вполне хватит класса хлеба с конструктором.
            +1
            Нет, не лишний. При «изготовлении» хлеба возможно надо провести кучу вычислений и заполнить состояние.
            Готовому хлебу совершенно не нужно знать, как его приготовили. А то откроет кто-то исходник класса «Хлеб», а там — целая пекарня.
            +2
            Да, спасибо за вашу статью. Я долго решался, писать свою или нет. У вас там сразу начали кидать решения, в том числе и на шарпе.

            Но решил писать отдельную, т.к. тема у меня другая — YAGNI. А у вас отличная идея, как могут приходить требования, разрушая планы.

            Я часто общался и спорил с коллегами по поводу целесообразности YAGNI. Но это никак нельзя объяснить просто на пальцах. Это навыки оценки и рефакторинга кода. А говорить о теории можно сколько угодно. У оппонентов возникают сомнения не в том, что это, а в том — как это вообще возможно.

            Вот для «как» нужен пример. С головы такой отвлеченный и удачный пример тяжело придумать. Даже если каждый день так кодишь. Обычные задачи либо слишком узконаправлены, нелаконичны и не подчеркивают то, что хочется сказать.

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

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

            Вообще спасибо автору за хорошую статью. Однако в истории про Бориса и Маркуса речь шла не о YAGNI, а о KISS — там шла речь о том, что система должна быть простой. Борис ничего лишнего, не относящегося к требованиям, не реализовывал.
            Вот в этой книжке приведен замечательный пример реализации данных принципов на реалистичном примере (реализация программы по работе с боулингом).
              0
              Спасибо за книгу.
              Ошибся, ответ ниже
                0
                Любые классы вообще поменять проще пареной репы, пока их никто не использует.
                Во избежании неверного толкования (которое имело место), поясняю, что имею ввиду, что созданные в рамках статьи классы не используют другие слои приложения и именно поэтому их вроде бы легко рефакторить.
                Например их наверняка будет использовать UI системы.
                  0
                  Согласен, вот так легко — бац и все поменялось. А в реальной жизни, часто нужно делать так, что бы и старое. и новое работало.
                0
                Книжка отличная. Кстати, есть в продаже более позднее издание 2011 года и в электронном виде: Тыц. P.S. В бумажном виде не осилил: тяжелая, мягкий переплет, все время захлопывается.
                +2
                Спасибо за книгу.

                Любые классы вообще поменять проще пареной репы, пока их никто не использует.


                А кто-то мешает рефакторить код, который использует наши любые классы? Принадлежность кода кому-то — это вредное явление.

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

                KISS и YAGNI по-моему, почти одно и то же. Простота и отсутствие функционала, без которого можно обойтись — близкие понятия. Если говорить о простоте, то очень относительное понятие. Борис делал много лишнего (или вы ошиблись, хотели сказать Маркус?). Откуда он взял микроволновки, откуда повара? То, что ему казалось логично — просто его представления о мире, которые он реализовывал, пытаясь угадать, как пойдет разработка дальше. Т.е. постелить солому туда, где может упасть. И очень быстро забросал соломой весь ландшафт и ходить не смог ))

                В комментариях там много говорилось о некой золотой середине. Т.к. предполагалось, что Маркус не делал рефакторинга и не думал вообще, чем его ветвления обернуться потом. Т.е. неверное понимание YAGNI.
                Как-то там стал вопрос — планировать (Борис) или нет (Маркус). И думали многие о середине. Тут нужна не середина. Однозначно — не планировать. Но и не запускать код, пока с него будет невозможно выбраться.
                  0
                  Еще раз спасибо за интересную статью.

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

                  KISS и YAGNI по-моему, почти одно и то же.
                  Вот я как раз и говорю о том что не стоит смешивать понятия. Они вам кажутся близкими, но это разные явления в принципе.

                  KISS — мы декларируем, что архитектура системы должна быть максимально простой. Это защита от желания разработчиков применить более сложные конструкции чем необходимо. Они это делают чтобы применить какой то паттерн, который изучили. Но это не значит, что не стоит абстрагироваться сильнее.

                  YAGNI — мы декларируем, что не будем реализовывать функциональность, которая нам не нужна. Это в первую очередь относится к требованиям. Защита от желания разработчиков реализовать функционал, который кажется им важным, но не имеет достаточного бизнес значения.

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

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

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

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

                  Возможно Вам стоило бы добавить в вашу замечательную статью границы применения данных принципов.
                    +1
                    Я знаю что вредной может быть высокая связанность слоев, но зависимость в принципе… с чего вдруг. Ваши классы по любому будет кто то использовать — ui например.


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

                    Чем более безличное программирование — тем лучше. Т.е. не важно, что кто-то использует ваши классы или нет. Вы спокойно меняете свои классы и меняете то место, где они используются. Вы одинаковый владелец всех слоев кода, как и все остальные. Мне не нравится ситуация, когда в программировании для бизнеса начинают считать — это гуи-девелопер, это девелопер на стороне СУБД, а этот серверную часть пишет. И вот тогда начинаются всякие нехорошие вещи.

                    В гибких технологиях разработки много внимания уделяется обмену знаниями, ревью кода и чтобы программисты менялись местами (никто не владел куском кода единолично).
                      0
                      Вы говорите о практике экстремального программирования Collective Code Ownership,
                      К Agile этот принцип имеет лишь очень косвенное отношение.
                      А практика Code Review — вообще к Agile никаким боком.

                      Говорить об agile стало так модно — статьи не пишет только ленивый. При всем уважении — настоятельно рекомендую ознакомится с мат частью. И начать рекомендую с Agile Manifesto.
                        0
                        Да, ладно. Как это не имеет? )

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

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

                        А то, что владение кодом плохо, я написал выше, основываясь на своем опыте. Если вы что-то меняете, у вас должна быть возможность менять в любом месте. Конечно, предупреждая других, если кто-то еще работает с той частью. Иначе страх перед изменениями в одном конце системы может породить кривой код или мусор в другом конце.
                          –2
                          Agile — это не собирательное название разных методик.
                          Это методология, основанная на правилах записанных в Agile Manifesto и специфическом наборе ценностей.

                          Менеджерские и девелоперские методики, которые удовлетворяют данным ценностям и правилам — мы называем Agile методиками. Да — экстремальное программирование в принципе Agile методика, но конкретно практика коллективного владения кодом не является прямым следствием правил Agile-а, а лишь косвенно с ними связана.

                          Коллективное владение и инспекция кода — никакие не пожелания а конкретные практики, которые применяются определенным образом.

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

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

                          Вот и все. Ничего сложного — если оперируешь конкретными понятиями. И я ничего не говорю о том, что хорошо, а что плохо — это механизмы, которые позволяют решать конкретные проблемы, в конкретных обстоятельствах.
                          И использовать их вот так — буздумно — как какие то мутные истины — вот это по настоящему плохо.
                            +1
                            Вы слишком жестко подходите к вопросу.

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

                            Ревью кода, в моем понимании, — это далеко не одобрение. На одобрение можно ложить с прибором. Считаем, что коллектив взрослый и папа никому не нужен. Если начнется одобрение/неодобрение, то начнутся нехорошие вещи:
                            1. Если разработчик, ведущий ревью, по рангу не выше, то неодобряя код может напороться на серьезное сопротивление.
                            2. Если нужно одобрение и не выполняется пункт 1, то тогда появляется некий папа, который ревьювит код и разрешает комитить.
                            Во втором случае не происходит обмена знаниями о коде, о частях системы. Поэтому ревью проводится не для одобрений на основе которых делаются выводы о комите. Ревью кода — это другие разработчики проводят, желательно, не близко связанные с этим участком. Тогда они вдумчиво читая, находят до 90 процентов багов. Даже юнит-тесты меньший процент дают. Далее, они действительно сохраняют комментарии в документе по поводу читаемых и нечитаемых мест. Что очень полезно разработчику, который писал код. Он может не обратить внимания на ревью или доказывать, что считает, что его код правильно написан. Но тем не менее, информация для него полезная и может принять к сведению.

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

                              0
                              Вы правы — я немного переборщил с определениями в угоду лучшего понимания моей мысли.

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

                              2) Ревью кода — это действительно не одобрение. Это проверка кода после его написания с целью нахождения проблем. Эта проверка действительно может проходить по разному и разными людьми. Однако это именно проверка после написания кода и именно другим человеком, мозги которого не повязаны на решаемую задачу. Конечно парное программирование — это не ревью кода — это разработка кода.

                              Практику код — ревью следует применять для улучшения качества кода. И совершенно наплевать есть ли парное программирование или нет. Главное достаточно ли хороший получается код.
                              И уж конечно код ревью не имеет никакого отношения к agile.
                  +1
                  Как мне кажется, это переупрощение. Позволю себе сочинить список требований.
                  1) Игра походовая. Сетка гексагональная. Солдаты ходят, друг друга рубят, либо стреляют.
                  2) Герой должен иметь свои характеристики, которые добавляются к солдатам, плюс сам должен уметь периодически швырять какое то заклинание. Сила регулируется какой-то характеристикой героя, плюс само заклинание может быть одного из трех уровней.
                  3) Добавить магов, Маги должны швырять ту же магию, что и герой по типу, но рассчет силы от количества солдат. Отличие от стрелков — магия накладывает пост-эффекты, например горение постепенно сжигает здоровье, а оглушение не дает ходить какое то время и т.п.
                  4) Добавить препятствия и летающих солдат. Ну и сразу непролетаемые препятствия и телепортирующихся солдат.
                  5) Добавить солдатам активные расходуемые навыки, например лучник может один раз за игру огненную стрелу запустить. Также пост-эффекты.
                  6) Добавить персональные свойства, например тяжелобронированные солдаты получают меньше урона в ближнем бою, а «ловкие» периодически уворачиваются от урона, но не от магии.

                  На этот момент аццкая лапша имхо обеспечена, если изначально подходить к делу энумерацией. А фантазий у геймдизайнера еще немало.
                    0
                    Нет, тут как раз энумерацией описываются только тип магов (или его специализация). Все объекты логики приложения остаются объектами (с отдельными классами) и в коде тоже. То есть солдат и маг — абсолютно разные объекты с разными характеристиками, пусть и унаследованные от общего класса юнит. Вообще в приложении любой объект со своей логикой должен быть отражён не больше и не меньше так же, как и в человеческом описании. То есть у вас есть Герой с Юнитами (маги, воины, стрелки, разведчики), Карта с Препятствиями (брёвна, озёра, горы и т.п.) вот как вы их описали бы дизайнеру или другому разработчику, так и должно оно выглядеть в коде. Чтобы я мог заглянуть и сразу найти класс Маг и посмотреть что и как я могу поправить в его интерфейсе/реализации, безо всяких лишних бредовых лишних сущностей.
                      0
                      Исходя из первого требования, солдат — не более, чем свойство клетки. Возможно, битовое. В какой момент и почему он станет объектом (и станет ли) — это еще вопрос. Возможно, только после того, как свободных битов не останется — но и тогда дешевле будет клетку сделать 64-битной.
                      +2
                      Отличные требования )
                      Правда писать весь код, чтобы показать как, это будет стоить дороговато. Хотя без графики за несколько дней можно написать, показывая, как инкрементально добавляются требования. Вот, если бы какое-то 7 требование противоречило более раннему, проблем было бы больше. Например, что сетки больше нет, а солдаты не рубят друг друга, а играют в карты ))) Проблемы с таким требованием были бы при любом подходе к программированию. Но эволюционное — снижает затраты.

                      1) Игра походовая. Значит, есть одно состояние в один момент. Как минимум. И возможность перехода в новое состояние. Есть солдаты и есть у них координаты. Плюс поведение — рубят и стреляют. Понятно.
                      2) Появляется Герой. По всей видимости, игра двух или несколько игроков, живых или компьютера. Игрок владеет героем, герой войском. Игрока мы не моделируем. К каждому солдату добавляется ссылка на Героя. Свойства становятся динамически вычисляемыми, в зависимости от свойств Героя.
                      3) Маги. Насколько я понял, это новый тип солдат. Ага, у нас бывают разные типы. Первые только рубились и стреляли. Маги только стреляют. Магией, а не пулями. Тогда мы расширяем солдат, делаем базовый и наследники. Маг, таким образом, также как и солдат, связаны с героем. Поэтому сделать расчет силы удара мага в зависимости от количества солдат, которыми владеет Герой — не представляет проблем. Такие умения даются почти даром в смысле кода. А, да, забыл. Это не обычная стрельба, т.к. ведет себя по другому. Ну и отлично, список действий фигур (будем так называть солдат и магов) расширяется до трех: удар, стрельба, швыряние магии. Горение от магии «постепенно» сжигает здоровье. Значит в фигуру добавляем еще список «заклятий» и счетчик связанных с ними ходов. Запрет ходить можно сделать пока отдельно, по сравнению с другими действиями, вроде горения. Не обобщаем.
                      4) Добавить препятствия? Легко. До этого мы структуру данных, моделирующую сетку, не делали. Была не нужна. Теперь сделаем. Тем более, что при появлении препятствий появляется более сложный алгоритм движения — поиск кратчайшего пути для бота, например. Хотя, о ботах не говорили. А и для человека нужно, если он будет указывать конечную точку движения солдата. Летающие солдаты и ходящие имеют разные алгоритмы передвижения. Реализуется какой-то «стратегией» (паттерном).
                      5) У солдат уже есть возможность гореть некоторое количество ходов, значит есть способ расходовать что-то по ходам. Если не надо по ходам, а это, допустим, количество стрел, так тогда делаем класс Оружие, который имеет конечный или бесконечный ресурс. Обычное оружие — бесконечный, лук — конечный (при каждом использовании счетчик уменьшается, пока не достигнет нуля). Пост-эффекты уже есть, маги постарались.
                      6) Тоже никаких проблем. Делаем свойства по умолчанию, потом, при создании экземпляра солдата, некоторые характеристики меняем. Но если нам нужно еще отображение этих свойств (доспехи или что-то подчеркивающее легкость и т.д.), добавляет список артефактов солдата, а его свойства суммируют его личные навыки и полученные от артефактов.

                      Мне тяжело представить, как это сделали бы без эволюционного проектирования. Вот, я писал эту статью. Концентрировался на коде. Казалось бы, очень простой код и каждый раз я с помощью элементарных соображений приходил к выводу, что и как менять. Но если конечную схему классов нарисовать, то она уже довольно большая. Если бы я сразу пытался всё охватить, все ваши требования по игре и нарисовать схему, то сложностей было бы больше. Я пытался бы всё охватить. А это тяжело и не нужно.
                        0
                        Ну вот, сразу появились «стратегии», базовые и наследуемые классы и пр. Я имел в виду то, что «хлеб» в исходной статье это переупрощение. Вряд ли кто-то пожелает, чтоб ему закодили сделать пустой класс, без состояния и поведения, в котором только тип BakeryProduct type. В реальном мире у него будут методы и события, в каждом торте и пирожке свое, и тут получится либо четырехкратный if-else, либо большой нечитаемый switch. Если его не делать изначально базовым с наследниками, то рано или поздно он придет к состоянию, когда все придется переписать с нуля, даже если требования заказчика не метания белки по лесу, а логичные и последовательные. Принцип YAGNI он, конечно же, более чем правилен, создание лишних сущностей и лишних связей между ними Борису явно не стоило даже пытаться делать. Но и Маркуса это никак не оправдывает, и то что, благодаря переупрощению, код получился без лапши еще не означает, что в реальном проекте ее не будет.
                        С одной стороны не надо пытаться предусмотреть какие то «изменяющиеся сущности», которых нет в требованиях, а с другой стороны не надо писать «универсальный код», когда эти сущности уже есть, лучше его разнести по классам, а не по if-else. Тут где-то и кроется та самая золотая середина, как мне кажется…
                          0
                          Пустой класс хлеб — вполне нормально. Если у него нет поведения пока по требованиям.

                          Я иногда и пустые интерфейсы пишу. Зачем? Чтобы знать, что класс его поддерживает.
                          Это конечно, редко и скорее всего не нужно. Но это на «вырост». Добавляются потом какие-то методы и т.д.

                          Так и с хлебом. Надо начинать развивать проект с чего-то? Есть хлеб, как класс, как сущность, которая отличается от других, но ПОКА не имеет ни состояния, ни поведения. А угадывать наперед я не хочу. И пока концентрируюсь на том, как закодить работу с этим объектом, а не что он делает. Это позволяет по частям развивать архитектуру.

                          switch и if-else не так плохи сами по себе. Тут тоже расчитывать надо баланс. Если любые ветвления заменять виртуальными методами, то ничего хорошего не получится. Код будет не читаем. Будет много классов, да еще скорее всего в разных файлах. И чтобы понять, каким образом происходит выбор, придется долго в код смотреть, переключать страницы. При небольшом количестве елементов в switch — может быть гораздо лучшим решением. Смотрите на метод и пытаетесь понять, насколько легко читается. Если легче будут уже классы — переходите на классы. Как промежуточный вариант, можно структурами данных моделировать — Dictionary<>
                            +1
                            Пустой, или как его еще называют, маркерный интерфейс это вполне нормальное явление, даже в готовом продукте. А вот пустой класс заранее понятно, что он должен будет что-то из себя представлять.
                            И кстати совсем необязательно делать общехлебный интерфейс, ведь он же понадобится только при потреблении хлеба, а не при производстве, те есть может он нам и не будет нужен совсем. Чуть более простым кажется вариант, где у печей по методу на хлеб, пирожок и торт. Ну и на кирпичи понятное дело.
                            Но, даже заранее не зная что будет дальше, делать их все одним классом с перечисляемым полем… ну даже не знаю, у всех свой путь самурая, конечно же.

                              0
                              Класс пустой — на вырост. Это просто декларация, что это объект и он другой, отличается от любого другого.
                              В общем, задача такая. Можно класс. Можно энам сразу было возвращать. Но я не хотел уже так всем голову морочить.

                              Писать пустой класс, я тоже часто пишу. Допустим, я знаю, что буду работать с неким объектом. Но мне надо написать некие алгоритмы работы с ним (позже они могут перенестись в него самого, а он стать базовым и т.д.).

                              Но пока, создал пустой класс и ни о чем не думаю, просто оперирую им. Я не думаю в этот момент, как его будут использовать. Потом, в другой задаче об этом буду думать. Таким образом я всегда смотрю на код как-то локально.

                              Конечно, тут очень надуманная задача. Если нет ГУИ и совершенно не объясняется, что с объектом делает конечный пользователь, то и делать ничего не надо. Обычно требования пляшут оттуда. Но мне хотелось показать принцип, как размышлять. А не вообще все нюансы.

                              Объект, в котором внутри перечисление всего лишь — это способ такой упрощенной упаковки типов и не создавания наследников. Как только они станут нужны, тогда и появятся. При этом не факт, что перечисление исчезнет. Оно часто используется и далее.
                                +1
                                Временный пустой класс, пустой метод и вообще любые заглушки не вызывают вопросов. Вызывает вопрос, почему класс один? Требование заказчика иметь три разных, хлеб, пирожок и торт. Зачем заниматься самодеятельностью и объединять их в один, награждая перечисляемым типом? Заранее же неизвестно будет ли у них что то общее вообще, состояние или поведение. А кирпич при этом отдельно, хотя про него нам известно ровно столько же, сколько и про торт, который впоследствии может оказаться сленговым названием газобетона например, и его имеет смысл с кирпичем объединять, а не с хлебом.
                                Совершенствоваться в применении YAGNI еще есть куда, не правда ли, хех.

                                PS мне не нравится, на самом деле попытка подогнать исходные данные под ответ, то есть принцип YAGNI под заведомо неудачное решение Маркуса.
                                  0
                                  Да в общем можно и так.

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

                                  Потом, это упрощение наследования, на данном этапе. Ведь полиморфизм можно организовать далеко не только виртуальными методами. Есть некоторый маркер типа и вы можете, например, выставлять наружу свойства-делегаты и подменять в зависимости от маркера типа.

                                  Но когда еще нет поведения, то остается просто маркер типа.

                                  Пример очень утрирован. Я его взял с предыдущей статьи другого человека. Понятное дело, многое пришлось за уши притягивать. Ваши требования для игры, возможно интереснее. Но статья получилась по ним бы гораздо объемнее.
                        0
                        Раз уж речь зашла о гейм-архитектуре, оставлю здесь эту диаграмму классов, датированную 2008 годом.
                        Много воды с тех пор утекло и нет особого смысла ее обсуждать, но не трудно догадаться какой ад последовал далее.
                        И пользуясь случаем еще раз передаю привет астронавтам архитектуры!
                        0
                        «Лично я знаю, что хлеб состоит из теста (муки, пшеницы или ржи…), что его можно купить в магазине и что его можно есть. Но это мои личные знания. Заказчик пока ни слова не сказал о каком-либо поведении или состоянии. А т.к. я человек ленивый, то хоть я и знаю немного больше, я и кнопку лишний раз не надавлю, без прямого указания на желаемое заказчиком.»

                        Все-таки в изначальной статье хлеб и печки были выбраны именно для того, чтобы не расписывать, что хлеб можно испечь (или купитьв магазине) и съесть, а кирпич съесть нельзя (не хотелось бы вместо хлеба все-таки). Ну т.е. подход YAGNI — ок, понятно, но здесь пример изначально гипертрофирован, что несколько портит эффект от дальнейшего чтения.
                          0
                          В этом и суть этой статьи!
                          Если вам заказчик говорит «хлеб», это ничего общего со знакомым хлебом не имеет.
                          Забудьте, что его можно печь или есть, пока такое требование явно не появится.

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

                            Ситуация #1:
                            Заказчик приходит и приносит ТЗ, в котором четко расписано, что такое хлеб, чего от него требуется в рамках разрабатываемого софта, описан техпроцесс изготовления и т.п. Ну т.е. достаточно четко, чтобы понять, что именно требуется заказчику, особенно если добавить сюда вводный бриф с заказчиком.
                            Тогда да, вспоминать, что хлеб еще бывает из кукурузной муки, или еще какие-нибудь достоверные, но не интересующие заказчика факты про хлеб — зло. YAGNI тогда именно то, что требуется.

                            Ситуация #2:
                            У заказчика есть бизнес-потребность. ТЗ нет, понимания, что это и зачем его готовить тоже нет. Тратить деньги и время на разработку ТЗ не хочет ни в какую. Говорит только, что ему нужна система, которая печет хлеб. Ок, по YAGNI делаем ему хлеб. Заказчик в ответ: вы чего, ребята? Этот ваш хлеб есть нельзя, вы его из чего вообще сделали? Ок, дорабатываем наш хлеб. И так много-много раз, пока заказчик не получит того, что ему нужно, либо не бросит эту затею из-за ощущения того, что он работает с дибилами, которые не понимают базовых вещей про хлеб.

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

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

                            P.S. мне как-то пришлось побывать в ситуации в роли аудитора вот в какой ситуации: заказчик не был в состоянии полностью, достаточно и непротиворечиво сформулировать требования к большой системе, а разработчики использовали подход, похожий на YAGNI, делая только то, что описано в требованиях к конкретной текущей задаче. Хорошо не получилось. Получилось «как-то», не смотря на то, что клиент был очень адекватен в своей области, с удовольствием и подробно рассказывал про то, зачем нужно система и как планируется ее использовать, а разработчики были достаточно квалифицированы, но не хотели углубляться в задачу заказчика. Как написано, так и делаем. Не хватило для хорошего результата здесь, на мой взгляд, только одного — тимлида команды (менеджера, аналитика, нужное подчеркнуть в зависимости от распределения ролей в команде), который бы использовал здравый смысл для того, чтобы понять, стоит уточнить у заказчика напрашивающийся потенциальный кейс использования или нет. И учесть полученную информацию.
                              0
                              Ситуация #2 вообще к проектированию классов отношения не имеет.
                              Даже при отсутствии ТЗ разработчики хотя бы между собой договариваются, какой функционал будет реализован на итерации. Здесь можно пофантазировать и попытаться связать потребность заказчика со своим опытом.

                              Таким образом, #2 приводится к #1 пусть не с внешним ТЗ, а хотя бы в голове одного разработчика, а дальше — YAGNI.
                                0
                                Ситуация 2 вполне часто бывает и обоснована.
                                Но в статье я всего лишь один срез показывал — ничего не делать без подтверждения заказчика. И это верно. Другое дело — вы не пассивны в общении с заказчиком. Там даже небольшой пример есть «вступаем в полемику».

                                Общение с заказчиком и «кристаллизация» требований не рассматривались. Заказчик не всегда может родить требования. И для этого существует общение. Можно вступать в спор, «полемику». Но только то, что после этого «утверждено» заказчиком, то и реализовывается. Если заказчик сильно отказывается, когда вам нужны уточнения — то тогда, я бы посоветовал с ним прощаться. Видимо он неадекватный. Обычно, заказчики заинтересованы в конечном результате, и поэтому в объяснению своих желаний тоже.

                                Есть вариант, что заказчик не в курсе обо всех тенденциях и нельзя от него получить какую-то конкретику по каким-то направлениям. Тогда можете реализовывать любое. Так же, как в статье — какой-то взвешенный алгоритм наименьших квадратов. Заказчик может быть вообще даже и близко не в теме. Но тем не менее, есть возможность реализовать что-то, а потом спрашивать уточнение. Это что-то вроде рабочей заглушки. Но надо помнить, что это заглушка. И потом уточнить, даже если заказчика устраивает.
                                  0
                                  А вот под этим я подпишусь.
                            0
                            Не знаю, кто как, а я после слова pasty не могу думать о коде, если речь идёт о cornish pasty! :)
                              0
                              Это — прекрасная иллюстрация того, чем ценен опыт работы вообще, и опыт работы в конкретной индустрии в частности. Разработчик с опытом работы в хлепопекарной индустрии с самого начала скажет себе: «Эге, господин заказчик, без печки да рецептов для разной выпечки никуда вам не деться, все равно понадобится», и заложит их в код сразу — без излишних деталей, но так, чтобы можно было без масштабных потрясений и переделок потом сделать все, как надо. Да и заказчику напомнит — а не забыл ли он про эти важные мелочи?

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

                                Этот ягни — глубокая философия. Он позволяет писать очень качественно, быстро. Позволяет не сломаться проекту при любых требованиях. И позволяет оценивать по настоящему что нужно, а что нет. Кто это не совсем понимает, тот никогда вообще адекватных архитектур не делал. Просто у таких не будет меры и оценки, что правильно, а что нет. В большинстве случаев, те, кто «проектируют наперед» — не проектируют ВООБЩЕ. У них критерии «красиво» (прямо как бабы на лугу, усюсюкаются ))), или «гибко» (ацки плохой критерий). Больше нет критерия, кроме «кажется».
                                  0
                                  Фраза «не надо с таким заказчиком работать» показывает, извините, ваш глубокий непрофессионализм. Дальнейший уровень ответа подтверждает это наблюдение.
                                    0
                                    Нет. Непрофессионализм — это когда человек готов за копейки ползать перед заказчиком и делать что угодно, потому что тот деньги платит. Даже нарушать процесс, технологию разработки.

                                    Программист — это самурай проекта, а не самурай заказчика или денег или начальства. Программист болеет ТОЛЬКО за проект. И грош цена такому профессионалу, если он через это перешагивает. Вы, допустим, заказчик. Вы бы согласились нанять подлизу, который пытается вам угодить на словах, а пишет говнокод?

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

                                    Ягни — это глубокая философия, как НАДО развиваться проекту. Это серьезная практика. Работа с кодом, умение оценивать, преобразовывать информацию.

                                    При этом, конечно, должно быть и прямое общение с заказчиком. Никто не мешает программисту предлагать некоторые пути развития проекта с т.з. требований. Только в общении рождаются требования. И программист тоже может придумывать требования. Но только ОБЯЗАТЕЛЬНО их обсуждать с заказчиком, а не делать и строить из себя фокусника. Делать он может только то, что утверждено в обсуждении с заказчиком.

                                      0
                                      Конечно, не со всеми заказчиками надо работать. И нарушать технологию по желанию заказчика тоже неправильно. Есть некие границы. Грубо говоря, в идеале заказчик говорит ЧТО СДЕЛАТЬ, а разработчик говорит КАК СДЕЛАТЬ. И, в идеале же, если заказчик начинает последовательно заползать на территорию разработчика, то работать с ним не надо (хотя, конечно, он вправе высказывать некие пожелания и давать некие советы).

                                      Проблема в том, что заказчик зачастую не знает сам до конца, что именно он хочет, и совершенно не может внятно сформулировать это. Опытный разработчик постарается объяснить это заказчику. В рамках хлебной аллегории, я как заказчик хотел бы, чтобы разработчик не относился к моим требованиям формально, а сразу поинтересовался: «Хлеб ведь, наверное, в печке надо будет печь?»

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

                                        Требования не даются просто заказчиком без обсуждения. Я вообще противник любых лишних звеньев, вроде пиэмов, системных аналитиков, которые препятствуют прямому общению с заказчиком. Требования идут от заказчика, но не так: я сказал, а вы делайте как хотите. Это всё, по нормальному, происходит в процессе общения. Дело в том, что заказчик знает свою предметную область. Программист знает языки программирования. Но в общении с заказчиком рождается нечто новое — новый язык, новые термины. И мы, программисты, с нашей точки зрения, называем именно это предметной областью. И мы ее моделируем. Так вот эта предметная область — это новый язык, который на стыке «наук» появляется. Программисту нужна связь, не только к нему, но и от него.
                                        Потому что, как я в статье показал, буквально, код — это перевод требований на язык программирования. Это лингвистическая по сути вещь. «Предметную область» не могут родить в общении с заказчиком люди, не имеющие отношение к программированию или даже не знающее именно того языка, на котором разработка ведется.

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

                                        А про хлеб. Ну, я ж брал пример статьи о Борисе и Маркусе. Какие были требования, так и переписал. Потом, в задачу статьи не входило объяснение, как вести переговоры. Да и это, скорее всего, не формализируется. А вот, когда требования сформированы — тут уже хорошо происходит формализация. На требования, даже пишут тесты. Таким образом, для заказчика, процесс разработки выглядит как черный ящик. Он, общаясь утвердил некоторые требования. Которые четко зафиксированы в документе. А на по окончанию, даже не проекта, а допустим небольшого этапа, спринта, он получает продукт, который в точности удовлетворяет записанным требованиям и это доказано автоматическими тестами. Ну и презентацию можно сделать, показать вручную прохождение тестов.
                                          0
                                          «ПиЭмы» и аналитики очень нужны для определенных типов проектов. Например, в финансовой сфере предметная область очень сложна, и прямой контакт технарей с заказчиком будет весьма неэффективен именно из-за «языкового барьера». Аналитик тут выступает как посредник, который понимает достаточно с одной и с другой стороны, чтобы обеспечить нормальное общение.

                                          Другой пример — разработка игр. Без project manager'a скоординировать работу сотни очень разномастных людей просто нереально. В этом случае он является каналом для общения с заказчиком — иначе будет полный хаос, и, опять-таки, конструктивного общения не получится.
                                –1
                                Фраза «не надо с таким заказчиком работать» показывает, извините, ваш глубокий непрофессионализм. Дальнейший уровень ответа подтверждает это наблюдение.
                                  0
                                  Что только люди не придумывают, когда нет нормального аналитика.

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

                                  Самое читаемое