Опыт лекций по введению в шаблоны проектирования

  • Tutorial
Позвольте небольшое предисловие — обозначу в нём цель статьи.
Я по субботам студентам младших курсов преподаю введение в шаблоны проектирования. Вот, хочу поделиться опытом, описать план нескольких первых лекций. Большинству читателей, я полагаю, сам излагаемый мной материал давно знаком, но, возможно, порядок и способ изложения покажутся любопытными.
Слишком часто, увы, нам рассказывают что-то, но не говорят, зачем это нужно, или говорят даже, но будто вскользь. Скажем, обыкновенно, говоря о C#, расскажут, что такое базовый класс и интерфейс, каким синтаксисом нужно пользоваться, чтобы написать их, приведут пример, где базовым будет класс «Птица», а наследниками «Утка» и «Орёл», но зачем всё это нужно, какая от всей — потенциально сложной — иерархии классов достигается польза, не говорят: это будто бы в тени, подразумевается само собою. И вот потому у многих учеников, ещё не успевших набить свои шишки, в голове перевёрнутая картина мира — они неплохо представляют, что за инструменты даны им в руки, но зачем они изобретены и к чему применимы, понимают смутно.
Вот поэтому я сочинил несколько учебных примеров, на которых можно показать зачем нужны некоторые подходы. Правда, придётся принять условность — будем бить из пушки по воробьям, а то и по воображаемым мишеням. Зато пристреляемся и уж во вражеский бруствер точно попадём, случись что.
Сразу скажу, что от вопросов совсем простых мы быстро перейдем к довольно сложным (ну, скажем, к компоновщику), потому читайте до конца, если уж не с начала.


Начнём с того, что решим самым простым образом какую-нибудь задачку. Ну, скажем, выведем на экран n первых простых чисел. Действовать будем совершенно прямолинейно — до оптимальных алгоритмов нам сейчас нет никакого дела.
    static void Main()
    {
        Console.WriteLine("Input n");
        var n = int.Parse(Console.ReadLine());
        
        var primeNumbers = new List<int>();
        int current = 2;
        while (primeNumbers.Count != n)
        {
            if (primeNumbers.All(x => current % x != 0))
            {
                primeNumbers.Add(current);
                Console.WriteLine(current);
            }
                
            current++;
        }
    }


Ну, здорово. Программа работает. Что дальше? Ну, допустим, мы её опубликуем — пусть люди пользуются нашим модулем для вычисления простых чисел. Будем потихоньку развивать, улучшать, оттачивать — и нам приятно, и людям польза.
Правда, тогда возникает вопрос — мы так и предоставим людям в пользование exe-шник, который требует ввода с клавиатуры? Обычно модули оформляют в библиотеке, скрывая лишнее и выставляя наружу только нужный пользователю метод. Прикинем его сигнатуру:

    public int GetPrimeNumber(int n) { ... }


Допустим, так. Правда, это не совсем то, что мы делали раньше. Мы-то выводили n чисел, а тут только последнее. Хорошо, добавим ещё метод, теперь у нас их два. Сразу поместим их в новый класс — просто чтобы лежали вместе и обращаться к ним было удобно.

    public class PrimeNumberCalculator
    {
        public int GetPrimeNumber(int n) { ... }
        public int[] GetPrimeNumbers(int n) { ... }
    }


Модуль модулем, однако, забывать о нашей программе нельзя. Она должна работать не хуже прежнего. Займёмся переделкой:

public class PrimeNumberCalculator
{
    public int GetPrimeNumber(int n)
    {
        return GetPrimeNumbers(n).Last();
    }
    
    public int[] GetPrimeNumbers(int n)
    {
        var primeNumbers = new List<int>();
        int current = 2;
        while (primeNumbers.Count != n)
        {
            if (primeNumbers.All(x => current % x != 0))
            {
                primeNumbers.Add(current);
                Console.WriteLine(current);
            }
                
            current++;
        }
        
        return primeNumbers.ToArray();
    }
}

class Program
{
    static void Main()
    {
        Console.WriteLine("Input n");
        var n = int.Parse(Console.ReadLine());
        
        new PrimeNumberCalculator().GetPrimeNumbers(n);
    }
}


Всё вроде бы хорошо. Вот только наш класс PrimeNumberCalculator зависит от Console. Что значит, вообще говоря, зависит? То значит, что использует. Наш класс, если вдруг предположить, что консоль перестанет работать, тоже работать не будет. Он требует её как обязательное условие. Зависимость от класса Console сейчас не слишком видна, потому как Console — статический класс. Мы не создаём его экземпляр явно через new, а пользуемся почти как пространством имён. Но всё же зависимость есть, о ней необходимо помнить.
Подчеркнём её. Создадим новый класс, который будет просто вызывать функциональность Console, но сам при этом не будет статическим. Смысла в этом пока немного — мы просто хотим подчеркнуть зависимости класса Calculator
public class MyConsole
{
    // заметим, у нас нет зависимости от чтения с консоли, потому не будем включать этот метод
    public void WriteLine(string value)
    {
        Console.WriteLine(value);
    }
}

public class PrimeNumberCalculator
{
    ....
    public int[] GetPrimeNumbers(int n)
    { 
        ....   
                new MyConsole().WriteLine(current.ToString());
        ....
    }    
}



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

