-
Notifications
You must be signed in to change notification settings - Fork 7.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Observable interface #744
Observable interface #744
Conversation
The IObservable interface supports the core contract of observability: handling of each element, detection of completion, and detection of errors. The concrete Observable class is now focused on its rightful role: providing a set of highly usable fluent utilities for working with IObservables. For another example of this pattern, see e.g. Guava’s FluentIterable vs. the JDK’s Iterable. (If not for backward compatibility, Observable might have been called “FluentObservable” and IObservable simply “Observable”.)
Utilities outside of the fluent Observable class are now more reusable as a result.
…rationSubscribe class. This refactoring makes these subscribe(…) forms more reusable with IObservables that aren’t of the concrete type Observable. See the changes in ChunkedOperation as an example.
Merged as of commit 'caeaf58d58c52b1028b7d4ec2d34dfc9b0baeb4d’. Conflicts: rxjava-core/src/main/java/rx/Observable.java rxjava-core/src/main/java/rx/joins/JoinObserver1.java rxjava-core/src/main/java/rx/joins/Pattern1.java rxjava-core/src/main/java/rx/joins/Pattern2.java rxjava-core/src/main/java/rx/joins/Pattern3.java rxjava-core/src/main/java/rx/observables/BlockingObservable.java rxjava-core/src/main/java/rx/operators/ChunkedOperation.java rxjava-core/src/main/java/rx/operators/OperationBuffer.java rxjava-core/src/main/java/rx/operators/OperationConcat.java rxjava-core/src/main/java/rx/operators/OperationDoOnEach.java rxjava-core/src/main/java/rx/operators/OperationFirstOrDefault.java rxjava-core/src/main/java/rx/operators/OperationJoin.java rxjava-core/src/main/java/rx/operators/OperationJoinPatterns.java rxjava-core/src/main/java/rx/operators/OperationLast.java rxjava-core/src/main/java/rx/operators/OperationMap.java rxjava-core/src/main/java/rx/operators/OperationMostRecent.java rxjava-core/src/main/java/rx/operators/OperationMulticast.java rxjava-core/src/main/java/rx/operators/OperationNext.java rxjava-core/src/main/java/rx/operators/OperationObserveOn.java rxjava-core/src/main/java/rx/operators/OperationToMap.java rxjava-core/src/main/java/rx/operators/OperationToMultimap.java rxjava-core/src/main/java/rx/operators/OperationWindow.java
…not just Observables.
Conflicts: rxjava-contrib/rxjava-computation-expressions/src/main/java/rx/operators/OperationConditionals.java rxjava-core/src/main/java/rx/Observable.java rxjava-core/src/main/java/rx/observables/BlockingObservable.java rxjava-core/src/main/java/rx/operators/OperationCombineLatest.java rxjava-core/src/main/java/rx/operators/OperationParallel.java rxjava-core/src/main/java/rx/plugins/RxJavaErrorHandler.java
…not just Observables.
RxJava-pull-requests #659 SUCCESS |
The library originally had an Observable interface and associated implementation. It was removed because in practice it does not work due to all methods hanging off of Observable. 100+ methods on an interface all needing complex implementations and by design intended to comply with a specific contract made alternate implementations impractical and thus the interface was noise. If we had extension methods like C# we would design it with a pure interface like Rx.Net, but Java doesn't support that. Observables should not be created via inheritance, they are created via Observable.create. Thus, the OnSubscribeFunc is the light-weight interface anything can implement and then become an Observable for application of operators. |
I agree that an interface with hundreds of methods would be ungainly and inappropriate. It would also not meet the goal of separating the core contract of observability from the utility methods, and it is not what I have done. (In my opinion, if observability cannot be expressed as concisely as iterability, the API has room for improvement.) The IObservable interface contains only a single method: subscribe(Observer). Please review my first commit, rickbw@bcb2a60; I think you will see what I mean. Here's an example:
For another example of the same pattern, see http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/collect/FluentIterable.html. The reason that OnSubscribeFunc works as well as it does is that it does what an (I)Observable interface itself should do. My next task will be to replace OnSubscribeFunc with IObservable. This should be trivial, since the only difference between them is a difference in the method name: subscribe vs. onSubscribe. Look for an update to this pull request soon. |
One could argue that having the separate |
What are you trying to achieve from this change? There is very little value in an If there is just an |
The purpose of OnSubscribeFunc was to provide a simple, composable API for observability. IObservable already does that, and with the same method signature, so there’s no need for a second API for the same thing. There are two benefits for the API: 1. The syntax for composing operations is cleaner, with less overhead in syntax and in object count. Instead of Observable.create(op(create(arg1), create(arg2), create(arg3))), you can now write simple op(arg1, arg2, arg3). 2. The Operation classes are more consistent with one another. Previously, some of them provided actual Observables, and others merely OnSubscribeFunc. Which one was expected when wasn’t particularly clear, and it was up to the caller to wrap or unwrap the object as needed. Now, every Operation provides IObservables of one concrete type or another.
RxJava-pull-requests #672 FAILURE |
I have completed replacing Ben, you write that there is little value in an observable interface with a single subscribe method on it. Yet in the master branch, that is exactly what My goal is to make the API easier to understand and compose by separating concerns: the ability to observe something is different from what algorithms you might devise if you had the ability to do so. I understand the concern about having to constantly wrap stuff, and I can't speak to your code base. But I can say from working with the RxJava code base that I have actually removed a lot more calls to |
Please give me an example of something that doesn't compose today.
Practically no code ever exposes or uses
Returning
Please don't spend time on this while there is still not agreement on the approach being taken. What you are suggesting be changed is something we actively moved away from over a year ago thus it would require several people all deciding to reverse course. I am also not happy with the Lastly, the possible migration to using |
Agreed. I am not suggesting returning
For example, see
...and in this pull request, which looks like this:
As you can see, objects which are required to behave like observables can now actually be used as observables. There are not more wrapper objects; there are fewer. Of course, you'll have to tell me the extent to which your application code resembles your test code.
That's an interesting perspective; it's not my experience. I've used
Agreed. I wanted something short that suggested that the object met the core contract of observability, and I couldn't come up with anything better. If we come to an agreement on the approach, then it will be trivial to change the name to whatever you like. Thanks for your engagement on this request. |
Re: From what I see in your gist, the fundamental operation would no longer Your proposed successor to |
Considering the current state of the master branch, what would you propose to be done differently? The signatures are now this: // Observable.create
public final static <T> Observable<T> create(OnSubscribe<T> f)
// Observable.OnSubscribe typed function interface
public static interface OnSubscribe<T> extends Action1<Observer<? super T>>
// lift function
public <R> Observable<R> lift(final Func1<Observer<? super R>, Observer<? super T>> bind)
// Observer
public abstract class Observer<T> implements Subscription {
public abstract void onNext(T t);
public abstract void onError(Throwable e);
public abstract void onCompleted();
public final void add(Subscription s)
public final void unsubscribe()
public final boolean isUnsubscribed()
}
// Subject
public abstract class Subject<T, R> extends Observer<T> {
public abstract Observable<R> toObservable();
} |
Bear with me, because I haven't been able to follow all of the reasoning that has gone into the changes in between when last we talked and the current state. We might have to iterate here. But here are my thoughts: I think notionally, there's an object that represents a stream of data ( Each of these objects has a really simple job to do, so it should be possible to describe each with an interface of just a couple of methods. When you write a // Note similarities to cancellation of Futures
public interface Subscription {
void unsubscribe();
boolean isUnsubscribed();
}
public interface Observer<T> {
void onNext(T t, Subscription s);
// Could pass Subscription here too, but by definition "iteration" is ending.
void onError(Throwable e);
void onCompleted();
}
public interface Observable<T> {
Subscription subscribe(Observer<? super T> o);
} Then you need a concrete class to provide a library of useful methods for operating on public class FluentObservable<T> implements Observable<T> {
private Observable<? super T> delegate; // ...initialize in constructor, not shown
// Replaces create(OnSubscribe):
public static FluentObservable<T> from(Observable<? super T> delegate) {
// ...return argument if already Fluent else return new wrapper
}
public <X> FluentIterable<X> combineInSomeWay(Observable<X> other) { ... }
// Lots more...
} Your
Is that description clear? What do you think? |
Thank you for the detailed thoughts on this. I agree with your assessment that Where I view things differently is that the public interface Observable<T> {
Subscription subscribe(Observer<? super T> o);
} Everyone would end up using With public Observable getData() They will instead return the type that creates all the value: public FluentObservable getData() otherwise all consumption would end up being: FluentObservable.from(getData()).map(...) instead of: getData().map(...) On real-world code with dozens of API calls all returning Thus, what you're actually describing is equivalent to what we now have as:
No API deals in or returns In other words, APIs return public Observable<SomeData> getData() {
return Observable.create(new OnSubscribe<SomeData>() {
@Override
public void call(Subscriber<? super SomeData> o) {
o.onNext(new SomeData());
o.onCompleted();
}
});
} They do not return the function: public OnSubscribe<SomeData> getDataFunction() {
return new OnSubscribe<SomeData>() {
@Override
public void call(Subscriber<? super SomeData> o) {
o.onNext(new SomeData());
o.onCompleted();
}
};
} as it would then need to be turned into an Now, if these types were being baked into the JDK, my position would be different on having a base interface as it would be a fixed signature that could never be changed once released and all libraries would be interoperating via the same interfaces with different implementations. For this library however the interface and implementation will always go together and there is not a common lingua-franca between libraries for what represents an "Observable". Interop between libraries will always require a bridge (like the These perspectives (and the experience of having tried separate interface/implementation) have shaped the design of RxJava and are why we choose for the Considering these reasons, what specifically are you trying to achieve by making |
I'm evaluating RxJava for a proposed data-access layer, primarily but not necessarily over HTTP/REST, for eHarmony. That layer would be used in many projects, by multiple teams, so I want to make sure that the API building blocks are as clean and simple as possible—for the sake of usability but also of training and understanding. You're right that Regarding those wrapper methods (i.e. Let me turn it around a little, if I may: You clearly recognize the value of a simple one-method interface for your users to implement, because you've given them one: Thanks. |
Here is what the API has become as of 0.17.0 to achieve the 'lift' and public final static <T> Observable<T> create(OnSubscribe<T> f);
/**
* Invoked when Obserable.subscribe is called.
*/
public static interface OnSubscribe<T> extends Action1<Subscriber<? super T>>
/**
* Operator function for lifting into an Observable.
*/
public interface Operator<R, T> extends Func1<Subscriber<? super R>, Subscriber<? super T>>
public final Subscription subscribe(Subscriber<? super T> observer);
Even if |
Closing this so it no longer is on the pull request page, but I'm okay continuing the discussion. |
These will almost all go away with the use of |
Thanks for your detailed thoughts in this discussion. I think the 0.17 API has the capabilities it needs to be useful. If I were writing it, I probably wouldn't have decomposed those capabilities the same way: e.g. the current API has even more places where user-supplied code and library-supplied code are blended into a single type; that seems like a code smell to me. However, the distance from one point to the other is now greater than it was when I started this endeavor, so I think my differences of opinion are largely moot at this point. :-) I will continue to use RxJava gladly, and I look forward to contributing in other ways in the future. |
The core purposes of the Observable type—visitation of elements, detection of completion, and detection of errors—are currently tangled with a set of fluent utility methods. This tangling makes the core contract of observability harder to understand, and it makes sub-typing less straightforward and less flexible.
To resolve this situation, I have refactored an interface out of Observable, which I have tentatively dubbed IObservable. (Ideally, the interface itself would be called Observable, and the class FluentObservable. See Guava's FluentIterable alongside the JDK's Iterable for another example of this pattern. However, I did not want to break backward compatibility.) All public methods now accept IObservable instead of Observable in their argument lists, while those that returned Observable continue to do so. This pattern gives the best of both worlds: any IObservable implementation can be used anywhere Observable and its subclasses can, and we don't lose the convenience of the fluent API. In particular, any IObservable can be converted into an Observable via the new method Observable.from(IObservable).
This change should be 100% backwards-compatible at compile time, though the changed signatures mean that it is not backwards-compatible at the binary level. I have observed a number of breaking API changes on master over the last few weeks, so hopefully that is not a deal breaker. If it is, we can always restore overloads that accept Observable in addition to IObservable, but that seemed like a pretty large amount of duplication and complexity for a small benefit, so I have not done it so far.