4

paluch.biz

 3 years ago
source link: https://paluch.biz/blog/184-introducing-code-to-reduce-code.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Introducing Code to reduce Code

How adding a functional utility helps to avoid code duplications and leads to more readable code.

In Spring Data Redis, we support multiple Redis clients – Jedis and Lettuce.
Commands can be invoked either directly, in a transaction, or using pipelining.

Direct commands get invoked – as the name probably reveals – directly by calling a client method and returning the result. In some cases, results require post-processing because the Redis driver reports a Long while Spring Data Redis would like to return Boolean or the driver returns a driver-specific type that requires conversion in a Spring Data type.

Spring Data Redis also wants to provide a consistent exception experience so exception translation is as well part of what’s required when interacting with a Redis driver.

In contrast to direct calls, we also have pipelining and transactions. Both of these defer the actual result until a later synchronization point. What’s happening when calling a Redis command on the Spring Data Redis API is that we call a pipelined or transactional variant of the Redis driver’s command method. These return typically a handle (Response or Future) that needs to be collected and potentially post-processed (conversion, exception translation).

That has lead to code like the following:

    // Jedis
    @Override
    public DataType type(byte[] key) {

        Assert.notNull(key, "Key must not be null!");

        try {
            if (isPipelined()) {
                pipeline(
                        connection.newJedisResult(connection.getRequiredPipeline().type(key), JedisConverters.stringToDataType()));
                return null;
            }
            if (isQueueing()) {
                transaction(connection.newJedisResult(connection.getRequiredTransaction().type(key),
                        JedisConverters.stringToDataType()));
                return null;
            }
            return JedisConverters.toDataType(connection.getJedis().type(key));
        } catch (Exception ex) {
            throw connection.convertJedisAccessException(ex);
        }
    }
    
    @Override
    public Set<byte[]> keys(byte[] pattern) {

        Assert.notNull(pattern, "Pattern must not be null!");

        try {
            if (isPipelined()) {
                pipeline(connection.newJedisResult(connection.getRequiredPipeline().keys(pattern)));
                return null;
            }
            if (isQueueing()) {
                transaction(connection.newJedisResult(connection.getRequiredTransaction().keys(pattern)));
                return null;
            }
            return connection.getJedis().keys(pattern);
        } catch (Exception ex) {
            throw connection.convertJedisAccessException(ex);
        }
    }
    
    // Lettuce
    @Override
    public Long exists(byte[]... keys) {

        Assert.notNull(keys, "Keys must not be null!");
        Assert.noNullElements(keys, "Keys must not contain null elements!");

        try {
            if (isPipelined()) {
                pipeline(connection.newLettuceResult(getAsyncConnection().exists(keys)));
                return null;
            }
            if (isQueueing()) {
                transaction(connection.newLettuceResult(getAsyncConnection().exists(keys)));
                return null;
            }
            return getConnection().exists(keys);
        } catch (Exception ex) {
            throw convertLettuceAccessException(ex);
        }
    }

There are multiple issues with this code:

  1. Repetition of exception translation in each method (try…catch blocks)
  2. Decision of the execution mode in each command (isPipelined, isQueueing)
  3. Multiple method exists (return)
  4. Duplication of pipeline and newLettuceResult/newJedisResult calls
  5. Code is error-prone for subtle bugs. Either the wrong method is called, or the args list can be incomplete or a result conversion might be missing.
  6. Probable even more issues here.

As you see, the current arrangement isn’t ideal. We started investigating on how to improve and came up with various approaches:

    public Long exists(byte[]... keys) {
        Assert.notNull(keys, "Keys must not be null!");
        Assert.noNullElements(keys, "Keys must not contain null elements!");
        
        return doCall(connection -> connection.exists(keys), asyncConnection -> asyncConnection.exists(keys));
    }

Much better as a lot of the duplication is already gone. Still duplications as we operate on different APIs. Luckily, for Lettuce we can simplify the variant to a single method call. We don’t require a distinction of method calls between direct/pipelining/transactional invocations as Lettuce can return a RedisFuture in every case and Spring Data Redis can sort out the synchronization:

return doCall(connection -> connection.exists(keys));

