What pattern or technique can I use in this case?

82 Views Asked by At

The following code is a small example of what has been bugging my brain for days: How to change this mutable-referenced struct's field value?.

I'm writing for the first time and by myself a small app that follows the "clean architecture" logic as I have done with other languages (garbage collected).

Can you help me understand what I'm doing wrong and what better pattern I can use (I'm thinking of a builder pattern, but also various setters methods that allow me to change the values when I need it, in several places)?

The biggest convenience I would like to achieve is to be able to use code like this:

if let Some(condition) = &mut input.condition {
    condition.set_subscribed_at_is_nil(true);
} else {
    input.condition = Some(PlayerConditions::new().unset_subscribed_at_is_nil());
}

or better:

input.condition.unwrap_or_default().set_subscribed_at_is_nil(true);

// or maybe input.condition = input.condition.unwrap_or_default().set_subscribed_at_is_nil(true);

if something_happened {
    input.condition.unset_it().set_another(Some("something"));
}

if something_else_happened {
    input.condition.set_another(Some("something")).check_something();
}

What can you suggest?

The code:

/*
[dependencies]
tokio = { version = "1", features = ["full"] }
*/

#[derive(Debug, Default)]
pub struct PlayerConditions {
    id: Option<String>,
    name: Option<String>,
    subscribed_at_is_nil: Option<bool>,
}

impl PlayerConditions {
    pub fn new() -> Self {
        Self::default()
    }
    pub fn is_default(&self) -> bool {
        self.id.is_none() && self.name.is_none() && self.subscribed_at_is_nil.is_none()
    }
    pub fn id(&self) -> &Option<String> {
        &self.id
    }
    pub fn set_id(&mut self, id: Option<String>) -> &mut Self {
        self.id = id;
        self
    }
    pub fn name(&self) -> &Option<String> {
        &self.name
    }
    pub fn set_name(&mut self, name: Option<String>) -> &mut Self {
        self.name = name;
        self
    }
    pub fn subscribed_at_is_nil(&self) -> &Option<bool> {
        &self.subscribed_at_is_nil
    }
    pub fn set_subscribed_at_is_nil(&mut self, subscribed_at_is_nil: Option<bool>) -> &mut Self {
        self.subscribed_at_is_nil = subscribed_at_is_nil;
        self
    }
}

#[derive(Debug, Default)]
pub struct PlayerListInput {
    pub condition: Option<PlayerConditions>,
    // This is not String: is a struct in real code
    pub order: Option<String>,
    // other fields here...
}

#[derive(Debug)]
pub struct DBPlayer {
    pub id: String,
    pub name: Option<String>,
    // For simplicity this is String in this example
    pub subscribed_at: Option<String>,
}

impl DBPlayer {
    fn query_construction<'a>(
        condition: Option<&'a mut PlayerConditions>,
        order: &Option<String>,
    ) -> String {
        // Sometimes I need to change condition here (so I need the mutability of it)

        let query = "SELECT * FROM players".to_string();

        if let Some(condition) = condition {
            if !condition.is_default() {
                // query = condition.add_each_one_to(query);
            }
        }

        if let Some(_order) = &order {
            // add order to query
        }

        query.to_string()
    }

    pub async fn query(
        _db: &str,      // This is not String: is a connection/transaction in real code
        input: &mut PlayerListInput,
    ) -> Vec<DBPlayer> {
        // Sometimes I need to change input here (so I need the mutability of it)

        let _query = Self::query_construction(input.condition.as_mut(), &input.order);

        let players = vec![]; // fetch_them_from_db_using_query_here;

        players
    }
}

async fn players_from_repo(input: &mut PlayerListInput) -> Vec<DBPlayer> {
    // Here I need to change input checking if something is already added, for example:

    if let Some(condition) = &mut input.condition {
        condition.set_subscribed_at_is_nil(Some(true));
    } else {
        input.condition = Some(PlayerConditions::new().set_subscribed_at_is_nil(Some(true)));
    }

    DBPlayer::query( "db_connection", input).await
}

#[tokio::main]
async fn main() {
    let mut input = PlayerListInput::default();

    let players = players_from_repo(&mut input).await;

    // I still need input ownership here
    dbg!(input);

    dbg!(players);
}
2

There are 2 best solutions below

3
Chayim Friedman On

What about simply:

if let Some(condition) = &mut input.condition {
    condition.set_subscribed_at_is_nil(Some(true));
} else {
    let mut new_condition = PlayerConditions::new();
    new_condition.set_subscribed_at_is_nil(Some(true));
    input.condition = Some(new_condition);
}

Yes, it's a bit longer, but it's crystal clear and we don't always need the shortest code.

0
kmdreko On

If you need to handle None as creating the value and then modifying it the same as if it were Some, you'd likely benefit from using one of the get_or_insert* functions on Option.

With it, you can write code like this:

input.condition
    .get_or_insert_with(PlayerConditions::new)
 // .set_id(None) -- can be chained since your methods take in `&mut Self` and return `&mut Self`
 // .set_name(None)
    .set_subscribed_at_is_nil(Some(true));