Предположим, тот пользователь, которому мы предоставили наш опубликованный модуль (который и состоял бы пока только из одного класса), хочет написать программу очень похожую на нашу — ему тоже хочется вывести куда-то последовательность простых чисел. Одна беда — он работает в вебе. У него вообще нет никакой консоли.
Хорошо, тогда отчего бы ему не взять уже готовый массив, собранный методом GetPrimeNumbers и не распечатать куда ему вздумается? Хоть в файл, хоть в html-разметку. Можно так? Можно, но тогда поведение поменяется — мы-то получаем число на экран как только оно посчитано, а наш пользователь вынужден будет сперва затратить время на обсчёт всех чисел, и только затем печатать. Не одно и то же — особенно, если вы просите программу выдать 100-тысячное простое число.
Не буду отвлекать читателя конкретной реализацией вывода «куда-нибудь, но не в консоль». Самое простое — напечатать в файл. Вот и предположим, что пользователь нашего модуля хочет печатать в файл.
Что будем делать?

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

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

public class FileWriter
{
    public void WriteLine(string value)
    {
        // дописывает значение в конец файла
    }
}


Тогда лобовое решение выглядит примерно так:
public class PrimeNumberCalculator
{
    public int GetPrimeNumber(int n)
    {
        return GetPrimeNumbers(n).Last();
    }

    public int[] GetPrimeNumbers(int n)
    {
        var primeNumbers = new List<int>();
        int current = 2;
        while (primeNumbers.Count != n)
        {
            if (primeNumbers.All(x => current % x != 0))
            {
                primeNumbers.Add(current);
                new MyConsole().WriteLine(current.ToString());
            }
                
            current++;
        }
        
        return primeNumbers.ToArray();
    }

    public int[] GetPrimeNumbersToFile(int n)
    {
        var primeNumbers = new List<int>();
        int current = 2;
        while (primeNumbers.Count != n)
        {
            if (primeNumbers.All(x => current % x != 0))
            {
                primeNumbers.Add(current);
                new FileWriter().WriteLine(current.ToString());
            }
                
            current++;
        }
        
        return primeNumbers.ToArray();
    }
}


Понятно, что так жить нельзя: хотя бы по двум причинам. Первая — мы безбожно дублируем код. Если придумаем какой-нибудь алгоритм получше, править нам придётся в двух местах. Если найдём и поправим ошибку, поправим её, вероятно, в одном месте, а во втором забудем. Словом код дублировать — так себе занятие. Причина вторая — мы позволяем пользователю выводить только на консоль и в файл. А если он хочет ещё куда-нибудь? Нам придётся менять наш код, компилировать и заливать новую версию, так что ли? Будем удовлетворять всем прихотям всех пользователей и создадим класс с двумя сотнями методов, которые умеют печатать куда угодно? Вздор, чушь. Так нельзя.
Возникает вопрос — что делать. А вообще — чего бы нам хотелось? Написать алгоритм подсчёта, который в нужный момент дёргает какого-то писателя. Причём любого. Какого угодно. Хоть писателя в файл, хоть писателя на консоль, хоть стукача в КГБ.
Ну и — ура! — у нас есть решение. Даже несколько (базовый класс, абстрактный класс, интерфейс). Я, в подобных случаях, предпочитаю интерфейс, понимая его как контракт. Класс, выполняющий интерфейс, обязуется выполнять условия договора (контракта), а мне, пользователю, неважно кто именно выполнит договор — лишь бы работа была сделана.
Итак, поехали:

public interface IWriter
{
    void WriteLine(string value);
}

public class ConsoleWriter : IWriter
{
    public void WriteLine(string value)
    {
        Console.WriteLine(value);
    }
}

public class FileWriter : IWriter
{
    public void WriteLine(string value)
    {
        // допишем значение в конец файла
    }
}

public class PrimeNumberCalculator
{
    public int GetPrimeNumber(int n, IWriter writer)
    {
        return GetPrimeNumbers(n, writer).Last();
    }
    
    public int[] GetPrimeNumbers(int n, IWriter writer)
    {
        var primeNumbers = new List<int>();
        int current = 2;
        while (primeNumbers.Count != n)
        {
            if (primeNumbers.All(x => current % x != 0))
            {
                primeNumbers.Add(current);
                writer.WriteLine(current.ToString());
            }
                
            current++;
        }
        
        return primeNumbers.ToArray();
    }
}

class Program
{
    static void Main()
    {
        Console.WriteLine("Input n");
        var n = int.Parse(Console.ReadLine());
        
        var writer = new ConsoleWriter();
        new PrimeNumberCalculator().GetPrimeNumbers(n, writer);
    }
}



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

А вот такой ещё вопрос. Допустим, мы хотим соединить функциональность нескольких писателей. Скажем, мне понадобился «писатель на консоль и в файл». Или «писатель на консоль и в файл, и в веб (что бы это ни значило)». Что делать?
Первый порыв очевиден: создадим этого нового писателя, и пусть пишет во все три источника. Одна беда — мы опять дублируем код. То есть, мы уже написали всех трёх в отдельности, а теперь будем смешивать логику. Хорошо, это решается так:

public class WriterToConsoleAndFile : IWriter
{
    public void WriteLine(string value)
    {
        new ConsoleWriter(value).WriteLine(value);
        new FileWriter(value).WriteLine(value);
    }
}