Better. But we can do probably even better which can improve readability. While having two closing parenthesis at the end isn’t a major concern, it’s still something that we can improve on. Also, we end up with a capturing lambda here. So let’s turn this code into a non-capturing Lambda with improved readability.

return connection.invoke().just(RedisKeyAsyncCommands::exists, keys);

We end up with a method reference and pass on the keys argument without capturing it. Conversion of results clearly adds to the complexity. With a proper fluent API declaration, using conversions becomes as simple as using the Java 8 Stream API:

connection.invoke().from(RedisKeyAsyncCommands::type, key).get(LettuceConverters.stringToDataType())

// List example:

return connection.invoke().fromMany(RedisHashAsyncCommands::hmget, key, fields)
                .toList(source -> source.getValueOrElse(null));

What we’ve achieved is reduction of duplications and surface for potential bugs. However, that refactoring requires a bit of infrastructure. The key here is to capture the intent of the method invocation which can be applied in different contexts: Direct, transactional, and pipelining. The method invocation is only complete when we also capture how the result gets consumed – with or without a converter. That led to the introduction of LettuceInvoker (we started with Lettuce first). The invoker defines a series of method overloads and functional interfaces:

<R> R just(ConnectionFunction0<R> function);
<R, T1> R just(ConnectionFunction1<T1, R> function, T1 t1);
…
<R, T1, T2, T3, T4, T5> R just(ConnectionFunction5<T1, T2, T3, T4, T5, R> function, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5);

interface ConnectionFunction0<R> {
    RedisFuture<R> apply(RedisClusterAsyncCommands<byte[], byte[]> connection);
}

interface ConnectionFunction1<T1, R> {
    RedisFuture<R> apply(RedisClusterAsyncCommands<byte[], byte[]> connection, T1 t1);
}

The underlying implementation delegates the actual invocation to a single place that decides over the execution mode (direct/pipelining/transactional) simplifying maintenance in return.
Right now, LettuceInvoker works with capturing lambdas internally, but that’s an implementation detail that is isolated within LettuceInvoker. It doesn’t sprawl across the entire code base. It can be improved without touching all other methods.

Here’s the full code of LettuceInvoker

There’s still a bit of challenge to improve debugging experience with LettuceInvoker. As net result, we introduced about 600 lines of code to remove 2900 lines of code which is a better result than expected.


Recommend

  • 48

    Data Classes Considered Harmful

  • 0

    Sense of urgency is out – sense of competition is in Image Credit: The Hot SALE, only today! GET IT NOW! You probably should have this kind of titles and promotion...

  • 6

    News & Events First-ever YouTube Small Biz Day on June 24 By...

  • 6
    • www.codementor.io 2 years ago
    • Cache

    Johnny B. (Ionică Bizău)

    OfflineJohnny B. (Ionică Bizău)This mentor has passed the Codementor approval process and is available for hire.(1453)

  • 4
    • ceoworld.biz 2 years ago
    • Cache

    ceoworld.biz

    This site can’t be reached The webpage at https://ceoworld.biz/2022/04/21/which-giant-companies-left-russia-because-of-the-current-situation-in-ukraine/ might be temporarily dow...

  • 3
    • codecondo.com 1 year ago
    • Cache

    The Biz of Writing Code

    Let’s consider you’ve completed four to five years as a coder. If asked what you plan to do in the next ten years, are you still willing to write and review code? Or do you want something else? If you’re looking for something more than writing lin...

  • 5
    • tweakyourbiz.com 1 year ago
    • Cache

    Tweak Your Biz

    When you’re creating marketing or visual content, the quality of your images can make all the difference. Poorly chosen images can hurt your brand and make it seem unprofessional. However, with some thought and planning, yo...

  • 2
    • www.producthunt.com 1 year ago
    • Cache

    AI Starter | An AI "Biz-in-a-Box"

    Support is great. Feedback is even better."Thanks for checking out our launch - if you have any APIs you think would be useful for us to aggregate as part of our modular package, do let us know! Our roadmap is pretty exciting and inc...

  • 2
    • www.informationweek.com 8 months ago
    • Cache

    Worldcoin Eyes Biz Users as Biometric Crypto Rises

    Worldcoin Eyes Biz Users as Biometric Crypto RisesThe recently launched cryptocurrency, which uses iris-scanning technology, is hoping to lure business and government users.

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK