#rust #abstract
Вопрос:
Допустим, у меня есть общая логика, которая тесно зависит от элементов данных, а также от части абстрактной логики. Как я могу написать это в типах rust, не переписывая один и тот же код для каждой реализации?
Вот игрушечный пример того, что я мог бы написать в scala. Обратите внимание, что абстрактный класс имеет конкретную логику, которая зависит как от элемента данных name
, так и от абстрактной логики formatDate()
.
abstract class Greeting(name: String) {
def greet(): Unit = {
println(s"Hello $namenToday is ${formatDate()}.")
}
def formatDate(): String
}
class UsaGreeting(name: String) extends Greeting {
override def formatDate(): String = {
// somehow get year, month, day
s"$month/$day/$year"
}
}
class UkGreeting(name: String) extends Greeting {
override def formatDate(): String = {
// somehow get year, month, day
s"$day/$month/$year"
}
}
Это всего лишь игрушечный пример, но мои реальные жизненные ограничения таковы:
- У меня есть несколько элементов данных, а не только один (
name
). - Каждый подкласс имеет одни и те же сложные методы, которые зависят как от этих элементов данных, так и от абстрактных функций, специфичных для подкласса.
- Для хорошего дизайна API важно, чтобы реализующие
struct
s продолжали хранить все эти элементы данных и сложные методы.
Вот несколько несколько неудовлетворительных идей, которые у меня были, которые могли бы заставить эту работу работать в rust:
- Я мог бы потребовать
get_name()
метод на черте, который потребуется для каждой реализации. Но это кажется излишне многословным и может также привести к снижению производительности, если получатель не будет встроен. - Я мог бы вообще не использовать черту ржавчины, вместо этого создав структуру с дополнительным элементом данных, который реализует отсутствующую абстрактную логику. Но это делает абстрактную логику недоступной во время компиляции и определенно приведет к снижению производительности.
- Я мог бы снова вообще избежать использования черты ржавчины, вместо этого создав структуру с общим, связанные функции которой завершают абстрактную логику. До сих пор это моя лучшая идея, но мне кажется неправильным использовать универсальные средства для заполнения недостающей логики.
Я не совсем доволен этими идеями, так есть ли лучший способ в rust смешивать абстрактную логику с конкретной логикой, которая зависит от элементов данных?
Комментарии:
1. Есть какая-то особая причина
Greeting
, по которой он сохраняет само название? Почему нетdef greet(name: String)
?2. Трудно ответить на этот вопрос, потому что он одновременно, по частям, очень широк и расплывчат (вы не можете ответить об общем дизайне no OO только в ответе SO), а затем демонстрирует конкретный случай, который может получить целенаправленный ответ.
3. Я проголосовал за «необходимо сосредоточиться», но я никогда не был уверен, что делать с этими вопросами архитектуры программного обеспечения, которые носят абстрактный характер. Вы не можете отделить дизайн программы от проблемы, которую она решает. если ваша цель состоит в том, чтобы форматировать различные виды приветствий, вероятно, существует сотня способов написать программу, которая делает то, что имело бы больше смысла, чем это. Но вы просто используете
Greeting
в качестве примера — это чистая структура, без каких-либо требований или ограничений, — поэтому невозможно дать обоснованную рекомендацию.4. @kmdreko Да. Поскольку это игрушечный пример, я вижу, что было бы заманчиво
name
каким-то образом вытащить его, но в моем реальном проекте существует сложная конкретная логика, которая много раз обращается как к элементам данных, так и к абстрактным функциям, поэтому их на самом деле невозможно разделить.
Ответ №1:
Как вы заметили, Rust не построен на принципе таксономии классов, поэтому дизайн обычно отличается, и вам не следует пытаться издеваться над языками OO в Rust.
Вы задаете очень общий вопрос, но есть много конкретных случаев, требующих различных решений.
Очень часто, когда у вас возникает соблазн в языке OO определить, какие объекты относятся к классам, вы бы использовали черты, чтобы указать некоторые аспекты поведения структур в Rust.
В вашем конкретном случае, предполагая, что правильное решение не должно включать параметризацию или утилиту i18n, я бы, вероятно, использовал как композицию, так и перечисление для приветствия:
pub struct Greeting {
name: String,
greeter: Greeter;
}
impl Greeting {
pub fn greet(amp;self) -> String {
// use self.greeter.date_format() and self.name
}
}
pub enum Greeter {
USA,
UK,
}
impl Greeter {
fn date_format(amp;self) -> amp;'static str {
match self {
USA => ...,
UK => ...,
}
}
}
Ваша составная реализация просто должна включать вариант, когда это необходимо.
(обратите внимание, что я не пишу реализацию в этом случае, потому что проблемы с производительностью, вероятно, потребовали бы от Rust другого дизайна, а не динамически интерпретируемого шаблона, но это увело бы нас далеко от вашего вопроса)
Комментарии:
1. Но откуда взялась бы зависимость от таких элементов данных, как
name
? Я не думаю , что есть способ потребовать, чтобы каждое значение перечисления имело значениеname: String
, не так ли?2. @mwlon Вы правы, я полностью упустил из виду эту часть кода вашего вопроса. Я обновил ответ. Конечно, вы могли бы добавить название к каждому варианту, но это было бы громоздко и смешивало бы проблемы, поэтому композиция здесь кажется более уместной.
3. Как и для любого вопроса дизайна, реальное решение сильно зависит от реальной проблемы и чаще всего также связано с личными предпочтениями. Я пытаюсь получить зацепки или подсказки, но не окончательный дизайн.
Ответ №2:
Наиболее общим решением, по-видимому, является моя оригинальная 3-я пуля: вместо признака создайте структуру с общим, связанные функции которой дополняют функциональность.
Для оригинального Greeting
примера ответ Дениса, вероятно, лучше всего. Но если Greeting
также необходимо быть универсальным с типом в зависимости от реализации, это больше не работает. В этом случае это было бы наиболее общим решением, где T
тип реализации зависит от конкретной реализации.
Этот шаблон добавления дополнительного универсального должен быть способен реализовать любую общую логику, которая зависит как от элементов данных, так и от абстрактной логики (фактически абстрактный класс).:
trait Locale<T> {
pub fn local_greeting(info: T) -> String;
}
pub struct Greeting<T, LOCALE> where LOCALE: Locale<T> {
name: String,
locale_specific_info: T,
locale: PhantomData<LOCALE>, // needed to satisfy compiler
}
impl<T, LOCALE> Greeting<T, LOCALE> where LOCALE: Locale<T> {
pub fn new(name: String, locale_specific_info: T) {
Self {
name,
locale_specific_info,
locale: PhantomData,
}
}
pub fn greet() {
let local_greeting = LOCALE::local_greeting(self.locale_specific_info);
format!("Hello {}nToday is {}", self.name, local_greeting);
}
}
pub struct UsaLocale {}
impl Locale<Date> for UsaLocale {
pub fn local_greeting(info: Date) -> {
format!("{}/{}/{}", info.month, info.day, info.year)
};
}
pub type UsaGreeting = Greeting<Date, UsaLocale>;
...
pub type UkGreeting = ...