![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Тут на хабре что-то про хлеб писали, мне в твиттерок спамило. Я зашел, дочитал до
И, естественно, дропнул к чертям. Но поднялся какой-то ажиотаж: хаскелисты хлеб выпекали и даже в асечке в меня этой буханкой запульнули. Ну, прочитал, я про этих пекарей и даже просмотрел расцвет экспертизы комментариях. Выводы, как заметил сам автор, немного предсказуемы: паттерноблять соснула и ООП вместе с ним. А вот ёба-функция на 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)
Date: 2012-10-05 01:18 am (UTC)(no subject)
Date: 2012-10-05 07:50 am (UTC)(no subject)
Date: 2012-10-05 10:07 am (UTC)-вариантность -- это то, как связаны подтипизация параметра типа и самого типа. Чуть другое.
(no subject)
Date: 2012-10-05 06:21 am (UTC)(no subject)
Date: 2012-10-05 09:23 am (UTC)(no subject)
Date: 2012-10-05 10:11 am (UTC)Но и на хачкеле можно понаписать всяческих абстракций и тоже будит красиво
(no subject)
Date: 2012-10-05 01:37 pm (UTC)Создавать класс типов:
class Oven a where
checkOven:: a -> Bool
......
А потом уже создавать типы, например:
data GasOven = GasOven {...}
instance Oven GasOven where
checkOven (GasOven {...}) = ....
(no subject)
Date: 2012-10-05 02:05 pm (UTC)Вырванные из контекста строчки мне ничего не говорят. Если хочешь рассказать мне за Хаскель, попробуй размышлять подобным образом, выполняя задания без учета того, что там будет дальше. И по результатам покажи код, чтобы можно было увидеть, как он эволюционирует.
(no subject)
Date: 2012-10-05 10:23 am (UTC)Хачкелисты вот пирожки с газом пекут. Не иначе как на украину экспортировать.
(no subject)
Date: 2012-10-05 12:33 pm (UTC)(no subject)
Date: 2012-10-05 12:39 pm (UTC)(no subject)
Date: 2012-10-05 01:07 pm (UTC)(no subject)
Date: 2012-10-05 01:09 pm (UTC)(no subject)
Date: 2012-10-07 03:03 pm (UTC)(no subject)
Date: 2012-10-07 03:27 pm (UTC)(no subject)
Date: 2012-10-17 06:25 am (UTC)(no subject)
Date: 2012-10-17 10:42 am (UTC)