KISS — принцип проектирования, содержащий все остальные принципы проектирования

    Постараюсь объяснить сущность принципа проектирования KISS просто и одновременно очень подробно. KISS – это очень общий и абстрактный принцип проектирования, который содержит в себе практически все остальные принципы проектирования. Принципы проектирования описывают как писать «хороший» код. Однако что значит хороший код? Некоторые считают, что это код, который выполняется максимально быстро, некоторые – что это код, в котором задействовано как можно больше паттернов проектирования… Но верный ответ лежит на поверхности. Код – это информация в чистом виде. А основные критерии ценности информации – это 1)достоверность 2)доступность 3)понятность. То, почему важны достоверностью и доступность – очевидно. От кода нет проку, если он работает с ошибками или если сервер с приложением «лежит». Почему же важна понятность кода? В понятном коде проще искать ошибки, проще его изменять, дорабатывать и сопровождать. Итак, понятность – основная ценность, к которой должен стремиться программист. Однако тут есть одна неувязочка. Дело в том, что понятность – вещь сугубо субъективная. Нужен некий более объективный критерий понятности. И этот критерий – простота. Действительно, простое приложение более понятное, нежели сложное. Однако простоты достичь сложно. Вот что пишет Питер Гудвин в книге «Ремесло программиста»:
    Если проект прост, его легко понять… Разработать простой проект не так легко. Для этого нужно время. Для всякой сколько-нибудь сложной программы окончательное решение получается в результате анализа огромного объема информации. Когда код хорошо спроектирован, кажется, что он и не мог быть иным, однако возможно, что его простота достигнута в результате напряженного умственного труда (и большого объема рефакторинга). Сделать простую вещь сложно. Если структура кода кажется очевидной, не надо думать, что это далось без труда.

    Итак, принцип проектирования KISS (keep it simple and straightforward) провозглашает, что простота кода – превыше всего, потому что простой код – наиболее понятный.
    Практически все принципы проектирования направлены на достижение понятности кода. Нарушая какой-либо принцип проектирования, вы уменьшаете понятность кода. Непонятный код автоматически вызывает у человека ощущение того, что код сложный, так как его сложно понимать и модифицировать. При нарушении любого из этих принципов также нарушается и принцип KISS. Поэтому можно говорить, что KISS включает почти все остальные принципы проектирования.
    Патерны проектирования описывают наиболее удачные, простые и понятные решения некоторых проблем. Если вы используете паттерн проектирования там, где нет проблемы, которую решает данный паттерн – то вы нарушаете KISS, внося ненужные усложнения в код. Если вы НЕ используете паттерн проектирования там, где есть проблема, соответствующая паттерну – то вы опять-таки нарушаете KISS, делая код сложнее, чем он мог бы быть.

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

    Всвязи с тем, что представления разных людей о таком понятии как «простота» могут различаться, приобрели широкое распространение следующая заблуждения относительно KISS-a:
    Заблуждение 1. Если считать, что простой код – это такой код, который проще всего написать, то можно истолковать, что принцип KISS призывает писать первое что взбредёт в голову, вообще не задумываясь о проектировании.
    Заблуждение 2. Если считать, что простой код – это такой код, для написания которого требуется как можно меньше знаний, то можно истолковать, что принцип KISS призывает не использовать паттерны проектирования.

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

    Пример на C#



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

    Как выглядит первое что приходит в голову:

    Развернуть код
        public interface IShape
        {
        }
    
        public class Circle : IShape
        {
        }
    
        public class Rectangle : IShape
        {
        }
    
        public class RoundedRectangle : IShape
        {
        }
    
        public class IntersectionFinder
        {
            public IShape FindIntersection(IShape shape, IShape shape2)
            {
                if (shape is Circle && shape2 is Rectangle)
                    return FindIntersection(shape as Circle, shape2 as Rectangle);
                
                if (shape is Circle && shape2 is RoundedRectangle)
                    return FindIntersection(shape as Circle, shape2 as RoundedRectangle);
                
                if (shape is RoundedRectangle && shape2 is Rectangle)
                    return FindIntersection(shape as RoundedRectangle, shape2 as Rectangle);
                
                return FindIntersection(shape2, shape);
            }
    
            private IShape FindIntersection(Circle circle, Rectangle rectangle)
            {
                return new RoundedRectangle(); //также код мог бы вернуть Rectangle или Circle, в зависимости от их размеров. Но для простоты будем считать что метод всегда возвращает RoundedRectangle
            }
    
            private IShape FindIntersection(Circle circle, RoundedRectangle rounedeRectangle)
            {
                return new Circle();
            }
    
            private IShape FindIntersection(RoundedRectangle roundedRectanglerectangle, Rectangle rectangle)
            {
                return new Rectangle();
            }  
        }
    



    Однако этот код противоречит двум пунктам из определения простоты: самый естественный и легко доступный для понимания. Код не естественный, потому что есть некий искусственный класс IntersectionFinder. Код не является легко доступным для понимания, потому что человеку, незнакомому с кодом, нужно будет просмотреть все места использования IShape, чтобы понять, реализован ли функционал вычисления пересечения фигур и как именно им воспользоваться. В проектах, насчитывающих несколько десятков (или даже сотен) тысяч строк кода это может оказаться не быстрым занятием. Есть ещё один неприятный момент, добавляющий трудностей к работе с классом IntersectionFinder: количество функций с именем FindIntersection возрастает в виде арифметической прогрессии от количества фигур, в результате чего класс IntersectionFinder очень быстро «раздувается» и при большом количестве фигур поиск нужной функции в нём становится затратным по времени занятием. Поэтому перенесём FindIntersection в IShape.

    Развернуть код
    
    public interface IShape
        {
            IShape FindIntersection(IShape shape);
        }
    
        public class Circle : IShape
        {
            public IShape FindIntersection(IShape shape)
            {
                if (shape is Rectangle)
                    return FindIntersection(shape as Rectangle);
    
                if (shape is RoundedRectangle)
                    return FindIntersection(shape as RoundedRectangle);
    
                return shape.FindIntersection(this);
            }
    
            private IShape FindIntersection(Rectangle rectangle)
            {
                return new RoundedRectangle();//также код мог бы вернуть Rectangle или Circle, в зависимости от их размеров. Но для простоты будем считать что метод всегда возвращает RoundedRectangle
            }
    
            private IShape FindIntersection(RoundedRectangle rounedeRectangle)
            {
                return new Circle();
            }
        }
    
        public class Rectangle : IShape
        {
            public IShape FindIntersection(IShape shape)
            {
                if (shape is RoundedRectangle)
                    return FindIntersection(shape as RoundedRectangle);
    
                return shape.FindIntersection(this);
            }
    
            private IShape FindIntersection(RoundedRectangle roundedRectangle)
            {
                return new Rectangle();
            }
        }
    
        public class RoundedRectangle : IShape
        {
            public IShape FindIntersection(IShape shape)
            {
                return shape.FindIntersection(this);
            }
        }
    
    



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

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

    Развернуть код
    
    public class Shape
        {
            public Shape FindIntersection(Shape shape)
            {
                var method = MethodFinder.Find(this.GetType(), "FindIntersection", shape.GetType());
    
                if (method != null)
                {
                    return (Shape)method.Invoke(this, new[] { shape });
                }
    
                return shape.FindIntersection(this);
            }
        }
    
        public class Circle : Shape
        {
            [UsedImplicitly]
            private Shape FindIntersection(Rectangle rectangle)
            {
                return new RoundedRectangle();//также код мог бы вернуть Rectangle или Circle, в зависимости от их размеров. Но для простоты будем считать что метод всегда возвращает RoundedRectangle
            }
    
            [UsedImplicitly]
            private Shape FindIntersection(RoundedRectangle rounedeRectangle)
            {
                return new Circle();
            }
        }
    
        public class Rectangle : Shape
        {
            [UsedImplicitly]
            private Shape FindIntersection(RoundedRectangle roundedRectangle)
            {
                return new Rectangle();
            }
        }
    
        public class RoundedRectangle : Shape
        {
        }
    
        public static class MethodFinder
        {
            public static MethodInfo Find(Type classType, string functionName, Type parameterType)
            {
                return
                    classType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
                    .FirstOrDefault(
                        x => x.Name == functionName
                        && x.GetParameters().Count() == 1 
                        && x.GetParameters().First().ParameterType == parameterType);
            }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                Shape shape = new Rectangle();
                var shapeIntersection = shape.FindIntersection(new Circle());
                Console.WriteLine(shapeIntersection.GetType());
            }
        }
    



    Как видно, из каждого конкретного класса фигуры исчезли методы public IShape FindIntersection(IShape shape), общее количество строк кода сократилось. Теперь добавлять новые типы фигур стало ещё проще. Метод FindIntersection(Shape shape) теперь находится в базовом классе и выглядит более просто и естественно (декларативно). Добавился новый класс MethodFinder, однако программисту не нужно знать его внутреннее устройство, т.к. он имеет понятный интерфейс и не реализует понятия из предметной области (а значит причины для его изменений будут редки), поэтому сложность кода практически не возросла при его добавлении.
    Тут может возникнуть мысль, что рефлексия — медленная штука, и для ускорения можно, например, кэшировать делегаты, динамически сформированные посредством ExpressionTree, однако KISS призывает писать как можно более простой код, поэтому стоит воздержаться от этой мысли до тех пор, пока быстродействие метода FindIntersection(Shape shape) действительно не станет узким местом программы, создающим проблемы для пользователя. Но вот что не следует откладывать, так это создание юнит-теста, который через рефлексию узнаёт всех наследников класса Shape и проверяет, что программист не забыл реализовать алгоритмы поиска пересечения для всех пар фигур.

    Посмотреть код теста
        [TestFixture]
        public class ShapeTest
        {
            [Test]
            public void AllIntersectsMustBeRealized()
            {
                var shapeTypes = typeof(Shape).Assembly.GetTypes().Where(x => x.IsSubclassOf(typeof (Shape)));
    
                var errorMessages = new List<string>();
    
                foreach (var firstType in shapeTypes)
                foreach (var secondType in shapeTypes)
                {
                    if (MethodFinder.Find(firstType, "FindIntersection", secondType) == null)
                    {
                        errorMessages.Add(string.Format("Не удалось найти метод для поиска пересечения фигур: {0} и {1}", firstType.Name, secondType.Name));
                    }
                }
    
                if (errorMessages.Any())
                    throw new Exception(string.Join("\r\n", errorMessages));
            }
        }
    



    Сравнив взглядом первый и третий пример, может показаться не очевидным, что третий пример проще. Однако давайте, представим, что типов фигур не 3, а 30. Тогда количество функций сравнения фигур — 465 (сумма арифметической прогрессии (1+30)*30\2). В первом случае механизм выбора нужной функции будет скрыт за 465 if-ами (или, как вариант, за контейнером с 465-ю указателями на методы, что не сильно лучше), и среди этого нагромождения if-ов незнакомому с кодом программисту нужно будет усмотреть некую систему. Тогда как в 3-м случае подход декларативен и не зависит от количества типов фигур. Этот пример хорош тем, что значительной части программистов может показаться, что третий пример является плохим решением, так как в нём используется рефлексия для доступа к приватным переменным (что является своеобразным табу в среде программистов), потому что они слышали из авторитетных источников, что использовать рефлексию для таких целей плохо, но не могут объяснить, почему это плохо. Этот психологический феномен называется фиксированностью ценностей.

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


    Узнать почему использование рефлексии для доступа к приватным методам в данном случае не является святотатством
    На самом деле, вызывать приватные методы вне типа данных, внутри которых объявлен метод — это нарушение инкапсуляции. Однако, как бы удивительно это не прозвучало, в данном примере инкапсуляция не нарушается. Концептуально класс-родитель и класс-наследник являются одним типом данных. Код родителя, инкапсулированный от внешнего мира, может вызываться в наследнике (protected), в то время как родитель может вызывать инкапсулированные методы наследника (protected virtual). Если вы копаетесь во «внутренностях» класса-наследника, вам неизбежно придётся просмотреть и внутреннее устройство класса-родителя, а если у родителя в свою очередь тоже есть родитель, то и его внутреннее устройстве тоже. Многие разработчики знают об этой особенности и предпочитают использовать композицию вместо наследования (если ситуация позволяет сделать это).


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

    Немного истории.
    Принцип KISS зародился в авиастроении и исторически переводится как «Keep it simple stupid» и расшифровывается как «сделайте это до идиотизма простым». В истории авиастроения известны случаи, когда слишком усердные рабочие прибивали на самолёт лишние пластины брони, чтобы сделать самолёт более живучим в бою, в результате чего масса самолёта становилась больше расчётной и самолёт попросту не мог взлететь. Кроме того, квалификация многих рабочих была низкой. В таких условиях конструкции самолётов, которые пьяный неквалифицированный рабочий не смог бы собрать неправильно, даже если бы захотел, обладали особенной ценностью. Один из отголосков конструкторских решений того времени — невозможность перепутать и воткнуть неверный штекер в гнездо внутри компьютера. Однако, если результатом труда авиа-инженера является чертёж, по которому будет создан продукт, то в случае с программистом продуктом является сам чертёж (образно выражаясь). В случае программиста он должен написать код так, чтобы пьяный неквалифицированный программист смог внести в него изменения в соответствии с изменившимися бизнес-требованиями (то есть изменить чертёж, а не собрать самолёт). В силу различий в специфике авиастроения и программирования, расшифровка «Keep it simple stupid», подходящая в авиастроении, уже не так хорошо отражает суть принципа для программиста. Многие ленивые программисты расшифровывают «сделайте это до идиотизма простым» как «не утруждайте себя проектированием» (сравните, например, описание принципа KISS в этой статье с вот этим описанием). К счастью, у KISS есть ещё и некоторые другие расшифровки, одна из которых, на мой взгляд, лучше всего отражает суть KISS в программировании — «keep it simple and straightforward». Straightforward переводится как простой, честный, прямолинейный, откровенный. «Keep it simple and straightforward», таким образом, можно вольно перевести как «Сделайте это простым и декларативным», а для достижения декларативности требуется проектирование.

    За пример следует благодарить Hokum, который подал первоначальную идею для примера, которую я немного изменил.
    Поделиться публикацией
    Комментарии 84
      +8
      Под простотой же в данном случае следует понимать

      Такие вещи гораздо лучше обсуждать на конкретных примерах. Как вариант — дать ссылки на опенсорсные библиотеки и выделить конкретное архитектурное решение.

      Иначе слишком абстрактно, каждый будет иметь свое понимание терминов.
        –4
        Почитайте «Рефакторинг» Фаулера и «Рефакторинг с использованием шаблонов» Кириевски, но даже примеры из этих двух книг не будут исчерпывающими. KISS — слишком общий и всеохватывающий принцип
          +3
          Дьявол кроется в деталях. Если примеров так много что даже в двух книгах нет исчерпывающего описания, почему у Вас не получилось привести хотя бы 2-3 примера в статье? Поддерживаю в данном вопросе dkukushkin. Без примеров эта статья вода который и так понятен если не всем, то подавляющему большинству разработчиков (К слову сказать я тоже как и остальные читал эти книги и думаю что большинство разработчиков прочитали хотя Фаулера). Возьмите какой нибудь один но неоднозначный пример. Например было поставлено условие выполнить некоторое действие — на это написали несколько строк кода. Затем оказалось что на каком то шаге надо выполнить другое действие и дописали условие. Ну и затем требования разрастаются на такие десяток условий. И в какой то момент приходит понимание что проще завести State Machine или обработчики и.д. и т.п. что с одной стороны усложняет код а с другой стороны на самом деле упрощает понимание кода. В какой момент стоит провести такой рефакторинг что бы было упрощение? Вместо двух условий? А может трех? Собственно ценность любой статьи кроется в раскрытии подобных ньюансов. Без этого можно было бы сократить статью до одного предложения без потери ценности.
            –2
            эта статья вода который и так понятен если не всем, то подавляющему большинству разработчиков

            Думаю, если человек знает, что KISS — принцип проектирования, содержащий все остальные принципы проектирования, то ему не следует открывать статью с одноимённым названием. А если всё-таки открыл, то не следует жаловаться что не узнал ничего нового.

            можно было бы сократить статью до одного предложения без потери ценности

            А вот если человек НЕ знает, что KISS — принцип проектирования, содержащий все остальные принципы проектирования, то название статьи и содержание статьи дадут ему разное количество новой и полезной информации

            почему у Вас не получилось привести хотя бы 2-3 примера в статье?

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

              Первое что пришло в голову, но что можно встретить в реальных проектах. Есть структура, в том числе хранит строки заранее известной максимальной длинны. Можно использовать массив символов, а можно std::string:
              struct S1
              {
              //прочие поля структуры...
              char name[50];
              };
              
              struct S2
              {
              //прочие поля структуры...
              std::string name;
              };
              


              Без дополнительной информации не очень ясно какой из вариантов соответствует KISS, а какой его нарушает.
                0
                К тому же два примера всё же в статье есть:
                Если вы используете паттерн проектирования там, где нет проблемы, которую решает данный паттерн – то вы нарушаете KISS, внося ненужные усложнения в код. Если вы НЕ используете паттерн проектирования там, где есть проблема, соответствующая паттерну – то вы опять-таки нарушаете KISS, делая код сложнее, чем он мог бы быть.
                  0
                  Это если читатель знает про паттерны. А если нет? Необходимость искать что-то в процессе чтения статьи не доставляет приятных эмоций. Статья хороша для начинающих, так как принцип KISS на слуху и их статья может заинтересовать. А парочка примеров, может даже на разных языках, добавят красок статье. Она станет более реальной более практичной более близкой читателю.
                    –1
                    Думаю, статья была бы лучше, если бы вы написали парочку хороших примеров, я бы добавил их в статью и написал кого благодарить за примеры. Возьмётесь? Примеры можете оставлять прямо в комментариях или отправить личным сообщением.
                      0
                      Пример илюстрирующий, что простой код и код требующий минимального количества
                      знаний не одно и то же. Разработчик должен знать и использовать все
                      возможности языка.

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

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

                      void DoHatchArea(Rectangle&, Rectangle&);
                      void DoHatchArea(Rectangle&, Ellipse&);
                      void DoHatchArea(Rectangle&, Poly&);
                      void DoHatchArea(Ellipse&, Poly&);
                      void DoHatchArea(Ellipse&, Ellipse&);
                      void DoHatchArea(Poly&, Poly&);
                      


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

                      void DoubleDispatch(Shape& lhs, Shape& rhs)
                      {
                          if(Rectangle* p1 = dynamic_cast<Rectangle*>(&lhs))
                          {
                              if(Rectangle* p2 = dynamic_cast<Rectangle*>(&rhs))
                                  DoHatchArea(*p1, *p2);
                              else if(Ellipse* p2 = dynamic_cast<Ellipse*>(&rhs))
                                  DoHatchArea(*p1, *p2);
                              else if(Poly* p2 = dynamic_cast<Poly*>(&rhs))
                                  DoHatchArea(*p1, *p2);
                              else
                                  Error("Неопределенное пересечение");
                          }
                          else if(Ellipse* p1 = dynamic_cast<Ellipse*>(&lhs))
                          {
                              if(Rectangle* p2 = dynamic_cast<Rectangle*>(&rhs))
                                  DoHatchArea(*p2, *p1);
                              else if(Ellipse* p2 = dynamic_cast<Ellipse*>(&rhs))
                                  DoHatchArea(*p1, *p2);
                              else if(Poly* p2 = dynamic_cast<Poly*>(&rhs))
                                  DoHatchArea(*p1, *p2);
                              else
                                  Error("Неопределенное пересечение");
                          }
                          else if(Poly* p1 = dynamic_cast<Poly*>(&lhs))
                          {
                              if(Rectangle* p2 = dynamic_cast<Rectangle*>(&rhs))
                                  DoHatchArea(*p2, *p1);
                              else if(Ellipse* p2 = dynamic_cast<Ellipse*>(&rhs))
                                  DoHatchArea(*p2, *p1);
                              else if(Poly* p2 = dynamic_cast<Poly*>(&rhs))
                                  DoHatchArea(*p1, *p2);
                              else
                                  Error("Неопределенное пересечение");
                          }
                          else
                          {
                              Error("Неопределенное пересечение");
                          }
                      }
                      


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

                      Что же делать? На помощь приходят шаблоны. Реализуем класс, который будет
                      содержать алгоритмы штриховки и обработку ошибки, если пересечение
                      неопределенно:
                      class HatchingExecutor
                      {
                      public:
                          //Разные алгоритмы штризовки области пересечения
                          void DoHatchArea(Rectangle&, Rectangle&);
                          void DoHatchArea(Rectangle&, Ellipse&);
                          void DoHatchArea(Rectangle&, Poly&);
                          void DoHatchArea(Ellipse&, Poly&);
                          void DoHatchArea(Ellipse&, Ellipse&);
                          void DoHatchArea(Poly&, Poly&);
                      
                          //Функция для обработки ошибок
                          void OnError(Shape&, Shape&);
                      };
                      


                      И теперь перепишем поиск:
                      template<
                          class Executor,
                          class BaseLhs,
                          class TypesLhs,
                          class BaseRhs = BaseLhs,
                          class TypesRhs = TypesLhs,
                          typename ResulType = void
                      >
                      class StaticDispatcher
                      {
                          typedef typename TypesLhs::Head Head;
                          typedef typename TypesLhs::Tail Tail;
                      public:
                          static ResultType Go(BaseLhs& lhs, BaseRhs& rhs, Executor& exec)
                          {
                              if(Head* p1 = dynamic_cast<Head*>(&lhs))
                              {
                                  return StaticDispatcher<
                                      Executor, 
                                      BaseLhs, NullType, 
                                      BaseRhs, TypesRhs>::DispatchRhs(*p1, rhs, exec);
                              }
                              else
                              {
                                  return StaticDispatcher<
                                      Executor,
                                      BaseLhs, Tail,
                                      BaseRhs, TypesRhs>::Go(lhs, rhs, exec);
                              }
                          }
                      
                          template<class SomeLhs>
                          static ResultType DispatchRhs(SomeLhs& lhs, BaseRhs& lhs, Executor& exec)
                          {
                              typedef typename TypesRhs::Head Head;
                              typedef typename TypesRhs::Tail Tail;
                      
                              if(Head* p2 = dynamic_cast<Head*>(&rhs))
                              {
                                  return exec.Fire(lhs, *p2);
                              }
                              else
                              {
                                  return StaticDispatcher<
                                      Executor,
                                      SomeLhs, NullType,
                                      BaseRhs, Tail>::DispatchRhs(lhs, rhs, exec);
                              }
                          }
                      }
                      
                      template<
                          class Executor,
                          class BaseLhs,
                          class BaseRhs,
                          class TypesRhs,
                          typename ResultType
                      >
                      class StaticDispatcher<
                          Executor,
                          BaseLhs, NullType,
                          BaseRhs, TypesRhs,
                          ResulType>
                      {
                          static void Go(BaseLhs& lhs, BaseRhs& rhs, Executor& exec)
                          {
                              exec.OnError(lhs, rhs);
                          }
                      }
                      
                      template<
                          class Executor,
                          class BaseLhs,
                          class TypesLhs
                          class BaseRhs,
                          class TypesRhs,
                          typename ResultType
                      >
                      class StaticDispatcher<
                          Executor,
                          BaseLhs, TypesLhs,
                          BaseRhs, NullType,
                          ResulType>
                      {
                          static void DispatchRhs(BaseLhs& lhs, BaseRhs& rhs, Executor& exec)
                          {
                              exec.OnError(lhs, rhs);
                          }
                      }
                      
                      


                      А тепрь как это все использовать:
                      typedef StaticDispatcher<
                          HatchingExecutor, Shape,
                          TYPELIST_3(Rectangle, Ellipse, Poly)> Dispatcher;
                      Shape* p1 = ...;
                      Shape* p2 = ...;
                      HatchingExecutor exec;
                      Dispatcher::Go(*p1, *p2, exec);
                      


                      Теперь в при добавлении новой фигуры достаточно заменить
                      TYPELIST_3(Rectangle, Ellipse, Poly) на TYPELIST_4(RoundedRectangle, Rectangle, Ellipse, Poly) и добавить методы в HatchingExecutor.

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


                      Пример взят из книги Андрея Александреску «Современное проектирование на C++».
                        0
                        Этот ваш так называемый «простой» код с StaticDispatcher-ом просто ужасен. Чтобы рассчитать пересечение фигур нужно знать о классах HatchingExecutor и StaticDispatcher. При добавлении новой фигуры нужно модифицировать строку typedef StaticDispatcher<...>, а без знания StaticDispatcher-а до этого не догадаться. Проблема усугубляется тем, что в устройстве StaticDispatcher-а очень сложно разобраться, в том числе благодаря тому что имена переменных (p1, p2, lhs) не информативны. Простым это решение точно не назовёшь. Как бы я сделал: добавил бы в интерфейс Фигура виртуальную функцию РассчитатьПересечениеС(Фигруа ДругаяФигура). Пусть есть три фигуры: треугольник, квадрат и круг. В конкретную фигуру треугольник поместил бы код умеющий рассчитывать пересечение с квадратом и кругом, а если это не квадрат и круг то просим неизвестную фигуру саму рассчитать пересечение. Квадрат не умеет рассчитывать пересечение с треугольником, считает треугольник неизвестной фигурой и сразу просит у треугольника рассчитать пересечение. Добавил бы простенькую защиту от зацикливания изменив параметры в функции: РассчитатьПересечениеС(Фигруа ДругаяФигура, Число НомерВызова), если НомерВызова>2, кидаем исключение. Добавил бы юнит-тест проверяющий что все фигуры умеют пересекаться; в языках, поддерживающих рефлексию, в тесте бы искал всех наследников Фигуры через рефлексию, чтобы тест не нужно было менять при добавлении новой фигуры.
                        Преимущество решения: при добавлении новой фигуры не нужно менять ранее написанные классы, чтобы найти код пересечения треугольника квадрата нужно посмотреть максимум 2 класса: квадрат и треугольник, а не устраивать расследование в стиле Шерлока Холмса. Этот код простой и интуитивно понятный, нет левых классов ДелательПересечений и СтатическийДелец.
                          0
                          РассчитатьПересечениеС(Фигруа ДругаяФигура, Число НомерВызова), если НомерВызова>2, кидаем исключение

                          … и все, «простота» потеряна. Если у вас для каждого вызова ожидается проверка, без которой нельзя обойтись — нафиг-нафиг такое счастье.
                            0
                            Можно эту ситуацию решить в соответствии с возможностями языка. Проверку можно вынести в базовый класс — в Фигуру. Можно сделать проверку аспектом, и аспект джойнить на всех наследников фигуры. Если в языке для наследников наследуется и контракт, можно указать условие в контракте базового класса. Лично мне больше нравится вариант с вынесением проверки в базовый класс. Для того чтобы вызвать функцию РассчитатьПересечениеС никаких проверок делать не нужно. И ещё, НомерВызова лучше сделать параметром со значением по-умолчанию.
                              0
                              Это все не «просто».
                                0
                                Сравнение числа с двойкой в классе Фигура — это не просто? А что в этом сложного?
                                  0
                                  Это лишняя операция, которая никак не обусловлена собственно задачей.

                                  Собственно, вот вам и демонстрация того, что правило «будь проще» слишком неоднозначно. У всех разные критерии «простоты».
                                    0
                                    Это лишняя операция, которая никак не обусловлена собственно задачей.

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

                                    У всех разные критерии «простоты».

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

                                      Stack overflow больше не приводит к падению?

                                      правила расчёта пересечений для всех фигур упал, а не зациклился

                                      Вот и у вас появились «правила расчета пересечений». Осталось вынести эти правила за пределы фигур (поскольку ни одна фигура, на самом деле, сама не знает, как считать пересечения с другой) — и будет хорошо.

                                      Я в статье дал определение простоты, взятое из словаря Ушакова, даже ссылку дал.

                                      Вот в вашей реализации есть «искусственность», поэтому определение Ушакова не подходит. Ну и вообще, определения из толкового словаря плохо подходят в качестве метрик при разработке ПО.
                                        0
                                        Stack overflow больше не приводит к падению?

                                        Вы умеете предвидеть будущее и гарантировать что код фигур не изменится так, что исключение StackOverflow будет кидаться в тесте по другим причинам, помимо не написания алгоритма расчёта пересечения для некоторой пары фигур? И сколько у вас в проекте по времени тесты выполняются?

                                        вынести эти правила за пределы фигур

                                        Есть такой принцип, ООП называется, который говорит что неплохо бы данные и код который с ними работает в одной сущности хранить. Например есть класс машина, у машины есть функция ехать. Так вот, машина не сама едет, её законы физики заставляют ехать. Но создавать класс ЗаконыФизики только ради того чтобы координаты машины изменить — глупо. Точно также создавать класс Пространство только ради того чтобы пересечение двух фигур посчитать — глупо. Для этого веские причины нужны, не стоит плодить лишние сущности. Вот если алгоритмы расчёта пересечений очень сложные, или если фигура очень много чего умеет помимо как пересекаться — тогда пожалуйста. Но даже тогда неплохо сделать чтобы фигура делегировала обращение расчёта пересечения пространству.Чаще удобнее машину просить поехать, а не законы физики или пространство.
                                          0
                                          Вы умеете предвидеть будущее и гарантировать что код фигур не изменится так, что исключение StackOverflow будет кидаться в тесте по другим причинам, помимо не написания алгоритма расчёта пересечения для некоторой пары фигур?

                                          Не умею. Но мне не критично, почему он кинулся, мне критично, что тест упал.

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

                                          Во-первых, ООП — не единственная парадигма программирования.
                                          А во-вторых, ваша задача изначально нарушает этот подход: ваше действие производится над двумя объектами, поэтому данные как минимум одного из них будут обрабатываться «снаружи». В вашем подходе этот объект выбирается случайным образом (нет никакой системы в том, какой объект получает данные какого). В подходе со внешним расчетчиком, по крайней мере, все понятно.
                                            0
                                            не критично, почему он кинулся, мне критично, что тест упал

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

                                            Во-первых, ООП — не единственная парадигма программирования.

                                            И что с того??? Напомню, вы говорили терминами ООП: «Осталось вынести эти правила за пределы фигур».

                                            А во-вторых, ваша задача изначально нарушает этот (ООП) подход: ваше действие производится над двумя объектами

                                            То есть, по-вашему, ООП невозможно если какие-то операции производятся более чем над ним объектом?
                                            Суть в том, что данные и код их использующий находятся в одной сущности. В примере квадрат не умеет сам рассчитывать пересечение с треугольником, и передаёт данные о себе треугольнику — квадрат использует свои данные для нахождения ответа. Делегирование ответственности и использование других классов для нахождение ответа — нормальное явление.
                                              0
                                              то есть, по-вашему, добавление одной операции сравнения двух чисел — слишком большая цена, чтобы сделать тест более понятным и не адски тормозным, а также получить информацию почему тест упал?

                                              Критично, что у вас детали реализации (защита от бесконечной рекурсии) просочились в интерфейс.

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

                                              И какие же данные передает о себе квадрат?
                                                0
                                                Критично, что у вас детали реализации (защита от бесконечной рекурсии) просочились в интерфейс.


                                                Согласен, это проблема. Один из вариантов решения — не передавать значение через аргумент функции, а передавать через protected переменную. Однако вариант с тем чтобы просто ловить исключение по переполнению стека проще. Как пришёл домой, проверил — стек довольно быстро переполняется :) Тут вы были правы. Спасибо что помогли сделать простое решение ещё проще.

                                                И какие же данные передает о себе квадрат?

                                                Самого себя
                                                  0
                                                  Один из вариантов решения — не передавать значение через аргумент функции, а передавать через protected переменную

                                                  Это тоже грязное решение. Просто в этом случае вы засоряете не public-интерфейс, а protected.

                                                  Самого себя

                                                  Мы же оба понимаем, что для вычисления пересечения двух фигур необходимо знать о них немножко больше, чем «сама фигура». Например, для квадрата нам понадобится как минимум одна пара координат (+угол и размер или +вторая пара координат). Эти данные вторая фигура будет должна получить из квадрата тем или иным способом. Вот ваши данные, нужные для расчета, и утекли наружу объекта.
                                                    0
                                                    Вот ваши данные, нужные для расчета, и утекли наружу объекта.

                                                    Повторюсь, квадрат использовал свои данные для получения ответа. Не важно, что в момент поиска ответа он использовал какие то другие классы, помимо квадрата. Это не противоречит ООП. Если бы противоречило, то все программы состояли из одного единственного класса.
                                                      0
                                                      Допустим есть класс человек, однажды у человека спросили: «мальчик, сколько тебе лет?». Тут то данные и утекли наружу класса. С тех пор мальчик сам решает, по детскому ему билету ехать или по взрослому. Кондуктора вскоре упразднили, потому что на вопрос «кто тут ещё не заплатил», все молчали, дабы не передавать никаких данных об объекте наружу объекта. :)
                                                        +1
                                                        Неа.

                                                        Вот пойдем на примере. Вот квадрат получает в методе Intersect фигуру. Предположим, что он не знает, как с ней считать пересечение. Что он делает? Правильно передает туда себя. Это важно — не свои данные (потому что у каждой фигуры «свои данные» отличаются, и вы не можете зафиксировать их в интерфейсе), а именно себя. this. Что происходит дальше? Фигура, куда он себя передал — предположим, что она знает, как считать пересечения с квадратом — берет из квадрата данные о нем (координаты), и решает задачу.

                                                        А теперь внимание, вопрос: чем с точки зрения ООП это отличается от «правило расчета перечислений получило квадрат и круг, из каждого получило данные, построило пересечение»? Так вот — ничем. Вообще.

                                                        Более того, даже любимый вами интерфейс можно сохранить: любая фигура, получив в метод Intersect другую фигуру, возвращает new Intersection<TThis, TOther>(this, other).Intersect().

                                                        ООП? ООП. Работает? Работает. Юнит-тестируемо? Элементарно.

                                                        А вот теперь, пожалуйста, назовите объективный критерий, по которому ваше решение проще этого или это решение проще вашего.
                                                          0
                                                          Тут уже пошла тавтология. Я по этому поводу уже высказывался:
                                                          создавать класс Пространство только ради того чтобы пересечение двух фигур посчитать — глупо. Для этого веские причины нужны, не стоит плодить лишние сущности. Вот если алгоритмы расчёта пересечений очень сложные, или если фигура очень много чего умеет помимо как пересекаться — тогда пожалуйста. Но даже тогда неплохо сделать чтобы фигура делегировала обращение расчёта пересечения пространству.Чаще удобнее машину просить поехать, а не законы физики или пространство.

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

                                                          вопрос: чем с точки зрения ООП это отличается от «правило расчета перечислений получило квадрат и круг, из каждого получило данные, построило пересечение»

                                                          В таком случае это процедурный подход, а не ООП. Здесь правило расчета перечислений — завистливая функция (Фаулер, «Рефакторинг»), это smell. Если функция использует больше данные постороннего класса, чем своего, то её нужно перенести в этот посторонний класс. Если использует данные нескольких классов — то в тот класс, из которого используется больше всего данных, или, как вариант, в тот класс, данные которого важнее в данном контексте использования.
                                                            0
                                                            А вот теперь, пожалуйста, назовите объективный критерий, по которому ваше решение проще этого или это решение проще вашего.

                                                            Довольно занимательная статья, которая поможет понять почему не стоит плодить сущности без необходимости.
                                                              0
                                                              Без необходимости — не стоит. Вопрос в том, как объективно определить эту необходимость.
                                                                0
                                                                «как объективно определить эту необходимость» — как с заказчиком договорились, так и объективная необходимость. Why not?
                                                                  0
                                                                  Because заказчику нечего делать в коде программы. В комментарии выше под сущностью понимается деталь реализации, а не бизнес-сущность.
                                                              0
                                                              Здесь правило расчета перечислений — завистливая функция (Фаулер, «Рефакторинг»), это smell.

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

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

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

                                                              Или вот: у вас была логика расчета для треугольников (три параметра на объект). Все круто. А теперь вы добавили круги (два параметра на объект). Будете дописывать логику в треугольники (где она должна быть по вашим правилам), или оставите в кругах (чтобы не нарушать OCP)?

                                                              Ну и да, объективного критерия простоты все еще нет.

                                                              PS Еще полезно подумать о том, как вы будете реализовывать расчет пересечений для n фигур.
                                                                0
                                                                А теперь вы добавили круги (два параметра на объект). Будете дописывать логику в треугольники

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


                                                                Smell — это не обязательно сигнал к исправлению

                                                                Завистливая функция — это сигнал к исправлению, давайте обойдёмся без софистики

                                                                Еще полезно подумать о том, как вы будете реализовывать расчет пересечений для n фигур

                                                                Легко. Поскольку пересечение — тоже фигура (скорее всего какой нибудь скруглённый многоугольник), то пересечение 3х фигур находится так: фигура1.Пересечение(фигура2.Пересечение(фигура3)).

                                                                В каком из них должна быть функция расчета

                                                                Экскаватор копает землю. Вопрос: где будете искать код копки земли? Экскаватор копает землю или земля копается экскаватором? Если ответ не напрашивается исходя из предметной области, то один из хороших подходов — когда код найдётся и там и там. В экскаваторе непосредственно и в земле как Visitor: Земля.Копаться(IКопатель копатель){копатель.Копать(Я);}. Если код копания находится не в земле и не в экскаваторе, а «на другой планете» — это плохой подход, потому что код очень сложно найти. Тут всё аналогично, только вместо экскаватора и земли два разных типа фигур.
                                                                  0
                                                                  Завистливая функция — это сигнал к исправлению, давайте обойдёмся без софистики

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

                                                                  Поскольку пересечение — тоже фигура (скорее всего какой нибудь скруглённый многоугольник), то пересечение 3х фигур находится так: фигура1.Пересечение(фигура2.Пересечение(фигура3)).

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

                                                                  один из хороших подходов — когда код найдётся и там и там.

                                                                  Это плохой подход, потому что он нарушает SRP.

                                                                  В общем, эта дискуссия наглядно демонстрирует: нет объективных критериев простоты (по крайней мере, не в каждом случае).
                                                                    0
                                                                    Не всякая функция, которая обрабатывает больше данных, чем ей доступно локально, является завистливой.

                                                                    В данном случае функция была завистливой, давайте обойдёмся без софистики

                                                                    Это плохой подход, потому что он нарушает SRP.

                                                                    По вашему, SRP — это в «каждом классе максимум одна публичная функция»? Кстати, в земле нет непосредственно самого кода копания. Предлагаете всё-таки запихать код «на другую планету»?
                                                                      0
                                                                      В данном случае функция была завистливой, давайте обойдёмся без софистики

                                                                      Вы не можете это формально доказать.

                                                                      По вашему, SRP — это в «каждом классе максимум одна публичная функция»? Кстати, в земле нет непосредственно самого кода копания.

                                                                      Нет, SRP — это «у каждого элемента должно быть не более одной причины для изменения».
                                                                        0
                                                                        Ну так, класс земля не изменится, если экскаватор будет её по другому копать.
                                                                          0
                                                                          Зато если функция «копать» внезапно начнет получать два параметра вместо одного (например, объем копаемого) — будет изменение.
                                                                            0
                                                                            В таком случае будет понятно, что код копания нужно искать в экскаваторе, и Visitor в земле уже будет не нужен
                                                                              0
                                                                              То есть сначала был в двух местах, а потом добавился параметр — и стало в одном месте.

                                                                              Кул.
                                                                    0
                                                                    объективного критерия простоты все еще нет

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

                                                                      Во-первых, нет прямой связи между «завистливыми» функциями и простотой.
                                                                      А во-вторых, вы можете автоматизировать поиск кандидатов на «завистливые» функции, но не финальное их определение.
                                                                        0
                                                                        вы можете автоматизировать поиск кандидатов на «завистливые» функции, но не финальное их определение


                                                                        Смогу при таком условии (я снова повторяюсь, не пора ли плавно заканчивать спор?):
                                                                        введения дополнительной не содержащейся в коде информации о предметной области

                                                                        Эта дополнительная информация будет о важности определённых данных в определённых контекстах использования. Это довольно сложно и на практике вряд ли кто-то будет так делать, но теоретически — вполне осуществимо.
                                                                          0
                                                                          Смогу при таком условии (я снова повторяюсь, не пора ли плавно заканчивать спор?):
                                                                          введения дополнительной не содержащейся в коде информации о предметной области

                                                                          Неформализуемо.
                                                                            0
                                                                            То что вы не можете это представить, не означает, что это невозможно в принципе
                                                                              0
                                                                              Это означает, что это невозможно прямо сейчас, потому что нет формальных полных способов описания в коде знаний о предметной области. Появятся способы — станет возможно.
                                                                                0
                                                                                Невозможно и на данный момент не написана программа — разные высказывания, которые приводят к прямо противоположным ответам в вопросе существует ли вообще критерий простоты.
                                                                                  0
                                                                                  Выше уже сказано, что определение завистливых функций не является прямым определением простоты кода.
                                      0
                                      Если реализовывать расчет пересечения как виртуальный метод класса Shape, все равно придется искать с какой именно фигурой мы пересекаемся, а это опять же проверки через динамическое приведение типов.

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

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

                                      Ваше решение проще реализовать, в нем проще разобраться, но поддерживать его сложнее.

                                      В варианте из Александреску — сложнее разобраться, но дальнейшее добавление фигур сводится к описанию класса новой фигуры и добавлению методов расчета пересечений, список типов можно передавать не отдельно, а вместе с классом ДелательПересечений, что-то типа:
                                      class ДелательПересечений
                                      {
                                      public:
                                          typedef TYPELIST_3<Прямоугольник, Элипс, Многоугольник> ПоддерживаемыеФигуры.
                                      }
                                      


                                      И опять же можно написать юнит тест.

                                      в том числе благодаря тому что имена переменных (p1, p2, lhs) не информативны

                                      Во-первых их можно переименовать (выбор названия для переменных это вообще отдельная тема), а во-вторых в чем не информативность — функция поиска пересечения двух фигур принимает параметры lhs, rhs — левый фигура, правая фигура. Можно просто shape1 и shape2. При таком использовании и учитывая что перемененных мало (lhs, rhs, p1, p2) понять их значение не сложно.

                                        0
                                        что для всех фигур предусмотрели расчет пресечения… поддерживать его сложнее

                                        Почитайте повнимательнее, там юнит-тест это проверяет. К тому же не уверен что ваше решение это умеет проверять.

                                        все это разбросанно по разным файлам и со временем может превратиться в кошмар

                                        Код пересечения конкретной пары фигур легко находится. Почти во всех современных IDE есть горячие клавиши для быстрого перехода к нужному классу. Вам принципиально хранить все функции расчёта пересечений в одном файле? А если фигур миллион?

                                        а это опять же проверки через динамическое приведение типов

                                        Во-первых, что в конкретном данном случае вам в этом не нравится? Во-вторых, а в вашем примере динамических приведений как будто нету? Head* p2 = dynamic_cast<Head*>(&rhs) — а это тогда что?
                                          0
                                          там юнит-тест это проверяет

                                          А в чем тогда смысл следовать KISS, если все проверки можно свалить на юнит тесты? Запуск юнит теста, только для проверки, что все варианты предусмотрели — дольше, чем просто пробежаться взглядом по десятку функций. Юнит тест интереснее использовать, для проверки правильности расчета пересечений, что без вникания в алгоритм сделать сложно, и все равно можно упустить какие-нибудь пограничные варианты.

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

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

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

                                          Опять же расчет пресечения фигур не является неотемлемым свойством фигуры. Это как домкрат или подъемник для машины, он используется только вместе с машиной, но выполнен он в виде внешней функции.

                                          а это опять же проверки через динамическое приведение типов

                                          Я как раз и имел ввиду, что от них никуда не деться, а они и есть самое запутанное и опасное место, в котором можно допустить ошибки и внесение изменений в него хотелось бы минимизировать. В вашем варианте каждый новый класс будет сложнее предыдущих по количеству таких проверок.
                                            0
                                            А в чем тогда смысл следовать KISS, если все проверки можно свалить на юнит тесты?

                                            Наличие юнит-тестов не гарантируют что код простой и понятный (вы сами то поняли что спросили?).

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

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

                                            Если навигация по коду не удобна без средств предоставляемых IDE, стоит задуматься, а все ли хорошо сделано.

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

                                            если код тяжело читается в простом текстовом редакторе без подсветки синтаксиса, то это плохой стиль кодирования

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

                                            а они и есть самое запутанное и опасное место, в котором можно допустить ошибки

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

                                            Опять же расчет пресечения фигур не является неотемлемым свойством фигуры.

                                            Это как посмотреть, всё зависит от конкретной ситуации. Я уже приводил пример, что вопрос, сама ли едет машина или её заставляют ехать законы физики, зависит от ситуации. Судя по постановке задачи, фигуры только то и нужны чтобы их пересечения искать, так что здесь расчет пресечения фигуры как раз и является основным и неотемлемым свойством фигуры. Опять таки, как я говорил, вынесение всех алгоритмов поиска пересечения в отдельный класс не противоречит моему решению, если использовать делегирование ответственности.
                                              0
                                              Наличие юнит-тестов не гарантируют что код простой и понятный (вы сами то поняли что спросили?).


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

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

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

                                              Можно и без помощи IDE найти два файлика. В любом случае просмотреть 2 маленьких файлика проще, чем один гигантский в 100 раз больше обоих.

                                              Откуда в 100 раз больше обоих — посмотреть заголовочный файл, в котором есть только объявление методов этого достаточно. На 10 фигурах это будет 55 методов, в последней фигуре будет 10 методов проверки пересечений + собственные методы (перегруженные из базового класса, сеттеры/геттеры...). Так что не в 100 раз больше. Но это вопрос вкуса, и то какие манипуляции в целом производятся с фигурами.

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

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

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

                                              Не нравится, что нужно вручную писать все проверки на то какой тип был передан в каждом методе поиска пересечений. Получается первоначальный вариант, но разнесенный по разным файлам. В варианте с шаблонами и списком типов это будет выполнено автоматически. Не надо создавать новых условий для проверки, только в список типов добавить еще один тип.
                                                0
                                                Давайте представим проект, в котором использовалось предложенное вами решение. Приходит на проект новый человек (ну или бывалый разработчик не открывал конкретно этот кусок кода с фигурами около полугода). Ему говорят: добавь-ка новую фигуру. Открывает он фигуру, и не видит никаких намёков на вычисление пересечения. Пересечение он не реализует. Юнит-теста не было написано, потому что, как вы говорите
                                                можно взглядом оценить, что ничего не забыли
                                                и
                                                если есть необходимость писать юнит тест для проверки, что добавили все методы, то это уже не очень простой код
                                                Так что через некоторое время выясняется что приложение иногда падает с ошибками при поиске пересечений… После этого программисту приходится изучать класс СтатическийДелец, который ничего общего не имеет ни с фигурами, ни с пересечениями, и задача решается.

                                                Вариант развития событий 2. Понадобилось программисту вычислить пересечение 2х фигур. Зашёл он в класс фигуры и ничего не увидел. После этого он стал смотреть весь код, в которых используются фигуры. Пролистав много кода, нашёл он класс HatchingExecutor (Делатель штриховки). Однако в этом классе есть только алгоритмы пересечений для конкретных фигур, а не для абстрактной фигуры, поэтому программист написал свой switch-case для поиска пересечений 2х абстрактных фигур. Однако кто-то это заметил и сказал что вроде где-то видел механизм поиска пересечений. После этого он стал искать в проекте все места использования HatchingExecutor, благо было оно всего одно. Изучив StaticDispatcher, программист смог таки вызвать функцию поиска пересечений двух фигур. Много кода пролистано, несколько часов потрачено, нужная функция вызвана. Хэппи энд. Ну или может быть такой вариант: толстенная инструкция по особенностям проекта пролистана, 20 минут потрачено, нужная функция вызвана (но для такого финала нужно создать документацию и постоянно тратить силы на её поддержание).

                                                Теперь рассмотрим ситуацию, если Фигура реализована так, как я предлагал:
                                                Нужно добавить фигуру. Смотрим треугольник, делаем по аналогии, всё работает.
                                                Нужно вызвать пересечение 2х фигур. Смотрим любую конкретную фигуру или абстрактную фигуру, вызываем нужную функцию, всё работает.
                                                  0
                                                  Так что через некоторое время выясняется что приложение иногда падает с ошибками при поиске пересечений…

                                                  Согласен, был не прав, юнит тест все таки нужен.

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

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

                                                  Вариант развития событий 2. Понадобилось программисту вычислить пересечение 2х фигур. Зашёл он в класс фигуры и ничего не увидел.

                                                  Можно сделать объявление функции поиска пересечений рядом с базовым классом для фигур принимающую в качестве параметров две ссылки(или указателя) на базовый класс. Тело функции будет содержать вызов метода StaticDispatcher c HatchingExecutor. И достаточно не толстенной документации а строчки с комментарием. И не придется ничего искать. А если функций для работы с фигурами несколько, то их объявление можно собрать в одном месте.

                                                  Смотрим треугольник, делаем по аналогии

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

                                                  В обоих случаях нужно реализовать алгоритмы штриховки новой фигуры со всеми остальными. Но в моем случае после этого нужно только добавить в список новый тип, а в вашем написать длинный блок if-else.
                                                    0
                                                    Дальше пограмист должен посмотреть на все оставшиеся классы (ну не на все, еще на пары другой было бы достаточно), что бы понять, что для каждой новой фигуры необходимо реализовать ее пересечение со всеми остальными

                                                    Программист обладает как минимум зачатками логики, и, зная что 1)есть треугольник, круг и трапеция, и 2)написав алгоритм расчёта пересечения треугольника и круга, догадается что расчёт пересечения треугольника и трапеции с неба не упадёт. А если не догадается, то посмотрит функцию вычисления пересечений и увидит что там помимо круга есть ещё и обработка остальных фигур (НеизвестнаяФигура.РассчитатьПересенияС(Я)). По-моему, проблема надуманная.

                                                    Можно сделать объявление функции поиска пересечений рядом с базовым классом для фигур принимающую в качестве параметров две ссылки(или указателя) на базовый класс.

                                                    Расположение кода в файлах служит второстепенным средством структурирование кода. Первостепенным (и более важным) является само устройство кода, т.е. расположение алгоритмов в методах и расположение данных и методов в классах.

                                                    в моем случае после этого нужно только добавить в список новый тип, а в вашем написать длинный блок if-else

                                                    Принцип KISS как раз таки и говорит, что «не нужно стрелять по воробьям из пушки». StaticDispatcher довольно сложен: он должен учитывать, что если трапеция является наследником многоугольника, то вызовется код расчёта именно трапеции (хотя фигура и является многоугольником), также он должен уметь переставлять местами аргументы функции (StaticDispatcher из книги это умеет). В конце концов, StaticDispatcher настолько сложен, что вы неправильно скопировали его куски из книги (он не компилируется по многим причинам), и никто этого не заметил. Кроме того, StaticDispatcher — протекающая абстракция, то есть чтобы его использовать, нужно знать его внутреннее устройство: при добавлении новой фигуры нужно изменять массив типов, так что игнорировать его сложность не удастся. Можно конечно написать инструкцию, но принципы проектирования направлены на создание понятного кода, а не кода, к которому требуются пояснения. В картинной галерее художник ведь не стоит возле картины с указкой и не говорит: «эта бесформенная клякса — собака, а эта непонятная загагулина — человек» (речь не идёт о всякого роде сюрреалистических картинах, где зритель получает удовольствие от осознания разгаданной загадки). Увидите вы кучу клякс, пожмёте плечами и пройдёте мимо. Только в случае с программой пройти мимо не получится… Сложность StaticDispatcher (пушки) может быть оправдана, только если блоков if-else будет очень много (гигантский воробей). Не думаю что их будет настолько много. Ну а если со временем вдруг количество фигур и увеличится, тогда можно будет и побороться с этой проблемой. Но в этом далеком будущем программа уже изменится, и лучшее решение в ней может отличаться от лучшего решения в настоящий момент. Кроме того, это будущее может так и не наступить.
                                                      0
                                                      Похоже, каждый из нас останется при своем мнении. У меня не хватает аргументов убедить Вас, что StaticDispatcher это не столь сложно как кажется, и в то же время меня не убеждают Ваши аргументы, что лучше оставить все как есть.

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

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

                                                      В любом случае, спасибо за дискуссию, для себя я почерпнул некоторые интересные мысли.
                                                        0
                                                        Мне тоже было приятно с вами пообщаться :)
                                                          +1
                                                          Добавил в статью пример с кодом, который, как мне кажется, устроит нас обоих. Пример написан на C#
                                                            0
                                                            Согласен, пример показывает ту же идею — как можно избавиться от кучи if — else, которая появитсья при реализации решения «в лоб» используя возможности языка. А остальное уже особенности конкретного языка и предпочтений разработчика.
                                  0
                                  «простой код и код требующий минимального количества знаний не одно и то же. Разработчик должен знать и использовать все возможности языка»

                                  Гипотеза о том, что разработчик якобы «должен» кому-либо что-либо кроме того, о чем он договорился с заказчиком, беспочвенна.

                                  Если мне как заказчику ПОДХОДИТ (здесь и сейчас) РЕЗУЛЬТАТ работы разработчика, то мне в среднем чуть более чем наплевать, какие возможности каких языков он использовал или не использовал.
                                    0
                                    Заказчику все равно и то, как система спроектирована, и какие паттерны применял или не применял разработчик, и каким методологиям он следовал. Заказчику важно получить продукт в срок, за оговоренные деньги и чтобы этот продукт работал так, как он себе это представляет. Программа может «глючить», могут быть «утечки» памяти, если программа не предназначена работать без перезапуска длительное время, главное, чтобы это не было заметно заказчику. Но разговор не о взаимоотношениях заказчик-исполнитель. Разговор о самосовершенствовании разработчика.

                                    Если программист хочет выполнять задачи эффективно, если ему не все равно, что думают о его коде другие разработчики, то он должен изучать свои инструменты и уметь их правильно использовать. А в отношении качества кода разработчик должен только себе (может быть, своим коллегам, если работает в команде). Иными словами, если человек хочет что-то уметь, он должен этому учиться. А кому он должен? — только себе.
                        0
                        Очень перекликается с en.wikipedia.org/wiki/Worse_is_better и кстати сам Richard P. Gabriel на его сайте очень интересно разбирает нетривиальные последствия подхода. В моем понимании, Worse-Is-Better победил — это современный Agile
                      +15
                      Исторически KISS расшифровывается как keep it simple stupid или keep it simple , stupid. А вариации на тему keep it S&S: simple & straightforward, smart & simple, simple silly, sort & simple, super simple, safe and sound и т.д. — подгонка аббревиатуры KISS к ситуации.
                        –6
                        На мой взгляд, вариации с «straightforward» и «short» наиболее точно отражают суть принципа KISS в программировании (но в других областях, например в авиастроении, могут оказаться более удачными и другие расшифровки). Из этих двух вариаций мне больше нравится «straightforward», потому что в очень редких случаях «короче» не означает «проще».
                          0
                          Интересно было бы узнать аргументы несогласных с этим.
                            +1
                            Аргумент есть прямо в комментарии, на который вы отвечаете.
                              0
                              Не совсем понимаю, почему люди не согласны с тем, что straightforward («простой», «прямолинейный», «честный», «открытый», «откровенный») наиболее точно отражает суть принципа KISS в программировании. Потому что историческое название из авиастроения, откуда появился принцип, «Keep it simple stupid»? Или почему то ещё?
                                0
                                Потому что KISS расшифровывается именно так, как расшифровывается. А все остальное, что вы ему приписываете — апокрифика.
                        +4
                        Цитата:
                        «На мой взгляд, принцип KISS может быть полезен лишь для начинающих проектировщиков, которые не знают или не понимают основных принципов проектирования.»
                        Он и есть основной принцип проектирования/разработки :)
                          –3
                          … но не единственный, есть и другие основные принципы
                            0
                            например SOLID и GRASP
                          0
                          KISS — это о балансе. Хотелось бы больше размышлений о поиске этого самого баланса, а не так прямолинейно, как здесь — «KISS нарушен, делаем по-другому».
                            0
                            Поэтому перенесём FindIntersection в IShape.

                            И вы хотите сказать, что количество методов в IShape не будет расти с увеличением количества фигур?

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

                            В каком классе находится метод, рассчитывающий пересечение круга и квадрата?
                              0
                              Нажмите мышью на «Развернуть код»
                                0
                                Спасибо, кэп. Как стороннему программисту, который первый раз видит код, это узнать, не перебирая все классы?
                                  0
                                  В каком классе находится метод, рассчитывающий пересечение круга и квадрата?

                                  Как стороннему программисту, который первый раз видит код, это узнать, не перебирая все классы?

                                  Там 3 примера кода. В первом примере — искать usages Круга или Квадрата, это сказано в статье. После первого рефакторинга, во втором и третьем примерах, поведение, специфичное для Круга и Квадрата, находится внутри классов Круг и Квадрат. Для того чтобы понять, что искать нужно именно там, нужно изучить ООП (хотя по usages тоже можно найти).
                                    0
                                    Ага, в Square просто нет метода расчета пересечения с кругом. Первая реакция — WTF?

                                    (Ну и да, если бы паттерн реализовывали правильно, искать нужно было бы наследника Intersection<Square,Circle>, что, в общем-то, существенно проще Find usages)
                                      0
                                      наследника Intersection<Square,Circle>

                                      В результате пересечения прямоугольника и круга может получится, в зависимости от размера фигур: прямоугольник, круг, скруглённый прямоугольник — посмотрите внимательнее код, там есть комментарий. Поэтому ваше предложение не подходит.

                                      в Square просто нет метода расчета пересечения с кругом. Первая реакция — WTF?

                                      Зато есть механизм расчёта пересечения Square с любой фигурой, можно его посмотреть, раз не хватает смекалки метод пересечения круга и квадрата поискать и в круге, и в квадрате. Если у вас есть идея как написать замечательный простой код, работать с которым сможет и codemonkey без зачатков логики, и в котором можно будет ещё проще найти нужный метод, то, может быть, поделитесь?
                                        0
                                        В результате пересечения прямоугольника и круга может получится, в зависимости от размера фигур: прямоугольник, круг, скруглённый прямоугольник — посмотрите внимательнее код, там есть комментарий. Поэтому ваше предложение не подходит.

                                        Правда не подходит? А то, что Intersection<T1,T2> в ходе выполнения может возвращать что угодно, вас не смущает?

                                        раз не хватает смекалки метод пересечения круга и квадрата поискать и в круге, и в квадрате

                                        Если нужна «смекалка», значит, код не очевиден.

                                        Что возвращает нас к обсуждению субъективности «простоты» кода.
                                          0
                                          Эммм… а кто-то говорил, что смекалка тут является обязательным требованием? Я же предложил и другой подход:
                                          Зато есть механизм расчёта пересечения Square с любой фигурой, можно его посмотреть, раз не хватает смекалки… поискать и в круге, и в квадрате

                                          хотя тут речь идёт не о смекалке, а скорее о понимании ООП. А как я говорил в статье, простой код и код, для написания которого требуется как можно меньше усилий и знаний — не одно и тоже.
                                            0
                                            понимании ООП

                                            А «понимание ООП» — это универсальная и объективная характеристика?

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

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

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