Ten principles of an excellent SDK interface design

Over the years, I have participated in and led the design and development of many audio and video SDKs, and have served dozens of toB customers, large and small. Among them, I have a deep understanding:

A PaaS technology middleware product, no matter how awesome or beautiful its server & kernel design and implementation are, the SDK ultimately delivered to the customer’s developer is the most critical element and appearance. It is well designed, even if there are Insufficiency can also be compensated to a certain extent; its poor design has almost abolished all the efforts of the bottom layer, and it will also add countless ineffective overtime and troubleshooting investment.

This article focuses on how an excellent SDK should design interface specifications to achieve the following goals: 

  1. Concise and clear, with clear boundaries, orthogonal interfaces (no two interfaces conflict with each other), users are not easy to step on pits
  2. The behavior of each API is determined, and the feedback of call errors or runtime exceptions is timely and accurate
  3. For advanced customers: rich configuration, rich callback, good business scalability and flexibility

I pay tribute to the writing mode of "Effective C++", and describe and exemplify my personal thoughts and conclusions in the form of terms (take the RTC SDK interface design that I participated in deeply recently as an example).

Item 1: Parameter configuration provides independent profile class, do not provide a set method for each parameter

// good case
// Remember to give a reasonable default value
class AudioProfile 
{
   int samplerate{44100};
   int channels{1};
};

// Remember to give a reasonable default value
class VideoProfile 
{
   int maxEncodeWidth{1280};
   int maxEncodeHeight{720};
   int maxEncodeFps{15};
};

// Can be extended well, such as SystemProfile, ScreenProfile...
class EngineProfile 
{
    AudioProfile audio;
    VideoProfile video;
};

class RtcEngine 
{
public:
    static RtcEngine* CreateRtcEngine(const EngineProfile& profile) = 0;
};

// bad case
// 1. The number of functions of the core interface class RtcEngine exploded
// 2. Unable to restrict the time for the business party to call the API (maybe after joining the room or at an inappropriate time to configure the parameters)
// 3. What if a configuration is expected to support dynamic updates? Usually the configuration is not recommended to update frequently (it will affect the internal behavior of the SDK),
// If necessary, please provide updateXXXX or switchXXX interface explicitly in engine
class RtcEngine 
{
public:
    static RtcEngine* CreateRtcEngine() = 0;
    
    virtual void setAudioSampelerate(int samplerate) = 0;
    virtual void setAudioChannels(int channels) = 0;
    virtual void setVideoMaxEncodeResolution(int width, int height) = 0;
    virtual void setVideoMaxEncodeFps(int fps) = 0;
};

Item 2: Non-runtime status & information query and configuration interface provides static methods

// good case
class RtcEngine 
{
public:
    static int GetSdkVersion();
    static void SetLogLevel(int loglevel);
};

Item 3: The key asynchronous method is accompanied by a closure callback to inform the result

// good case
typedef std::function<void(int code, string message)> Callback;

class RtcEngine 
{
public:
    // The customer can handle events in the callback in time, such as: change the UI state | prompt an error | try again
    virtual void Publish(Callback const& callback = nullptr) = 0;
    virtual void Subscribe(Callback const& callback = nullptr) = 0;
};

// bad case
class RtcEngine 
{
public:
    class Listener
    {
        // Need to judge the error event in detail according to the code, and may not be able to match the error generated by which API call
        // There are many kinds of errors, and out of the original logic, many business parties will ignore to deal with some key errors here
        virtual void OnError(int code, string message) = 0;
    };

    void SetListener(Listener * listener) 
    {
        _listener = listener;
    }
    
    virtual void Publish() = 0;
    virtual void Subscribe() = 0;
    
private:
    Listener * _listener;
};

Article 4: All interfaces try to ensure "orthogonal" relationship (there is no conflict between two interfaces)

// bad case
// EnalbeAudio and other API interfaces are not "orthogonal", it is easy to use wrong combination
// MuteLocalAudioStream(true) & MuteAllRemoteAudioStreams(true) rely on the user to call EnalbeLocalAudio(true) first
class RtcEngine 
{
public:
    // EnalbeLocalAudio + MuteLocalAudioStream + MuteRemoteAudioStream
    virtual void EnalbeAudio(bool enable) = 0;
    // Turn on the local audio device (microphone & speaker)
    virtual void EnalbeLocalAudio(bool enable) = 0;
    // Publish/unpublish local audio stream
    virtual void MuteLocalAudioStream(bool mute) = 0;
    // Subscribe/unsubscribe remote audio stream
    virtual void MuteAllRemoteAudioStreams(bool mute) = 0;
};

Clause 5: Considering extensibility, try to use structures instead of atomic types for abstract objects

// good case
class RtcUser
{
    string userId;
    string metadata;
};

