stdray: (Default)
[personal profile] stdray
Тут на хабре что-то про хлеб писали, мне в твиттерок спамило. Я зашел, дочитал до
Один из них умный, прочёл кучу статей на Хабре, знает каталог GoF наизусть, а Фаулера — в лицо.

И, естественно, дропнул к чертям. Но поднялся какой-то ажиотаж: хаскелисты хлеб выпекали и даже в асечке в меня этой буханкой запульнули. Ну, прочитал, я про этих пекарей и даже просмотрел расцвет экспертизы комментариях. Выводы, как заметил сам автор, немного предсказуемы: паттерноблять соснула и ООП вместе с ним. А вот ёба-функция на 100500 строк - это, оказывается, быстрая разработка.

Я считаю, что еще одна диванная икспертиза будет не лишней. И использую гнобимый в интернетах, но такой распространенный на практике ОО-подход. Буду делать на C# и все написанное относится именно к нему, никаких SmallTalk, никаких рассуждений с высоты птичьего полета о сути ООП. Только инструмент и его возможности.
приходит проектный менеджер и говорит:
— Ребята, нам нужно, чтобы делался хлеб.
Именно так, «делался», без уточнения способа производства.

Требований нет, додумывать занятие неблагодарное, да и богатый опыт в разработке энторпрайз опреденей не стоит переоценивать. Потому, что говорят, то и делаю:
  1. class Bread { }  
  2.   
  3. class Bakehouse {  
  4.     public Bread BakeBread() {  
  5.            return new Bread();  
  6.     }  
  7. }  

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

Нифига подобного. Я тут хлеб пеку, а не печка. Она лишь используется в этом процессе потому
  1. class Oven {}  
  2.   
  3. class Bakehouse {  
  4.     public Bread BakeBread(Oven oven) {  
  5.         //как-то там его в печи выпекаем   
  6.         return new Bread;  
  7.     }  
  8. }  

Далее
— Нам нужно, чтобы печки были разных видов.

Вот тут уже есть 2 варианта: делать базовый класс или делать интерфейс. В ентом ООП свалены в кучу наследование реализации и наследование требований к объекту. Вот наследование реализации - вещь не очень хорошая. Сценариев использования всегда больше, чем можно продумать на этапе проектирования. Потому лучше даже не пытаться, а просто задать ограничения для объекта, который будет использоваться как печь. Выбирая из абстрактных классов и интерфейсов в такой ситуации, я предпочитаю интерфейс. Бла-бла-бла, одиночное наследование очень хорошо, да. В итоге, используемые методы печи выносятся в интерфейс АйПечь и код практически не меняется
  1. interface IOven { }  
  2.   
  3. class Bakehouse {  
  4.     public Bread BakeBread(IOven oven) {      }  
  5. }  

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

А вот это уже пушечка. Потому что 100% надо ванговать. Во-первых, чего нам делать, если нет газа? Можно ли начать выпечку и в печке бросить исключение, что газа нет или любая попытка запустить печь ведет к нежелательным эффектам? Во-вторых, может ли, например, печь использующая картофельную энергию работать без картошки? Все это непонятно, потому я отделаюсь отмазкой, дав возможность опрашивать любую печь, на тему, готова ли она выпекать. А газовую, соответственно, надо научить отвечать на этот вопрос, в зависимости от того, есть у нее газ или нет.
  1. interface IOven {  
  2.     bool CanBake { get; }  
  3. }  
  4.   
  5. class GasOven : IOven {  
  6.     private double _gasLevel;  
  7.     public GasOven(double gasLevel) {  
  8.         _gasLevel = gasLevel;  
  9.     }  
  10.     public bool CanBake {  
  11.         get { return _gasLevel > 0; }  
  12.     }  
  13.   
  14. }  
  15.   
  16. class Bakehouse {  
  17.     public Bread BakeBread(IOven oven) {  
  18.         if(oven.CanBake == false) {  
  19.             throw new ArgumentException("Ваша печь не работает!");  
  20.         }  
  21.         retuen new Bread()  
  22.     }  
  23. }  

