6

We as a team are writing an C# SDK which communicates with a Server endpoint. All our API's till now have been Task based. Like

Task DoOperationAsync()

Recently we across a need for API which gets an intermediate update (file location) before the final one if the user opts for it.

DoSomething(bool interestedInIntermediateFile)

There are two option which came on the table

  1. Rx.IObservable < OpResult > DoSomething(bool interestedInIntermediateFile)

  2. Task < OpResult > DoSomething(Action < string > intermediateFile)

Where OpResult { Enum CompletionReason, enum FileType, string FileLoc }

FileType - Intermediate, Final

CompletionReason - Reason (only applicable when FileType is final)

Wanted to know from the experts which would be a good API design.

Notes : We do see that some of the new future API would be observable based. So it will not be case that this is only observable API.


Adding a additional note - since it might not be so obvious from above.

CompletionReason contains a value only in the final update the intermediate update will have this as null. As the intermediate update, final result though related but not exact (in terms of populating fields) has been raised as a downside to observable. Is it a genuine downside.

Thanks in advance.

Shenron
  • 71
  • 1
  • 5
  • 1
    please don't **[cross-post](https://meta.stackexchange.com/tags/cross-posting/info "'Cross-posting is frowned upon'")**: https://stackoverflow.com/questions/50054808/api-design-observable-vs-callback "Cross-posting is frowned upon as it leads to fragmented answers splattered all over the network..." – gnat Apr 27 '18 at 08:23
  • "CompletionReason being applicable only when FileType = Final has been raised as a downside to observable. Is it a genuine downside." Please clarify this. I don't see what the implication is here. – Sentinel Apr 27 '18 at 14:56
  • Added more clarification to the completion reason point. – Shenron Apr 27 '18 at 15:22

2 Answers2

7

The decision on what technology to use is largely going to be about the skills in your team, and the audience to which your SDK is aimed, and to some extent whether or not your API will in the longer term benefit from being a provider of observable streams.

Rx.NET is a very powerful and flexible way of doing things, and I would recommend any .NET developer who needs to handle concurrency and/or streams of events to look into it.

(I assume that by "IObservable" you mean Rx.Net. Or are you looking into Akka Streams or some such?)

However, getting into the IObservable way of thinking can be a steep learning curve.

There is a lot of overlap between Task and Rx.NET. One explanation of the overlap is here but in my opinion is biased in favour of Task.

If you anticipate that your API will be dealing with asynchronous streams, such as returning long lists of data, or driving a push model, or wanting to combine multiple streams, then look into Rx.NET

If you anticipate that your API will benefit from LINQ over streams (with Rx.NET you can easily query IObservables as they are the 'push' version of IEnumerable, the 'pull' version), then look into Rx.NET. For example, look into .Where .Select .SelectMany .Buffer .Zip .Scan operators in Rx.NET and decide if you will have many uses for them.

If you see that your API will eventually become a push-model stream service and if you have the skills on hand, you might want to seriously consider Rx.NET

If it's just the small problem you described in your original post, it might not be worth the overhead of learning the Rx way unless you already have skills on hand. In essence, your chain of tasks (see Attached child tasks here ) is a dumbed down Rx IObservable. The advantage is that this code is intuitive for many developers. You don't need to view it as IObservable vs Callback, a Task is not a callback, and your child tasks (intermediate steps) can be returned as a collection of tasks (eg: you could compose your tasks as a list like final task + intermediate task, and then wait for When Any or some such, maybe attach the intermediate as child tasks).


Update: The concern you raise in your update that the operation completion message will be raised with a null value during intermediate steps makes no sense. Whichever approach you take, intermediate steps will not describe the operation completion reason, unless of course that intermediate step is post-hoc actualized as the final step. In the 1st scenario (Tasks): your intermediate action will not raise a completion reason (null). In the 2nd scenario (Observables): your intermediate actions will not raise a completion reason. What is the difference? What you should do in the case of the Observable is declare it an observable of "action results". This is semantically equivalent to Task (a future result). Declare X types of result message. Eg:

public class ResultMessage

public class FinalResultMessage: ResultMessage

etc

You can then implement Task or IObservable as you wish.

If you go the route of Observable. You can offer .Finally either in the subscriber or the observable to handle the FinalResultMessage.

In short, do not make the API an IObservable but IObservable if you go that route, and apply the same thinking for Task too.

