Entry tags:
Выпечка хлеба на C# и такие вредные паттерны
Тут на хабре что-то про хлеб писали, мне в твиттерок спамило. Я зашел, дочитал до
И, естественно, дропнул к чертям. Но поднялся какой-то ажиотаж: хаскелисты хлеб выпекали и даже в асечке в меня этой буханкой запульнули. Ну, прочитал, я про этих пекарей и даже просмотрел расцвет экспертизы комментариях. Выводы, как заметил сам автор, немного предсказуемы: паттерноблять соснула и ООП вместе с ним. А вот ёба-функция на 100500 строк - это, оказывается, быстрая разработка.
Я считаю, что еще одна диванная икспертиза будет не лишней. И использую гнобимый в интернетах, но такой распространенный на практике ОО-подход. Буду делать на C# и все написанное относится именно к нему, никаких SmallTalk, никаких рассуждений с высоты птичьего полета о сути ООП. Только инструмент и его возможности.
Требований нет, додумывать занятие неблагодарное, да и богатый опыт в разработке энторпрайз опреденей не стоит переоценивать. Потому, что говорят, то и делаю:
Никаких менеджеров и фабрик. Вещи надо называть своими именами, пусть и на английском языке. Открыли пекарню, так пусть это и будет пекарня, а не очередной манагер.
Нифига подобного. Я тут хлеб пеку, а не печка. Она лишь используется в этом процессе потому
Далее
Вот тут уже есть 2 варианта: делать базовый класс или делать интерфейс. В ентом ООП свалены в кучу наследование реализации и наследование требований к объекту. Вот наследование реализации - вещь не очень хорошая. Сценариев использования всегда больше, чем можно продумать на этапе проектирования. Потому лучше даже не пытаться, а просто задать ограничения для объекта, который будет использоваться как печь. Выбирая из абстрактных классов и интерфейсов в такой ситуации, я предпочитаю интерфейс. Бла-бла-бла, одиночное наследование очень хорошо, да. В итоге, используемые методы печи выносятся в интерфейс АйПечь и код практически не меняется
Ну и делаем какие-то реализации айпечки. Если есть какие-то фрагменты, которые можно использовать повторно, то наследуем реализации, а если нет, то спокойно пишем с нуля.
А вот это уже пушечка. Потому что 100% надо ванговать. Во-первых, чего нам делать, если нет газа? Можно ли начать выпечку и в печке бросить исключение, что газа нет или любая попытка запустить печь ведет к нежелательным эффектам? Во-вторых, может ли, например, печь использующая картофельную энергию работать без картошки? Все это непонятно, потому я отделаюсь отмазкой, дав возможность опрашивать любую печь, на тему, готова ли она выпекать. А газовую, соответственно, надо научить отвечать на этот вопрос, в зависимости от того, есть у нее газ или нет.
Тут, на мой взгляд, несколько нюансов.
Можно было бы поменять тип возвращаемого Bake значения на Maybe[Bread]. Но это школоподход, потому что в настоящих оперденях, если хлеб выпечен не был, надо точно знать, кто, где и почему навернулся. Эта информация должна быть залогирована, а ответственность делегирована в нужном направлении: специалистам по печам, если печепроблемы, программистам, если там нуллреференс какой. "Хлеба нет" - никого не устраивает. Either[string, Bread] или Either[Exception, Bread] тоже не нужны, потому в C# все наловчились бросать и ловить исключения, а не таскать их с собой руками по стеку.
Можно заменить if и throw контрактами:
Так будет наглядней и, глядишь, валидатор сможет шепнуть, что вы тут балаган устроили со своими сломанными печами.
И последнее, что я считаю важным, на этом этапе - это какой-то базовый класс для исключений. Я, честно говоря, ленивый, то есть писать catch(Exception ex) вроде не культурно, а лазить в декларации или код, чтобы смотреть какие _свои_ исключения бросает функция или библиотека в падлу. Потому надо бы зделоть что-то вроде
После чего использовать BakehouseException для детектирования всех пекарня-специфичный проблем, как например неработающая печь.
И тут ситуация почти ничем не отличается от той, когда внезапно стало много печек. Делаем интерфейс АйВыпечка, и наследуем его для хлеба, торта и пирожков
С пирожками, можно поступить несколькими способами. Можно сделать наследников для пирожка, можно сделать генерик-пирожок, который будет принимать начинку в качестве параметра типа, можно енум. Это зависит от задач, но поскольку их нет, нет смысла плодить какие-то иерархии или параметрический полиморфизм. Пирожок в курсе из чего он состоит и нам рассказать может. Я думаю, этого достаточно. Далее я наплодил всяких методов для выпекания. Никаких обобщений тут, по-большому счету, сделать нельзя. Процесс выпекания всего это дела разный, требования разные, параметры разные. И вот для того же пирожка начинку можно строкой передавать, но нафиг эти строки нужны? Начнут еще пирожки с говном выпекать в газовой печи.
Вот тут же интересней. Рецепт - это алгоритм, по которому можно что-то приготовить. По сути это функция от нескольких аргументов (ингридиенты, инструменты) возвращающая некоторый продукт. У нас пекарня, потому один аргумент известен точно - это печь. Однако, возможно рецепты придется читать из файла или еще-то с ними творить. Потому стоит сделать отдельным объектом. Делаем интерфейс и реализации для пирожков, к примеру
Теперь в пекарню добавяется метод выпечки по рецептам.
И последнее требование
Не приносит ничего нового
и метод для выпекания кирпича. Например такой:
Ну и какую-то диаграмму классов я постарался сделать. Нормально не получилось, но, я хотябы расположением попытался показать что к чему.

На этом все, наверное. Можно нарисовать еще печек, порефакторить, переписав методы выпечки без рецептов с использованием каких-то рецептов по умолчанию или начинку пирожков енумом сделать, но это уже тонкости и внутренняя кухня. Почему-то, я, следуя указаниям ентого пээма, ни разу не менял архитектуру. А только понемногу наращивал функционал и занимался обобщением в тех местах, где возникало соответствующее требование. Какого, блять, он пишет
20 минут на нетбуке в процессе просмотра фильма по зомбоящику. Каскадного изменения никакого нет, и даже те места, которые приходится менять, локализованы внутри моего небольшого неймспейса. Единственное ломающее изменение - это добавление параметра к BakeBread на втором этапе. Хотя и здесь можно было бы ничего не ломать, а просто использовать первую попавшуюся свободную печь. Это - банальная абстракция, которая позволяет извиваться змеей, когда надо с одной стороный вополнять новые требования, добавляя функционал, а с другой - обратная совместимость. К тому же вырабатывается некоторый интерфейс системы, который потом может быть использован при создании публичного API. Там рассуждают, что наговнокодить одну функцию в 100500 строк - это быстрый старт. Этот старт придется выкинуть и писать все с нуля, потому что ситуация, когда рефакторинг дороже полного переписывания, наступит так же быстро, как этот воображаемый говнокодер стартанул.
Теперь к хаскельному варианту.
Автор либо считает, что никаких других печей не существует, либо, по его мнению, они просто не нужны. И то и другое плохо, потому что моя печь "Окама" меня устраивает и я хочу выпекать в ней. А значит, мне обязательно понадобятся исходники, где я буду дописывать варианты в АТД. Мда...
Газ он либо есть, либо нет. Это спорное утверждение. Но не оно меня здесь напрягает. Я немножко шокирован, что в параметрах к функции, выпекающей хлеб, поступил газ. Это что вообще? Пекарня или хим. завод?
Так, вся еда тоже строго переписана. Захотел печь терамису или там булку с маком - иди правь чужие исходники.
Чем с точки зрения этой программы кирпич отличается от торта и пирожков? Не знаю, но пофиг, хочет выделять кирпич - пусть. Дело в том, что автору пришлось расширить спектр выпекаемых продуктов и он пошел дублировать инфраструктуру.
В этом коде все прибито гвозядми. А что будет, если к этойхуйне нашей воображаемой пекарне выкатить какие-то нормальные требования? Например, ресурсы какие-то добавить, которые будут расходоваться, заканчиваться и пополняться, печи, работающие в параллельных потоках, проверку, годится ли печка для выпекания по определенному рецепту? Я себе представляю, что будет с моим кодом дальше. Ну будет там какой-нибудь менеджер ресурсов, который будет приходить рецепту параметром, будет там локами заниматься и всем таким, а это уже стейт. А в Хаскеле что? Монада. И тут точно придется весь код выкинуть, и начать писать новый обернутый монадами. И интерфейсы по данным тоже как-то не вырисовываются. И это очень плохо, я считаю. Все в прорубь. Я не хаскелист, потому, наверное, зря за Хаскель рассуждаю. Но в данном случае, мне не нравится, поскольку я не вижу принципиальных отличий от того парня, который таскал интом тип выпекаемого продукта и все делал на месте.
Это, конечно, весело троллировать, мол боллейрплейтнутый ООП-код: абстрактная фабрика абстрактных фабрик и тд. Только оно не с потолка берется. Я тоже в школе сел читать банду, быстро понял, что они поехавшие, и отложил. А оно вон как вышло, что я какие-то вещи делал, названий не знал, но работало неплохо. А тут стал читать и с удовольствием прямо. Чо, нормально люди классифицировали. И хоть я не всегда с ними был согласен, но их мотивы мне понятны. А говорить, что в ентом нашем ФП все само волшебно происходит, глупо. Разделять и абстрагироваться все равно надо, просто способов к этому меньше, чем в ООП, потому очень уж набедокурить сложнее, наверное.
Один из них умный, прочёл кучу статей на Хабре, знает каталог GoF наизусть, а Фаулера — в лицо.
И, естественно, дропнул к чертям. Но поднялся какой-то ажиотаж: хаскелисты хлеб выпекали и даже в асечке в меня этой буханкой запульнули. Ну, прочитал, я про этих пекарей и даже просмотрел расцвет экспертизы комментариях. Выводы, как заметил сам автор, немного предсказуемы: паттерноблять соснула и ООП вместе с ним. А вот ёба-функция на 100500 строк - это, оказывается, быстрая разработка.
Я считаю, что еще одна диванная икспертиза будет не лишней. И использую гнобимый в интернетах, но такой распространенный на практике ОО-подход. Буду делать на C# и все написанное относится именно к нему, никаких SmallTalk, никаких рассуждений с высоты птичьего полета о сути ООП. Только инструмент и его возможности.
приходит проектный менеджер и говорит:
— Ребята, нам нужно, чтобы делался хлеб.
Именно так, «делался», без уточнения способа производства.
Требований нет, додумывать занятие неблагодарное, да и богатый опыт в разработке энторпрайз опреденей не стоит переоценивать. Потому, что говорят, то и делаю:
- class Bread { }
- class Bakehouse {
- public Bread BakeBread() {
- return new Bread();
- }
- }
Никаких менеджеров и фабрик. Вещи надо называть своими именами, пусть и на английском языке. Открыли пекарню, так пусть это и будет пекарня, а не очередной манагер.
— Нам нужно, чтобы хлеб не просто делался, а выпекался в печке.
Нифига подобного. Я тут хлеб пеку, а не печка. Она лишь используется в этом процессе потому
- class Oven {}
- class Bakehouse {
- public Bread BakeBread(Oven oven) {
- //как-то там его в печи выпекаем
- return new Bread;
- }
- }
Далее
— Нам нужно, чтобы печки были разных видов.
Вот тут уже есть 2 варианта: делать базовый класс или делать интерфейс. В ентом ООП свалены в кучу наследование реализации и наследование требований к объекту. Вот наследование реализации - вещь не очень хорошая. Сценариев использования всегда больше, чем можно продумать на этапе проектирования. Потому лучше даже не пытаться, а просто задать ограничения для объекта, который будет использоваться как печь. Выбирая из абстрактных классов и интерфейсов в такой ситуации, я предпочитаю интерфейс. Бла-бла-бла, одиночное наследование очень хорошо, да. В итоге, используемые методы печи выносятся в интерфейс АйПечь и код практически не меняется
- interface IOven { }
- class Bakehouse {
- public Bread BakeBread(IOven oven) { }
- }
Ну и делаем какие-то реализации айпечки. Если есть какие-то фрагменты, которые можно использовать повторно, то наследуем реализации, а если нет, то спокойно пишем с нуля.
— Нам нужно, чтобы газовая печь не могла печь без газа.
А вот это уже пушечка. Потому что 100% надо ванговать. Во-первых, чего нам делать, если нет газа? Можно ли начать выпечку и в печке бросить исключение, что газа нет или любая попытка запустить печь ведет к нежелательным эффектам? Во-вторых, может ли, например, печь использующая картофельную энергию работать без картошки? Все это непонятно, потому я отделаюсь отмазкой, дав возможность опрашивать любую печь, на тему, готова ли она выпекать. А газовую, соответственно, надо научить отвечать на этот вопрос, в зависимости от того, есть у нее газ или нет.
- interface IOven {
- bool CanBake { get; }
- }
- class GasOven : IOven {
- private double _gasLevel;
- public GasOven(double gasLevel) {
- _gasLevel = gasLevel;
- }
- public bool CanBake {
- get { return _gasLevel > 0; }
- }
- }
- class Bakehouse {
- public Bread BakeBread(IOven oven) {
- if(oven.CanBake == false) {
- throw new ArgumentException("Ваша печь не работает!");
- }
- retuen new Bread()
- }
- }
Тут, на мой взгляд, несколько нюансов.
Можно было бы поменять тип возвращаемого Bake значения на Maybe[Bread]. Но это школоподход, потому что в настоящих оперденях, если хлеб выпечен не был, надо точно знать, кто, где и почему навернулся. Эта информация должна быть залогирована, а ответственность делегирована в нужном направлении: специалистам по печам, если печепроблемы, программистам, если там нуллреференс какой. "Хлеба нет" - никого не устраивает. Either[string, Bread] или Either[Exception, Bread] тоже не нужны, потому в C# все наловчились бросать и ловить исключения, а не таскать их с собой руками по стеку.
Можно заменить if и throw контрактами:
Contract.Requires(oven.CanBake);
Так будет наглядней и, глядишь, валидатор сможет шепнуть, что вы тут балаган устроили со своими сломанными печами.
И последнее, что я считаю важным, на этом этапе - это какой-то базовый класс для исключений. Я, честно говоря, ленивый, то есть писать catch(Exception ex) вроде не культурно, а лазить в декларации или код, чтобы смотреть какие _свои_ исключения бросает функция или библиотека в падлу. Потому надо бы зделоть что-то вроде
- class BakehouseException : Exception {
- public BakehouseException(string msg) : base(msg) {}
- public BakehouseException(string msg, Exception ex) : base(msg, ex){}
- }
- class OvenException : BakehouseException {
- public IOven Oven { get; private set; }
- public OvenException(IOven oven, string msg) : base(msg) {}
- public OvenException(IOven oven, string msg, Exception ex) : base(msg, ex) {}
- }
После чего использовать BakehouseException для детектирования всех пекарня-специфичный проблем, как например неработающая печь.
— Нам нужно, чтобы печки могли выпекать ещё и пирожки (отдельно — с мясом, отдельно — с капустой), и торты.
И тут ситуация почти ничем не отличается от той, когда внезапно стало много печек. Делаем интерфейс АйВыпечка, и наследуем его для хлеба, торта и пирожков
- interface IСook { }
- class Bread : IСook { }
- class Cake : IСook { }
- class Pasty : IСook {
- public string Filling { get; private set; }
- public Pasty(string filling) {
- Filling = filling;
- }
- }
- class Bakehouse {
- public Bread BakeBread(IOven oven) {
- return Bake(oven, o => new Bread());
- }
- public Cake BakeCake(IOven oven) {
- return Bake(oven, o => new Cake());
- }
- public Pasty BakeMeatPasty(IOven oven) {
- return Bake(oven, o => new Pasty("Meat"));
- }
- public Pasty BakeCabbagePasty(IOven oven) {
- return Bake(oven, o => new Pasty("Cabbage"));
- }
- public T Bake<T>(IOven oven, Func<IOven, T> maker) where T: ICook{
- Requires(oven.CanBake, _ => new OvenException(oven, "Ваша печь не работает!"));
- Requires(makeCook!=null, _ => new BakehouseException("Вы не дали рецепт"));
- return maker(oven);
- }
С пирожками, можно поступить несколькими способами. Можно сделать наследников для пирожка, можно сделать генерик-пирожок, который будет принимать начинку в качестве параметра типа, можно енум. Это зависит от задач, но поскольку их нет, нет смысла плодить какие-то иерархии или параметрический полиморфизм. Пирожок в курсе из чего он состоит и нам рассказать может. Я думаю, этого достаточно. Далее я наплодил всяких методов для выпекания. Никаких обобщений тут, по-большому счету, сделать нельзя. Процесс выпекания всего это дела разный, требования разные, параметры разные. И вот для того же пирожка начинку можно строкой передавать, но нафиг эти строки нужны? Начнут еще пирожки с говном выпекать в газовой печи.
— Нам нужно, чтобы хлеб, пирожки и торты выпекались по разным рецептам.
Вот тут же интересней. Рецепт - это алгоритм, по которому можно что-то приготовить. По сути это функция от нескольких аргументов (ингридиенты, инструменты) возвращающая некоторый продукт. У нас пекарня, потому один аргумент известен точно - это печь. Однако, возможно рецепты придется читать из файла или еще-то с ними творить. Потому стоит сделать отдельным объектом. Делаем интерфейс и реализации для пирожков, к примеру
- interface IRecipe<T> where T : ICook {
- public T Cook(IOven oven);
- }
- class MeatPasstyRecipe : IRecipe<Pasty> {
- public Pasty Cook(IOven oven) {
- return new Pasty("Meat");
- }
- }
- class MeatPasstyRecipe : IRecipe<Pasty>{
- public Pasty Cook(IOven oven) {
- return new Pasty("Cabbage");
- }
- }
Теперь в пекарню добавяется метод выпечки по рецептам.
- public T Bake<T>(IOven oven, IRecipe<T> recipe) where T : ICook {
- return Bake(oven, recipe.Cook);
- }
И последнее требование
— Нам нужно, чтобы в печи можно было обжигать кирпичи.
Не приносит ничего нового
- class Brick : ICook { }
и метод для выпекания кирпича. Например такой:
- public Brick BakeBrick(IOven oven) {
- return Bake(oven, o => new Brick());
- }
Ну и какую-то диаграмму классов я постарался сделать. Нормально не получилось, но, я хотябы расположением попытался показать что к чему.

На этом все, наверное. Можно нарисовать еще печек, порефакторить, переписав методы выпечки без рецептов с использованием каких-то рецептов по умолчанию или начинку пирожков енумом сделать, но это уже тонкости и внутренняя кухня. Почему-то, я, следуя указаниям ентого пээма, ни разу не менял архитектуру. А только понемногу наращивал функционал и занимался обобщением в тех местах, где возникало соответствующее требование. Какого, блять, он пишет
Но времени на создание такого количества классов уйдёт неприлично много, и каждое изменение требований обернётся каскадным изменением кода.
20 минут на нетбуке в процессе просмотра фильма по зомбоящику. Каскадного изменения никакого нет, и даже те места, которые приходится менять, локализованы внутри моего небольшого неймспейса. Единственное ломающее изменение - это добавление параметра к BakeBread на втором этапе. Хотя и здесь можно было бы ничего не ломать, а просто использовать первую попавшуюся свободную печь. Это - банальная абстракция, которая позволяет извиваться змеей, когда надо с одной стороный вополнять новые требования, добавляя функционал, а с другой - обратная совместимость. К тому же вырабатывается некоторый интерфейс системы, который потом может быть использован при создании публичного API. Там рассуждают, что наговнокодить одну функцию в 100500 строк - это быстрый старт. Этот старт придется выкинуть и писать все с нуля, потому что ситуация, когда рефакторинг дороже полного переписывания, наступит так же быстро, как этот воображаемый говнокодер стартанул.
Теперь к хаскельному варианту.
data Oven = ElectricOven | GasOven | MicrowaveOven
Автор либо считает, что никаких других печей не существует, либо, по его мнению, они просто не нужны. И то и другое плохо, потому что моя печь "Окама" меня устраивает и я хочу выпекать в ней. А значит, мне обязательно понадобятся исходники, где я буду дописывать варианты в АТД. Мда...
data GasStatus = GasAvailable | GasUnavailable
createBread oven gas
| breadCouldBeCreated oven gas = Just Bread
| otherwise = Nothing
Газ он либо есть, либо нет. Это спорное утверждение. Но не оно меня здесь напрягает. Я немножко шокирован, что в параметрах к функции, выпекающей хлеб, поступил газ. Это что вообще? Пекарня или хим. завод?
data Food = Cake | Bread | Pasty Stuffing
Так, вся еда тоже строго переписана. Захотел печь терамису или там булку с маком - иди правь чужие исходники.
data Brick = Brick
makeBrick oven gas
| ovenCouldBeUsed oven gas = Just Brick
| otherwise = Nothing
Чем с точки зрения этой программы кирпич отличается от торта и пирожков? Не знаю, но пофиг, хочет выделять кирпич - пусть. Дело в том, что автору пришлось расширить спектр выпекаемых продуктов и он пошел дублировать инфраструктуру.
В этом коде все прибито гвозядми. А что будет, если к этой
Это, конечно, весело троллировать, мол боллейрплейтнутый ООП-код: абстрактная фабрика абстрактных фабрик и тд. Только оно не с потолка берется. Я тоже в школе сел читать банду, быстро понял, что они поехавшие, и отложил. А оно вон как вышло, что я какие-то вещи делал, названий не знал, но работало неплохо. А тут стал читать и с удовольствием прямо. Чо, нормально люди классифицировали. И хоть я не всегда с ними был согласен, но их мотивы мне понятны. А говорить, что в ентом нашем ФП все само волшебно происходит, глупо. Разделять и абстрагироваться все равно надо, просто способов к этому меньше, чем в ООП, потому очень уж набедокурить сложнее, наверное.
no subject
Вырванные из контекста строчки мне ничего не говорят. Если хочешь рассказать мне за Хаскель, попробуй размышлять подобным образом, выполняя задания без учета того, что там будет дальше. И по результатам покажи код, чтобы можно было увидеть, как он эволюционирует.