Как обойти отсутствие абстрактных классов в rust?

#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 = ...