Sentinel
  • 432
  • 3
  • 10
  • This is the right answer. I would really prefer using Observable. But that is primarily because I have great experience working with it. – Euphoric Apr 27 '18 at 08:01
  • @Euphoric, I disagree that this is the right answer. In my view, it's too focused on promoting a framework, rather than offering advice on software engineering principles. Not worthy of a downvote, but not quite good enough for +1 either. – David Arno Apr 27 '18 at 08:38
  • @DavidArno Well, you could substitute the phrase Rx.NET with 'reactive programming' in the answer, but there are not many well maintained implementations of IObservable out there. Rx.NET in the .NET sphere is practically synonymous with the engineering principle of reactive programming. Essentially OP is asking for Task vs IObservable. Task itself is a framework. – Sentinel Apr 27 '18 at 08:44
  • @Sentinel, `Task` is just an implementation detail. The question is asking whether to stick with the current notify on completion and add an optional notify on intermediate step mechanism; or switch over to a multiple notification step mechanism. Whether than latter mechanism is achieved via `IObservable` or Rx.NET is largely orthogonal to the question. In my view. – David Arno Apr 27 '18 at 08:52
  • @DavidArno That's incorrect. Both approaches return multiple notifications. Task is a .NET framework sub-framework for returning *futures*. So is Rx.NET. There are no default implementations of IObservable in .NET, but Rx.NET is an MS supported/documented installable extension, so when someone starts talking about IObservable, they are usually talking about Rx. Perhaps OP should confirm. – Sentinel Apr 27 '18 at 08:55
  • @DavidArno There are a lot of nuances too. Cancellation tokens. Team skills. Audience. You can't separate framework choice from team skill choice and vice versa. *That* kind of decision is software engineering. – Sentinel Apr 27 '18 at 08:57
  • @sentinel - Thanks for taking out time to answer the question. Done some minor edits, to add little more clarity. – Shenron Apr 27 '18 at 14:42
  • @Shenron I don't understand the final update there. The completion reason only being final aspect. Can you explain? The "has been raised as a downside to observable" part. What is the downside? – Sentinel Apr 27 '18 at 14:47
  • @sentinel - In the intermediate result completion reason would have it null/None whereas the final result would have a valid value with this. Since the intermediate update and final update though related but not exact (as some values are not populated). This is not a good fit for observable. That's the downside that was raised. – Shenron Apr 27 '18 at 14:59
  • @Shenron That is an odd way of looking at it. That is like saying that when your intermediate Task completes, or when you call back if you use an event or action, you get no operation result. With Rx.Iobservable it is also not necessarily true. You can simply add a .Scan(, ....) declaring a type that has an 'Operation Status' property that updates with each event. So you could consider that as a plus rather than a minus. Also, your operation "result" could be considered as what happens when your OnCompleted calls on your subscriber. – Sentinel Apr 27 '18 at 15:10
  • @Shenron Also don't forget you have .Finally on your observable. Or .Concat(Observable.Return("Finished")) – Sentinel Apr 27 '18 at 15:11
  • @Shenron Updated answer. – Sentinel Apr 27 '18 at 16:36
  • @Sentinel - Thanks for the update. One point, wanted to bring to your notice is in the second option Task < OpResult > DoSomething(Action < string > intermediateFile) The intermediate is returned via the callback function and gets the file name. Just wanted to know your thoughts on this approach. – Shenron May 02 '18 at 01:51
  • @Shenron pls see info above about child tasks. Basically you have to make sure your callback completes before your final task. Another option would be to chain tasks with .ContinueWith or do that anyway in your implementation. – Sentinel May 02 '18 at 07:19
1

It depends on your API consumer.

Overall Though, I would try to make sure that you API matches to some extent your underlying server calls.

By Providing an Observable you have essentially introduced state into the equation. I need to hang onto that Observable until its finished sending me updates. The client needs to associate that Observable with incoming updates from the server related to the original request.

If your server is sending a constant stream of data then This would be a good match. (although you might want to look into a Stream)

If your server splits the job into two operations, a Start and a PollToGetResult for example. Then you have introduced state where you needn't have. It would be better to expose the separate operations on your API

Ewan
  • 70,664
  • 5
  • 76
  • 161
  • There is difference in observables in WPF, which use INotifyPropertyChanged and observables using IObservable and RX stuff. WPF doesn't at all understand IObservable. – Euphoric Apr 27 '18 at 10:59
  • ahh im getting mixed up with ObservableCollection – Ewan Apr 27 '18 at 11:03
  • @Ewan I thought you might have been...the Rx stuff tends to delete state from your system, rather than introduce it. I agree it has to depend on the consumer anyway. – Sentinel Apr 27 '18 at 12:49
  • I can see how it hides the state from your code – Ewan Apr 27 '18 at 12:58