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:09 pm (UTC)
From: [identity profile] stdray.livejournal.com
на моей пекарне пищепром клином не сошелся

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