Conditionals, An Easier Design Decision But A Poor Choice

Switch case has made our lives so much easier since the day we started programming.

It solves problems when we have multiple options for a single input and have to decide which path to go.

….but often we use it and make our lives tough when it comes to scalability or when we have to modify the use-case.

There is a very popular software principle "replace conditionals with polymorphism".

It is easier to understand the concept but it is much difficult to understand and implement it on daily basis.

If you want to understand the concept, how it is beneficial, and how you as a developer can leverage it to write better code then we should dive in.

Problem Statement

We have data that we want to share with other applications using the Intent. The data can be text or a web URL.

We have the following mediums

  • Text (Default)
  • SMS
  • Web URL

Solutions

I'll discuss three different approaches to solve the above problem and discuss the pros and cons of each one of them.

Steps

We can break up the process into two steps

  • Create the intent
  • Start the activity with the intent

Method #1 (Using utility functions)

You can simply create multiple utility functions to create the intent and start the intent.

fun shareSimpleText(context: Context, text: String) {
    val intent = ShareCompat.IntentBuilder(context)
        .setType("text/plain")
        .setText(text)
        .intent

    val intentWithChooser = Intent.createChooser(intent, "Sharing with")
    context.startActivity(intentWithChooser)
}

fun shareViaSms(context: Context, sms: Sms) {
    val intent = Intent().apply {
        action = Intent.ACTION_VIEW
        data = Uri.parse("sms: ${sms.phoneNumber}")
        putExtra("sms_body", sms.body)
    }

    val intentWithChooser = Intent.createChooser(intent, "Sharing with")
    context.startActivity(intentWithChooser)
}

fun openWebPage(context: Context, url: String) {
    val intent = Intent().apply {
        action = Intent.ACTION_VIEW
        data = url.toUri()
    }

    val intentWithChooser = Intent.createChooser(intent, "Open url using")
    context.startActivity(intentWithChooser)
}

This is a fine approach, to begin with, but there are better ones out there and let's find them out.

Pros

  • We have individual methods for each operation which means we can add many different intent types without affecting the existing types.

Cons

  • A lot of code is repeated in the utility functions

This brings me to the second method which uses sealed classes.

Let's get down into this rabbit hole and see why it is a better approach but not the best.

Method #2 (Using sealed classes)

In this approach, you need to first create a sealed class to define all the possible sharing types.

Steps

  • Define a sealed class with all the sharing types.
  • Create the intent
  • Start the activity with the intent

So, let's define the sealed class, SharingType

sealed class SharingType {
  data class Default(val text: String) : SharingType()
  data class SmsType(val sms: Sms) : SharingType()
  data class WebBrowserType(val url: String) : SharingType()
}

Now create a factory method to get the intent object using the sharing type.

class SharingIntentFactory {

    fun prepareIntent(context: Context, sharingType: SharingType): Intent {
        return when (sharingType) {
            is SharingType.Default -> {
                ShareCompat.IntentBuilder(context)
                    .setType("text/plain")
                    .setText(sharingType.text)
                    .intent
            }
            is SharingType.SmsType -> {
                Intent().apply {
                    action = Intent.ACTION_VIEW
                    data = Uri.parse("sms: ${sharingType.sms.phoneNumber}")
                    putExtra("sms_body", sharingType.sms.body)
                }
            }
            is SharingType.WebBrowserType -> {
                Intent().apply {
                    action = Intent.ACTION_VIEW
                    data = sharingType.url.toUri()
                }
            }
        }
    }
}

Lastly, use the factory to prepare the intent and start the activity using the intent.

Before we move to the above step, we can even move the step to start the activity using the intent to its method and further reduce the repetition.

fun startActivity(context: Context, intent: Intent, chooserTitle: String) {
  val intentWithChooser = Intent.createChooser(intent, chooserTitle)
  context.startActivity(intentWithChooser)
}

phew, all the preparations for this approach are done, now we have to implement it.

val sharingIntentFactory = SharingIntentFactory()

fun shareViaSms(context: Context, sms: Sms) {
  val sharingType = SharingType.SmsType(sms)
  val intent = sharingIntentFactory.prepareIntent(context, sharingType)
  startActivity(context, intent, "Sharing with")
}

fun shareSimpleText(context: Context, text: String) {
  val sharingType = SharingType.Default(text)
  val intent = sharingIntentFactory.prepareIntent(context, sharingType)
  startActivity(context, intent, "Sharing with")
}

fun openWebPage(context: Context, url: String) {
  val sharingType = SharingType.WebBrowserType(url)
  val intent = sharingIntentFactory.prepareIntent(context, sharingType)
  startActivity(context, intent, "Open url using")
}

