julianomoraes / componentizationArch

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

What if you reuse the same component in a single screen?

jorgegil96 opened this issue · comments

I'm playing around with this architecture and hit a roadblock early on..

I have a component I want to reuse in the same fragment. Since the Event class I'm emitting to the bus is the same for both components, you can't tell if some of the events are meant for one or other component.

Example:

My RateButtonsComponent:

open class RateButtonsComponent(
    parent: ViewGroup,
    private val bus: EventBusFactory
) {
  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
  val uiView = initView(parent, bus)

  open fun initView(parent: ViewGroup, bus: EventBusFactory): RateButtonsUiView {
    return RateButtonsUiView(parent, bus)
  }

  init {
    bus.getSafeManagedObservable(RateEvents::class.java)
        .subscribe { event ->
          when (event) {
            ThumbsUpSelected -> uiView.setSelected(THUMBS_UP)
            ThumbsDownSelected -> uiView.setSelected(THUMBS_DOWN)
          }
        }
  }

  fun getUserInteractionEvents(): Observable<RateButtonsUiView.RateUserInteractionEvent> {
    return bus.getSafeManagedObservable(RateButtonsUiView.RateUserInteractionEvent::class.java)
  }

  sealed class RateEvents : ComponentEvent() {
    object ThumbsUpSelected : RateEvents()
    object ThumbsDownSelected : RateEvents()
  }
}

My fragment:

override fun onViewCreated() {
    firstQuestionRateBtnsComponent = RateButtonsComponent(firstQuestionButtons, eventBusFactory)
      firstQuestionRateBtnsComponent.getUserInteractionEvents()
        .subscribe { event ->
          when (event) {
            RateUserInteractionEvent.ThumbsUpPressed -> {
              eventBusFactory.emit(RateEvents::class.java, RateEvents.ThumbsUpSelected)
            }
            RateUserInteractionEvent.ThumbsDownPressed -> {
              eventBusFactory.emit(RateEvents::class.java, RateEvents.ThumbsDownSelected)
            }
          }
        }

    secondQuestionRateBtnsComponent = RateButtonsComponent(secondQuestionButtons, eventBusFactory)
    secondQuestionRateBtnsComponent.getUserInteractionEvents()
        .subscribe { event ->
          when (event) {
            RateUserInteractionEvent.ThumbsUpPressed -> {
              eventBusFactory.emit(RateEvents::class.java, RateEvents.ThumbsUpSelected)
            }
            RateUserInteractionEvent.ThumbsDownPressed -> {
              eventBusFactory.emit(RateEvents::class.java, RateEvents.ThumbsDownSelected)
            }
          }
        }
}

So when I click thumbsUp on my firstQuestionComponent I receive an RateUserInteractionEvent.ThumbsUpPressed event that then emits eventBusFactory.emit(RateEvents::class.java, RateEvents.ThumbsUpSelected). The issue is that both instances of the RateButtonsComponent receive this event since they're both listening for RateEvents::class.java..

Is this not the type of component reusability that this architecture can solve? Am I doing something wrong?

You could define a field in the event class called origin or source. Then you may use this to know where the event comes from. Think of it like a publish/subscribe pattern where you subscribe to a topic. The class type will be the topic but the message itself brings more useful metadata to identify the sender and such.

I thought about that and gave it a try, but I'm not sure how I feel about it, it feels a bit dirty.