Всё работает, но есть недостатки. Мы уже говорили, что разрешать зависимости просто создавая объект — дурной тон. А что в этом дурного?
Да вот смотрите. Когда мы писали «новый писатель в консоль», мы уточняли, что именно в консоль, а ни куда-нибудь. Его нельзя было заметить на другой, не поменяв наш код, мы жёстко зависели именно от писателя в консоль. Мало того, от писателя в консоль созданного именно этим образом. Может быть, я хочу создать такой же класс, но изменить его настройки — сделаю по умолчанию синий фон и жёлтые буквы. То есть, я даже не пользуюсь полиморфизмом (то есть, не передаю писателя через интерфейс/базовый класс), но всё равно добиваюсь большей свободы выбора, больше возможностей предоставляю пользователю. Пусть он сам создаст объект и настроит его по собственному усмотрению. Моё же дело — пользоваться полученным инструментом. Итак, от слов — к делу.

public class WriterToConsoleAndFile : IWriter
{
    private ConsoleWriter _consoleWriter;
    private FileWriter _fileWriter;
    
    public WriterToConsoleAndFile(
        ConsoleWriter consoleWriter,
        FileWriter fileWriter)
    {
        _consoleWriter = consoleWriter;
        _fileWriter = fileWriter;
    }
    
    public void WriteLine(string value)
    {
        _consoleWriter.WriteLine(value);
        _fileWriter.WriteLine(value);
    }
}


Теперь остаётся заметить, что мы вовсе не пользуемся особыми свойствами «писателя в консоль» или «писателя в файл». Нам ни к чему здесь менять путь к файлу и вообще знать о его существовании. Мы не хотим менять цвет шрифта консоли — этим занимались не мы, а тот, кто создал экземпляр ConsoleWriter. А это означает, что нам хватит IWriter. Не будем просить больше, чем нужно. Вот:
public class WriterToConsoleAndFile : IWriter
{
    private IWriter _consoleWriter;
    private IWriter _fileWriter;
    
    public WriterToConsoleAndFile(
        IWriter consoleWriter,
        IWriter fileWriter)
    {
        _consoleWriter = consoleWriter;
        _fileWriter = fileWriter;
    }
    
    public void WriteLine(string value)
    {
        _consoleWriter.WriteLine(value);
        _fileWriter.WriteLine(value);
    }
}


Но теперь ведь нам в конструктор могут передать совсем других писателей. Не «в файл» и «в консоль», а что угодно. Что же, для наших целей это хорошо. Мы только что создали класс, который умеет объединить двух любых писателей. Куда там РСП!
Теперь остался всего один шаг: от двух — к произвольному числу. Вот он:

public class ManyWriters : IWriter
{
    private IWriter[] _writers;
    
    public ManyWriters(IWriter[] writers)
    {
        _writers = writers;
    }
    
    public void WriteLine(string value)
    {
        foreach (var writer in _writers)
        {
            writer.WriteLine(value);
        }
    }
}


Ура, мы получили компоновщик.
Что ещё? Всё ещё не хватит? Да уже неплохо, но одно но. Теперь нам может быть не так-то просто собрать писателя. Создать и настроить одного, создать и настроить другого, потом объединить их при помощи компоновщика… Трудновато, правда? Можно упражнения ради создать файл с настройками, где будет написано, какими писателями мы хотим пользоваться.
И наш новый класс будет заниматься тем, что прочитает файл с настройками, создаст нужных писателей, проставит настройки, положит их в одного ManyWriters, закроет интерфейсом IWriter, чтобы никто вся остальная программа знать не знала о том, каким именно писателем мы пользуемся, и передаст его на выход… И это будет фабрика.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 80

    +1
    Мы в похожем творческом стиле пробовали сделать учебные материалы по шаблонам. Плюс задачки с шутками-прибаутками. Если интересно — могу выложить на какой-нибудь файлообменник и скинуть сюда ссылку. Скажите только куда принято выкладывать файлы на хабре (habratorage только для картинок, как я понимаю).
      +2
      github?
        0
        Понял. Буду знать. Вот ссылочка. Там вордовский файлик и exe-файл, собранный из флеша (интерактивный пример). Делал это ещё на четвёртом курсе, перечитал сейчас — конечно, глупости есть. Можете глянуть если любопытно — просто чтобы стиль был ясен.
          0
          О, здорово, посмотрю.
            –5
            Это похоже у вас перевернутая картина мира. Все уже отходят назад от классов и интерфейсов в купе с зависимостями обратно к объектам и массивам, а вы этому школьников учите. Так делать нездорово, а даже вредно.
              +3
              Пожалуйста, не говорите за всех. Лучше объясните, зачем уходить обратно.
                +1
                Все уже отходят назад от классов и интерфейсов в купе с зависимостями обратно к объектам и массивам

                Можете пример привести? Просто интересно стало, кто эти «все»?
                  0
                  Я думаю, имеются в виду всякие модные штуки, например, из Go и Rust — примеси и собирание классов из кусочков. Могу ошибаться, но, как по мне, это никак не отменяет возможность конструирования архитектуры на основе старых добрых шаблонов… Больше того — сами по себе примеси могут быть сконструированы в рамках объектного программирования, о чём написано на википедии — там вообще пример примеси на основе шаблонов С++.
                    0
                    Наверное. Хотя, очень странно было услышать, что «все отходят» — означает именно появление Rust и Go, языков, каждый из которых уже занял определенную нишу.
                  0
                  Все уже отходят назад от классов и интерфейсов в купе с зависимостями обратно к объектам и массивам, а вы этому школьников учите.

                  Кто «все»?

                  Так делать нездорово, а даже вредно.

                  Аргументируйте.
                    –9
                    Все — это просветленные. Те, кто понимают разницу между массивом, объектом, контентом, контейнером и контекстом. Что, когда и как применять. Для чего на самом деле нужны функции. И т.д. и т.п. А классы — это зло и тупик! Забудьте про них! И чем скорее, тем лучше! Просто совет.
                      +3
                      Все — это просветленные.

                      Конкретные примеры?

                      Те, кто понимают разницу между массивом, объектом, контентом, контейнером и контекстом.

                      Эту разницу понимает любой образованный программист, ничего просветленного в ней нет.

                      А классы — это зло и тупик!

                      Аргументировать как-то можете?
                        –9
                        Если вы понимаете, то аргументы не нужны, вы сами приходите к такому же выводу. Неизбежно.
                          +1
                          Я так понимаю, вы никаких аргументов в пользу своего громкого заявления высказать не можете (как, кстати, и конкретных примеров «просветленных»).
                    0
                    О, это более чем возможно. Переверните её обратно, в таком случае — я готов вас выслушать.
            +4
            Зависимость PrimeNumberCalculator от Console кажется притянутой за уши. При решении реальных задач — это прямое нарушение SRP
              0
              Прошу прощения, но вы неправы. Если перед нами стоит задача выводить числа в режиме их получения, мы должны её реализовать. Можно рассматривать более сложные варианты — скажем, вызывать событие, когда число найдено. Или возвращать IEnuerable с отложенным вычислением в стиле Хаскеля.
              SRP мы нарушаем, когда смешиваем разную логику в одном классе. Здесь же мы — фактически — пришли к тому, что делаем шаг алгоритма, а потом передаём управление кому-то, чтобы там что-то сделали с нашим найденным числом. Логика содержится в этом ком-то. Так что нет, не нарушается SRP.

              Но, повторю, согласен с тем, что лучше бы тут вызывать событие.
                +2
                Нарушаем. этот класс «Считает И делегирует» а не только считает и только делегирует. На мой взгляд, корректно этот вопрос решает FRP — добавить третий метод в интерфейс класса — который будет возвращать стрим простых чисел — на который на другом уровне мы можем уже навесить печать — ибо разные задачи.
                А необходимость передавать управления внутри шага алгоритма вызвана лишь наложенными самостоятельно (не необходимыми) ограничениями о том, что мы должны вернуть массив И напечатать.
                  +1
                  Делегация в данном случае, по-моему, есть лишь способ возвращения результата. Считает и возвращает результат. Не самым обычным способом, но возвращает.
                    0
                    У функции должен быть один вход и один выход. В этом случае, он возвращает его не только в консоль, но и обычным return.

                    Да и то, что метод GetPrimeNumbers принимает обязательный аргумент IWriter — это как? Если бы я был blackbox тестировщиком или сторонним пользователем — я бы, во-первых, удивился, а во-вторых, послал бы туда null, если мне нужен просто набор простых чисел, без вывода, например, для дальнейшей обработки. И это привело бы к исключению.
                      0
                      Более того, мы вызываем GetPrimeNumber… он возвращает одно число, но в консоль выводит все (!) простые числа вплоть до этого. Это не годится.
                    0
                    Всегда было трудно читать примеры, в которых сразу видится простое альтернативное решение.
                    Например, простая функция NextPrimeNumber(n);
                    prime2 = NextPrimeNumber(1);
                    prime3 = NextPrimeNumber(prime2);
                    prime4 = NextPrimeNumber(prime3);
                    ...
                    

                    С такой функцией можно что угодно делать — перебирать по порядку простые числа сколько нужно, писать в файл, складывать в массив.
                      0
                      Вот кстати очень вдохновляющий пример проектирования с помощью классов:
                      www.carlopescio.com/2012/03/life-without-controller-case-1.html
                        0
                        только тут есть скрытое состояние, что не есть хорошо
                          0
                          Скрытое состояние тут — внутренняя оптимизация, сокрытая от клиента.
                          0
                          Да, согласен — уже многие отметили, что условный yield return подошёл бы лучше.
                          Я думаю сделать этот ход следующим шагом.
                      +1
                      Книга "Design Patterns via C#"
                      Авторы:
                      Александр Шевчук
                      Дмитрий Охрименко
                      Андрей Касьянов

                      *бесплатно.
                        +2
                        Раз пошла такая пьянка, книга от SergeyT. Причины использования паттернов, применение языковых средств при реализациях, плюсы-минусы реализаций в наличии. Хотя для «Введения» будет сложновато.
                          0
                          Поддерживаю, книга отличная. И актуальная.
                        +2
                        Ребят, а можно ли объяснить (в принципе) необходимость или потребнсть в хорошем дизайне?
                        Меня иногда просто де-мотивируют вопросы коллег-программистов из серии «А зачем так усложнять?».
                        Опускаются руки.
                          +2
                          Я тоже фанатею от крутой архитектуры, за что тоже часто страдаю…
                          На самом деле, всё сильно зависит от масштаба и перспектив роста системы. Если пишешь калькулятор валют, например, или крестики-нолики — то не имеет смысла много времени на проектирование тратить. Если игру-стратегию, либо редактор разнотипных объектов — то стоит подумать наперёд о том, как будет меняться всё со временем.
                              +3
                              Трудовые будни в сообществе любителей ооп:

                              Ребят, я люблю возводить объектно-ориентированные горы. Коллеги спрашивают «Зачем?», но я не знаю что ответить. Объясните, какими доводами можно подкрепить это желание?
                              +2

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

                              Может есть варианты кроме ооп?
                              -2
                                0
                                там нужно проектировать иерархии объектов более тщательно и обильно, делая решение сложной задачи ещё сложнее.


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

                                Может есть варианты кроме ооп?


                                Громадную статью на английском по ссылке не осилил — к сожалению, у меня не такой уж беглый английский… Можете нам, дремучим работникам ООП, рассказать её смысл в двух словах?
                                  +2
                                  более сложное отображение в объектное представление.


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

                                  Громадную статью на английском по ссылке не осилил — к сожалению, у меня не такой уж беглый английский… Можете нам, дремучим работникам ООП, рассказать её смысл в двух словах?


                                  Статья, проницательно копающая в суть проблемы. Одна из мыслей в следующем: есть вещи (nouns) и действия (verbs).

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

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

                                  Есть же языки мультипарадигменные — JS (прототипное наследование / элементы функциональной парадигмы), Clojure (куча отличных идей из разных парадигм), Scala.

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

                                  Суть в том, что ооп — это всего лишь один из способов мышления над решениями задач. Концептуально это хорошая идея, но после ознакомления с другими подходами 90% времени вы не будете по ним скучать.
                                    0
                                    Ну, сейчас, наверное, все мэйнстрим языки уже можно считать мультипарадигменными. Пускай без сахара, а иногда и очень коряво, но сочетают подходы, как минимум, ООП и ФП. Как часто и насколько уместно используют в одной программе тот или иной подход конкретные программисты и их команды — отдельный разговор, о возможность сочетать подходы у них есть
                                      0
                                      Да, в последнее время так и происходит, и это хорошее направление) Разговор, конечно, и о том что инженерный подход — выбирать инструмент в зависимости от специфики задачи, а не подминать решение под любимую парадигму / инструмент.
                                        0
                                        Чтобы выбирать инструмент под задачу, увы, нужно знать хотя бы два. Сейчас же зачастую неплохие вроде программисты, смотрят как баран на новые ворота, когда говоришь им сделать свертку/редьюс массива. Посчитать какую-то характеристику этого массива в цикле, рекурсии, или даже параллельно, они могут, но что делают не понимают.
                                          0
                                          Может, им просто термин незнаком? Сейчас загуглил, оказалось, я эту свертку много раз делал.
                                            0
                                            Не, там другое — нет понимания, что это операция над массивом как единым целым, даже если выделили операцию в отдельный метод, где массив — единственный параметр, просто такой способ передать набор значений
                                      0
                                      Спасибо за ответ. Возможно, я снова не до конца понял, но я не согласен с тем, что в объектном программировании существует возведение вещей в абсолютный приоритет. При использовании существующего ООП-кода всегда изучается только интерфейс класса: то есть его поведенческое наполнение. Лезть в реализацию считается моветоном. Это и есть инкапсуляция.

                                      Суть в том, что ооп — это всего лишь один из способов мышления над решениями задач. Концептуально это хорошая идея, но после ознакомления с другими подходами 90% времени вы не будете по ним скучать.


                                      Не могу представить себе какая парадигма может быть сильнее объектной, либо околообъектной (прототипной, компонентной, и.т.д) в подавляющем большинстве задач. Сугубое ИМХО, соглашусь сразу что тут вопрос субъективного удобства описания модели реального мира в рамках выбранной парадигмы (где-то неделю назад вёл подобное обсуждение по поводу функциональных языков) — но, как по мне, именно взгляд на предмет как на нечто, обладающее набором свойств, с которыми можно взаимодействовать через набор определённого множества действий — именно такой взгляд естественен для человеческого восприятия. Нас с детства так учат: «Вот машинка, она может гудеть и ездить: ж-ж-ж… А вот печка. Её не трогай, она горячая». То есть есть объект — как кусок реальности — взаимодействуя с которым определённым образом можно получить результат. При этом в подобном восприятии, на мой взгляд, объект выступает самодостаточным элементом, а не каким-то аргументом, который подставляется в функцию воздействия на объект. То есть всё-таки именно вещи оказываются на первом месте.
                                        0
                                        Удобно когда вы работаете с печками и машинками, но в программировании есть вещи, которые объектами тяжело описать. Например парсер. Конечно его можно написать чисто ООП, но смешать это с ФП может сэкономить много строк и головной боли.
                                          0
                                          Так кто спорит-то? Иногда другие подходы хороши — в рамках какого-то спектра задач. Но в комментарии Naissur была заявка про то, что без ООП можно обойтись в 90% случаев.
                                            0
                                            Позвольте, влезу в обсуждение. Я согласен с вами, что часто ФП может в несколько раз сократить код, сделать его и куда как более читаемым (при намётанном глазе), и исключить многие ошибки. Но отбросить ООП как таковое — слишком решительный шаг. Господствуют на рынке именно ООП-языки, и даже на продвинутом C# сложновато писать функционально. Есть ещё, конечно, Scala, но так ли много людей на ней пишут?
                                            Так вот, статья посвящена была введению именно в ООП. Не вижу, как рассказывать его одновременно с ФП.
                                              0
                                              Неверно считать функциональное программирование прямым конкурентом объектно-ориентированного подхода. Это, скорее, разные «мощности» организации проекта. Функциональная композиция в качестве способа минимизации описательной сложности проекта вовсе не отменяет полезных свойств ООП, а надстраивается именно над объектной моделью, выражающую предметную область решаемой задачи. Из академических кругов функциональное программирование уже уверенно пролезло в энтерпрайз именно как ответ на постоянное усложнение разрабатываемых информационных систем. «Любая достаточно сложная программа на C или Фортране содержит заново написанную, неспецифицированную, глючную и медленную реализацию половины языка Common Lisp».
                                                0
                                                («Любая достаточно сложная программа на C или Фортране содержит заново написанную, неспецифицированную, глючную и медленную реализацию половины языка Common Lisp» — это становится особенно очевидным по ООП-коду, если его автор уже обладает опытом вынесения бизнес-логики в отдельный архитектурный слой.)
                                                  0
                                                  а надстраивается именно над объектной моделью, выражающую предметную область решаемой задачи


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

                                                  Любая достаточно сложная программа на C или Фортране содержит заново написанную, неспецифицированную, глючную и медленную реализацию половины языка Common Lisp


                                                  Вот эту фразу слышал… Но каким боком она касается ООД?
                                                    0
                                                    > Но каким боком она касается ООД?
                                                    Не очень понял вопроса (причём тут ООД?). Попробую пояснить смысл цитаты «вообще», без вопроса.

                                                    Если достаточно опытному разработчику рассказать о каких-то паттернах проектирования, то он их сможет отыскать и в своих проектах, которые он разрабатывал до того, как ему рассказали об этих паттернах. Просто потому, что паттерны проектирования — это не какая-то сакральная тайна, а лишь обобщение и так очевидных (опытному разработчику) решений. И вот если такому опытному программисту, никогда не прикасавшемуся к FP, но поучаствовавшему в крупных проектах, объяснить логику LISP-машины, то он наверняка узнает в ней архитектурный слой своего проекта, ответственный за описание и исполнение бизнес-логики.
                                                      0
                                                      Я так понимаю, вы используете функциональное программирования для рабочих задач… Можете рассказать в чём главные достоинства функциональной парадигмы программирования? Да, я могу загуглить — но интересно мнение человека, имеющего опыт коммерческой разработки.

                                                      То, что я успел схватить из чтения случайных постов:
                                                      1. Возможность простой организации фильтрации данных.
                                                      2. Возможности передавать функции как аргументы других функций, задавая таким образом не только контекст данных для выполнения действия, но и контекст поведения.
                                                      3. Определённая простота оптимизации и параллелизации кода за счёт уменьшения связности по данным (то, что передаётся как аргумент может обрабатываться только в рамках функции, в которую этот аргумент передаётся).

                                                      П.С.: По поводу одиозной фразы про lisp-машину — это фраза ни о чём. Так же можно сказать, что любую программу, написанную на функциональном языке, можно представить в рамках плохо оптимизированной и медленной объектно-ориентированной программы. Конечно, можно… Но зачем?
                                                        0
                                                        > Можете рассказать в чём главные достоинства функциональной парадигмы программирования?

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

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

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

                                                          А за советы спасибо. Возможно, пригодятся.
                                              +2
                                              Свойства вещей — это да, нормальное, восприятие окружающего мира. Есть какие-то сущности, обладающие каким-то набором свойств, которые могут изменяться (когда понятно в результате чего, когда — нет), о состоянии которых мы можем узнать активно (потрогать горячее или нет) или пассивно (услышали гудок). Но вот процессы взаимодействия они обычно снаружи вещи, а не внутри, это именно взаимодействия каких-то субъектов или воздействие субъекта на объект. В ООП же традиционно в методы объекта вкладываются его действия над состоянием самого себя, а не его действия с другими объектами, что в целом для человеческого восприятия не характерно: не говорим мы печке «нагрейся», мы переводим переключатель в нужное состояние и ожидаем, что она начнёт нагреваться, периодически контролируя этот процесс. Не stove.heat(), а turnOnStove(conctreteStov), не file.close() должно быть, а fileSystem.close(file) — так характерно для человеческого восприятия, «сезам, откройся» и «горшочек, не вари» даже в сказках не всегда работает, хотя предполагается, что сезам и горшочек — объекты, а не субъекты.
                                                0
                                                С самого начала, семантика «метода» в ООП обозначала отправку сообщения объекту, а не действие над объектом. Т.е. объект здесь является получателем сообщения, а не исполнителем. Субъект, который отправляет сообщение — это собственно сама программа. Поэтому запись file.close() читается как [я отправляю]file[сообщение]close().

                                                Понимание метода как действия, приводит обычно к скатыванию в «процедурщину»: модулям, библиотекам функций и т.п. (это не в смысле «хорошо» или «плохо», а в смысле «не-ООП»). Это, кстати, хорошо видно в вашем примере, где появляется дополнительная инструментина для выполнения действий над файлами «fileSystem.close()», а сам file автоматически превращается тупо в данные.
                                                  0
                                                  В догонку. Тут можно рассмотреть еще и языковые особенности. Так, в английском языке порядок слов в предложении фиксирован: подлежащее, сказуемое и т.п. При этом сказуемое не может быть без подлежащего. В процедурных языках роль подлежащего выполняют слова «модуль» или «программа», которые содержат действия.
                                                  program Example;
                                                  begin
                                                    Write('Hello, ');
                                                    WriteLn('World!');
                                                  end.
                                                  

                                                  Здесь оба глагола Write() и WriteLn() связаны с подлежащим program Example (читаются как program Example Write('Hello '), program Example WriteLn('World!')), а не существуют сами по себе.

                                                  В отличие от английского, в русском языке, можно опускать разные части предложения (программу можно читать в безличной форме как «написать(»Hello "); написать(«World! „)). Это создает новые семантические контексты в понимании “естественности». Не думаю, что это хорошо, скорее просто выносит мозг, поскольку приводит к неправильным выводам.
                                                    0
                                                    Тут можно рассмотреть еще и языковые особенности. Так, в английском языке порядок слов в предложении фиксирован: подлежащее, сказуемое и т.п. При этом сказуемое не может быть без подлежащего.

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

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

                                                    А это просто натяжка.

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

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

                                                    Это создает новые семантические контексты в понимании “естественности».

                                                    Не, не создает. Смотрите:

                                                    программу можно читать в безличной форме как «написать(»Hello "); написать(«World! „)

                                                    … это не безличное предложение, а повелительное наклонение. Строго аналогично можно читать и в английском: write line, write line, write line — это не неполные предложения (сказуемые без подлежащего), это императивы.
                                                  0
                                                  не говорим мы печке «нагрейся», мы переводим переключатель в нужное состояние и ожидаем, что она начнёт нагреваться, периодически контролируя этот процесс


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

                                                  Согласен с funca и по поводу подлежащего и сказуемого. Объектный подход, помимо всего прочего, ещё относительно хорошо передаёт семантику естественных языков (которые, в свою очередь, также отображают мышление человека). В описанном вами варианте нет понятия объекта — есть только понятие действия. Объект выступает просто набором данных — контекстом — в котором выполняется действие. Что даже в приведённом примере не отвечает сути. Да, есть абстрактное понятие нагревания чего угодно, но когда мы работаем с печкой, мы мыслим принципами управления печкой — и тут, на мой взгляд, спуск на более абстрактный уровень нагревания чего угодно только усложняет ситуацию.

                                                  В объектном подходе чётко выражается объект (то, что идёт «до точки») и действие, которое этот объект должен выполнить (то, что идёт «после точки»). stove.heat() — именно так. Пример алгоритма из жизни: "-Mom, how can I cook a dinner? -Just heat the stove and put the pan on it, honney". Мама могла бы сказать: «Just heat something and put the pan on it» — и это было бы действительно более абстрактно, но при этом последовал бы вопрос как именно можно нагреть это самое «что-то». И всё равно пришлось бы объяснять интерфейс печки с методами взаимодействия с печкой (было бы интересно глянуть реализацию функции в вашем примере, кстати).

                                                  Stove theStove;
                                                  Pan thePan;
                                                  
                                                  // heat a pan - читается в обратную сторону, но это издержки синтаксиса
                                                  // языка программирования.
                                                  theStove.heat();
                                                  
                                                  // put the pan on the stove - тут пришлось переконструировать предложение - 
                                                  // потому, что печка тут выступает "главным" объектом с точки зрения ООД.
                                                  // Это уже вопрос архитектуры, который должен решаться в зависимости от
                                                  // взгляда на вещи.
                                                  theStove.setObjectOn(thePan);
                                                  


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

                                                  П.С.: Я прекрасно понимаю что мы имеем дело с вопросом взгляда на вещи. Это так же трудно (и, наверно, невозможно) объяснить другому, как, например, убедить современного астроном в черепахе, на которой стоит Земля… Или как убедить металлиста слушать Ранеток. Но, может, получится донеси мысль друг другу… Было бы круто, во всяком случае.
                                                  0
                                                  Я не собираюсь вас убеждать в том, что в объектно-ориентированном программировании речь идёт прежде всего об объектах, содержащих методы — это слишком неочевидное утверждение.

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

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

                                        Учебные примеры по применению паттернов часто грешат высасыванием абстракций из пальца (чтобы продемонстрировать принцип), и нередко можно увидеть как простой и понятный код в 10 строк превращается в нагромождение методов, классов и интерфейсов. В реальной жизни, если после рефакторинга код стал сложнее, чем был до него — что-то сделано не так.
                                          0
                                          Да, но как иначе? Это именно учебный пример, потом приходится принять условности — мы явно будем делать больше необходимого. Невозможно на лекции развернуть и показать сложный проект, в котором рефакторинг и в самом деле применим. Или возможно — но я не знаю как.
                                            0
                                            Может взять примеры из личного опыта, или из общеизвестных опенсорсных проектов? Проекты целиком показывать необязательно, только необходимые фрагменты кода, где применяются или не применяются паттерны. Заодно можно обсудить целесообразность применения этих паттернов и альтернативные варианты.
                                        • UFO just landed and posted this here
                                            +4
                                            И вроде бы Success, но… фраза «А если завтра сменится поставщик БД»? Так почему бы завтра и не решить этот вопрос?
                                            Гораздо неудобнее будет, если он никогда не сменится, а время потрачено.
                                            • UFO just landed and posted this here
                                                +1
                                                Ну вот, выходит что такой подход работает только если вы пророк… Эх, мне бы так.
                                                  +1
                                                  Пророчество — суть планирование на основе выводов из уже пережитого опыта. Нет никаких формальных алгоритмов или правил (как минимум, пока не изобрели strong AI) для проектирования информационной системы без сознательного участия проектировщика и независимых от его опыта/сообразительности. Впрочем, всегда найдутся всевозможные тренеры и «продавцы серебряной пули», которые постараются опровергнуть данный тезис (за ваши деньги). Паттерны проектирования — это, скорее, удобный способ описать (коллегам и себе) то, что ты сделал УЖЕ, а не способ постичь то, что ты ещё никогда не делал.
                                                    0
                                                    Ну вот да… На основании опыта. Видимо, как раз его не очень достаточно у меня пока. Часто хочется учесть все задачи — но оказывается, что этого вообще не нужно было, и я получаю по голове от шефа (либо получаю лишнюю трату своего времени — если пишу pet-project).
                                            0
                                            Хороший дизайн способствует снижению издержек на модификацию и поддержку кода.

                                            Если на пальцах. Абстрактный проект в вакууме живет 3-5 лет. Время на разработку составляет 3-5 месяцев (т.е. всего лишь 10% срока жизни, Карл!). Остальные 90 приходятся на этапы внедрения и эксплуатации. 3 года в современном мире это большой строк, за который бизнес-ситуация может поменяться несколько раз. Соответственно, команде разработки неизбежно приходится сталкиваться с задачами модификации кода. В определенном смысле, код переходит в состояние эксплуатации сразу как только закрывается задача в трекере на его разработку. Необходимость в модификациях может выявится уже на самых ранних этапах — тестирования, внедрения, и т.д. Модификации влекут за собой риски возникновения дополнительных ошибок и регрессий в ранее отлаженном приложении и соответствующих издержек на их устранение (включая упущенную прибыль, финансовые риски по взаимоотношениям с клиентами, следующими из SLA, и т.п.). Учитывая масштабы, решение вопросов минимизации регрессий и снижения затрат на эксплуатацию становится, как минимум, интересным.

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

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

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

                                            Шаблоны проектирования описывают типовые приемы декомпозиции в типовых случаях. Система описания шаблонов достаточно универсальна, она не связана с ООД, хотя основная масса литературы выпускается именно про них.

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

                                              Если честно, то правильно опускаются. Дизайн должен быть простым, и любое усложнение должно быть обосновано. Вопрос разумный, так как усложнять ради усложнения — тупик. Если вы не можете обосновать причину изменений — значит изменение не нужно.
                                              0
                                              (удалено)
                                                +2
                                                В последнем примере массив _writers не объявлен.
                                                  –1
                                                  Токо хотел написать, а тут уже написали об этом…
                                                    0
                                                    Спасибо, поправил
                                                    +4
                                                    Enumerator бы подошел гораздо лучше (который через yield). А пользователь пусть сам делает с результатами что угодно.
                                                      0
                                                      Да, согласен. Народ уже советовал yield return. Думаю, это вполне можно сделать следующим шагом преобразований.
                                                      0
                                                      Паттерн это подход к проектированию который влияет на то, как ты будешь кодить.
                                                        –1
                                                        Из текста на 10 строк сделали говно из 100500 классов, в которых без бутылки не разберешься.
                                                          0
                                                          Ну, а что делать? Да, это учебный пример, и да, это явный overdesign. На лекции не развернуть реально сложный пример, где рефакторинг ведёт к заметному упрощению.
                                                            +1
                                                            Я бы советовал взять книжку «refactoring to patterns». Там как раз есть примеры как приводить более-менее правдоподобный код к паттернам и есть выгода в этом.

                                                            Не надо пытаться показать более двух паттернов на одном примере из 10 строк. Паттерны — типичные решения типичных проблем проектирования. Очень сомневаюсь что в маленьком коде есть столько проблем.
                                                          +1
                                                          Проблема такого подхода к обучению паттернам в его неправдоподобии и ущербности в описанном случае. Из Вашего синтетического примера студент не поймет значимости и «крутизны» паттернов. У каждого паттерна есть свой конкретный случай удачного применения в реальной жизни: Строитель для создания sql или http запросов; Фабрика для создания Адаптеров к API операторов доставки или платежным системам; Фасад для ORM ActiveRecord и т.п. Сделайте примеры каждого паттерна на основе реальной задачи, где преимущества подхода очевидны, причем не следует объединять несколько паттернов в одном примере, тогда проблем с пониманием у студентов не будет.

                                                          Only users with full accounts can post comments. Log in, please.