Pros

  • The logic for creating intents is abstracted properly using factory design pattern.
  • We have intent types in a sealed class

Cons

  • We have intent types in a sealed class

Ah! wait… Abhishek I think you made a mistake, you listed the pro under the cons subheading, or maybe you listed the con under the pros subheading.

I'm confused!

Well, this isn't a typo, allow me to explain.

This is a well-defined solution that uses the factory design pattern and all the steps are properly abstracted out which makes this approach much better than the last one.

The only flaw with this approach is that this is a procedural approach and not an object-oriented approach.

In other words, we have defined the data structure using sealed classes that means that whenever we have to update the intent types, we'll have to find and update all the places where the when statement is used to determine the behavior like in the SharingIntentFactory and this could spread across multiple places in a huge codebase and it can make the modification to the types a lot difficult and a tedious. Hence, this approach may not be the best one.

This brings me to the last approach which is an OO approach to overcome the drawbacks of sealed classes.

Method #3 (Object Oriented approach)

This approach will look a lot like the previous one but without the sealed classes.

Steps

  • Create an interface for intent type.
  • Define the concrete intent type class.
  • Create the intent
  • Start the activity with the intent

So, let's create an interface

interface SharingIntent {
  val intentChooserTitle: String
  fun prepareIntent(): Intent
}

Now we have to create implementations for each intent type, i.e, default, SMS, and web.

  • Text (default)
class DefaultSharingIntent(
    private val context: Context,
    private val body: String,
    chooserTitle: String = "Share using",
) : SharingIntent {
  override val intentChooserTitle: String = chooserTitle

  override fun prepareIntent(): Intent {
      return ShareCompat.IntentBuilder(context)
          .setType("text/plain")
          .setText(body)
          .intent
  }
}
  • SMS
class SmsSharingIntent(
    private val sms: Sms,
    chooserTitle: String = "Please select app to send SMS with",
) : SharingIntent {

  override val intentChooserTitle: String = chooserTitle

  override fun prepareIntent(): Intent = Intent().apply {
      action = Intent.ACTION_VIEW
      data = Uri.parse("sms: ${sms.phoneNumber}")
      putExtra("sms_body", sms.body)
  }
}
  • Web URL
class WebBrowserSharingIntent(
    private val url: String,
    chooserTitle: String = "Please select the web browser",
) : SharingIntent {

  override val intentChooserTitle: String = chooserTitle

  override fun prepareIntent(): Intent {
    return Intent().apply {
        action = Intent.ACTION_VIEW
        data = url.toUri()
    }
  }
}

So, the first step is now completed.

Next, create a utility function to start the intent.

fun shareIntent(context: Context, sharingIntent: SharingIntent) {
  val intent = sharingIntent.prepareIntent()
  val shareIntent = Intent.createChooser(intent, sharingIntent.intentChooserTitle)
  context.startActivity(shareIntent)
}

Lastly, we have to create the instances of the concrete implementation and start the activity.

fun shareViaSms(context: Context, sms: Sms) {
  val chooserTitle = "Sharing with"
  val smsSharingIntent = SmsSharingIntent(sms, chooserTitle)
  shareIntent(context, smsSharingIntent)
}

fun shareSimpleText(context: Context, text: String) {
  val chooserTitle = "Sharing with"
  val textSharingIntent = DefaultSharingIntent(context, text, chooserTitle)
  shareIntent(context, textSharingIntent)
}

fun openWebPage(context: Context, url: String) {
  val chooserTitle = "Open url using"
  val webBrowserSharingIntent = WebBrowserSharingIntent(url, chooserTitle)
  shareIntent(context, webBrowserSharingIntent)
}

…and we are done!

Pros

  • Concrete implementation for each intent type.
  • Adding or removing intent types won't cause ripple effects

Cons

  • Adding new behavior to intent will mean that we'll have to add it in all the implementations.

The above method which uses the OO approach is a really good solution to the above problem statement.

If you think about the cons, then you can overcome them by using the default method instead of the abstract method in the SharingIntent interface.

Lastly, I just want to say, both the procedural approach, as well as the object-oriented approach, are good. It depends on the problem you are trying to solve using them.

A rule of thumb that you follow while choosing one over the other is that when you have to frequently add/remove the type then use the object-oriented approach and when you want to frequently add/remove the behavior then use the procedural approach.

Conclusion

The different approaches that we discussed above are good in their own way but each subsequent to them overcomes the drawbacks of the previous one. In the article, we started with a procedural approach and ended up with an object-oriented approach.

You can find the implementation of all the three approaches listed above in this project.

How do you solve a similar problem in your project? Comment below or reach out to me on Twitteror LinkedIn.

Thank you very much for reading the article. Don't forget to 👏 if you liked it.