To sum up:

  1. I had to pass a componentId to my RateButtonsComponent in the constructor
  2. In my fragment I assign different IDs to each component, so RateBtnsComponent(.., .., ID_1) and `RateBtnsComponent(.., .., ID_2)
  3. Then you need to pass the ID from the component to the UiView so that events can be emitted with the ID
  4. So the InteractionEvent needs to hold the id: InteractionEvent.ThumbsUpPressed(val compId: Int) and is emitter onClickListener
  5. This event is received back in the fragment, and since both components in the fragment are receiving the InteractionEvent you need to add an if check to see it the origin matches the component before acting on it.
  6. If the if (event.compId == ID_1) check succeeds then you emit a state event, ThumbsUpSelected that is observed by the component.
  7. But since this Event is also being observed by the second component you also need to add the componentId to the state event, and then add another if(event.compId == this.componentId) check to see if the uiView.doSomething() method should be called.

That's 3 if (event.id == ID) checks (1 in the component, 2 for the two components I have in my fragment). Multiply that by 2 since there's also a thumbs down button and you got 6. Imagine if my thumbs up/down was 5 smiley/sad faces instead and I had 10 questions in my fragment..(that's 5 ifs in the component + 10*5 in the fragment = 55 if checks)

I guess my point is that something seems off to me about what I'm doing, sure it works, but I feel like there has to be a better way of doing this. It might just be this particular use case that is adding complexity.

Though to be fair, maybe that "complexity" of 6 or 55 if checks is still better than other architectures

Well, definitely you have demystified one common problem that occurs in a Publish/Subscribe architecture and also in the Actor Model. Basically what happens is that you have a set of Components that send events between each other. There are many ways these components can interact among each other. Typically the component that emits events behaves like a reactive processor(Publisher) while those who subscribe behave like observers(Consumers). The Consumers can subscribe in three different ways:
1- Exclusive
2- Multicast
3- Shared(one listener get the event pipe at a time)

A component can behave as both publisher and consumer at the same time. If you look at AKKA(Actor Model), Kafka, Pulsar and other popular Messaging Event architectures, you will see that each entity has an ID somehow. ID which they use to control all these possible scenarios like the one you are facing.
That means that having an ID per Component is not necessarily insane or crazy, although it adds certain overhead is convenient in many situations.
The overhead is what you are experiencing, that now you have bellow type of code in every Component.

if (event.compId == ID_1)

But I think this is something you have to learn to live with if you take the route. I advice you to do a quick search on Google on how Actors interact between each other, in the Actor Model. You will see that types and IDs are key for that.

There are techniques to minimize nesting if/else or switch logic. You can always create Abstractions and use the map() function in the RX Stream to select the proper implementation.

//instead of:
eventObservable.subscribe { event ->
    if (event.ID == EV_1) {
        ...logic 1
    } else if (event.ID == EV_2) {
        ...logic 2
    }
}

// do
eventObservable.map { event -> // it takes an event and map it to its EventHandlerImpl
    switch(event.ID) {
            case EV_1: return EventHandlerImpl_1
            case EV_2: return EventHandlerImpl_2
            ...
        } 
}
.subscribe { eventHandlerInterface
    eventHandlerInterfaceImpl.doLogic()
}

I hope you got the idea.

Denifitely! Thanks for the insight. I do like the EventHandlerImpl_1/2 you got there, looks kinda like the strategy pattern.

Great discussion, thank you for all the thoughts.

The way I see it is every component that you have has a State, the state is ThumbsUpSelected(true|false). When you fire an event from the user click you must specify which component was clicked adding the ID to the event. Then the reducer(Fragment) will emit a RateEvent(id) and inside every component you check for it and render accordantly. Like:

bus.getSafeManagedObservable(RateEvents::class.java) .subscribe { event -> when (event) { 
   ThumbsUpSelected -> if (it.id == this.id) uiView.setSelected(THUMBS_UP) 
   ThumbsDownSelected -> if (it.id == this.id) uiView.setSelected(THUMBS_DOWN) 
} }

Or you can always play with Rx like:

bus.getSafeManagedObservable(RateEvents::class.java)
   .filter {it.id == this.id}
   .subscribe { event -> when (event) { 
      ThumbsUpSelected -> uiView.setSelected(THUMBS_UP) 
      ThumbsDownSelected -> uiView.setSelected(THUMBS_DOWN) 
   } }

'Filter` will make sure the logic will run only for events that have that component as target.

Hope it helps!