Подтвердить что ты не робот

Поместить метод в черту или в случае класса?

Существует два способа определения метода для двух разных классов, наследующих одну и ту же черту в 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

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

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

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" }
4b9b3361

Ответ 1

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

Почему? Просто сохранить Открытый/Закрытый Принцип.

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

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

ОБНОВЛЕНИЕ:

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

По-прежнему лучше иметь когезионные методы, чем смешивать целое внутри одного метода, представляя столько параметров, сколько ожидает метод union.

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

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

Поскольку ссылка объясняет:

Почему принцип open-closed имеет значение:

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

ОБНОВЛЕНИЕ 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"}

Выражение типа "вид":)

Ответ 2

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

Ответ 3

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

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

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

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

Ответ 4

Обратите внимание, что с Dotty (основа 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"