A kotlin library for refactoring code. Port of GitHub's scientist.

Overview

Scientist Build Status

A kotlin library for carefully refactoring critical paths in your application.

This library is inspired by the ruby gem scientist.

How do I science?

Let's pretend you're changing the way you handle permissions in a large web app. Tests can help guide your refactoring, but you really want to compare the current and refactored behaviors under load.

fun isAllowed(user: User): Boolean = scientist<Boolean, Unit>() conduct {
    experiment { "widget-permissions" }
    control { user.isAllowedOldWay() }
    candidate { user.isAllowedNewWay() }
}

Wrap a control lambda around the code's original behavior, and wrap candidate around the new behavior. When conducting the experiment conduct will always return whatever the control lambda returns, but it does a bunch of stuff behind the scenes:

  • It decides whether or not to run the candidate lambda,
  • Randomizes the order in which control and candidate lambdas are run,
  • Measures the durations of all behaviors,
  • Compares the result of candidate to the result of control,
  • Swallows (but records) any exceptions thrown in the candidate lambda, and
  • Publishes all this information.

Scientist and Experiment

Compared to other scientist libraries this library separates the concepts of a scientist and the experiment. Which in turn gives you more freedom and flexibility to compose and reuse scientists and experiments (especially with dependency injection frameworks).

val scientist = scientist<Boolean, Unit>()
val experiment = experiment<Boolean, Unit>() {
    control { true }
}

val result = scientist conduct experiment

Setting up the scientist

The scientist is responsible for setting up the environment of an experiment and conducting it.

Publishing results

The examples above will run, but they're not really doing anything. The candidate lambdas run every time and none of the results get published. Add a publisher to control the result reporting:

val scientist = scientist<Boolean, Unit> {
    publisher { result -> logger.info(result) }
}

You can also extend the publisher typealias which then can be used as a parameter of the publisher lambda:

val logger = loggerFactory.call()

class LoggingPublisher(val logger: Logger) : Publisher<Boolean, Unit> {
    override fun invoke(result: Result<Boolean, Unit>) {
        logger.info(result)
    }
}

val loggingPublisher = LoggingPublisher(logger)

val scientist = scientist<Boolean, Unit> {
    publisher(loggingPublisher)
}

Controlling matches

Scientist compares if control and candidate values have matched by using ==. To override this behavior, use match to define how to compare observed values instead:

val scientist = scientist<Boolean, Unit> {
    match { candidate, control -> candidate != control }
}

candidate and control are both of type Outcome (a sealed class) which either can be Success or Failure. As an example take a look at the default implementation:

class DefaultMatcher<in T> : Matcher<T> {
    override fun invoke(candidate: Outcome<T>, control: Outcome<T>): Boolean = when(candidate) {
        is Success -> when(control) {
            is Success -> candidate.value == control.value
            is Failure -> false
        }
        is Failure -> when(control) {
            is Success -> false
            is Failure -> candidate.errorMessage == control.errorMessage
        }
    }
}

A Success outcome contains the value that has been evaluated. A Failure outcome contains the exception that was caught while evaluating a control or candidate statement.

Adding context

To provide additional data to the scientist Result and Experiments you can use the context lambda to add a context provider:

val scientist = scientist<Boolean, Map<String, Boolean>> {
    context { mapOf("yes" to true, "no" to false) }
}

The context is evaluated lazily and is exposed to the publishable Result by evaluating val context = result.contextProvider() and in the experiments conductibleIf lambda that will be described further down the page.

Ignoring mismatches

During the early stages of an experiment, it's possible that some of your code will always generate a mismatch for reasons you know and understand but haven't yet fixed. Instead of these known cases always showing up as mismatches in your metrics or analysis, you can tell the scientist whether or not to ignore a mismatch using the ignore lambda. You may include more than one lambda if needed:

val scientist = scientist<Boolean, Map<String, Boolean>> {
    ignore { candidate, control -> candidate.isFailure() }
}

Like in match candidate and control are of type Outcome.

Testing

When running your test suite, it's helpful to know that the experimental results always match. To help with testing, Scientist defines a throwOnMismatches field. Only do this in your test suite!

To throw on mismatches:

val scientist = scientist<Boolean, Map<String, Boolean>> {
    throwOnMismatches { true }
}

Scientist will throw a MismatchException exception if any observations don't match.

Setting up an experiment

With an experiment you are setting up tests for the critical paths of your application by specifying a control and candidate lambda.

fun experiment = experiment<Boolean, Unit> {
    name { "widget-permissions" }
    control { user.isAllowedOldWay() }
    candidate { user.isAllowedNewWay() }
}

Enabling/disabling experiments

Sometimes you don't want an experiment to run. Say, disabling a new code path for anyone who isn't member. You can disable an experiment by setting a conductibleIf lambda. If this returns false, the experiment will merely return the control value.

experiment<Boolean, Unit> {
    // ...
    conductibleIf { user.isMember() }
}

The conductibleIf lambda can also take a contextProvider as a parameter:

experiment<Boolean, Map<String, Boolean>> {
    // ...
    conductibleIf { context -> context()["externalCondition"]!! }
}

Handling errors

Scientist catches and tracks all exceptions thrown in a control or candidate lambda. To catch a more restrictive set of exceptions add a catches lambda to your experiment setup:

experiment<Boolean, Unit> {
    // ...
    catches { e -> e is NullPointerException }
}

This tells the scientist to catch NullPointerException and throw any other exception when running the control and candidate lambdas.

Complete DSL example

You can compose and execute experiments by putting conduct between scientist and an experiment context:

