Поместить метод в трейт или в класс case?

Есть два способа определить метод для двух разных классов, наследующих один и тот же признак в Scala.

sealed trait Z { def minus: String }
case class A() extends Z { def minus = "a" }
case class B() extends Z { def minus = "b" }

Альтернатива следующая:

sealed trait Z { def minus: String = this match {
    case A() => "a"
    case B() => "b"
}
case class A() extends Z
case class B() extends Z

Первый метод повторяет имя метода, тогда как второй метод повторяет имя класса.

Я думаю, что лучше всего использовать первый метод, потому что коды разделены. Однако я обнаружил, что часто использую второй для сложных методов, так что добавление дополнительных аргументов может быть сделано очень легко, например, так:

sealed trait Z {
  def minus(word: Boolean = false): String = this match {
    case A() => if(word) "ant" else "a"
    case B() => if(word) "boat" else "b"
}
case class A() extends Z
case class B() extends Z

Каковы другие различия между этими практиками? Есть ли какие-то баги, которые меня ждут, если я выберу второй подход?

РЕДАКТИРОВАТЬ: я цитировал принцип открытости/закрытости, но иногда мне нужно изменить не только вывод функций в зависимости от новых классов case, но и ввод из-за рефакторинга кода. Есть ли шаблон лучше, чем первый? Если я хочу добавить ранее упомянутую функциональность в первом примере, это приведет к уродливому коду, в котором ввод повторяется:

sealed trait Z { def minus(word: Boolean): String  ; def minus = minus(false) }
case class A() extends Z { def minus(word: Boolean) = if(word) "ant" else "a" }
case class B() extends Z { def minus(word: Boolean) = if(word) "boat" else "b" }

person Mikaël Mayer    schedule 10.05.2013    source источник
comment
Позвольте мне предположить, что добавление любого нетривиального метода в класс case нецелесообразно с точки зрения объектно-ориентированного программирования. Экземпляр класса Case раскрывает все свои внутренности, а хорошие объекты — нет. Таким образом, это не настоящий объект, а такой же примитив, как строка или кортеж.   -  person lambdas    schedule 27.02.2016


Ответы (4)


Начиная с версии Scala 3, у вас есть возможность использовать параметры черт. (точно так же, как классы имеют параметры), что в данном случае значительно упрощает задачу:

trait Z(x: String) { def minus: String = x }
case class A() extends Z("a")
case class B() extends Z("b")
A().minus // "a"
B().minus // "b"
person Xavier Guihot    schedule 21.05.2019
comment
Очень интересно. Есть ли способ выставить переменную x, например. черта Z(val x: String) ? - person Mikaël Mayer; 23.05.2019
comment
@MikaëlMayer, вы действительно можете открыть переменную типажа: trait Z(val x: String) { def minus: String = x }, чтобы получить к ней доступ: A().x. Вы также можете добавить несколько параметров к признаку и выбрать, какие из них использовать в слове условие: trait Z(x: String, y: String) { def minus(word: Boolean = false): String = if (word) y else x } - case class A() extends Z("a", "c") - A().minus(true), что возвращает "c". - person Xavier Guihot; 23.05.2019

Я бы выбрал первое.

Почему ? Просто для соблюдения принципа открытости/закрытости.

Действительно, если вы хотите добавить еще один подкласс, скажем, case class C, вам придется изменить суперчерту/суперкласс, чтобы вставить новое условие... некрасиво

Ваш сценарий похож на Java с шаблоном template/strategy против условного.

ОБНОВЛЕНИЕ:

В вашем последнем сценарии вы не можете избежать «дублирования» ввода. Действительно, тип параметра в Scala нельзя вывести.

Все же лучше иметь связные методы, чем смешивать все в одном методе, представляющем столько параметров, сколько ожидает объединение методов.

Просто представьте десять условий в вашем методе суперчерт. Что, если вы непреднамеренно измените поведение каждого из них? Каждое изменение будет сопряжено с риском, и модульные тесты суперпризнаков всегда должны запускаться каждый раз, когда вы его изменяете...

Более того, непреднамеренное изменение входного параметра (не ПОВЕДЕНИЯ) вовсе не «опасно». Почему? потому что компилятор скажет вам, что тип параметра/параметра больше не актуален. И если вы хотите изменить его и сделать то же самое для каждого подкласса... обратитесь к своей IDE, она любит рефакторинг подобных вещей одним щелчком мыши.

Как объясняется в этой ссылке:

Почему важен принцип открытого-закрытого:

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

ОБНОВЛЕНИЕ 2:

Вот пример, избегающий дублирования входных данных, соответствующий вашим ожиданиям:

sealed trait Z { 
     def minus(word: Boolean): String = if(word) whenWord else whenNotWord
     def whenWord: String
     def whenNotWord: String             
  }

case class A() extends Z { def whenWord = "ant"; def whenNotWord = "a"}

Вывод типа спасибо :)

person Mik378    schedule 10.05.2013
comment
и если вы хотите добавить в функцию новый аргумент, вам нужно изменить его в каждой подфункции, даже если в этом нет необходимости. Это тоже не некрасиво? - person Mikaël Mayer; 10.05.2013
comment
@Mikaël Mayer В вашем последнем сценарии (обновление) мы хорошо видим, что поведение B вообще не нуждается в emphasizeA. Таким образом, я могу вернуть этот вопрос к вашему первому комментарию: что лучше? Сплоченная функция, быстро понятная и короткая, или навязывание всех поведений из подкласса под конкретную сигнатуру, даже они им не нужны... - person Mik378; 10.05.2013
comment
Хорошо, я уточню свой вопрос. - person Mikaël Mayer; 10.05.2013
comment
Возможно, если всегда есть только один аргумент, который сам является классом, у меня может быть первый подход, позволяющий очень легко добавлять новые функции без переписывания аргументов. - person Mikaël Mayer; 10.05.2013
comment
@ Микаэль Майер повторно обновлен ;) - person Mik378; 10.05.2013
comment
@Mikaël Mayer Повторение аргумента при работе с наследованием НИКОГДА не было чем-то плохим. Это сущность контракта интерфейса и уточненных подклассов. - person Mik378; 10.05.2013

