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

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

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

Это, конечно, весело троллировать, мол боллейрплейтнутый ООП-код: абстрактная фабрика абстрактных фабрик и тд. Только оно не с потолка берется. Я тоже в школе сел читать банду, быстро понял, что они поехавшие, и отложил. А оно вон как вышло, что я какие-то вещи делал, названий не знал, но работало неплохо. А тут стал читать и с удовольствием прямо. Чо, нормально люди классифицировали. И хоть я не всегда с ними был согласен, но их мотивы мне понятны. А говорить, что в ентом нашем ФП все само волшебно происходит, глупо. Разделять и абстрагироваться все равно надо, просто способов к этому меньше, чем в ООП, потому очень уж набедокурить сложнее, наверное.
This account has disabled anonymous posting.
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

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