val result = scientist<Double, Unit> {
    publisher { result -> println(result) }
} conduct { // or conduct experiment {
    experiment { "experiment-dsl" }
    control { Math.random() }
    candidate { Math.random() }
}

Java interop

The Java interoperability can certainly be improved but should be sufficient for now:

public boolean isAllowed(User user) {
    Scientist<Boolean, String> scientist = Setup.scientist(setup -> setup
            .context(() -> "execute")
    );

    Experiment<Boolean, String> experiment = Setup.experiment(setup -> setup
            .name(() -> "experiment-name")
            .control("test-control", () -> user.isAllowedOldWay())
            .candidate("test-candidate", () -> user.isAllowedNewWay())
            .conductibleIf((contextProvider) -> contextProvider.invoke().equals("execute"))
    );

    return scientist.evaluate(experiment);
}

Installation

Maven:

<dependency>
  <groupId>com.github.spoptchev</groupId>
  <artifactId>scientist</artifactId>
  <version>1.0.2</version>
</dependency>

Gradle:

compile 'com.github.spoptchev:scientist:1.0.2'
You might also like...
Starter code for Android Basics in Kotlin

Inventory - Starter Code Starter code for Android Basics in Kotlin. Introduction This app is an stater code for an Inventory tracking app. Demos how t

Inventory-App - Starter code for Android Basics in Kotlin

Inventory - Starter Code Starter code for Android Basics in Kotlin. Introduction

Solution code for Android Kotlin Fundamentals Codelab 8.1 Getting data from the internet

MarsRealEstateNetwork - Solution Code Solution code for Android Kotlin Fundamentals Codelab 8.1 Getting data from the internet Introduction MarsRealEs

Kotlin Example of how to organize your code using MVC and some patterns seen in class
Kotlin Example of how to organize your code using MVC and some patterns seen in class

Kotlin Example of how to organize your code using MVC and some patterns seen in class

Copilot: Copy & Paste Code

copilot Template ToDo list Create a new IntelliJ Platform Plugin Template project. Verify the pluginGroup, plugin ID and sources package. Review the L

Plugin and Desktop app for parsing layout xml into Composable code

composed-xml Inspired by - Recompose composed-xml is a tool for parsing Android layouts into Jetpack Compose code. It can work as both Desktop app or

Used to generate the template code of GetX framework
Used to generate the template code of GetX framework

Language: English | 中文简体 statement some fast code snippet prompt come from getx-snippets-intelliJ Description install Plugin effect Take a look at the

This folder contains the source code for the Words app codelab.

Words App This folder contains the source code for the Words app codelab. Introduction Words app allows you to select a letter and use Intents to navi

A collection of code generators powered by ksp.

AutoKsp A collection of code generators powered by ksp. status: working in progress Projects AutoGradlePlugin - Generate gradle plugin properties file

Comments
Owner
Spas Poptchev
Freelance Full-Stack Software Engineer
Spas Poptchev
A multifunctional Android RAT with GUI based Web Panel without port forwarding.

AIRAVAT A multifunctional Android RAT with GUI based Web Panel without port forwarding. Features Read all the files of Internal Storage Download Any M

The One And Only 336 Dec 27, 2022
Remote Administrator Tool [ RAT For Android ] No Port Forwarding

XHUNTER RAT for Android ?? · Telegram · ⚖️ Legal Disclaimer: For Educational Purpose Only Usage of XHUNTER for attacking targets without prior mutual

Anon 79 Dec 31, 2022
Advent of Code 2021 in Kotlin, solved by myself. Focus on code readability. With GitHub Actions all puzzles are executed and their solutions printed

Advent of Code 2021 in Kotlin Focus on Code Readability. With CI Solution Printing. Welcome to the Advent of Code1 Kotlin project created by michaeltr

Michael Troger 1 Dec 12, 2021
Akka-in-action-kotlin - Accompanying source code for Akka in Action by Kotlin

Akka実践バイブル Kotlin版サンプルコード ( Accompanying source code for Akka in Action by Kotli

nrs 7 Jul 26, 2022
Tools for Kotlin/Kscript to easy write shell command line in kotlin code

Kscript Tools Easy way to run shell command line in kotlin and other tools Usage Used in kscript: @file:DependsOn("com.sealwu:kscript-tools:1.0.2") Us

Seal 4 Dec 12, 2022
Repository with source code from http://rosettacode.org/wiki/Category:Kotlin

Rosetta Code Kotlin This is a repository with the Kotlin source code from RosettaCode wiki. The main motivation for extracting all the code into a rep

Dmitry Kandalov 20 Dec 27, 2022
Simple and extendable code highlighter in Kotlin Multiplatform

Kighlighter Simple and extendable code highlighter in Kotlin Multiplatform with a composable output to display the code highlighted on Android and Des

Gérard Paligot 21 Dec 2, 2022
Code for the Advanced Android Kotlin Testing Codelab 5.1-5.3

TO-DO Notes - Code for 5.1-5.3 Testing Codelab Code for the Advanced Android Kotlin Testing Codelab 5.1-5.3 Introduction TO-DO Notes is an app where y

Jorge M 1 Jun 7, 2022
Sample app to demonstrate the integration code and working of Dyte SDK for android, using Kotlin.

Dyte Kotlin Sample App An example app in kotlin using the Dyte Mobile SDK Explore the docs » View Demo · Report Bug · Request Feature Table of Content

Dyte 8 Dec 3, 2021
Starter code for Android Basics in Kotlin

Inventory - Starter Code Starter code for Android Basics in Kotlin. Introduction This app is an stater code for an Inventory tracking app. Demos how t

Nora 0 Dec 8, 2021