class RtcEngineEventListenr 
{
public:
    // The information and attributes of User can be easily expanded in the future
    virtual void OnUserJoined(const RtcUser& user) = 0;
};

// bad case
class RtcEngineEventListenr 
{
public:
    // Once the interface is provided, some extended information and attributes about the User object cannot be added in the future
    virtual void OnUserJoined(string userId, string metadata) = 0;
};

Clause 6: Use explicit OnExit for unrecoverable exit events and give reasons

When customers face the OnError callback event provided by the SDK, they often do not know how to deal with and deal with them due to the large number of errors. It is recommended to have a clear document to inform the solution. In addition, when an event that must destroy the object and exit the page occurs in the SDK, it is recommended to provide an independent callback function for the customer to deal with.

enum ExitReason {
    EXIT_REASON_FATAL_ERROR, // Unknown critical exception
    EXIT_REASON_RECONNECT_FAILED, // The number of times & time limit for automatic reconnection after disconnection
    EXIT_REASON_ROOM_CLOSED, // The room is closed
    EXIT_REASON_KICK_OUT, // was kicked out of the room
};

class RtcEngineEventListenr 
{
public:
    // Some warning messages, don’t get in the way, then use
    virtual void OnWarning(int code, const string &message) = 0;
    // An event that must destroy the SDK object has occurred, please close the page
    virtual void OnExit(ExitReason reason, const string &message) = 0;
};

Clause 7: The SDK of PaaS products should not contain business logic and information

// bad case
enum ClientRole {
    CLIENT_ROLE_BROADCASTER, // anchor, can push or pull
    CLIENT_ROLE_AUDIENCE // Audience, can't push stream, only can pull stream
};

class RtcEngine 
{
public:
    // A clear document is needed to introduce the roles corresponding to different roles and the behaviors caused by role switching
    // This API is not "orthogonal" to other APIs, such as: Publish
    virtual void SetClientRole(ClientRole& role) = 0;
};

// good case
// It is recommended to encapsulate the atomic interfaces of multiple SDKs in examples or best practices to achieve the functions of the above API
class RoleManager
{
public:
    // In this way, customers can explicitly perceive a series of actions behind this API
    void SetClientRole(ClientRole& role)
    {
        // _engine->xxxxx1();
        // _engine->xxxxx2();
        // _engine->xxxxx3();
    }
    
private:
    RtcEngine * _engine;
};

Article 8: Please provide all necessary status queries and event callbacks, and do not let the user cache the status

// good case
class RtcUser
{
    string userId;
    string metadata;
    bool audio{false}; // Whether to open and publish the audio stream
    bool video{false}; // Whether to open and publish the video stream
    bool screen{false}; // Whether to open and publish the screen flow
};

class RtcEngine 
{
public:
    // The SDK internally maintains the user status (the most accurate and real-time) and provides a clear query API
    // Instead of letting customers cache the state in their own code (it is easy to have inconsistent states on both sides)
    virtual list<RtcUser> GetUsers() = 0;
    virtual RtcUser GetUsers(const string& userId) = 0;
};

Clause 9: Provide enumeration capabilities for parameter configuration as much as possible, and return bool to inform configuration results

class VideoProfile 
{
public:
    // Provide capability enumeration and configuration results to prevent the configuration that customers think is inconsistent with the actual situation
    bool IsHwEncodeSupported();
    bool SetHwEncodeEnabled(bool enabled);

    // Provide capability enumeration and configuration results to prevent the configuration that customers think is inconsistent with the actual situation
    int GetSupportedMaxEncodeWidth();
    int GetSupportedMaxEncodeHeight();
    bool SetMaxEncodeResolution(int width, int height);
};

Article 10: The location and naming style of interface files shall maintain certain rules and relationships

// good case
// The directory structure of a code repo (Of course, only Android package customers can perceive, and C++ libraries cannot perceive the directory structure)
// It is recommended that all external interface header files are in the root directory, and the implementation files are hidden in the internal folder
// Reasonable header file location relationship can help developers and customers to accurately perceive which are interface files and which are internal files
// All external header files are not allowed to include internal files, otherwise there will be header file pollution problems
// All interface Class names start with a uniform style, such as RtcXXXX, callbacks are called XXXCallback, etc.
src
- base
- audio
- video
- utils
- metrics
- rtc_types.h
- rtc_engine.h
- rtc_engine_event_listener.h

summary

This is the end of the SDK interface design experience. Everyone will have their own style and preferences. Here are only some of my personal views and opinions. Welcome to leave a message to discuss or write to [email protected] for communication, or follow me WeChat public account @Jhuster for more follow-up articles and information~~

Guess you like

Origin blog.51cto.com/ticktick/2598082