From 092d7652d30a5c6648ef0e17d3cdf4027d6180eb Mon Sep 17 00:00:00 2001 From: Christopher Vogt Date: Thu, 2 Mar 2017 01:59:34 +0000 Subject: add design decision documentation about various CBT features --- doc/design.txt | 140 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 137 insertions(+), 3 deletions(-) (limited to 'doc') diff --git a/doc/design.txt b/doc/design.txt index 9478d31..28bbdaa 100644 --- a/doc/design.txt +++ b/doc/design.txt @@ -1,5 +1,139 @@ +# Design +This chapter explains some of CBT's less obvious design decisions and +the reasons behind them. Hopefully this answers some of the questions +often unanswered when trying to understand a new tool. -## Related gitter chats -### Why CBT uses inheritance -https://gitter.im/cvogt/cbt?at=58a95663de50490822e869e5 + +## Why does CBT use inheritance? + +First of all CBT uses classes, because needs some place to put tasks and +name them. Methods in classes are one way to do it. Methods calling each +other allows to effectively build a graph of dependent tasks. + +Inheritance on top of that allows patching this graph to insert additional +steps, e.g. formatting the code before compiling it. Patching of the task +graph is what you do with sbt andso with CBT via inheritance/overrides. + +This was also discussed in gitter here: https://gitter.im/cvogt/cbt?at=58a95663de50490822e869e5 + +Taking a graph an contrinuously patching it can be confusing, which is +why inheritance is confusing. You can even build non-terminating call +cycles. CBT's logic is not coupled to the inheritance layer. You can +write CBT builds without inheritance. They require a bit more code, but +may end up easier to maintain and understand. + +## Task composition and aborting failed tasks and dependents + +In CBT build tasks are methods. Task can depend on each +other by invoking each other. E.g. `package` is a method that invokes `compile`. +Build tasks can fail. By convention, if a task fails it is expected to throw +an Exception in order to also abort the execution of dependent tasks. When +`compile` finds compile errors it throws an exception and thereby also +drops out of the `package` execution pth. CBT catches and handles the +exception and returns control back to the user. + +This design was chosen for simplicity and because build code lives in a +world with exceptions anyways as a lot of it is directly or indirectly +using java libraries. We might reconsider this design at some point. Or not. +The hope is that Exceptions are without major drawbacks and more +approachable to newcomers than monadic error handling, which would require +wrapper types and value composition via .map and for-expressions. Not using +a Monad however also means that CBT cannot reason about task composition +automatically, which means parallel task execution is not automatic, but +manual and opt-in where wanted. + +## Why do CBT plugins use case classes where methods seem simpler? + +In CBT plugins you may see + +``` +trait SomePlugin{ + def doSomething = SomePluginLib(...context).doSomething( a, b, ... ) +} +case class SomePluginLib(...context...)(implicit ...more context...){ + case class doSomething(a: A, b: B, ...){ + def apply = { + // logic + } + } +} +``` + +Why this? This seems much simpler: + +``` +trait SomePlugin{ + def doSomething = SomePluginLib.doSomething( a, b, ... )(...context) +} +object SomePluginLib + def doSomething(a: A, b: B, ...)(...context...)(implicit ...more context...) = { + // logic + } +} +``` + +The reason is that the former allows easy patching while +the second does not. Let's pretend your build is this: +``` +class Build(val context: Context) extends SomePlugin{ + override def doSomething = super.doSomething.copy( b = someOtherB ) +} +``` +Such a simple replacement of `b` while keeping all other arguments would +not be easily possible if doSomething was a def not a case class. + +## What is newBuild and why do we need it? + +Methods in a class can call each other and thereby effectively form a graph. +Subclasses can patch this graph. Sometimes you want to create several +different variants of a graph. Let's say one building artifacts with a +-SNAPSHOT version and another one building a stable release. Or one with +scalaVersion 2.10 and one with scalaVersion 2.11. Or one optmizing your +code and one not optimizing it. You can subclass your build once for +each alternative. That's fine if you know which class to subclass, but +plugins for your build do not know what your concrete build class is +and cannot produce subclasses for it. Example + +``` +class Build(val context: Context){ + // this works fine + def with210 = new Build(context){ + override def scalaVersion = "2.10" + } + def with211 = new Build(context){ + override def scalaVersion = "2.11" + } +} +``` +Imagine however +``` +// defined by you: +class Build(val context: Context) extends CrossVersionPlugin +// defined by the plugin author: +trait CrossVersionPlugin{ + // this does not compile, Build does not exists when the plugin is compiled + def with210 = new Build(context){ + override def scalaVersion = "2.10" + } + def with211 = new Build(context){ + override def scalaVersion = "2.11" + } +} +``` +Luckily CBT can cheat because it has a compiler available. +``` +// defined by the plugin author: +trait CrossVersionPlugin{ + // method `newBuild` generates a new subclass of Build at runtime and creates an instance. + def with210 = newBuild{""" + override def scalaVersion = "2.10" + """} + def with211 = newBuild{""" + override def scalaVersion = "2.11" + """} +} +``` + +Problem solved. In fact this allows for a very, very flexible way of +creating differents variants of your build. -- cgit v1.2.3