Тут, на мой взгляд, несколько нюансов.
Можно было бы поменять тип возвращаемого Bake значения на Maybe[Bread]. Но это школоподход, потому что в настоящих оперденях, если хлеб выпечен не был, надо точно знать, кто, где и почему навернулся. Эта информация должна быть залогирована, а ответственность делегирована в нужном направлении: специалистам по печам, если печепроблемы, программистам, если там нуллреференс какой. "Хлеба нет" - никого не устраивает. Either[string, Bread] или Either[Exception, Bread] тоже не нужны, потому в C# все наловчились бросать и ловить исключения, а не таскать их с собой руками по стеку.
Можно заменить if и throw контрактами:
Contract.Requires(oven.CanBake);
Так будет наглядней и, глядишь, валидатор сможет шепнуть, что вы тут балаган устроили со своими сломанными печами.
И последнее, что я считаю важным, на этом этапе - это какой-то базовый класс для исключений. Я, честно говоря, ленивый, то есть писать catch(Exception ex) вроде не культурно, а лазить в декларации или код, чтобы смотреть какие _свои_ исключения бросает функция или библиотека в падлу. Потому надо бы зделоть что-то вроде
  1. class BakehouseException : Exception {  
  2.      public BakehouseException(string msg) : base(msg) {}  
  3.      public BakehouseException(string msg, Exception ex) : base(msg, ex){}  
  4. }  
  5.   
  6. class OvenException : BakehouseException {  
  7.     public IOven Oven { getprivate set; }  
  8.     public OvenException(IOven oven, string msg) : base(msg) {}  
  9.     public OvenException(IOven oven, string msg, Exception ex) : base(msg, ex) {}  
  10. }  

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

И тут ситуация почти ничем не отличается от той, когда внезапно стало много печек. Делаем интерфейс АйВыпечка, и наследуем его для хлеба, торта и пирожков
  1. interface IСook { }  
  2. class Bread : IСook { }  
  3. class Cake : IСook { }  
  4. class Pasty : IСook {  
  5.     public string Filling { getprivate set; }  
  6.     public Pasty(string filling) {  
  7.         Filling = filling;  
  8.     }  
  9. }  
  10.   
  11. class Bakehouse {  
  12.     public Bread BakeBread(IOven oven) {  
  13.         return Bake(oven, o => new Bread());  
  14.     }  
  15.     public Cake BakeCake(IOven oven) {  
  16.         return Bake(oven, o => new Cake());  
  17.     }  
  18.     public Pasty BakeMeatPasty(IOven oven) {  
  19.         return Bake(oven, o => new Pasty("Meat"));  
  20.     }  
  21.     public Pasty BakeCabbagePasty(IOven oven) {  
  22.         return Bake(oven, o => new Pasty("Cabbage"));  
  23.     }  
  24.     public T Bake<T>(IOven oven, Func<IOven, T> maker) where T: ICook{  
  25.         Requires(oven.CanBake, _ => new OvenException(oven, "Ваша печь не работает!"));  
  26.         Requires(makeCook!=null, _ => new BakehouseException("Вы не дали рецепт"));  
  27.         return maker(oven);  
  28.     }  

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

Вот тут же интересней. Рецепт - это алгоритм, по которому можно что-то приготовить. По сути это функция от нескольких аргументов (ингридиенты, инструменты) возвращающая некоторый продукт. У нас пекарня, потому один аргумент известен точно - это печь. Однако, возможно рецепты придется читать из файла или еще-то с ними творить. Потому стоит сделать отдельным объектом. Делаем интерфейс и реализации для пирожков, к примеру
  1. interface IRecipe<T> where T : ICook {  
  2.     public T Cook(IOven oven);  
  3. }  
  4.   
  5. class MeatPasstyRecipe : IRecipe<Pasty> {  
  6.     public Pasty Cook(IOven oven) {  
  7.         return new Pasty("Meat");  
  8.     }  
  9. }  
  10.   
  11. class MeatPasstyRecipe : IRecipe<Pasty>{  
  12.     public Pasty Cook(IOven oven) {  
  13.         return new Pasty("Cabbage");  
  14.     }  
  15. }  

Теперь в пекарню добавяется метод выпечки по рецептам.
  1. public T Bake<T>(IOven oven, IRecipe<T> recipe) where T : ICook {  
  2.     return Bake(oven, recipe.Cook);  
  3. }  

И последнее требование
— Нам нужно, чтобы в печи можно было обжигать кирпичи.

Не приносит ничего нового
  1. class Brick : ICook { }  

и метод для выпекания кирпича. Например такой:
  1. public Brick BakeBrick(IOven oven) {  
  2.     return Bake(oven, o => new Brick());  
  3. }  

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

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

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)
From: [identity profile] gds.livejournal.com
фп -- это не х-ь. Чисто-функциональные объекты вполне так существуют, пригодны на практике, обеспечивают подтипизацию (как следствие -- интерфейсы), радуют на реальных задачах.