Лично я бы держался подальше от второго подхода. Каждый раз, когда вы добавляете новый подкласс Z, вы должны прикасаться к общему минусовому методу, потенциально подвергая риску поведение, связанное с существующими реализациями. При первом подходе добавление нового подкласса не оказывает потенциального побочного эффекта на существующие структуры. Здесь может быть немного принципа Открытости/Закрытости, и ваш второй подход может нарушить его.

person cmbaxter    schedule 10.05.2013

Принцип Open/Closed может быть нарушен при обоих подходах. Они ортогональны друг другу. Первый позволяет легко добавлять новый тип и реализовывать необходимые методы, он нарушает принцип Open/Closed, если вам нужно добавить новый метод в иерархию или рефакторить сигнатуры методов до такой степени, что он ломает любой клиентский код. В конце концов, именно по этой причине в интерфейсы Java8 были добавлены методы по умолчанию, чтобы старый API можно было расширить, не требуя адаптации клиентского кода. Такой подход типичен для ООП.

Второй подход более характерен для ФП. В этом случае легко добавить методы, но сложно добавить новый тип (здесь нарушается O/C). Это хороший подход для закрытых иерархий, типичным примером являются алгебраические типы данных (ADT). Стандартизированный протокол, который не предназначен для расширения клиентами, может быть кандидатом.

Языки изо всех сил пытаются позволить разработать API, который имел бы оба преимущества - легко добавлять типы, а также добавлять методы. Эта проблема называется проблемой выражения. Scala предоставляет шаблон Typeclass для решения этой проблемы, который позволяет добавлять функциональные возможности к существующим типам ситуативным и выборочным образом.

Какой из них лучше, зависит от вашего варианта использования.

person petrn    schedule 26.01.2017
comment
Здравствуйте, спасибо за это понимание, можете ли вы показать пример использования шаблонов Typeclass? - person Mikaël Mayer; 27.01.2017