DEV Community

Isaac Batista
Isaac Batista

Posted on • Edited on

Refactoring toast presentation with OOP + Template Method

Context

Today I discovered a bug in my app code.

Last week, I have implemented a toast rendering for displaying an warning. And, if you don't know what a toast is, he's this little guy:

Toast example

Really cute, but here's what I was trying to do:

  • User presses a button with invalid field values
  • The toast should appear with some warning like "you cant do this with this values bro"

Simple as that.

It should be, but I did not thought that after receiving the message, the user would press that button insanely. And of course open billions of toasts at the same time.

So, that's my problem.

To figure out how to solve it, let's look at my code:

type ToastDuration = 'SHORT' | 'LONG'

type ToastInterface = {
  show(message: string, duration?: ToastDuration): void;
};

export const getPlatformToast = (): ToastInterface => {
  if (Platform.OS === 'android') {
    return {
      show(message: string, duration = 'SHORT') {
        ToastAndroid.show(message, ToastAndroid[duration]);
      },
    };
  }

  return {
    show(message: string, duration = 'SHORT') {
      Toast.show(message, {duration: Toast.durations[duration]});
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

Finding out a solution

As you can see, I have a native option for Android and a default option for other platforms. Besides that, none of the libs return me if the toast is open or not.

So, how would I prevent it from open?

I have to know if it's open, so that, when user rage press that button, not even a single extra toast appears.

Think with me:

  • When I press the button the toast opens
  • I know for how many seconds the toast should be there (that duration option).
  • I could, therefore, to start a timer when user presses the button for the first time.
  • If the user presses again and the timer is still on, nothing happens.
  • When the timer finishes, the toast should be openable again.

Yea bug, you have been burned.

Hash tag burned

It's time to apply!

Time to apply

The first thing I figured out was that I had to use some state to control if the toast was open.

I started to write for some seconds and wait... I have to write it twice, once for each interface implementation (android and default).

So, let's think again: state management, polymorphism..

SOUNDS LIKE FUN OBJECT ORIENTED MODELING TIME
Barney plays guitar

Awesome solo and lazer blasts

FUN OBJECT ORIENTED MODELING TIME

Back to the code, I like starting from concrete, then abstracting what makes sense. So I'll start with android implementation.

(btw I have changed some names, since I understood I'm not implementing the toast itself, but the one that opens/shows/presents it)

type Duration = 'SHORT' | 'LONG';
type ToastPresenter = {
  show(message: string, duration?: Duration): void;
};

class ToastPresenterAndroid implements ToastPresenter {
  private static readonly durationInMs: Record<Duration, number> = {
    LONG: 3500,
    SHORT: 2000,
  };

  private isShowing = false;

  show(message: string, duration: Duration = 'SHORT'): void {
    if (!this.isShowing) {
      ToastAndroid.show(message, ToastAndroid[duration]);
      this.isShowing = true;
      setTimeout(() => {
        this.isShowing = false;
      }, ToastPresenterAndroid.durationInMs[duration]);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Really nice.

With some concrete code implemented, it's much easier to see what's abstract (applies for any implementation) and what's concrete (specific for this android implementation).

(If you cant see it, write the second concrete implementation as well to make it more easy to see the common code.)

In the end it doesnt even matterrrr the line bellow here is the only and lonely concrete code:

ToastAndroid.show(message, ToastAndroid[duration]);

ALL the rest could and will be applied for that default implementation. Even the durationInMs are standardized. Then, how can I reutilize almost all the code and just customize that little piece in the middle?

Design Patterns to the rescue!!112!

We'll use template method.

As it's described:

Template Method is a behavioral design pattern that defines the skeleton of an algorithm in the superclass but lets subclasses override specific steps of the algorithm without changing its structure.

Exactly what we are trying here. It's really creepy, seems like they read our mind.

get out of my mind

How can we apply it? First we'll take that common code to a abstract class. And just that concrete line (which uses ToastAndroid) won't be there, we'll declare an abstract method and use it.

Since we are at this abstraction level, it doesnt make sense to use concrete libs now, specific behaviors wont be implemented

abstract class ToastPresenterAbstract implements ToastPresenter {
  private static readonly durationInMs: Record<Duration, number> = {
    LONG: 3500,
    SHORT: 2000,
  };

  private isShowing = false;

  show(message: string, duration: Duration = 'SHORT'): void {
    if (!this.isShowing) {
      this.showConcrete(message, duration);
      this.isShowing = true;
      setTimeout(() => {
        this.isShowing = false;
      }, ToastPresenterAbstract.durationInMs[duration]);
    }
  }

  protected abstract showConcrete(message: string, duration?: Duration): void;
}
Enter fullscreen mode Exit fullscreen mode

Finally, we can fill that template with concrete implementations, that will extends this basic common behavior:

class ToastPresenterAndroid extends ToastPresenterAbstract {
  protected showConcrete(message: string, duration: Duration): void {
    ToastAndroid.show(message, ToastAndroid[duration]);
  }
}

// "Root" is just a reference to the lib name "react-native-root-toast" xD
class ToastPresenterRoot extends ToastPresenterAbstract {
  protected showConcrete(message: string, duration: Duration) {
    const distanceFromBottom = -120;
    ToastRoot.show(message, {duration: ToastRoot.durations[duration], position: distanceFromBottom});
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice that all sorts of concrete behavior will rest on this layer. Such as that distanceFromBottom, it's a specific definition for this lib, to make the toast appears where it should. This was not necessary at the android implementation (even though some other thing could be necessary).

Returning, then, to the beginning, this is how my original function became:

export const getPlatformToast = (): ToastPresenter => {
  if (Platform.OS === 'android') {
    return new ToastPresenterAndroid();
  }

  return new ToastPresenterRoot();
};
Enter fullscreen mode Exit fullscreen mode

Bonus tip: as I was working in react, I had to create a ref, so the instance would keep alive through the component life cycle (keeping the isShowing state, the core of our logic). So, I made a custom hook:

const useToast = () => {
  const ref = useRef(getPlatformToast())

  return ref.current
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)