(no subject)

Date: 2012-10-05 07:50 am (UTC)
From: [identity profile] stdray.livejournal.com
Подтипизация - это про ко- и контр- вариатность? Как это помогает составлять интерфейсы.

(no subject)

Date: 2012-10-05 10:07 am (UTC)
From: [identity profile] gds.livejournal.com
подтипизация -- возможность использовать значения одних типов вместо значений других типов.

-вариантность -- это то, как связаны подтипизация параметра типа и самого типа. Чуть другое.

(no subject)

Date: 2012-10-05 06:21 am (UTC)
From: [identity profile] thedeemon.livejournal.com
интерфейс АйДаВыпечка :)

(no subject)

Date: 2012-10-05 09:23 am (UTC)
From: [identity profile] stdray.livejournal.com
добавить пирожки с яблочками и открывать булочную с таким названием можно.

(no subject)

Date: 2012-10-05 10:11 am (UTC)
From: [identity profile] hls-1141.livejournal.com
какой мягкий и пушистый сишарпо-код =) Таки всё понятно.
Но и на хачкеле можно понаписать всяческих абстракций и тоже будит красиво

(no subject)

Date: 2012-10-05 01:37 pm (UTC)
From: [identity profile] hls-1141.livejournal.com
Я придумал только вот что:
Создавать класс типов:

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)
From: [identity profile] stdray.livejournal.com
Идея статьи на хабре была в том, что серия последовательных незапланированных изменений при ОО-подходе приводит к постоянному переписыванию кода. Я провел подобный эксперимент и удовлетворен его результатами: достаточно гибко, единой физической точки расширения нет, инкапсуляция есть, понятный публичный интерфейс есть. Дело тут не в красоте или не в количестве сущностей.

Вырванные из контекста строчки мне ничего не говорят. Если хочешь рассказать мне за Хаскель, попробуй размышлять подобным образом, выполняя задания без учета того, что там будет дальше. И по результатам покажи код, чтобы можно было увидеть, как он эволюционирует.
Edited Date: 2012-10-05 02:13 pm (UTC)

(no subject)

Date: 2012-10-05 10:23 am (UTC)
From: [identity profile] satanus.livejournal.com
> Начнут еще пирожки с говном выпекать в газовой печи.

Хачкелисты вот пирожки с газом пекут. Не иначе как на украину экспортировать.

(no subject)

Date: 2012-10-05 12:33 pm (UTC)
From: [identity profile] n16bs.livejournal.com
Чёрт, есть захотелось.

(no subject)

Date: 2012-10-05 12:39 pm (UTC)
From: [identity profile] stdray.livejournal.com
А я пряник заточил

(no subject)

Date: 2012-10-05 01:07 pm (UTC)
From: [identity profile] hls-1141.livejournal.com
Твоя пекарня не умеет делать пряники.

(no subject)

Date: 2012-10-05 01:09 pm (UTC)
From: [identity profile] stdray.livejournal.com
на моей пекарне пищепром клином не сошелся

