使用RxJava实现类似旋转门的运算符

时间:2015-01-06 11:23:36

标签: reactive-programming rx-java

我需要帮助在RxJava(RxScala)中实现类似旋转门的运算符。我花了很多时间思考它,但我似乎陷入困境。

该功能的类型应如下:

def turnstile[T](queue: Observable[T], turnstile: Observable[Boolean]): Observable[T]

这个想法是操作员的行为应该与真正的旋转门非常相似。有人来(queue),并且有一个turnstile已准备好接受新的人(旋转门中的true元素,你可以把它想象成插入旋转栅门的一个标记),或关闭(旋转门中的false,取消之前的标记)。对于旋转栅门中的每个true元素,只有一个人可以通过。

此外,在没有人通过的情况下连续插入几个令牌(旋转门中的几个true项)与仅插入一个令牌相同,旋转门不会计算令牌。

换句话说,旋转栅门最初是关闭的。当其中出现true元素时,它会为单个人打开。如果一个人出现,它会通过(到输出)并且十字转门再次关闭。如果旋转门中出现false元素,则旋转门也会关闭。

queue       ----A---B-------------C--D--
turnstile   --T--------T--T-T-T-T------T
            ============================
output      ----A------B----------C----D

一张大理石图,显示等待人A的开启旋转门,然后等待旋转门开启的人B,然后几个令牌表现为单人C通过,但是人D必须再次等待新的令牌

----A----B--
--T---T-F-T-
============
----A-----B-

一张大理石图,显示旋转门中的false元素如何再次关闭旋转门。

感谢任何帮助。我认为在不编写自定义运算符的情况下实现此操作的唯一方法是以某种方式使用zip运算符,因为它可能是唯一一个使一个序列中的元素等待来自另一个序列的元素的运算符(或者是否有其他运算符)我不知道?)。但是我需要拉链一些旋转门元素,这取决于他们是否与某人配对......

我认为这是一个有趣的问题,我对它的一些很好的解决方案非常好奇。

2 个答案:

答案 0 :(得分:2)

所以我认为我有一个更清洁,完全Rx的解决方案。这实际上是一个非常有趣的问题需要解决。如果它能够满足您的需求,我认为它最终会变得非常优雅,虽然它需要很长时间才能达到它。

可悲的是,我不了解Scala,因此您将不得不处理我的Java8 lambda。 :d

整个实施:

public static Observable<String> getTurnstile(final Observable<String> queue, final Observable<Boolean> tokens) {
    return queue.publish(sharedQueue ->
            tokens.switchMap(token -> token ? sharedQueue.limit(1) : Observable.empty()));
}

所以,这里发生的是我们使用publish来创建我们可以多次订阅的人员队列的共享可观察对象。在其中,我们在令牌流上使用switchMap,这意味着无论何时从switchMap发出新的Observable,它都会删除最后一个并订阅新的Observable。只要令牌为真,它就会对人员队列进行新的订阅(并且连续多个真实,因为它取消了旧订阅)。如果它是假的,它只会抛弃一个空的Observable而不浪费时间。

还有一些(通过)测试用例:

@RunWith(JUnit4.class)
public class TurnstileTest {
    private final TestScheduler scheduler = new TestScheduler();
    private final TestSubscriber<String> output = new TestSubscriber<>();

    private final TestSubject<Boolean> tokens = TestSubject.create(scheduler);
    private final TestSubject<String> queue = TestSubject.create(scheduler);

    @Before
    public void setup() {
        Turnstile.getTurnstile(queue, tokens).subscribe(output);
    }

    @Test
    public void allowsOneWithTokenBefore() {
        tokens.onNext(true, 0);
        queue.onNext("Bill", 1);
        queue.onNext("Bob", 2);

        assertPassedThrough("Bill");
    }

    @Test
    public void tokenBeforeIsCancelable() {
        tokens.onNext(true, 0);
        tokens.onNext(false, 1);
        queue.onNext("Bill", 2);

        assertNonePassed();
    }

    @Test
    public void tokensBeforeAreCancelable() {
        tokens.onNext(true, 0);
        tokens.onNext(true, 1);
        tokens.onNext(true, 2);
        tokens.onNext(false, 3);
        queue.onNext("Bill", 4);

        assertNonePassed();
    }

    @Test
    public void eventualPassThroughAfterFalseTokens() {
        tokens.onNext(false, 0);
        queue.onNext("Bill", 1);
        tokens.onNext(false, 2);
        tokens.onNext(false, 3);
        queue.onNext("Jane", 4);
        queue.onNext("Bob", 5);
        tokens.onNext(true, 6);
        tokens.onNext(true, 7);
        tokens.onNext(false, 8);
        tokens.onNext(false, 9);
        queue.onNext("Phil", 10);
        tokens.onNext(false, 11);
        tokens.onNext(false, 12);
        tokens.onNext(true, 13);

        assertPassedThrough("Bill", "Jane", "Bob");
    }

    @Test
    public void allowsOneWithTokenAfter() {
        queue.onNext("Bill", 0);
        tokens.onNext(true, 1);
        queue.onNext("Bob", 2);

        assertPassedThrough("Bill");
    }

    @Test
    public void multipleTokenEntriesBeforeOnlyAllowsOneAtATime() {
        tokens.onNext(true, 0);
        tokens.onNext(true, 1);
        tokens.onNext(true, 2);
        queue.onNext("Bill", 3);
        tokens.onNext(true, 4);
        tokens.onNext(true, 5);
        queue.onNext("Jane", 6);
        queue.onNext("John", 7);

        assertPassedThrough("Bill", "Jane");
    }

    @Test
    public void noneShallPassWithoutToken() {
        queue.onNext("Jane", 0);
        queue.onNext("John", 1);

        assertNonePassed();
    }

    private void closeSubjects() {
        scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS);
        scheduler.triggerActions();
        tokens.onCompleted();
        queue.onCompleted();
        scheduler.triggerActions();
    }

    private void assertNonePassed() {
        closeSubjects();
        output.assertReceivedOnNext(Lists.newArrayList());
    }

    private void assertPassedThrough(final String... names) {
        closeSubjects();
        output.assertReceivedOnNext(Lists.newArrayList(names));
    }
}

如果您发现任何与此无关的边缘情况,请告诉我,特别是如果它实时出现问题,因为测试显然是在受控环境中。

答案 1 :(得分:0)

好的,我找到了一个解决方案,灵感来自Dave Sexton的评论。最后我没有使用zip因为我无法找到解决方案。

我基本上将十字转门实现为具有三个状态变量的状态机:它是否被锁定,等待通过十字转门的元素队列,以及通过十字转门的最后一个元素(这些被收集在结束产生实际产出。)

状态机的输入是转换请求流,它从两个输入流合并:锁定/解锁请求流和通过旋转门的元素流。我只使用scan处理转场,然后collect处理结果状态中传递的元素。

/** sample elements from queue through turnstile, one at a time
*
* @param queue source of elements to pass through the turnstile.
* @param turnstile For every `true` in the turnstile pass one element through from the queue
* @tparam T type of the elements
* @return the source of queue elements passing through the turnstile
*/
def queueThroughTurnstile[T](queue: Observable[T], turnstile: Observable[Boolean]): Observable[T] = {
  import scala.collection.immutable.Queue

  case class State(isOpen: Boolean, elementsInQueue: Queue[T], maybeLastEmittedElement: Option[T])
  sealed abstract class Transition
  case object Lock extends Transition
  case object Unlock extends Transition
  case class Element(element: T) extends Transition

  val initialState = State(isOpen = false, Queue.empty, None)

  queue.map(element ⇒ Element(element))
    .merge(turnstile map (unlock ⇒ if (unlock) Unlock else Lock))
    .scan(initialState) { case (State(isOpen, elementsInQueue, _), transition) ⇒ transition match {
    case Lock ⇒ State(isOpen = false, elementsInQueue, None)
    case Unlock ⇒ {
      if (elementsInQueue.isEmpty)
        State(isOpen = true, elementsInQueue, None)
      else {
        val (firstElement, newQueue) = elementsInQueue.dequeue
        State(isOpen = false, newQueue, Some(firstElement))
      }
    }
    case Element(newElement) ⇒ {
      if (isOpen) {
        if (elementsInQueue.isEmpty)
          State(isOpen = false, Queue.empty, Some(newElement))
        else {
          val (firstElement, newQueue) = elementsInQueue.dequeue
          State(isOpen = false, newQueue enqueue newElement, Some(firstElement))
        }  
      } else {
        State(isOpen = false, elementsInQueue enqueue newElement, None)
      }
    }
  }
  }.collect { case State(_, _, Some(lastEmittedElement)) ⇒ lastEmittedElement}
}