(no subject)

Date: 2012-10-07 03:03 pm (UTC)
From: [identity profile] sassa-nf.livejournal.com
    public Bread BakeBread(BreadFactory oven) {  
        return oven.newInstance();  
    } 

(no subject)

Date: 2012-10-07 03:27 pm (UTC)
From: [identity profile] stdray.livejournal.com
Нет. Так нельзя)

(no subject)

Date: 2012-10-17 06:25 am (UTC)
From: [identity profile] isorecursive.livejournal.com
{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, UnicodeSyntax, KindSignatures, LambdaCase #-}

import Prelude.Unicode

-- Ребята, нам нужно чтобы делался хлеб
{-
data Bread = Bread

bakeBread = Bread
-}

-- Нам нужно, чтобы хлеб не просто делался, а выпекался в печке
{-
data Oven (m ∷ * → *) = Oven

bakeBread ∷ Monad m ⇒ Oven m → m Bread
bakeBread oven = return Bread
-}

-- Нам нужно, чтобы печки были разных видов
{-
class Oven (o ∷ (* → *) → *) where
-}

-- Нам нужно, чтобы газовая печь не могла печь без газа
data GasOven m = GasOven { gasAvailable ∷ m Bool }

class Oven (o ∷ (* → *) → *) where
  canBake ∷ o m → m Bool

instance Oven GasOven where
  canBake = gasAvailable

-- Нам нужно, чтобы печки могли выпекать ещё и пирожки (отдельно - с мясом, отдельно - с капустой), и торты
data Bread   = Bread
data Cake    = Cake
data Patty f = Patty { filling ∷ f }

bake ∷ (Monad m, Oven oven) ⇒ oven m → (oven m → m bakery) → m bakery
bake oven wf = canBake oven >>= \case
  True  → wf oven
  False → error "Your oven is dead!"

bakeBread        oven = bake oven $ return ∘ const Bread
bakeCake         oven = bake oven $ return ∘ const Cake
bakeMeatPatty    oven = bake oven $ return ∘ const (Patty "Meat")
bakeCabbagePatty oven = bake oven $ return ∘ const (Patty "Cabbage")

-- Нам нужно, чтобы хлеб, пирожки, и торты выпекались по разным рецептам
class Recipe bakery ingredients where
  cook ∷ (Monad m, Oven oven) ⇒ oven m → ingredients → m bakery

data Meat    = Meat
data Cabbage = Cabbage
   
instance Recipe (Patty Meat) Meat where
  cook oven Meat = bake oven $ return ∘ const (Patty Meat)

instance Recipe (Patty Cabbage) Cabbage where
  cook oven Cabbage = bake oven $ return ∘ const (Patty Cabbage)

-- Нам нужно, чтобы в печи можно было обжигать кирпичи
data Brick = Brick

bakeBrick oven = bake oven $ return ∘ const Brick
Edited Date: 2012-10-17 10:20 am (UTC)

(no subject)

Date: 2012-10-17 10:42 am (UTC)
From: [identity profile] stdray.livejournal.com
Как-то так, да. Очень похоже то, что я делал на C#. Единственное, у меня на продукцию некоторые ограничения накладывались, хотя не уверен, что это было нужно. Тут, видимо, в качестве задела на будущее, делается предположение о монадичности некоторых операций, хотя ни одно из требований задачи явно об этом не говорит. То есть приходится размышлять над дизайном системы, развязывая себе руки в определенных местах. Такой-то ОО-подход в стиле Хаскель получился (я таки надеюсь, что именно получился в результате размышлений, а не просто пруф того, что можно C#-код 1 в 1 переложить на Хаскель). Ну и количество получаемых сущностей не сильно меньше, хотя кого это должно волновать. Если не кидаться интами-строками как в хабрастатье, а делать нормальные расширяемые типы, то особо срезать углы не получается. И более-менее одинаково вышло.

December 2019

S M T W T F S
1234567
891011121314
15161718192021
222324252627 28
293031    

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags