├── project ├── build.properties └── plugins.sbt ├── .gitignore ├── core └── src │ └── main │ └── scala │ └── scaladci │ ├── package.scala │ └── util │ ├── expectCompileError.scala │ └── MacroHelper.scala ├── coretest └── src │ └── test │ └── Scala │ └── scaladci │ ├── util │ └── DCIspecification.scala │ ├── semantics │ ├── Overriding.scala │ ├── RolePlayerLifetime.scala │ ├── NestedContexts.scala │ ├── ObjectIdentity.scala │ ├── RoleReference.scala │ ├── ObjectInstantiation.scala │ ├── Interactions.scala │ ├── RoleDefinition.scala │ ├── Context.scala │ ├── RoleBinding.scala │ ├── RoleBody.scala │ ├── MethodResolution.scala │ └── RoleContract.scala │ └── syntax │ ├── DCIcontext.scala │ └── RoleAsKeyword.scala ├── examples └── src │ └── test │ └── scala │ └── scaladci │ └── examples │ ├── dijkstra │ ├── Step2_Unvisited.scala │ ├── Data.scala │ ├── other_implementations_2013_01_18 │ │ ├── EgonElbre_JavaScript_1 │ │ ├── EgonElbre_JavaScript_2 │ │ ├── AndreasSoderlund_Coffee2 │ │ └── AndreasSoderlund_Coffee1 │ ├── Step3_4_Calculate.scala │ ├── Step1_TentDist.scala │ └── Step5_6_Recurse.scala │ ├── MoneyTransfer2.scala │ ├── DuckTyping.scala │ ├── MoneyTransfer1.scala │ ├── Dijkstra_self.scala │ ├── Dijkstra.scala │ ├── ShoppingCart4c.scala │ ├── ShoppingCart4b.scala │ ├── ShoppingCart4a.scala │ ├── ShoppingCart3.scala │ ├── ShoppingCart1.scala │ ├── ShoppingCart2.scala │ ├── ShoppingCart6.scala │ └── ShoppingCart5.scala ├── README.md └── LICENSE.txt /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.13 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .idea_modules 3 | demo/.idea 4 | demo/.idea_modules 5 | target 6 | project/boot 7 | *.swp 8 | project.vim 9 | tags 10 | .lib 11 | *~ 12 | *# 13 | z_local.sbt 14 | project/.ivy 15 | project/.boot 16 | project/.sbt 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /core/src/main/scala/scaladci/package.scala: -------------------------------------------------------------------------------- 1 | import scala.language.dynamics 2 | 3 | package object scaladci { 4 | 5 | // role foo {...} 6 | val role = roleO 7 | private[scaladci] object roleO extends Dynamic { 8 | def applyDynamic(obj: Any)(roleBody: => Unit) = roleBody 9 | } 10 | } -------------------------------------------------------------------------------- /coretest/src/test/Scala/scaladci/util/DCIspecification.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package util 3 | import org.specs2.mutable._ 4 | 5 | trait DCIspecification extends Specification { 6 | 7 | // Data class 8 | case class Data(i: Int) { 9 | // Instance method 10 | def number = i 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /coretest/src/test/Scala/scaladci/semantics/Overriding.scala: -------------------------------------------------------------------------------- 1 | //package scaladci 2 | //package semantics 3 | //import org.specs2.mutable._ 4 | // 5 | //class Overriding extends Specification { 6 | // 7 | // case class Data() { 8 | // def number = 1 9 | // } 10 | // 11 | // "Role method overrides data class method" >> { 12 | // 13 | // @context 14 | // case class Context(myRole: Data) { 15 | // 16 | // def trigger = myRole.number 17 | // 18 | // role myRole { 19 | // def number = 2 20 | // } 21 | // } 22 | // 23 | // Context(Data()).trigger === 2 24 | // 25 | // } 26 | //} 27 | -------------------------------------------------------------------------------- /coretest/src/test/Scala/scaladci/semantics/RolePlayerLifetime.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package semantics 3 | import util._ 4 | 5 | class RolePlayerLifetime extends DCIspecification { 6 | 7 | "Is limited to Context execution" >> { 8 | 9 | @context 10 | case class Context(myRole: Data) { 11 | def trigger = myRole.bar 12 | 13 | role myRole { 14 | def bar = myRole.i * 2 15 | } 16 | } 17 | val obj = Data(42) 18 | 19 | // `obj` plays myRole in Context 20 | Context(obj).trigger === 42 * 2 21 | 22 | // `obj` no longer plays myRole 23 | expectCompileError( 24 | "obj.bar", 25 | "value bar is not a member of RolePlayerLifetime.this.Data") 26 | 27 | success 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /coretest/src/test/Scala/scaladci/semantics/NestedContexts.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package semantics 3 | import scaladci.util._ 4 | 5 | class NestedContexts extends DCIspecification { 6 | 7 | "Nested" >> { 8 | 9 | @context 10 | case class Inner(roleB: Data) { 11 | def trigger = roleB.bar 12 | 13 | role roleB { 14 | def bar = roleB.i 15 | } 16 | } 17 | 18 | @context 19 | case class Outer(roleA: Data) { 20 | def trigger = roleA.foo 21 | 22 | role roleA { 23 | // Sequential execution of inner context 24 | def foo = "foo" + Inner(roleA).trigger + "bar" 25 | } 26 | } 27 | 28 | val obj = Data(42) 29 | Outer(obj).trigger === "foo42bar" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /coretest/src/test/Scala/scaladci/semantics/ObjectIdentity.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package semantics 3 | import util._ 4 | 5 | class ObjectIdentity extends DCIspecification { 6 | 7 | "Is same when playing a Role" >> { 8 | 9 | @context 10 | case class Context(roleA: Data) { 11 | val roleB = Data(42) 12 | 13 | def foo = roleA.me 14 | def bar = roleB.me 15 | 16 | role roleA { 17 | def me = self 18 | } 19 | 20 | role roleB { 21 | def me = self 22 | } 23 | } 24 | 25 | val obj1 = Data(42) 26 | val obj2 = Data(42) 27 | 28 | // BEWARE: 29 | // `==` or `equals` only compares values 30 | (obj1 == obj2) === true 31 | (obj1 equals obj2) === true 32 | 33 | // whereas `eq` compares referential identity of objects 34 | (obj1 eq obj2) === false 35 | 36 | // Object identity is preserved 37 | (obj1 eq Context(obj1).foo) === true 38 | 39 | // Although `bar` has same value (Data(42)) as foo, they're still two different objects 40 | (obj1 eq Context(obj1).bar) === false 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /coretest/src/test/Scala/scaladci/semantics/RoleReference.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package semantics 3 | import util._ 4 | 5 | class RoleReference extends DCIspecification { 6 | 7 | "Can be a Role name" >> { 8 | 9 | @context 10 | case class Context(myRole: Data) { 11 | def useRoleName = myRole.value 12 | 13 | role myRole { 14 | def value = myRole.i * 2 15 | } 16 | } 17 | 18 | val obj = Data(42) 19 | 20 | Context(obj).useRoleName === 42 * 2 21 | } 22 | 23 | 24 | "Can be `self`" >> { 25 | 26 | @context 27 | case class Context(myRole: Data) { 28 | def useSelf = myRole.value 29 | 30 | role myRole { 31 | def value = self.i * 2 32 | } 33 | } 34 | 35 | val obj = Data(42) 36 | 37 | Context(obj).useSelf === 42 * 2 38 | } 39 | 40 | 41 | "Can be `this`" >> { 42 | 43 | @context 44 | case class Context(myRole: Data) { 45 | def useSelf = myRole.value 46 | 47 | role myRole { 48 | def value = this.i * 2 49 | } 50 | } 51 | 52 | val obj = Data(42) 53 | 54 | Context(obj).useSelf === 42 * 2 55 | } 56 | } -------------------------------------------------------------------------------- /coretest/src/test/Scala/scaladci/semantics/ObjectInstantiation.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package semantics 3 | import util._ 4 | 5 | class ObjectInstantiation extends DCIspecification { 6 | 7 | "In environment (passed to Context)" >> { 8 | 9 | @context 10 | case class Context(myRole: Data) { 11 | 12 | def trigger = myRole.foo 13 | 14 | role myRole { 15 | def foo = myRole.i * 2 16 | } 17 | } 18 | 19 | val obj = Data(42) 20 | Context(obj).trigger === 42 * 2 21 | } 22 | 23 | 24 | "In Context" >> { 25 | 26 | @context 27 | case class Context(i: Int) { 28 | 29 | val myRole = Data(i) 30 | 31 | def trigger = myRole.foo 32 | 33 | role myRole { 34 | def foo = myRole.i * 2 35 | } 36 | } 37 | 38 | Context(42).trigger === 42 * 2 39 | } 40 | 41 | 42 | "As new variable referencing already instantiated object/RolePlayer" >> { 43 | 44 | @context 45 | case class Context(roleA: Data) { 46 | 47 | val roleB = roleA 48 | 49 | def trigger = roleB.bar 50 | 51 | role roleB { 52 | def bar = roleB.i * 2 53 | } 54 | } 55 | 56 | val obj = Data(42) 57 | Context(obj).trigger === 42 * 2 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /coretest/src/test/Scala/scaladci/semantics/Interactions.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package semantics 3 | import scaladci.util._ 4 | 5 | class Interactions extends DCIspecification { 6 | 7 | "Are preferably distributed between Roles" >> { 8 | 9 | @context 10 | case class Context(data: Data) { 11 | val roleA = data 12 | val roleB = data 13 | val roleC = data 14 | 15 | def distributedInteractions = roleA.foo 16 | 17 | role roleA { 18 | def foo = 2 * roleB.bar 19 | } 20 | 21 | role roleB { 22 | def bar = 3 * roleC.baz 23 | } 24 | 25 | role roleC { 26 | def baz = 4 * self.i 27 | } 28 | } 29 | Context(Data(5)).distributedInteractions === 2 * 3 * 4 * 5 30 | } 31 | 32 | 33 | "Can occasionally be centralized in Context (mediator pattern)" >> { 34 | 35 | @context 36 | case class Context(data: Data) { 37 | val roleA = data 38 | val roleB = data 39 | val roleC = data 40 | 41 | def centralizedInteractions = roleA.foo * roleB.bar * roleC.baz * roleC.number 42 | 43 | role roleA { 44 | def foo = 2 45 | } 46 | 47 | role roleB { 48 | def bar = 3 49 | } 50 | 51 | role roleC { 52 | def baz = 4 53 | } 54 | } 55 | Context(Data(5)).centralizedInteractions === 2 * 3 * 4 * 5 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /coretest/src/test/Scala/scaladci/syntax/DCIcontext.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package syntax 3 | import util._ 4 | 5 | class DCIcontext extends DCIspecification { 6 | 7 | "Can only be a class, abstract class, case class, trait or object" >> { 8 | 9 | @context 10 | class ClassContext(myRole: Data) { 11 | role myRole {} 12 | } 13 | 14 | @context 15 | abstract class AbstractClassContext(myRole: Data) { 16 | role myRole {} 17 | } 18 | 19 | @context 20 | case class CaseClassContext(myRole: Data) { 21 | role myRole {} 22 | } 23 | 24 | @context 25 | trait TraitContext { 26 | val myRole = Data(42) 27 | role myRole {} 28 | } 29 | 30 | @context 31 | object ObjectContext { 32 | // Objects to play a Role are instantiated inside the Context object 33 | val myRole = Data(42) 34 | role myRole {} 35 | } 36 | 37 | expectCompileError( 38 | """ 39 | @context 40 | val outOfContext = "no go" 41 | """, 42 | """ 43 | |Only classes/case classes/traits/objects can be transformed to DCI Contexts. Found: 44 | |val outOfContext = "no go" 45 | """) 46 | 47 | success 48 | } 49 | 50 | 51 | "Can be an empty stub" >> { 52 | 53 | @context 54 | class EmptyClassContext 55 | 56 | success 57 | } 58 | 59 | 60 | "Cannot be named `role`" >> { 61 | 62 | expectCompileError( 63 | """ 64 | @context 65 | class role 66 | """, 67 | "Context class can't be named `role`") 68 | 69 | success 70 | } 71 | } -------------------------------------------------------------------------------- /coretest/src/test/Scala/scaladci/semantics/RoleDefinition.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package semantics 3 | import scaladci.util._ 4 | 5 | class RoleDefinition extends DCIspecification { 6 | 7 | "Can only be at top level in Context" >> { 8 | 9 | @context 10 | class OkContext(myRole: Data) { 11 | role myRole {} 12 | } 13 | 14 | expectCompileError( 15 | """ 16 | @context 17 | class Context(myRole: Data) { 18 | def subLevel() { 19 | role myRole (()) // Supplying Unit argument as `()` 20 | } 21 | } 22 | """, 23 | "(1) Using `role` keyword on a sub level of the Context is not allowed") 24 | 25 | expectCompileError( 26 | """ 27 | @context 28 | class Context(myRole: Data) { 29 | def subLevel() { 30 | role myRole {} 31 | } 32 | } 33 | """, 34 | "(1) Using `role` keyword on a sub level of the Context is not allowed") 35 | 36 | expectCompileError( 37 | """ 38 | @context 39 | class Context(myRole: Data) { 40 | def subLevel() { 41 | role myRole 42 | } 43 | } 44 | """, 45 | "(4) Using `role` keyword on a sub level of the Context is not allowed") 46 | 47 | success 48 | } 49 | 50 | 51 | "Cannot be defined twice (defining same Role name twice)" >> { 52 | 53 | expectCompileError( 54 | """ 55 | @context 56 | class Context(myRole: Data) { 57 | role myRole {} 58 | role myRole {} 59 | } 60 | """, 61 | "Can't define role `myRole` twice") 62 | 63 | success 64 | } 65 | } -------------------------------------------------------------------------------- /coretest/src/test/Scala/scaladci/semantics/Context.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package semantics 3 | import util._ 4 | 5 | class Context extends DCIspecification { 6 | 7 | "Cannot define a nested DCI Context" >> { 8 | 9 | expectCompileError( 10 | """ 11 | @context 12 | class OuterContext1 { 13 | @context 14 | class NestedContext1 15 | } 16 | """, 17 | "Can't define nested DCI context `NestedContext1` inside DCI context `OuterContext1`") 18 | 19 | expectCompileError( 20 | """ 21 | @context 22 | class OuterContext2(myRole: Data) { 23 | role myRole { 24 | @context 25 | class NestedContext2 26 | } 27 | } 28 | """, 29 | "Can't define nested DCI context `NestedContext2` inside DCI context `OuterContext2`") 30 | 31 | expectCompileError( 32 | """ 33 | @context 34 | class OuterContext3(myRole: Data) { 35 | role myRole { 36 | def roleMethod { 37 | @context 38 | class NestedContext3 39 | } 40 | } 41 | } 42 | """, 43 | "Can't define nested DCI context `NestedContext3` inside DCI context `OuterContext3`") 44 | 45 | success 46 | } 47 | 48 | 49 | "Todo: Can instantiate a nested Context" >> { 50 | 51 | // Todo 52 | ok 53 | } 54 | 55 | 56 | "Todo: Can play a Role in another Context" >> { 57 | 58 | // Todo 59 | ok 60 | } 61 | 62 | 63 | 64 | // "Only one DCI Context can be active at a time" 65 | // Isn't that only possible with parallel execution ?? 66 | 67 | "Todo: Cannot be active at the same time as another Context" >> { 68 | 69 | // Todo ?? 70 | ok 71 | } 72 | } -------------------------------------------------------------------------------- /coretest/src/test/Scala/scaladci/semantics/RoleBinding.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package semantics 3 | import util._ 4 | 5 | class RoleBinding extends DCIspecification { 6 | 7 | "Through identifier of environment object" >> { 8 | 9 | // Data object `myRole` binds to Role `myRole` since they share name 10 | @context 11 | case class Context(myRole: Data) { 12 | def trigger = myRole.foo 13 | 14 | role myRole { 15 | def foo = myRole.i * 2 16 | } 17 | } 18 | 19 | val obj = Data(42) 20 | Context(obj).trigger === 42 * 2 21 | } 22 | 23 | 24 | "Through variable name identifying new object" >> { 25 | 26 | @context 27 | case class Context(i: Int) { 28 | // Variable `myRole` identifies instantiated `Data` object in Context 29 | // Variable `myRole` binds to Role `myRole` since they share name 30 | val myRole = Data(i) 31 | def trigger = myRole.foo 32 | 33 | role myRole { 34 | def foo = myRole.i * 2 35 | } 36 | } 37 | 38 | Context(42).trigger === 42 * 2 39 | } 40 | 41 | 42 | "Dynamically through new variable name identifying another object/RolePlayer in Context" >> { 43 | 44 | @context 45 | case class Context(roleA: Data) { 46 | 47 | // Variable `roleB` references `roleA` object 48 | // Variable `roleB` binds to Role `roleB` since they share name 49 | val roleB = roleA 50 | 51 | def trigger = roleB.bar 52 | 53 | role roleB { 54 | def bar = roleB.i * 2 55 | } 56 | } 57 | 58 | val obj = Data(42) 59 | Context(obj).trigger === 42 * 2 60 | } 61 | 62 | 63 | "Todo: All Roles in a Context are bound to objects in a single, atomic operation" >> { 64 | 65 | // Todo: How to demonstrate/reject un-atomic bindings? 66 | ok 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/dijkstra/Step2_Unvisited.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package examples.dijkstra 3 | import scala.collection.mutable 4 | 5 | /* 6 | Unvisited set... 7 | 8 | 2. Set the INITIAL INTERSECTION as CURRENT INTERSECTION. 9 | Create a set called DETOURS containing all intersections except the INITIAL INTERSECTION. 10 | */ 11 | 12 | object Step2_Unvisited extends App { 13 | 14 | // Context ################################################################## 15 | 16 | @context 17 | class CalculateShortestPath( 18 | city: ManhattanGrid, 19 | currentIntersection: Intersection, 20 | destination: Intersection 21 | ) { 22 | 23 | // todo: infer type from right hand side expression 24 | private val tentativeDistances: mutable.HashMap[Intersection, Int] = mutable.HashMap[Intersection, Int]() 25 | private val detours : mutable.Set[Intersection] = mutable.Set[Intersection]() 26 | 27 | tentativeDistances.assigntentativeDistances 28 | detours.markAsUnvisited 29 | 30 | // All Intersections will still be unvisited at this point 31 | println("Unvisited Intersections:\n" + detours.toList.sortBy(_.name).mkString("\n")) 32 | 33 | 34 | // Roles ################################################################## 35 | 36 | role tentativeDistances { 37 | def assigntentativeDistances() { 38 | tentativeDistances.put(currentIntersection, 0) 39 | city.intersections.filter(_ != currentIntersection).foreach(tentativeDistances.put(_, Int.MaxValue / 4)) 40 | } 41 | } 42 | 43 | // A second role is added in a similar fashion to the first one 44 | role detours { 45 | 46 | // STEP 2 - All intersections are unvisited from the start so we simply copy the Intersections from the Grid 47 | def markAsUnvisited() { 48 | detours ++= city.intersections 49 | } 50 | } 51 | } 52 | 53 | // Execution ########################################################## 54 | val startingPoint = ManhattanGrid().a 55 | val destination = ManhattanGrid().i 56 | val shortestPath = new CalculateShortestPath(ManhattanGrid(), startingPoint, destination) 57 | } -------------------------------------------------------------------------------- /core/src/main/scala/scaladci/util/expectCompileError.scala: -------------------------------------------------------------------------------- 1 | package scaladci.util 2 | 3 | import scala.language.experimental.macros 4 | import scala.reflect.macros.whitebox.{Context => MacroContext} 5 | import scala.reflect.macros.TypecheckException 6 | 7 | // Modified `illTyped` macro by Stefan Zeiger (@StefanZeiger) 8 | 9 | object expectCompileError { 10 | def apply(code: String): Unit = macro applyImplNoExp 11 | 12 | def apply(code: String, expected: String): Unit = macro applyImpl 13 | 14 | def applyImplNoExp(c: MacroContext)(code: c.Expr[String]) = applyImpl(c)(code, null) 15 | 16 | def applyImpl(c: MacroContext)(code: c.Expr[String], expected: c.Expr[String]): c.Expr[Unit] = { 17 | import c.universe._ 18 | 19 | val codeStr = code.tree match { 20 | case Literal(Constant(s: String)) => s.stripMargin.trim 21 | case x => c.abort(c.enclosingPosition, "Unknown code tree in compile check: " + showRaw(x)) 22 | } 23 | 24 | val (expPat, expMsg) = expected match { 25 | case null => (null, "EXPECTED SOME ERROR!") 26 | case Expr(Literal(Constant(s: String))) => val exp = s.stripMargin.trim; (exp, "EXPECTED ERROR:\n" + exp) 27 | } 28 | 29 | try { 30 | val dummy = TermName(c.freshName) 31 | c.typecheck(c.parse(s"{ val $dummy = { $codeStr } ; () }")) 32 | c.abort(c.enclosingPosition, 33 | s"""Type-checking succeeded unexpectedly!!! 34 | |CODE: 35 | |$codeStr 36 | |$expMsg 37 | |CODE: 38 | |${show(c.typecheck(c.parse("{ " + codeStr + " }")))} 39 | |-------------------- 40 | |AST: 41 | |${showRaw(c.typecheck(c.parse("{ " + codeStr + " }")))} 42 | |-------------------- 43 | """.stripMargin) 44 | } catch { 45 | case e: TypecheckException => 46 | val msg = e.getMessage.trim 47 | if ((expected ne null) && !msg.startsWith(expPat)) 48 | c.abort(c.enclosingPosition, 49 | s"""Type-checking failed in an unexpected way. 50 | |CODE: 51 | |$codeStr 52 | |$expMsg 53 | |ACTUAL ERROR: 54 | |$msg 55 | |-------------------- 56 | """.stripMargin) 57 | } 58 | 59 | reify(()) 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/MoneyTransfer2.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package examples 3 | import scala.language.reflectiveCalls 4 | import org.specs2.mutable._ 5 | 6 | /* 7 | More elaborate version of the canonical Money Transfer example 8 | Inspired by Rune Funch's implementation at 9 | http://fulloo.info/Examples/Marvin/MoneyTransfer/ 10 | */ 11 | 12 | class MoneyTransfer2 extends Specification { 13 | 14 | case class LedgerEntry(message: String, amount: Int) 15 | 16 | case class Account(account: String, initialLedgers: List[LedgerEntry]) { 17 | private val ledgers = new { 18 | var ledgerList = initialLedgers 19 | def addEntry(message: String, amount: Int) { ledgerList = ledgerList :+ new LedgerEntry(message, amount) } 20 | def getBalance = ledgerList.foldLeft(0)(_ + _.amount) 21 | } 22 | 23 | def balance = ledgers.getBalance 24 | def increaseBalance(amount: Int) { ledgers.addEntry("depositing", amount) } 25 | def decreaseBalance(amount: Int) { ledgers.addEntry("withdrawing", -amount) } 26 | } 27 | 28 | 29 | "Money transfer with ledgers Marvin/Rune" >> { 30 | 31 | @context 32 | case class MoneyTransfer(source: Account, destination: Account, amount: Int) { 33 | 34 | def transfer() { 35 | source.transfer 36 | } 37 | 38 | role source { 39 | def withdraw() { 40 | self.decreaseBalance(amount) 41 | } 42 | def transfer() { 43 | println("source balance is: " + self.balance) 44 | println("destination balance is: " + destination.balance) 45 | destination.deposit() 46 | withdraw() 47 | println("source balance is now: " + self.balance) 48 | println("destination balance is now: " + destination.balance) 49 | } 50 | } 51 | 52 | role destination { 53 | def deposit() { 54 | self.increaseBalance(amount) 55 | } 56 | } 57 | } 58 | 59 | // Test 60 | val source = Account("salary", List(LedgerEntry("start", 0), LedgerEntry("first deposit", 1000))) 61 | val destination = Account("budget", List()) 62 | 63 | source.balance === 1000 64 | destination.balance === 0 65 | 66 | val context = MoneyTransfer(source, destination, 245) 67 | context.transfer() 68 | 69 | source.balance === 1000 - 245 70 | destination.balance === 0 + 245 71 | } 72 | } -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/DuckTyping.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package examples 3 | import org.specs2.mutable._ 4 | 5 | import scala.language.reflectiveCalls 6 | 7 | /* 8 | Role interface with duck typing 9 | 10 | If a Role asks any object to just have a specific set of 11 | method signatures available, which could be seen as the 12 | "Role interface" or Role contract", then we are actually 13 | reducing safety. We would have no clue _which_ object 14 | has those methods. They could do terrible things. 15 | 16 | On the contrary we know much more from a type although 17 | polymorphism can sabotage as much. 18 | */ 19 | 20 | class DuckTyping extends Specification { 21 | 22 | // Data 23 | class Account(name: String, var balance: Int) { 24 | def increaseBalance(amount: Int) { balance += amount } 25 | def decreaseBalance(amount: Int) { balance -= amount } 26 | } 27 | 28 | // Evil account that satisfy a "Role contract" 29 | case class HackedAccount(name: String, var balance: Int) { 30 | def decreaseBalance(amount: Int) { balance -= amount * 10 } 31 | } 32 | 33 | // Class having a subset of the original methods. As an alternative 34 | // to duck typing we can pass this instead to our context. 35 | case class MyAccount(name: String, initialAmount: Int) extends Account(name, initialAmount) { 36 | override def increaseBalance(amount: Int) { balance += amount } 37 | 38 | // We could disallow certain methods in a sub class like this 39 | override def decreaseBalance(amount: Int) = ??? // Not implemented 40 | } 41 | 42 | 43 | "Ducktyping is less safe" >> { 44 | 45 | @context 46 | class MoneyTransfer( 47 | source: {def decreaseBalance(amount: Int)}, // <- structural (duck) type 48 | destination: MyAccount, 49 | amount: Int) { 50 | 51 | source.withdraw 52 | 53 | role source { 54 | def withdraw() { 55 | source.decreaseBalance(amount) 56 | destination.deposit 57 | } 58 | } 59 | 60 | role destination { 61 | def deposit() { 62 | destination.increaseBalance(amount) 63 | } 64 | } 65 | } 66 | 67 | // Test 68 | val salary = HackedAccount("Salary", 3000) 69 | val budget = MyAccount("Budget", 1000) 70 | 71 | new MoneyTransfer(salary, budget, 700) 72 | 73 | salary.balance === 3000 - 700 * 10 // Auch! I've been hacked 74 | budget.balance === 1000 + 700 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/dijkstra/Data.scala: -------------------------------------------------------------------------------- 1 | package scaladci.examples.dijkstra 2 | 3 | //Defining the basic Data structures... 4 | 5 | object Data extends App { 6 | 7 | // Data #################################################################### 8 | 9 | // Street/Avenue intersection (former "Node") 10 | case class Intersection(name: Char) 11 | 12 | // City blocks that connect each Intersection (former "Pair", "Edge", "Arc" etc.) 13 | // http://en.wikipedia.org/wiki/City_block 14 | case class Block(x: Intersection, y: Intersection) 15 | 16 | 17 | // Test data ################################################################ 18 | 19 | // Using only immutable values, so the Grid will remain the same always. 20 | 21 | case class ManhattanGrid() { 22 | 23 | // Intersections 24 | val intersections = ('a' to 'i').map(Intersection(_)).toList 25 | val (a, b, c, d, e, f, g, h, i) = (intersections(0), intersections(1), intersections(2), intersections(3), intersections(4), intersections(5), intersections(6), intersections(7), intersections(8)) 26 | 27 | // Street blocks 28 | val nextDownTheStreet = Map(a -> b, b -> c, d -> e, e -> f, g -> h, h -> i) 29 | 30 | // Avenue blocks 31 | val nextAlongTheAvenue = Map(a -> d, b -> e, c -> f, d -> g, f -> i) 32 | 33 | // Now we have defined the grid (without distances yet): 34 | 35 | // a - - - b - - - c 36 | // | | | 37 | // | | | 38 | // | | | 39 | // d - - - e - - - f 40 | // | | 41 | // | | 42 | // | | 43 | // g - - - h - - - i 44 | 45 | // Add distances between intersections ("how long is the block?") 46 | val blockLengths = Map( 47 | Block(a, b) -> 2, 48 | Block(b, c) -> 3, 49 | Block(c, f) -> 1, 50 | Block(f, i) -> 4, 51 | Block(b, e) -> 2, 52 | Block(e, f) -> 1, 53 | Block(a, d) -> 1, 54 | Block(d, g) -> 2, 55 | Block(g, h) -> 1, 56 | Block(h, i) -> 2, 57 | Block(d, e) -> 1 58 | ) 59 | 60 | // The complete Manhattan grid 61 | 62 | // a - 2 - b - 3 - c 63 | // | | | 64 | // 1 2 1 65 | // | | | 66 | // d - 1 - e - 1 - f 67 | // | | 68 | // 2 4 69 | // | | 70 | // g - 1 - h - 2 - i 71 | } 72 | 73 | println("Manhattan grid distances:\n" + ManhattanGrid().blockLengths.mkString("\n")) 74 | } -------------------------------------------------------------------------------- /core/src/main/scala/scaladci/util/MacroHelper.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package util 3 | 4 | import scala.reflect.macros.whitebox.{Context => MacroContext} 5 | 6 | trait MacroHelper[C <: MacroContext] { 7 | val c0: C 8 | import c0.universe._ 9 | 10 | def expr(tree: Tree) = { 11 | val typeCheckedTree = c0.typecheck(tree) 12 | c0.Expr(typeCheckedTree)(c0.WeakTypeTag(typeCheckedTree.tpe)) 13 | } 14 | 15 | def abort(t: Any, i: Int = 0) = { 16 | val j = if (i > 0) s"($i) " else "" 17 | c0.abort(c0.enclosingPosition, j + t.toString.trim) 18 | } 19 | 20 | 21 | def comp(t1: Any, t2: Any) { 22 | abort(s"\n$t1\n-------------------\n$t2") 23 | } 24 | 25 | def comp(t1: List[Tree], t2: List[Tree]) { 26 | val source = code(t1) + "\n\n================\n" + code(t2) 27 | val ast = raw(t1) + "\n\n================\n" + raw(t2) 28 | abort(s"\n$source\n\n#################################################\n\n$ast") 29 | } 30 | 31 | def comp(t1: Tree, t2: Tree) { 32 | val source = t1 + "\n\n================\n" + t2 33 | val ast = showRaw(t1) + "\n\n================\n" + showRaw(t2) 34 | abort(s"\n$source\n\n#################################################\n\n$ast") 35 | } 36 | 37 | def l(ts: List[Tree]) { 38 | abort(code(ts)) 39 | } 40 | 41 | def r(t: Any) { 42 | c0.abort(c0.enclosingPosition, showRaw(t)) 43 | } 44 | 45 | def r(ts: List[Tree]) { 46 | abort(raw(ts)) 47 | } 48 | 49 | def sep(ts: List[Tree]) = { 50 | code(ts) + "\n\n================\n" + raw(ts) 51 | } 52 | 53 | def lr(ts: List[Tree]) { 54 | abort(sep(ts)) 55 | } 56 | 57 | def code(ts: List[Tree]) = ts.zipWithIndex.map { 58 | case (t, i) => s"\n-- $i -- " + t 59 | } 60 | 61 | def raw(ts: List[Tree]) = ts.zipWithIndex.map { 62 | case (t, i) => s"\n-- $i -- " + showRaw(t) 63 | } 64 | 65 | // def raw(ts: List[Tree]) = ts.map("\n----- " + showRaw(_)) 66 | 67 | def err(t: Tree, msg: String) = { 68 | abort(msg + t) 69 | t 70 | } 71 | 72 | case class debug(clazz: String, threshold: Int, max: Int = 9999) { 73 | def apply(id: Int, params: Any*): Unit = { 74 | if (id >= threshold && id <= max) { 75 | val sp = "\n... " 76 | val x = s"$id, $clazz \n" + params.toList.zipWithIndex.map { 77 | case (l: List[_], i) => s"${i + 1} ## List($sp" + l.mkString(s"$sp") + " )" 78 | // case (l@h :: t, i) => s"${i + 1} ## List($sp" + l.mkString(s"$sp") + " )" 79 | case (value, i) => s"${i + 1} ## $value" 80 | }.mkString("\n") + "\n---------------------------------" 81 | c0.abort(NoPosition, x) 82 | } 83 | } 84 | } 85 | 86 | implicit class RichModifiersApi(mods: ModifiersApi) { 87 | def hasCtxAnnotation = mods.annotations.collectFirst { 88 | case Apply(Select(New(Ident(TypeName("context"))), termNames.CONSTRUCTOR), _) => true 89 | }.getOrElse(false) 90 | } 91 | } 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/MoneyTransfer1.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package examples 3 | import org.specs2.mutable._ 4 | 5 | /* 6 | Simplest versions of the canonical Money Transfer example 7 | 8 | NOTE: If a role method has the same signature as an instance method of the original 9 | Data object, the role method will take precedence/override the instance method. This 10 | applies to all versions below. 11 | */ 12 | 13 | class MoneyTransfer1 extends Specification { 14 | 15 | // Data 16 | case class Account(name: String, var balance: Int) { 17 | def increaseBalance(amount: Int) { balance += amount } 18 | def decreaseBalance(amount: Int) { balance -= amount } 19 | } 20 | 21 | 22 | // Test the various syntaxes 23 | // Note that the Account instance objects keep their identity throughout 24 | // all the roles they play in the various Contexts (no wrapping here). 25 | 26 | "With various role syntaxes" >> { 27 | 28 | // Using role name as reference to the Role Player - `source.decreaseBalance(amount)` 29 | 30 | @context 31 | case class MoneyTransfer(source: Account, destination: Account, amount: Int) { 32 | 33 | source.withdraw 34 | 35 | role source { 36 | def withdraw { 37 | source.decreaseBalance(amount) 38 | destination.deposit 39 | } 40 | } 41 | 42 | role destination { 43 | def deposit { 44 | destination.increaseBalance(amount) 45 | } 46 | } 47 | } 48 | 49 | 50 | // Using `self` as reference to the Role Player - `self.decreaseBalance(amount)` 51 | 52 | @context 53 | case class MoneyTransfer_self(source: Account, destination: Account, amount: Int) { 54 | 55 | source.withdraw 56 | 57 | role source { 58 | def withdraw { 59 | self.decreaseBalance(amount) 60 | destination.deposit 61 | } 62 | } 63 | 64 | role destination { 65 | def deposit { 66 | self.increaseBalance(amount) 67 | } 68 | } 69 | } 70 | 71 | 72 | /* 73 | Using `this` as reference to the Role Player - `this.decreaseBalance(amount)` 74 | ATTENTION: 75 | Using `this` inside a role definition will reference the role-playing object (and not the Context object)! 76 | */ 77 | @context 78 | case class MoneyTransfer_this(source: Account, destination: Account, amount: Int) { 79 | 80 | source.withdraw 81 | 82 | role source { 83 | def withdraw { 84 | this.decreaseBalance(amount) 85 | destination.deposit 86 | } 87 | } 88 | 89 | role destination { 90 | def deposit { 91 | this.increaseBalance(amount) 92 | } 93 | } 94 | } 95 | 96 | // Test 97 | val salary = Account("Salary", 3000) 98 | val budget = Account("Budget", 1000) 99 | 100 | // Using role name 101 | MoneyTransfer(salary, budget, 700) 102 | salary.balance === 3000 - 700 103 | budget.balance === 1000 + 700 104 | 105 | // Using `self` 106 | MoneyTransfer_self(salary, budget, 100) 107 | salary.balance === 2300 - 100 108 | budget.balance === 1700 + 100 109 | 110 | // Using `this` 111 | MoneyTransfer_this(salary, budget, 50) 112 | salary.balance === 2200 - 50 113 | budget.balance === 1800 + 50 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/dijkstra/other_implementations_2013_01_18/EgonElbre_JavaScript_1: -------------------------------------------------------------------------------- 1 | 2 | CalculateShortestPath = Context(@ 3 | function(initial, destination, graph) { 4 | Initial = initial; 5 | Graph = graph; 6 | Tentative = new ObjectMap(); 7 | Unvisited = new ObjectMap(); 8 | Path = new ObjectMap(); // best path (to --> from) 9 | Graph.nodes.map(function(node) { 10 | Unvisited.put(node); 11 | Tentative.put(node, Infinity); 12 | }); 13 | Tentative.put(Initial, 0); 14 | 15 | Current = Initial; 16 | Current.markVisited(); 17 | 18 | while(!Unvisited.isEmpty()) { 19 | Current.relaxDistances(); 20 | Current.markVisited(); 21 | 22 | if(!Unvisited.has(destination)) break; 23 | 24 | Current = Unvisited.findNearest(); 25 | if(Current === undefined) break; 26 | } 27 | return Path.to(destination); 28 | }, { 29 | Initial: {}, 30 | Neighbor: { 31 | visited: function() { 32 | return !Unvisited.has(this); 33 | } 34 | }, 35 | Current: { 36 | markVisited: function() { 37 | Unvisited.remove(this); 38 | }, 39 | getNeighbors: function() { 40 | return Graph.neighbors(this); 41 | }, 42 | relaxDistances: function() { 43 | Current.getNeighbors().map(function(node) { 44 | Neighbor = node; 45 | if(Neighbor.visited()) return; 46 | 47 | var alternate = Tentative.get(Current) + Current.distanceTo(Neighbor); 48 | if(alternate < Tentative.get(Neighbor)) { 49 | Tentative.put(Neighbor, alternate); 50 | Path.put(Neighbor, Current); 51 | } 52 | }); 53 | }, 54 | distanceTo: function(other) { 55 | return Graph.distance(this, other); 56 | } 57 | }, 58 | Graph: { 59 | distance: function(from, to) { 60 | if(from === to) return 0; 61 | return this.nodes.get(from).get(to) || Infinity; 62 | }, 63 | neighbors: function(node) { 64 | return this.nodes.get(node); 65 | } 66 | }, 67 | Tentative: {}, 68 | Unvisited: { 69 | findNearest: function() { 70 | var nearest = undefined, 71 | distance = Infinity; 72 | this.map(function(node) { 73 | var dist = Tentative.get(node); 74 | if(dist < distance) { 75 | nearest = node; 76 | distance = dist; 77 | } 78 | }) 79 | return nearest; 80 | } 81 | }, 82 | Path: { 83 | to: function(to) { 84 | var path = [to], 85 | cur = to; 86 | while(cur != Initial) { 87 | cur = this.get(cur); 88 | path.unshift(cur); 89 | if(cur === undefined) { 90 | return undefined; 91 | } 92 | } 93 | return path; 94 | } 95 | } 96 | }@); 97 | 98 | function mkGraph(edges) { 99 | var nodes = new ObjectMap(); 100 | 101 | var forceMap = function(node) { 102 | var map = nodes.get(node); 103 | if(map === undefined) { 104 | map = new ObjectMap(); 105 | nodes.put(node, map); 106 | } 107 | return map; 108 | }; 109 | 110 | for(var i = 0; i < edges.length; i += 1) { 111 | var edge = edges[i], 112 | from = edge[0], 113 | to = edge[1], 114 | dist = edge[2]; 115 | 116 | forceMap(to); 117 | var cur = forceMap(from); 118 | cur.put(to, dist); 119 | } 120 | return { 121 | nodes: nodes 122 | }; 123 | } 124 | 125 | var a = {id:'a'}, 126 | b = {id:'b'}, 127 | c = {id:'c'}, 128 | d = {id:'d'}, 129 | e = {id:'e'}, 130 | f = {id:'f'}, 131 | g = {id:'g'}, 132 | h = {id:'h'}, 133 | i = {id:'i'}; 134 | 135 | var edges = [ 136 | [a,b,2], 137 | [a,d,1], 138 | [b,c,3], 139 | [b,e,2], 140 | [c,f,1], 141 | [d,e,1], 142 | [d,g,2], 143 | [e,f,1], 144 | [f,i,4], 145 | [g,h,1], 146 | [h,i,2]]; 147 | 148 | var graph = mkGraph(edges); 149 | var path = CalculateShortestPath(a, i, graph); 150 | 151 | var proper = []; 152 | for(var i = 0; i < path.length; i += 1) 153 | proper.push(path[i].id); 154 | log(proper.join(" -> ")); 155 | -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/dijkstra/other_implementations_2013_01_18/EgonElbre_JavaScript_2: -------------------------------------------------------------------------------- 1 | 2 | CalculateShortestPath = (function(){ 3 | var Initial,Neighbor,Current,Graph,Tentative,Unvisited,Path; 4 | var Neighbor$visited = function () { 5 | return !Unvisited.has(this); 6 | }; 7 | var Current$markVisited = function () { 8 | Unvisited.remove(this); 9 | }; 10 | var Current$getNeighbors = function () { 11 | return Graph$neighbors.call(Graph,this); 12 | }; 13 | var Current$relaxDistances = function () { 14 | Current$getNeighbors.call(Current).map(function(node) { 15 | Neighbor = node; 16 | if(Neighbor$visited.call(Neighbor)) return; 17 | 18 | var alternate = Tentative.get(Current) + Current$distanceTo.call(Current,Neighbor); 19 | if(alternate < Tentative.get(Neighbor)) { 20 | Tentative.put(Neighbor, alternate); 21 | Path.put(Neighbor, Current); 22 | } 23 | }); 24 | }; 25 | var Current$distanceTo = function (other) { 26 | return Graph$distance.call(Graph,this, other); 27 | }; 28 | var Graph$distance = function (from, to) { 29 | if(from === to) return 0; 30 | return this.nodes.get(from).get(to) || Infinity; 31 | }; 32 | var Graph$neighbors = function (node) { 33 | return this.nodes.get(node); 34 | }; 35 | var Unvisited$findNearest = function () { 36 | var nearest = undefined, 37 | distance = Infinity; 38 | this.map(function(node) { 39 | var dist = Tentative.get(node); 40 | if(dist < distance) { 41 | nearest = node; 42 | distance = dist; 43 | } 44 | }) 45 | return nearest; 46 | }; 47 | var Path$to = function (to) { 48 | var path = [to], 49 | cur = to; 50 | while(cur != Initial) { 51 | cur = this.get(cur); 52 | path.unshift(cur); 53 | if(cur === undefined) { 54 | return undefined; 55 | } 56 | } 57 | return path; 58 | }; 59 | return function (initial, destination, graph) { 60 | Initial = initial; 61 | Graph = graph; 62 | Tentative = new ObjectMap(); 63 | Unvisited = new ObjectMap(); 64 | Path = new ObjectMap(); // best path (to --> from) 65 | Graph.nodes.map(function(node) { 66 | Unvisited.put(node); 67 | Tentative.put(node, Infinity); 68 | }); 69 | Tentative.put(Initial, 0); 70 | 71 | Current = Initial; 72 | Current$markVisited.call(Current); 73 | 74 | while(!Unvisited.isEmpty()) { 75 | Current$relaxDistances.call(Current); 76 | Current$markVisited.call(Current); 77 | 78 | if(!Unvisited.has(destination)) break; 79 | 80 | Current = Unvisited$findNearest.call(Unvisited); 81 | if(Current === undefined) break; 82 | } 83 | return Path$to.call(Path,destination); 84 | };})(); 85 | 86 | function mkGraph(edges) { 87 | var nodes = new ObjectMap(); 88 | 89 | var forceMap = function(node) { 90 | var map = nodes.get(node); 91 | if(map === undefined) { 92 | map = new ObjectMap(); 93 | nodes.put(node, map); 94 | } 95 | return map; 96 | }; 97 | 98 | for(var i = 0; i < edges.length; i += 1) { 99 | var edge = edges[i], 100 | from = edge[0], 101 | to = edge[1], 102 | dist = edge[2]; 103 | 104 | forceMap(to); 105 | var cur = forceMap(from); 106 | cur.put(to, dist); 107 | } 108 | return { 109 | nodes: nodes 110 | }; 111 | } 112 | 113 | var a = {id:'a'}, 114 | b = {id:'b'}, 115 | c = {id:'c'}, 116 | d = {id:'d'}, 117 | e = {id:'e'}, 118 | f = {id:'f'}, 119 | g = {id:'g'}, 120 | h = {id:'h'}, 121 | i = {id:'i'}; 122 | 123 | var edges = [ 124 | [a,b,2], 125 | [a,d,1], 126 | [b,c,3], 127 | [b,e,2], 128 | [c,f,1], 129 | [d,e,1], 130 | [d,g,2], 131 | [e,f,1], 132 | [f,i,4], 133 | [g,h,1], 134 | [h,i,2]]; 135 | 136 | var graph = mkGraph(edges); 137 | var path = CalculateShortestPath(a, i, graph); 138 | 139 | var proper = []; 140 | for(var i = 0; i < path.length; i += 1) 141 | proper.push(path[i].id); 142 | log(proper.join(" -> ")); 143 | -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/dijkstra/Step3_4_Calculate.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package examples.dijkstra 3 | import scala.collection.mutable 4 | /* 5 | Now comes the meaty part of calculating the tentative distances: 6 | 7 | 3. For the CURRENT INTERSECTION, check its EASTERN and SOUTHERN NEIGHBOR INTERSECTIONS and calculate 8 | their TENTATIVE DISTANCES. For example, if the CURRENT INTERSECTION A is marked with a DISTANCE of 6, 9 | and the BLOCK connecting it with its EASTERN NEIGHBOR INTERSECTION B has length 2, then the distance 10 | to B (via A) will be 6 + 2 = 8. If this distance (8) is less than the previously recorded TENTATIVE 11 | DISTANCE of B, then overwrite that distance. Even though a neighbor intersection has been considered, 12 | it is not marked as a SHORTCUT at this time, and it remains in DETOURS. 13 | 14 | 4. When we are done considering the EASTERN and SOUTHERN NEIGHBOR INTERSECTIONS of the CURRENT 15 | INTERSECTION, remove the CURRENT INTERSECTION from DETOURS. 16 | */ 17 | 18 | object Step3_4_Calculate extends App { 19 | 20 | // Context ################################################################## 21 | 22 | @context 23 | class CalculateShortestPath( 24 | city: ManhattanGrid, 25 | currentIntersection: Intersection, 26 | destination: Intersection 27 | ) { 28 | 29 | // todo: infer type from right hand side expression 30 | private val tentativeDistances: mutable.HashMap[Intersection, Int] = mutable.HashMap[Intersection, Int]() 31 | private val detours : mutable.Set[Intersection] = mutable.Set[Intersection]() 32 | private val EastNeighbor = city.nextDownTheStreet.get(currentIntersection) 33 | private val SouthNeighbor = city.nextAlongTheAvenue.get(currentIntersection) 34 | private val shortcuts = mutable.HashMap[Intersection, Intersection]() 35 | 36 | tentativeDistances.initialize 37 | detours.initialize 38 | 39 | println("Unvisited before:\n" + detours.toList.sortBy(_.name).mkString("\n")) 40 | 41 | currentIntersection.calculateTentativeDistanceOfNeighbors 42 | 43 | // Current intersection ('a') is no longer in the unvisited set: 44 | println("\nUnvisited after :\n" + detours.toList.sortBy(_.name).mkString("\n")) 45 | 46 | 47 | // Roles ################################################################## 48 | 49 | role tentativeDistances { 50 | def initialize() { 51 | tentativeDistances.put(currentIntersection, 0) 52 | city.intersections.filter(_ != currentIntersection).foreach(tentativeDistances.put(_, Int.MaxValue / 4)) 53 | } 54 | } 55 | 56 | role detours { 57 | def initialize() { detours ++= city.intersections } 58 | } 59 | 60 | role currentIntersection { 61 | def calculateTentativeDistanceOfNeighbors() { 62 | 63 | // STEP 3 in the algorithm 64 | // The foreach call is only executed if we have a neighbor (Intersection 'c' doesn't have an eastern neighbor) 65 | EastNeighbor.foreach(updateNeighborDistance(_)) 66 | SouthNeighbor.foreach(updateNeighborDistance(_)) 67 | 68 | // STEP 4 in the algorithm (included in this version since it's just a one-liner) 69 | detours.remove(currentIntersection) 70 | } 71 | 72 | // example of "internal role method". it could have been private if we wanted to prevent access from the Context. 73 | def updateNeighborDistance(neighborIntersection: Intersection) { 74 | if (detours.contains(neighborIntersection)) { 75 | val newTentDistanceToNeighbor = currentDistance + lengthOfBlockTo(neighborIntersection) 76 | val currentTentDistToNeighbor = tentativeDistances(neighborIntersection) 77 | if (newTentDistanceToNeighbor < currentTentDistToNeighbor) { 78 | tentativeDistances.update(neighborIntersection, newTentDistanceToNeighbor) 79 | shortcuts.put(neighborIntersection, currentIntersection) 80 | } 81 | } 82 | } 83 | def currentDistance = tentativeDistances(currentIntersection) 84 | def lengthOfBlockTo(neighbor: Intersection) = city.distanceBetween(currentIntersection, neighbor) 85 | } 86 | 87 | role city { 88 | def distanceBetween(from: Intersection, to: Intersection) = city.blockLengths(Block(from, to)) 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/Dijkstra_self.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package examples 3 | import org.specs2.mutable._ 4 | 5 | import scala.collection.mutable 6 | 7 | // DCI implementation of the Dijkstra algorithm 8 | // Using `self` as role identifier 9 | 10 | class Dijkstra_self extends Specification { 11 | 12 | // Data 13 | case class Intersection(name: Char) 14 | case class Block(x: Intersection, y: Intersection) 15 | 16 | 17 | // Environment 18 | case class ManhattanGrid() { 19 | val intersections = ('a' to 'i').map(Intersection).toList 20 | val (a, b, c, d, e, f, g, h, i) = (intersections(0), intersections(1), intersections(2), intersections(3), intersections(4), intersections(5), intersections(6), intersections(7), intersections(8)) 21 | val nextDownTheStreet = Map(a -> b, b -> c, d -> e, e -> f, g -> h, h -> i) 22 | val nextAlongTheAvenue = Map(a -> d, b -> e, c -> f, d -> g, f -> i) 23 | val blockLengths = Map(Block(a, b) -> 2, Block(b, c) -> 3, Block(c, f) -> 1, Block(f, i) -> 4, Block(b, e) -> 2, Block(e, f) -> 1, Block(a, d) -> 1, Block(d, g) -> 2, Block(g, h) -> 1, Block(h, i) -> 2, Block(d, e) -> 1) 24 | 25 | // a - 2 - b - 3 - c 26 | // | | | 27 | // 1 2 1 28 | // | | | 29 | // d - 1 - e - 1 - f 30 | // | | 31 | // 2 4 32 | // | | 33 | // g - 1 - h - 2 - i 34 | } 35 | 36 | "Using `self` as a Role identifier" >> { 37 | 38 | @context 39 | class Dijkstra( 40 | city: ManhattanGrid, 41 | currentIntersection: Intersection, 42 | destination: Intersection, 43 | tentativeDistances: mutable.HashMap[Intersection, Int] = mutable.HashMap[Intersection, Int](), 44 | detours: mutable.Set[Intersection] = mutable.Set[Intersection](), 45 | shortcuts: mutable.HashMap[Intersection, Intersection] = mutable.HashMap[Intersection, Intersection]() 46 | ) { 47 | 48 | // Algorithm 49 | if (tentativeDistances.isEmpty) { 50 | tentativeDistances.initialize 51 | detours.initialize 52 | } 53 | currentIntersection.calculateTentativeDistanceOfNeighbors 54 | if (detours contains destination) { 55 | val nextCurrent = detours.withSmallestTentativeDistance 56 | new Dijkstra(city, nextCurrent, destination, tentativeDistances, detours, shortcuts) 57 | } 58 | 59 | // Context helper methods 60 | def pathTo(x: Intersection): List[Intersection] = { if (!shortcuts.contains(x)) List(x) else x :: pathTo(shortcuts(x)) } 61 | def shortestPath = pathTo(destination).reverse 62 | 63 | // Roles 64 | 65 | role tentativeDistances { 66 | def initialize { 67 | self.put(currentIntersection, 0) 68 | city.intersections.filter(_ != currentIntersection).foreach(self.put(_, Int.MaxValue / 4)) 69 | } 70 | } 71 | 72 | role detours { 73 | def initialize { self ++= city.intersections } 74 | def withSmallestTentativeDistance = { self.reduce((x, y) => if (tentativeDistances(x) < tentativeDistances(y)) x else y) } 75 | } 76 | 77 | role currentIntersection { 78 | def calculateTentativeDistanceOfNeighbors { 79 | city.eastNeighbor foreach updateNeighborDistance 80 | city.southNeighbor foreach updateNeighborDistance 81 | detours remove self 82 | } 83 | def updateNeighborDistance(neighborIntersection: Intersection) { 84 | if (detours.contains(neighborIntersection)) { 85 | val newTentDistanceToNeighbor = currentDistance + lengthOfBlockTo(neighborIntersection) 86 | val currentTentDistToNeighbor = tentativeDistances(neighborIntersection) 87 | if (newTentDistanceToNeighbor < currentTentDistToNeighbor) { 88 | tentativeDistances.update(neighborIntersection, newTentDistanceToNeighbor) 89 | shortcuts.put(neighborIntersection, self) 90 | } 91 | } 92 | } 93 | def currentDistance = tentativeDistances(currentIntersection) 94 | def lengthOfBlockTo(neighbor: Intersection) = city.distanceBetween(currentIntersection, neighbor) 95 | } 96 | 97 | role city { 98 | def distanceBetween(from: Intersection, to: Intersection) = self.blockLengths(Block(from, to)) 99 | def eastNeighbor = self.nextDownTheStreet.get(currentIntersection) 100 | def southNeighbor = self.nextAlongTheAvenue.get(currentIntersection) 101 | } 102 | } 103 | 104 | // Test 105 | val startingPoint = ManhattanGrid().a 106 | val destination = ManhattanGrid().i 107 | val shortestPath = new Dijkstra(ManhattanGrid(), startingPoint, destination).shortestPath 108 | shortestPath.map(_.name).mkString(" -> ") === "a -> d -> g -> h -> i" 109 | } 110 | } -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/Dijkstra.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package examples 3 | import org.specs2.mutable._ 4 | 5 | import scala.collection.mutable 6 | 7 | // DCI implementation of the Dijkstra algorithm 8 | // Using role name as role identifier 9 | 10 | class Dijkstra extends Specification { 11 | 12 | // Data 13 | case class Intersection(name: Char) 14 | case class Block(x: Intersection, y: Intersection) 15 | case class ManhattanGrid() { 16 | val intersections = ('a' to 'i').map(Intersection).toList 17 | val (a, b, c, d, e, f, g, h, i) = (intersections(0), intersections(1), intersections(2), intersections(3), intersections(4), intersections(5), intersections(6), intersections(7), intersections(8)) 18 | val nextDownTheStreet = Map(a -> b, b -> c, d -> e, e -> f, g -> h, h -> i) 19 | val nextAlongTheAvenue = Map(a -> d, b -> e, c -> f, d -> g, f -> i) 20 | val blockLengths = Map(Block(a, b) -> 2, Block(b, c) -> 3, Block(c, f) -> 1, Block(f, i) -> 4, Block(b, e) -> 2, Block(e, f) -> 1, Block(a, d) -> 1, Block(d, g) -> 2, Block(g, h) -> 1, Block(h, i) -> 2, Block(d, e) -> 1) 21 | 22 | // a - 2 - b - 3 - c 23 | // | | | 24 | // 1 2 1 25 | // | | | 26 | // d - 1 - e - 1 - f 27 | // | | 28 | // 2 4 29 | // | | 30 | // g - 1 - h - 2 - i 31 | } 32 | 33 | "Using Role name as a Role identifier" >> { 34 | 35 | @context 36 | class Dijkstra( 37 | city: ManhattanGrid, 38 | currentIntersection: Intersection, 39 | destination: Intersection, 40 | tentativeDistances: mutable.HashMap[Intersection, Int] = mutable.HashMap[Intersection, Int](), 41 | detours: mutable.Set[Intersection] = mutable.Set[Intersection](), 42 | shortcuts: mutable.HashMap[Intersection, Intersection] = mutable.HashMap[Intersection, Intersection]() 43 | ) { 44 | 45 | // Algorithm 46 | if (tentativeDistances.isEmpty) { 47 | tentativeDistances.initialize 48 | detours.initialize 49 | } 50 | currentIntersection.calculateTentativeDistanceOfNeighbors 51 | if (detours contains destination) { 52 | val nextCurrent = detours.withSmallestTentativeDistance 53 | new Dijkstra(city, nextCurrent, destination, tentativeDistances, detours, shortcuts) 54 | } 55 | 56 | // Context helper methods 57 | def pathTo(x: Intersection): List[Intersection] = if (!shortcuts.contains(x)) List(x) else x :: pathTo(shortcuts(x)) 58 | def shortestPath = pathTo(destination).reverse 59 | 60 | // Roles 61 | 62 | role tentativeDistances { 63 | def initialize { 64 | tentativeDistances.put(currentIntersection, 0) 65 | city.intersections.filter(_ != currentIntersection).foreach(tentativeDistances.put(_, Int.MaxValue / 4)) 66 | } 67 | } 68 | 69 | role detours { 70 | def initialize { detours ++= city.intersections } 71 | def withSmallestTentativeDistance = { detours.reduce((x, y) => if (tentativeDistances(x) < tentativeDistances(y)) x else y) } 72 | } 73 | 74 | role currentIntersection { 75 | def calculateTentativeDistanceOfNeighbors { 76 | city.eastNeighbor foreach updateNeighborDistance 77 | city.southNeighbor foreach updateNeighborDistance 78 | detours remove currentIntersection 79 | } 80 | def updateNeighborDistance(neighborIntersection: Intersection) { 81 | if (detours.contains(neighborIntersection)) { 82 | val newTentDistanceToNeighbor = currentDistance + lengthOfBlockTo(neighborIntersection) 83 | val currentTentDistToNeighbor = tentativeDistances(neighborIntersection) 84 | if (newTentDistanceToNeighbor < currentTentDistToNeighbor) { 85 | tentativeDistances.update(neighborIntersection, newTentDistanceToNeighbor) 86 | shortcuts.put(neighborIntersection, currentIntersection) 87 | } 88 | } 89 | } 90 | def currentDistance = tentativeDistances(currentIntersection) 91 | def lengthOfBlockTo(neighbor: Intersection) = city.distanceBetween(currentIntersection, neighbor) 92 | } 93 | 94 | role city { 95 | def distanceBetween(from: Intersection, to: Intersection) = city.blockLengths(Block(from, to)) 96 | def eastNeighbor = city.nextDownTheStreet.get(currentIntersection) 97 | def southNeighbor = city.nextAlongTheAvenue.get(currentIntersection) 98 | } 99 | } 100 | 101 | // Test 102 | val startingPoint = ManhattanGrid().a 103 | val destination = ManhattanGrid().i 104 | val shortestPath = new Dijkstra(ManhattanGrid(), startingPoint, destination).shortestPath 105 | shortestPath.map(_.name).mkString(" -> ") === "a -> d -> g -> h -> i" 106 | } 107 | } -------------------------------------------------------------------------------- /coretest/src/test/Scala/scaladci/semantics/RoleBody.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package semantics 3 | import util._ 4 | 5 | class RoleBody extends DCIspecification { 6 | 7 | "Can define role method(s)" >> { 8 | 9 | @context 10 | case class Context(myRole: Data) { 11 | def trigger = myRole.bar 12 | 13 | role myRole { 14 | def bar = 2 * baz 15 | def baz = 3 * buz 16 | def buz = 4 * myRole.i 17 | } 18 | } 19 | Context(Data(5)).trigger === 2 * 3 * 4 * 5 20 | } 21 | 22 | 23 | "Cannot be assigned to a Role definition" >> { 24 | 25 | expectCompileError( 26 | """ 27 | @context 28 | class Context(myRole: Data) { 29 | role myRole = {} 30 | } 31 | """, 32 | "(1) Can't assign a Role body to `myRole`. Please remove `=` before the body definition") 33 | 34 | @context 35 | class Context(roleA: Data, roleB: Data) { 36 | role roleA {} // ok without `=` 37 | } 38 | 39 | success 40 | } 41 | 42 | 43 | "Cannot define state" >> { 44 | 45 | expectCompileError( 46 | """ 47 | @context 48 | class Context(myRole: Int) { 49 | role myRole { 50 | val notAllowed = 42 // <-- no state in Roles! 51 | } 52 | } 53 | """, 54 | """ 55 | |Roles are only allowed to define methods. 56 | |Please remove the following code from `myRole`: 57 | |CODE: val notAllowed = 42 58 | """) 59 | 60 | expectCompileError( 61 | """ 62 | @context 63 | class Context(myRole: Int) { 64 | role myRole { 65 | var notAllowed = 42 // <-- no state in Roles! 66 | } 67 | } 68 | """, 69 | """ 70 | |Roles are only allowed to define methods. 71 | |Please remove the following code from `myRole`: 72 | |CODE: var notAllowed = 42 73 | """) 74 | 75 | success 76 | } 77 | 78 | 79 | "Cannot define types aliases (??)" >> { 80 | 81 | expectCompileError( 82 | """ 83 | @context 84 | class Context(myRole: Int) { 85 | role myRole { 86 | type notAllowed = String // <-- no type definitions in Roles ...?! 87 | } 88 | } 89 | """, 90 | """ 91 | |Roles are only allowed to define methods. 92 | |Please remove the following code from `myRole`: 93 | |CODE: type notAllowed = String 94 | """) 95 | success 96 | } 97 | 98 | 99 | "Cannot define other types" >> { 100 | 101 | expectCompileError( 102 | """ 103 | @context 104 | class Context(myRole: Int) { 105 | role myRole { 106 | class NoClass // <-- no class definitions in Roles! 107 | } 108 | } 109 | """, 110 | """ 111 | |Roles are only allowed to define methods. 112 | |Please remove the following code from `myRole`: 113 | |CODE: class NoClass extends scala.AnyRef { 114 | | def () = { 115 | | super.(); 116 | | () 117 | | } 118 | |} 119 | """) 120 | 121 | expectCompileError( 122 | """ 123 | @context 124 | class Context(myRole: Int) { 125 | role myRole { 126 | case class NoCaseClass() // <-- no class definitions in Roles! 127 | } 128 | } 129 | """, 130 | """ 131 | |Roles are only allowed to define methods. 132 | |Please remove the following code from `myRole`: 133 | |CODE: case class NoCaseClass extends scala.Product with scala.Serializable { 134 | | def () = { 135 | | super.(); 136 | | () 137 | | } 138 | |} 139 | """) 140 | 141 | expectCompileError( 142 | """ 143 | @context 144 | class Context(myRole: Int) { 145 | role myRole { 146 | trait NoTrait 147 | } 148 | } 149 | """, 150 | """ 151 | |Roles are only allowed to define methods. 152 | |Please remove the following code from `myRole`: 153 | |CODE: abstract trait NoTrait extends scala.AnyRef 154 | """) 155 | 156 | expectCompileError( 157 | """ 158 | @context 159 | class Context(myRole: Int) { 160 | role myRole { 161 | object NoObject 162 | } 163 | } 164 | """, 165 | """ 166 | |Roles are only allowed to define methods. 167 | |Please remove the following code from `myRole`: 168 | |CODE: object NoObject extends scala.AnyRef { 169 | | def () = { 170 | | super.(); 171 | | () 172 | | } 173 | |} 174 | """) 175 | 176 | success 177 | } 178 | 179 | 180 | "Cannot define a nested role" >> { 181 | 182 | expectCompileError( 183 | """ 184 | @context 185 | class Context(myRole: Int) { 186 | role myRole { 187 | role nestedRole {} 188 | } 189 | } 190 | """, 191 | """ 192 | |[ContextTransformer:roleBodyTransformer] Roles are only allowed to define methods. 193 | |Please remove the following code from `myRole`: 194 | |CODE: role.nestedRole(()) 195 | """) 196 | 197 | success 198 | } 199 | } -------------------------------------------------------------------------------- /coretest/src/test/Scala/scaladci/syntax/RoleAsKeyword.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package syntax 3 | import util._ 4 | 5 | class RoleAsKeyword extends DCIspecification { 6 | 7 | "Can define implemented Roles" >> { 8 | 9 | @context class Context(myRole: Data) { 10 | role myRole { 11 | def roleMethod = 42 12 | } 13 | } 14 | 15 | success 16 | } 17 | 18 | 19 | "Can define methodless Roles" >> { 20 | 21 | @context class Context1(myRole: Data) { 22 | role myRole() 23 | } 24 | 25 | @context class Context2(myRole: Data) { 26 | role.myRole() 27 | } 28 | 29 | @context class Context3(myRole: Data) { 30 | role myRole {} 31 | } 32 | 33 | success 34 | } 35 | 36 | 37 | "Can only define Roles" >> { 38 | 39 | @context 40 | class ContextWithOkRoleUse(myRole: Data) { 41 | role myRole {} 42 | } 43 | 44 | // Rejected uses of `role` keyword 45 | // @context class Context(myRole: Data) {role => 46 | // 47 | // val value = role 48 | // var value = role 49 | // 50 | // val role = 42 51 | // var role = 42 52 | // 53 | // def role() = 42 54 | // 55 | // class role 56 | // case class role() 57 | // trait role 58 | // object role {} 59 | // 60 | // type role 61 | // 62 | // val x = this.role 63 | // role.myRole 64 | // } 65 | 66 | expectCompileError( 67 | """ 68 | @context 69 | class Context { 70 | role => 71 | } 72 | """, 73 | "Using `role` keyword as a template name is not allowed") 74 | 75 | expectCompileError( 76 | """ 77 | @context 78 | class Context { 79 | val foo = role 80 | } 81 | """, 82 | "Using `role` keyword as a return value is not allowed") 83 | 84 | expectCompileError( 85 | """ 86 | @context 87 | class Context { 88 | val role = 42 // val 89 | } 90 | """, 91 | "Using `role` keyword as a variable name is not allowed") 92 | 93 | expectCompileError( 94 | """ 95 | @context 96 | class Context { 97 | val role = 42 // var 98 | } 99 | """, 100 | "Using `role` keyword as a variable name is not allowed") 101 | 102 | expectCompileError( 103 | """ 104 | @context 105 | class Context { 106 | def role() = 42 107 | } 108 | """, 109 | "Using `role` keyword as a method name is not allowed") 110 | 111 | expectCompileError( 112 | """ 113 | @context 114 | class Context { 115 | def hej = { 116 | trait role 117 | 42 118 | } 119 | } 120 | """, 121 | "Using `role` keyword as a trait name is not allowed") 122 | 123 | expectCompileError( 124 | """ 125 | @context 126 | class Context { 127 | trait role 128 | } 129 | """, 130 | "Using `role` keyword as a trait name is not allowed") 131 | 132 | expectCompileError( 133 | """ 134 | @context 135 | class Context { 136 | case class role() 137 | } 138 | """, 139 | "Using `role` keyword as a case class name is not allowed") 140 | 141 | expectCompileError( 142 | """ 143 | @context 144 | class Context { 145 | class role 146 | } 147 | """, 148 | "Using `role` keyword as a class name is not allowed") 149 | 150 | expectCompileError( 151 | """ 152 | @context 153 | class Context { 154 | object role 155 | } 156 | """, 157 | "Using `role` keyword as an object name is not allowed") 158 | 159 | expectCompileError( 160 | """ 161 | @context 162 | class Context { 163 | type role 164 | } 165 | """, 166 | "Using `role` keyword as a type alias is not allowed") 167 | 168 | expectCompileError( 169 | """ 170 | @context 171 | class Context { 172 | val x = this.role 173 | } 174 | """, 175 | "Using `role` keyword as a selector name after a quantifier is not allowed") 176 | 177 | success 178 | } 179 | 180 | 181 | "Needs a Role name" >> { 182 | 183 | expectCompileError( 184 | """ 185 | @context 186 | class Context(myRole: Data) { 187 | role 188 | } 189 | """, 190 | "(1) `role` keyword without Role name is not allowed") 191 | 192 | success 193 | } 194 | 195 | 196 | "Needs body to avoid postfix clashes" >> { 197 | 198 | expectCompileError( 199 | """ 200 | @context 201 | class Context(myRole: Data) { 202 | role myRole 203 | } 204 | """, 205 | "(1) To avoid postfix clashes, please write `role myRole {}` instead of `role myRole`") 206 | 207 | @context 208 | class Context1(myRole: Data) { 209 | role myRole {} // with body 210 | } 211 | 212 | expectCompileError( 213 | """ 214 | @context 215 | class Context(roleA: Data, roleB: Data) { 216 | role roleA // two lines after each other ... 217 | role roleB // ... would unintentionally become `role.roleA(role).roleB` 218 | } 219 | """, 220 | "(2) To avoid postfix clashes, please write `role roleA {}` instead of `role roleA`") 221 | 222 | @context 223 | class Context2(roleA: Data, roleB: Data) { 224 | role roleA {} 225 | role roleB {} 226 | } 227 | 228 | success 229 | } 230 | } -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/dijkstra/other_implementations_2013_01_18/AndreasSoderlund_Coffee2: -------------------------------------------------------------------------------- 1 | describe "Ivento.Dci.Examples.Dijkstra", -> 2 | 3 | class ShortestManhattanPath extends Ivento.Dci.Context 4 | 5 | _tentativeDistances: new Hashtable() 6 | _distances: new Hashtable() 7 | _unvisited: new Hashtable() 8 | _pathTo: new Hashtable() 9 | _initialNode: null 10 | 11 | constructor: (nodes, initialNode) -> 12 | 13 | convertNode = (n) -> 14 | node: if n? then n[0] else null 15 | distance: if n? then n[1] else null 16 | 17 | for node in nodes 18 | current = node[0] 19 | 20 | # Make a list of distance to the east and south of the node 21 | @_distances.put current, 22 | east: convertNode node[1] 23 | south: convertNode node[2] 24 | 25 | # Assign to every node a tentative distance value: 26 | # set it to zero for our initial node and to infinity for all other nodes. 27 | @_tentativeDistances.put current, if current is initialNode then 0 else Infinity 28 | 29 | # Create a set of the unvisited nodes called the unvisited set 30 | # consisting of all the nodes except the initial node. 31 | @_unvisited.put current, true if current isnt initialNode 32 | 33 | @_initialNode = initialNode 34 | 35 | # Bind roles 36 | # Set the initial node as current 37 | @_rebindNode @_initialNode 38 | 39 | # For rebinding to next node that should be calculated. All binding operations 40 | # should be done within one method. 41 | rebind: (newNode, unvisitedSet, tentativeDistances, bestPath, distances) -> 42 | 43 | @bind(unvisitedSet).to('unvisitedSet') 44 | @bind(tentativeDistances).to('tentativeDistances') 45 | @bind(bestPath).to('bestPath') 46 | 47 | @bind(newNode).to('currentNode') 48 | @bind(newNode).to('currentIntersection') 49 | 50 | distance = distances.get(newNode) 51 | 52 | @bind(distance).to('edge') 53 | @bind(distance.east.node).to('eastNeighbor') 54 | @bind(distance.south.node).to('southNeighbor') 55 | 56 | # Simple rebinding when only changing node. 57 | _rebindNode: (newNode) -> 58 | @rebind newNode, @_unvisited, @_tentativeDistances, @_pathTo, @_distances 59 | 60 | # ===== Roles ===== 61 | 62 | tentativeDistances: 63 | _contract: ['get', 'put'] 64 | 65 | distanceTo: (node) -> 66 | @get node 67 | 68 | set: (node, distance) -> 69 | @put node, distance 70 | 71 | bestPath: 72 | _contract: ['get', 'put'] 73 | 74 | fromStartTo: (destination) -> 75 | output = [destination] 76 | output.unshift @get(output[0]) while output[0] != @context._initialNode 77 | output 78 | 79 | currentIntersection: 80 | unvisitedNeighbors: () -> 81 | output = [] 82 | output.push @context.eastNeighbor if @context.eastNeighbor? 83 | output.push @context.southNeighbor if @context.southNeighbor? 84 | output 85 | 86 | edge: 87 | _contract: ['east.distance', 'south.distance'] 88 | 89 | currentNode: 90 | tentativeDistance: () -> 91 | @context.tentativeDistances.get(@) 92 | 93 | edgeDistanceTo: (neighbor) -> 94 | return @context.eastNeighbor.eastNeighborDistance() if neighbor is @context.eastNeighbor 95 | return @context.southNeighbor.southNeighborDistance() if neighbor is @context.southNeighbor 96 | 97 | isBestPathTo: (neighbor) -> 98 | @context.bestPath.put(neighbor, @) 99 | 100 | eastNeighbor: 101 | eastNeighborDistance: () -> 102 | @context.edge.east.distance 103 | 104 | southNeighbor: 105 | southNeighborDistance: () -> 106 | @context.edge.south.distance 107 | 108 | unvisitedSet: 109 | _contract: ['remove', 'containsKey'] 110 | 111 | smallestTentativeDistanceNode: () -> 112 | outputDistance = Infinity 113 | output = null 114 | 115 | @context.tentativeDistances.each (node, distance) => 116 | return if not @containsKey(node) 117 | if output is null or distance < outputDistance 118 | outputDistance = distance 119 | output = node 120 | 121 | output 122 | 123 | # ===== Interactions ===== 124 | 125 | to: (destinationNode) -> 126 | 127 | # For the current node, consider all of its unvisited neighbors and calculate their 128 | # tentative distances. 129 | for neighbor in @currentIntersection.unvisitedNeighbors() 130 | distance = @currentNode.tentativeDistance() + @currentNode.edgeDistanceTo neighbor 131 | 132 | # If the distance is less than the previously recorded tentative distance of 133 | # the neighbor, overwrite that distance. 134 | if distance < @tentativeDistances.distanceTo neighbor 135 | @tentativeDistances.set neighbor, distance 136 | 137 | # Store this as the best path to the neighbor 138 | @currentNode.isBestPathTo neighbor 139 | 140 | # Mark the current node as visited and remove it from the unvisited set. 141 | @unvisitedSet.remove @currentNode 142 | 143 | # Finish if the destination node has been marked visited. 144 | return @bestPath.fromStartTo destinationNode if @currentNode is destinationNode 145 | 146 | # Set the unvisited node marked with the smallest tentative distance as the 147 | # next "current node" 148 | nextNode = @unvisitedSet.smallestTentativeDistanceNode() 149 | 150 | # Rebind the Context to the next node and calculate its distances. 151 | @_rebindNode nextNode 152 | @to destinationNode 153 | 154 | 155 | # ===== Test ============================== 156 | 157 | describe "Using Dijkstras algorithm", -> 158 | 159 | it "should find the shortest path from a to i", -> 160 | 161 | a = new String 'a' 162 | b = new String 'b' 163 | c = new String 'c' 164 | d = new String 'd' 165 | e = new String 'e' 166 | f = new String 'f' 167 | g = new String 'g' 168 | h = new String 'h' 169 | i = new String 'i' 170 | 171 | nodes = [ 172 | # Node, [East, dist], [South, dist] 173 | [a, [b,2], [d,1]] 174 | [b, [c,3], [e,2]] 175 | [c, null, [f,1]] 176 | [d, [e,1], [g,2]] 177 | [e, [f,1], null ] 178 | [f, null , [i,4]] 179 | [g, [h,1], null ] 180 | [h, [i,2], null ] 181 | [i, null , null ] 182 | ] 183 | 184 | ### 185 | a - 2 - b - 3 - c 186 | | | | 187 | 1 2 1 188 | | | | 189 | d - 1 - e - 1 - f 190 | | | 191 | 2 4 192 | | | 193 | g - 1 - h - 2 - i 194 | ### 195 | 196 | output = new ShortestManhattanPath(nodes, a).to(i) 197 | 198 | expect(output.join " -> ").toEqual("a -> d -> g -> h -> i") -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/dijkstra/Step1_TentDist.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package examples.dijkstra 3 | import scala.collection.mutable 4 | 5 | /* 6 | Some background info about how the type macro transformation of the Abstract Syntax Tree (AST) enables us to 7 | bind instance objects to roles: 8 | 9 | When our CalculateShortestPath extends Context there's not yet a Context type. There's no Context class or 10 | trait. In a pre-compilation phase (don't ask me how this goes!), the Scala compiler will look for a Context 11 | type and see that our type macro can generate one! The type macro has access to the AST of the child class 12 | (CalculateShortestPath) and is free to manipulate any part of it before returning a new and transformed 13 | Context type! 14 | 15 | We can therefore peek into the source code of CalculateShortestPath in advance and transform various sections 16 | of it to allow our instance objects to play Roles. 17 | 18 | Inside curly braces that matches the second arguments list (roleMethods: => Unit), we can supply our role 19 | methods as we do with our first role method assigntentativeDistances in the Role "tentativeDistances": 20 | 21 | role tentativeDistances { 22 | def assigntentativeDistances { 23 | ... 24 | } 25 | } 26 | 27 | After AST transformation, the role method assigntentativeDistances is prefixed with the role name 28 | tentativeDistances and the whole method is lifted to the Context namespace: 29 | 30 | private def tentativeDistances_assigntentativeDistances { 31 | ... 32 | } 33 | 34 | This is in line with what Risto Välimäki suggested here: 35 | https://groups.google.com/d/msg/object-composition/ulYGsCaJ0Mg/rF9wt1TV_MIJ 36 | 37 | So we are in no way modifying the instance object. We are not "injecting" methods into it or adding anything 38 | to the instance class. We are simply adding a role method to the Context, that we then call. This is useful 39 | when we also transform the calls to the transformed role methods, so that for instance: 40 | 41 | tentativeDistances.assigntentativeDistances 42 | 43 | is transformed into: 44 | 45 | tentativeDistances_assigntentativeDistances 46 | 47 | Since the transformed role methods are private there's no risk anyone can use those directly. We only need 48 | to think about the untransformed methods as we see them in code. 49 | 50 | After the type macro is done transforming the AST, the Scala compiler type checks the resulting AST as though 51 | it was normal code and we can therefore rely on normal type safety enforced by the compiler (to the extend we 52 | don't do crazy things with the type macro ;-) 53 | 54 | Before our IDE understands type macro transformations (the feature is still in an experimental stage), our 55 | role method calls will look like invalid code and we won't get any help to autoComplete role method names 56 | (like assigntentativeDistances). But should we happen to spell a role method name wrong, it won't compile 57 | later anyway, so we're still type safe. 58 | 59 | There's still much work to be done to analyze and implement how the type macro handles role method overloading 60 | of instance methods etc... 61 | 62 | Now to the first step of the Dijkstra algorithm 63 | 64 | 1. Assign a TENTATIVE DISTANCE of zero to our INITIAL INTERSECTION and infinity to all other intersections. 65 | 66 | We start out with one role: TentativeDistance which is mostly a "housekeeping" role that takes care of the 67 | initial assignment of tentative distances as described in step 1 of the algorithm. Notice that we use an 68 | object created inside the Context to play the TentativeDistance role. 69 | */ 70 | 71 | 72 | // Data ##################################################################### 73 | case class Intersection(name: Char) 74 | case class Block(x: Intersection, y: Intersection) 75 | 76 | // Test data ################################################################ 77 | case class ManhattanGrid() { 78 | val intersections = ('a' to 'i').map(Intersection(_)).toList 79 | val (a, b, c, d, e, f, g, h, i) = (intersections(0), intersections(1), intersections(2), intersections(3), intersections(4), intersections(5), intersections(6), intersections(7), intersections(8)) 80 | val nextDownTheStreet = Map(a -> b, b -> c, d -> e, e -> f, g -> h, h -> i) 81 | val nextAlongTheAvenue = Map(a -> d, b -> e, c -> f, d -> g, f -> i) 82 | val blockLengths = Map(Block(a, b) -> 2, Block(b, c) -> 3, Block(c, f) -> 1, Block(f, i) -> 4, Block(b, e) -> 2, Block(e, f) -> 1, Block(a, d) -> 1, Block(d, g) -> 2, Block(g, h) -> 1, Block(h, i) -> 2, Block(d, e) -> 1) 83 | 84 | // a - 2 - b - 3 - c 85 | // | | | 86 | // 1 2 1 87 | // | | | 88 | // d - 1 - e - 1 - f 89 | // | | 90 | // 2 4 91 | // | | 92 | // g - 1 - h - 2 - i 93 | } 94 | 95 | object Step1_TentDist extends App { 96 | 97 | // Context ################################################################## 98 | 99 | @context 100 | class CalculateShortestPath( 101 | City: ManhattanGrid, 102 | CurrentIntersection: Intersection, 103 | destination: Intersection 104 | ) { 105 | 106 | // Initialization of a data-holding role player in the Context 107 | private val tentativeDistances: mutable.HashMap[Intersection, Int] = mutable.HashMap[Intersection, Int]() 108 | // private val tentativeDistances = mutable.HashMap[Intersection, Int]() 109 | 110 | // Run 111 | tentativeDistances.assigntentativeDistances 112 | 113 | // Tentative distance of Intersection 'a' has been set to 0 and the rest to infinity: 114 | println("Tentative distances after :\n" + tentativeDistances.mkString("\n")) 115 | 116 | // Adding the first "housekeeping" role, given a role name after the data it "administrates" 117 | role tentativeDistances { 118 | 119 | // First role method defined 120 | def assigntentativeDistances { 121 | 122 | // STEP 1: 123 | 124 | // We access the instance methods by using the passed instance identifier: 125 | tentativeDistances.put(CurrentIntersection, 0) 126 | 127 | // We can access Context parameters directly: 128 | City.intersections.filter(_ != CurrentIntersection).foreach(tentativeDistances.put(_, Int.MaxValue / 4)) 129 | } 130 | } 131 | } 132 | 133 | // Execution ########################################################## 134 | val startingPoint = ManhattanGrid().a 135 | val destination = ManhattanGrid().i 136 | val shortestPath = new CalculateShortestPath(ManhattanGrid(), startingPoint, destination) 137 | } 138 | -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/dijkstra/Step5_6_Recurse.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package examples.dijkstra 3 | import collection.mutable 4 | 5 | /* 6 | Now comes the recursive part since we want to calculate tentative distances to neighbor Intersections until we 7 | can determine the shortest path to our destination. 8 | 9 | The data we had as fields in the Context before, we now change to constructor arguments with default values. On each 10 | recursion we pass those values on to the next nested Context object. We are not creating any new objects apart from the 11 | Context itself! 12 | 13 | Our recursion ends when we have either reached our destination or there are no more unvisited nodes 14 | 15 | From the city Map Model description: 16 | 17 | Continue this process of updating the NEIGHBORing intersections with the shortest distances, then marking the 18 | CURRENT INTERSECTION as visited and moving onto the closest UNVISITED INTERSECTION until you have marked the 19 | DESTINATION as visited. Once you have marked the DESTINATION as visited (as is the case with any visited 20 | intersection) you have determined the SHORTEST PATH to it, from the STARTING POINT, and can trace your way back, 21 | following the arrows in reverse. 22 | 23 | From the Manhattan street model algorithm: 24 | 25 | 5. If the DESTINATION intersection is no longer in DETOURS, then stop. The algorithm has finished. 26 | 27 | 6. Select the intersection in DETOURS with the smallest TENTATIVE DISTANCE, and set it as the new 28 | CURRENT INTERSECTION then go back to step 3. 29 | */ 30 | 31 | object Step5_6_Recurse extends App { 32 | 33 | // Context ################################################################## 34 | 35 | @context 36 | class Dijkstra( 37 | city: ManhattanGrid, 38 | currentIntersection: Intersection, 39 | destination: Intersection, 40 | tentativeDistances: mutable.HashMap[Intersection, Int] = mutable.HashMap[Intersection, Int](), 41 | detours: mutable.Set[Intersection] = mutable.Set[Intersection](), 42 | shortcuts: mutable.HashMap[Intersection, Intersection] = mutable.HashMap[Intersection, Intersection]() 43 | ) { 44 | 45 | // Since we recurse now, we only want those to be initialized the first time 46 | if (tentativeDistances.isEmpty) { 47 | tentativeDistances.initialize 48 | detours.initialize 49 | } 50 | 51 | // Main part of algorithm 52 | currentIntersection.calculateTentativeDistanceOfNeighbors 53 | 54 | // Try to run this version to watch identities change and how tentative distances calculates... 55 | println(s"\n==============================") 56 | println(s"This context is new: " + this.hashCode()) 57 | println(s"Intersection 'a' is the same all the way: " + city.a.hashCode()) 58 | println(s"\nCurrent $currentIntersection East ${city.eastNeighbor.getOrElse("...............")}") 59 | println(s"South ${city.southNeighbor.getOrElse("...............")}") 60 | println("\n" + tentativeDistances.mkString("\n")) 61 | 62 | // STEP 5 - If we haven't found a good route to destination yet, we need to check more intersections... 63 | if (detours.contains(destination)) { 64 | 65 | // STEP 6 - Select Intersection with smallest tentative distance remaining unconsidered intersections 66 | val nextCurrent = detours.withSmallestTentativeDistance 67 | 68 | // STEP 3 REPEATED HERE - Recurse until we reach destination 69 | new Dijkstra(city, nextCurrent, destination, tentativeDistances, detours, shortcuts) 70 | } 71 | 72 | // Context helper methods 73 | 74 | // Get the (reversed) shortest path from the starting point to destination 75 | def shortestPath = pathTo(destination).reverse 76 | 77 | // Recursively compound the shortcuts going from destination backwards to the starting point 78 | def pathTo(x: Intersection): List[Intersection] = { 79 | if (!shortcuts.contains(x)) 80 | List(x) 81 | else 82 | x :: pathTo(shortcuts(x)) 83 | } 84 | 85 | 86 | // Roles ################################################################## 87 | 88 | role tentativeDistances { 89 | def initialize() { 90 | tentativeDistances.put(currentIntersection, 0) 91 | city.intersections.filter(_ != currentIntersection).foreach(tentativeDistances.put(_, Int.MaxValue / 4)) 92 | } 93 | } 94 | 95 | role detours { 96 | def initialize() { detours ++= city.intersections } 97 | def withSmallestTentativeDistance = { detours.reduce((x, y) => if (tentativeDistances(x) < tentativeDistances(y)) x else y) } 98 | } 99 | 100 | role currentIntersection { 101 | def calculateTentativeDistanceOfNeighbors() { 102 | city.eastNeighbor.foreach(updateNeighborDistance(_)) 103 | city.southNeighbor.foreach(updateNeighborDistance(_)) 104 | detours.remove(currentIntersection) 105 | } 106 | def updateNeighborDistance(neighborIntersection: Intersection) { 107 | if (detours.contains(neighborIntersection)) { 108 | val newTentDistanceToNeighbor = currentDistance + lengthOfBlockTo(neighborIntersection) 109 | val currentTentDistToNeighbor = tentativeDistances(neighborIntersection) 110 | if (newTentDistanceToNeighbor < currentTentDistToNeighbor) { 111 | tentativeDistances.update(neighborIntersection, newTentDistanceToNeighbor) 112 | shortcuts.put(neighborIntersection, currentIntersection) 113 | } 114 | } 115 | } 116 | def currentDistance = tentativeDistances(currentIntersection) 117 | def lengthOfBlockTo(neighbor: Intersection) = city.distanceBetween(currentIntersection, neighbor) 118 | } 119 | 120 | role city { 121 | def distanceBetween(from: Intersection, to: Intersection) = city.blockLengths(Block(from, to)) 122 | def eastNeighbor = city.nextDownTheStreet.get(currentIntersection) 123 | def southNeighbor = city.nextAlongTheAvenue.get(currentIntersection) 124 | } 125 | } 126 | 127 | // Data ##################################################################### 128 | case class Intersection(name: Char) 129 | case class Block(x: Intersection, y: Intersection) 130 | 131 | // Test data ################################################################ 132 | case class ManhattanGrid() { 133 | val intersections = ('a' to 'i').map(Intersection(_)).toList 134 | val (a, b, c, d, e, f, g, h, i) = (intersections(0), intersections(1), intersections(2), intersections(3), intersections(4), intersections(5), intersections(6), intersections(7), intersections(8)) 135 | val nextDownTheStreet = Map(a -> b, b -> c, d -> e, e -> f, g -> h, h -> i) 136 | val nextAlongTheAvenue = Map(a -> d, b -> e, c -> f, d -> g, f -> i) 137 | val blockLengths = Map(Block(a, b) -> 2, Block(b, c) -> 3, Block(c, f) -> 1, Block(f, i) -> 4, Block(b, e) -> 2, Block(e, f) -> 1, Block(a, d) -> 1, Block(d, g) -> 2, Block(g, h) -> 1, Block(h, i) -> 2, Block(d, e) -> 1) 138 | 139 | // a - 2 - b - 3 - c 140 | // | | | 141 | // 1 2 1 142 | // | | | 143 | // d - 1 - e - 1 - f 144 | // | | 145 | // 2 4 146 | // | | 147 | // g - 1 - h - 2 - i 148 | } 149 | 150 | val startingPoint = ManhattanGrid().a 151 | val destination = ManhattanGrid().i 152 | val shortestPath = new Dijkstra(ManhattanGrid(), startingPoint, destination).shortestPath 153 | println(shortestPath.map(_.name).mkString(" -> ")) 154 | // a -> d -> g -> h -> i 155 | } -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/dijkstra/other_implementations_2013_01_18/AndreasSoderlund_Coffee1: -------------------------------------------------------------------------------- 1 | (function() { 2 | var __hasProp = {}.hasOwnProperty, 3 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 4 | 5 | describe("Ivento.Dci.Examples.Dijkstra", function() { 6 | var ShortestManhattanPath; 7 | ShortestManhattanPath = (function(_super) { 8 | 9 | __extends(ShortestManhattanPath, _super); 10 | 11 | ShortestManhattanPath.prototype._tentativeDistances = new Hashtable(); 12 | 13 | ShortestManhattanPath.prototype._distances = new Hashtable(); 14 | 15 | ShortestManhattanPath.prototype._unvisited = new Hashtable(); 16 | 17 | ShortestManhattanPath.prototype._pathTo = new Hashtable(); 18 | 19 | ShortestManhattanPath.prototype._initialNode = null; 20 | 21 | function ShortestManhattanPath(nodes, initialNode) { 22 | var convertNode, current, node, _i, _len; 23 | convertNode = function(n) { 24 | return { 25 | node: n != null ? n[0] : null, 26 | distance: n != null ? n[1] : null 27 | }; 28 | }; 29 | for (_i = 0, _len = nodes.length; _i < _len; _i++) { 30 | node = nodes[_i]; 31 | current = node[0]; 32 | this._distances.put(current, { 33 | east: convertNode(node[1]), 34 | south: convertNode(node[2]) 35 | }); 36 | this._tentativeDistances.put(current, current === initialNode ? 0 : Infinity); 37 | if (current !== initialNode) { 38 | this._unvisited.put(current, true); 39 | } 40 | } 41 | this._initialNode = initialNode; 42 | this._rebindNode(this._initialNode); 43 | } 44 | 45 | ShortestManhattanPath.prototype.rebind = function(newNode, unvisitedSet, tentativeDistances, bestPath, distances) { 46 | var distance; 47 | this.bind(unvisitedSet).to('unvisitedSet'); 48 | this.bind(tentativeDistances).to('tentativeDistances'); 49 | this.bind(bestPath).to('bestPath'); 50 | this.bind(newNode).to('currentNode'); 51 | this.bind(newNode).to('currentIntersection'); 52 | distance = distances.get(newNode); 53 | this.bind(distance).to('edge'); 54 | this.bind(distance.east.node).to('eastNeighbor'); 55 | return this.bind(distance.south.node).to('southNeighbor'); 56 | }; 57 | 58 | ShortestManhattanPath.prototype._rebindNode = function(newNode) { 59 | return this.rebind(newNode, this._unvisited, this._tentativeDistances, this._pathTo, this._distances); 60 | }; 61 | 62 | ShortestManhattanPath.prototype.tentativeDistances = { 63 | _contract: ['get', 'put'], 64 | distanceTo: function(node) { 65 | return this.get(node); 66 | }, 67 | set: function(node, distance) { 68 | return this.put(node, distance); 69 | } 70 | }; 71 | 72 | ShortestManhattanPath.prototype.bestPath = { 73 | _contract: ['get', 'put'], 74 | fromStartTo: function(destination) { 75 | var output; 76 | output = [destination]; 77 | while (output[0] !== this.context._initialNode) { 78 | output.unshift(this.get(output[0])); 79 | } 80 | return output; 81 | } 82 | }; 83 | 84 | ShortestManhattanPath.prototype.currentIntersection = { 85 | unvisitedNeighbors: function() { 86 | var output; 87 | output = []; 88 | if (this.context.eastNeighbor != null) { 89 | output.push(this.context.eastNeighbor); 90 | } 91 | if (this.context.southNeighbor != null) { 92 | output.push(this.context.southNeighbor); 93 | } 94 | return output; 95 | } 96 | }; 97 | 98 | ShortestManhattanPath.prototype.edge = { 99 | _contract: ['east.distance', 'south.distance'] 100 | }; 101 | 102 | ShortestManhattanPath.prototype.currentNode = { 103 | tentativeDistance: function() { 104 | return this.context.tentativeDistances.get(this); 105 | }, 106 | edgeDistanceTo: function(neighbor) { 107 | if (neighbor === this.context.eastNeighbor) { 108 | return this.context.eastNeighbor.eastNeighborDistance(); 109 | } 110 | if (neighbor === this.context.southNeighbor) { 111 | return this.context.southNeighbor.southNeighborDistance(); 112 | } 113 | }, 114 | isBestPathTo: function(neighbor) { 115 | return this.context.bestPath.put(neighbor, this); 116 | } 117 | }; 118 | 119 | ShortestManhattanPath.prototype.eastNeighbor = { 120 | eastNeighborDistance: function() { 121 | return this.context.edge.east.distance; 122 | } 123 | }; 124 | 125 | ShortestManhattanPath.prototype.southNeighbor = { 126 | southNeighborDistance: function() { 127 | return this.context.edge.south.distance; 128 | } 129 | }; 130 | 131 | ShortestManhattanPath.prototype.unvisitedSet = { 132 | _contract: ['remove', 'containsKey'], 133 | smallestTentativeDistanceNode: function() { 134 | var output, outputDistance, 135 | _this = this; 136 | outputDistance = Infinity; 137 | output = null; 138 | this.context.tentativeDistances.each(function(node, distance) { 139 | if (!_this.containsKey(node)) { 140 | return; 141 | } 142 | if (output === null || distance < outputDistance) { 143 | outputDistance = distance; 144 | return output = node; 145 | } 146 | }); 147 | return output; 148 | } 149 | }; 150 | 151 | ShortestManhattanPath.prototype.to = function(destinationNode) { 152 | var distance, neighbor, nextNode, _i, _len, _ref; 153 | _ref = this.currentIntersection.unvisitedNeighbors(); 154 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 155 | neighbor = _ref[_i]; 156 | distance = this.currentNode.tentativeDistance() + this.currentNode.edgeDistanceTo(neighbor); 157 | if (distance < this.tentativeDistances.distanceTo(neighbor)) { 158 | this.tentativeDistances.set(neighbor, distance); 159 | this.currentNode.isBestPathTo(neighbor); 160 | } 161 | } 162 | this.unvisitedSet.remove(this.currentNode); 163 | if (this.currentNode === destinationNode) { 164 | return this.bestPath.fromStartTo(destinationNode); 165 | } 166 | nextNode = this.unvisitedSet.smallestTentativeDistanceNode(); 167 | this._rebindNode(nextNode); 168 | return this.to(destinationNode); 169 | }; 170 | 171 | return ShortestManhattanPath; 172 | 173 | })(Ivento.Dci.Context); 174 | return describe("Using Dijkstras algorithm", function() { 175 | return it("should find the shortest path from a to i", function() { 176 | var a, b, c, d, e, f, g, h, i, nodes, output; 177 | a = new String('a'); 178 | b = new String('b'); 179 | c = new String('c'); 180 | d = new String('d'); 181 | e = new String('e'); 182 | f = new String('f'); 183 | g = new String('g'); 184 | h = new String('h'); 185 | i = new String('i'); 186 | nodes = [[a, [b, 2], [d, 1]], [b, [c, 3], [e, 2]], [c, null, [f, 1]], [d, [e, 1], [g, 2]], [e, [f, 1], null], [f, null, [i, 4]], [g, [h, 1], null], [h, [i, 2], null], [i, null, null]]; 187 | /* 188 | a - 2 - b - 3 - c 189 | | | | 190 | 1 2 1 191 | | | | 192 | d - 1 - e - 1 - f 193 | | | 194 | 2 4 195 | | | 196 | g - 1 - h - 2 - i 197 | */ 198 | 199 | output = new ShortestManhattanPath(nodes, a).to(i); 200 | return expect(output.join(" -> ")).toEqual("a -> d -> g -> h -> i"); 201 | }); 202 | }); 203 | }); 204 | 205 | }).call(this); -------------------------------------------------------------------------------- /coretest/src/test/Scala/scaladci/semantics/MethodResolution.scala: -------------------------------------------------------------------------------- 1 | //package scaladci 2 | //package semantics 3 | //import scala.language.reflectiveCalls 4 | //import scaladci.util._ 5 | // 6 | // 7 | //class MethodResolution extends DCIspecification { 8 | // 9 | // class Obj { 10 | // def foo = "FOO" 11 | // def bar = "BAR" 12 | // } 13 | // 14 | // // "In various languages" >> { 15 | // // 16 | // // class Obj { 17 | // // def foo = "FOO" 18 | // // def bar = "BAR" 19 | // // } 20 | // // 21 | // // @context 22 | // // case class SelfSemanticsContext(roleName: Obj) { 23 | // // 24 | // // def bar = "CTX" 25 | // // def resolve = roleName.foo 26 | // // 27 | // // role a { 28 | // // def foo = bar + self.bar + this.bar + roleName.bar 29 | // // def bar = "bar" 30 | // // } 31 | // // } 32 | // // SelfSemanticsContext(new Obj).resolve === "barBARBARbar" 33 | // // } 34 | // 35 | // "Role method takes precedence over instance method" >> { 36 | // 37 | // @context 38 | // case class Context1(a: Obj) { 39 | // 40 | // def resolve = a.foo 41 | // 42 | // role a { 43 | // def foo = "foo" 44 | // } 45 | // } 46 | // Context1(new Obj).resolve === "foo" 47 | // 48 | // 49 | // // https://groups.google.com/d/msg/object-composition/MhrHIn9LaNs/ShkSPbJ-rlsJ 50 | // 51 | // @context 52 | // case class Context2(a: Obj, b: Obj) { 53 | // 54 | // def resolve = a.foo + b.foo 55 | // 56 | // role a { 57 | // def foo = "foo" 58 | // } 59 | // role b {} 60 | // } 61 | // Context2(new Obj, new Obj).resolve === "fooFOO" 62 | // 63 | // 64 | // // Is this example correctly interpreted, Rune? 65 | // @context 66 | // case class Context3(a: Obj) { 67 | // 68 | // def resolve = a.foo 69 | // 70 | // role a { 71 | // def foo = self.bar 72 | // } 73 | // } 74 | // Context3(new Obj).resolve === "BAR" 75 | // 76 | // 77 | // @context 78 | // case class Context4(a: Obj) { 79 | // 80 | // def resolve = a.foo + a.bar 81 | // 82 | // role a { 83 | // def foo = self.bar 84 | // def bar = "bar" 85 | // } 86 | // } 87 | // Context4(new Obj).resolve === "barbar" 88 | // 89 | // 90 | // @context 91 | // case class ContextOverriding(a: Obj) { 92 | // 93 | // def bar = "CTX" 94 | // def resolve = a.foo 95 | // 96 | // role a { 97 | // def foo = bar + self.bar + this.bar + a.bar 98 | // def bar = "bar" 99 | // } 100 | // } 101 | // ContextOverriding(new Obj).resolve === "barbarbarbar" 102 | // 103 | // 104 | // @context 105 | // case class ContextNotOverriding(a: Obj) { 106 | // 107 | // def bar = "CTX" 108 | // def resolve = a.foo 109 | // 110 | // role a { 111 | // def foo = bar + self.bar + this.bar + a.bar 112 | // } 113 | // } 114 | // ContextNotOverriding(new Obj).resolve === "CTXBARBARBAR" 115 | // 116 | // 117 | // //@context 118 | // //case class ContextNotOverriding2(a: Obj) { 119 | // // 120 | // // def resolve = a.foo 121 | // // 122 | // // role a { 123 | // // def foo = bar 124 | // // } 125 | // //} 126 | // //ContextNotOverriding2(new Obj).resolve === "BAR" 127 | // 128 | // @context 129 | // class ContextWithRoleClass { 130 | // 131 | // def bar = "CTX" 132 | // def resolve = new A().foo 133 | // 134 | // // Role as class 135 | // class A extends Obj { 136 | // override def foo = bar + this.bar 137 | // override def bar = "bar" 138 | // } 139 | // } 140 | // new ContextWithRoleClass().resolve === "barbar" 141 | // 142 | // 143 | // @context 144 | // case class Context6(a: Obj, b: Obj) { 145 | // 146 | // def resolve = a.foo + b.bar 147 | // 148 | // role a { 149 | // def foo = self.bar 150 | // } 151 | // role b { 152 | // def bar = "bar" 153 | // } 154 | // } 155 | // Context6(new Obj, new Obj).resolve === "BARbar" 156 | // 157 | // 158 | // @context 159 | // case class Context7(a: Obj, b: Obj) { 160 | // 161 | // def resolve = a.foo 162 | // 163 | // role a { 164 | // def foo = self.bar + b.bar 165 | // } 166 | // role b { 167 | // def bar = "bar" 168 | // } 169 | // } 170 | // Context7(new Obj, new Obj).resolve === "BARbar" 171 | // 172 | // // Todo: are these tests shredding sufficient light on method resolution?! 173 | // } 174 | // 175 | // 176 | // "With name clashes in Context (??)" >> { 177 | // 178 | // // https://groups.google.com/d/msg/object-composition/QfvHXzuP2wU/M622DO1y_JYJ 179 | // // https://groups.google.com/d/msg/object-composition/QfvHXzuP2wU/PIkAZdcWp5QJ 180 | // // https://gist.github.com/runefs/4338821 181 | // 182 | // class A { 183 | // def foo = "a" 184 | // } 185 | // 186 | // type roleContract = {def foo: String} 187 | // 188 | // { 189 | // @context 190 | // case class C(var y: roleContract, var z: roleContract) { 191 | // 192 | // role y {} 193 | // 194 | // role z { 195 | // def foo = "z" 196 | // } 197 | // 198 | // def print() { println(s"foo of y ${y.foo}\nfoo of z ${z.foo}") } 199 | // 200 | // def foo = "context" 201 | // 202 | // def initialize(someT: roleContract, someK: roleContract) { 203 | // y = someT 204 | // z = someK 205 | // } 206 | // def rebind(a: roleContract) { 207 | // initialize(a, this) 208 | // print() 209 | // initialize(this, a) 210 | // } 211 | // } 212 | // 213 | // def doIt(someT: roleContract, someK: roleContract) { 214 | // val c = new C(someT, someK) 215 | // c.print() 216 | // c.rebind(someT) 217 | // c.print() 218 | // } 219 | // 220 | // val a = new A 221 | // doIt(a, a) 222 | // 223 | // /* Prints as expected: 224 | // 225 | // foo of y a 226 | // foo of z z 227 | // foo of y a 228 | // foo of z z 229 | // foo of y context 230 | // foo of z z 231 | // 232 | // */ 233 | // success 234 | // } 235 | // } 236 | // 237 | // 238 | // "Egon Elbre's Torture Test, the Tournament" >> { 239 | // 240 | // // https://groups.google.com/d/msg/object-composition/AsvEI7iJSDs/_HX5S4Ep9Q4J 241 | // 242 | // case class Player(name: String, msg: String) { 243 | // def say() { 244 | // println(s"[$name] $msg") 245 | // } 246 | // } 247 | // 248 | // def doInterview(player: Player) { 249 | // println("[Interviewer] Hello!") 250 | // player.say() 251 | // } 252 | // 253 | // def callback(fn: () => Unit) { 254 | // fn() 255 | // } 256 | // 257 | // { 258 | // @context 259 | // case class Battle(id: Int, bear: Player, lion: Player) { 260 | // 261 | // def start() { 262 | // println(id + " battle commencing:") 263 | // bear.fight() 264 | // } 265 | // def interview() { 266 | // doInterview(bear) 267 | // doInterview(lion) 268 | // } 269 | // 270 | // role bear { 271 | // def say() { 272 | // println(id + " [" + self.name + "] Grrrr.....") 273 | // } 274 | // def fight() { 275 | // bear.say() 276 | // lion.fight() 277 | // } 278 | // } 279 | // 280 | // role lion { 281 | // def say() { 282 | // println(id + " [" + self.name + "] Meow.....") 283 | // } 284 | // def fight() { 285 | // callback(lion.say) 286 | // } 287 | // } 288 | // } 289 | // 290 | // val human = Player("Jack", "says Hello!") 291 | // val cpu = Player("Cyborg", "bleeps Hello!") 292 | // 293 | // val b1 = Battle(1, human, cpu) 294 | // val b2 = Battle(2, cpu, human) 295 | // val b3 = Battle(3, cpu, cpu) 296 | // 297 | // b1.start() 298 | // b2.start() 299 | // b3.start() 300 | // 301 | // b1.interview() 302 | // 303 | // /* Prints as expected: 304 | // 305 | // 1 battle commencing: 306 | // 1 [Jack] Grrrr..... 307 | // 1 [Cyborg] Meow..... 308 | // 2 battle commencing: 309 | // 2 [Cyborg] Grrrr..... 310 | // 2 [Jack] Meow..... 311 | // 3 battle commencing: 312 | // 3 [Cyborg] Grrrr..... 313 | // 3 [Cyborg] Meow..... 314 | // [Interviewer] Hello! 315 | // [Jack] says Hello! 316 | // [Interviewer] Hello! 317 | // [Cyborg] bleeps Hello! 318 | // 319 | // */ 320 | // success 321 | // } 322 | // } 323 | //} 324 | -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/ShoppingCart4c.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package examples 3 | import org.specs2.mutable.Specification 4 | 5 | /* 6 | Shopping cart example (version 4c) - no Roles!! 7 | 8 | No role methods, no interaction between objects (except with the cart). 9 | 10 | No DCI anymore - only procedural algorithms in the Context which could be 11 | any class now (notice it's not extending Context anymore). 12 | 13 | Could we call it a Service now? If so, the specifications/requirements 14 | below doesn't tell a story any more. 15 | 16 | See discussion at: 17 | https://groups.google.com/forum/?fromgroups=#!topic/object-composition/JJiLWBsZWu0 18 | 19 | =========================================================================== 20 | shopping cart Service (disclaimer: don't know how to specify a Service...) 21 | 22 | Specifications: 23 | --------------------------------------------------------------------------- 24 | Add product to cart: 25 | - reserve product in warehouse 26 | - add item to order 27 | - show updated contents of cart to customer 28 | 29 | Review order: 30 | - present cart with current items/prices to customer 31 | 32 | Pay order: 33 | - confirm sufficient funds are available 34 | - initiate transfer of funds 35 | - confirm purchase to customer 36 | 37 | Remove item from cart: 38 | - show updated cart to customer 39 | 40 | Requirements 41 | --------------------------------------------------------------------------- 42 | Product out of stock: 43 | - don't add item to cart 44 | - inform customer of shortage 45 | 46 | customer has gold membership: 47 | - calculate discount on products 48 | 49 | customer has insufficient funds to pay Order: 50 | - inform customer of insufficient funds on credit card 51 | =========================================================================== 52 | */ 53 | 54 | class ShoppingCart4c extends Specification { 55 | import ShoppingCartModel._ 56 | 57 | { 58 | // No DCI Context any longer - is it a "Service" now?? 59 | class PlaceOrder(shop: Company, customer: Person) { 60 | // No Role any longer 61 | private val cart = Order(customer) 62 | 63 | // Service methods 64 | def customerMarksdesiredProductInshop(productId: Int): Option[Product] = { 65 | if (!shop.stock.isDefinedAt(productId)) 66 | return None 67 | val product = shop.stock(productId) 68 | 69 | // get price with discount if any 70 | val customerIsGoldMember = shop.goldMembers.contains(customer) 71 | val goldMemberReduction = 0.5 72 | val discountFactor = if (customerIsGoldMember) goldMemberReduction else 1 73 | val discountedPrice = (product.price * discountFactor).toInt 74 | 75 | val desiredProduct = product.copy(price = discountedPrice) 76 | cart.items.put(productId, desiredProduct) 77 | Some(desiredProduct) 78 | } 79 | 80 | def customerRequestsToReviewOrder: Seq[(Int, Product)] = { 81 | cart.items.toIndexedSeq.sortBy(_._1) 82 | } 83 | 84 | def customerPaysOrder: Boolean = { 85 | val orderTotal = cart.items.map(_._2.price).sum 86 | if (orderTotal > customer.cash) 87 | return false 88 | 89 | customer.cash -= orderTotal 90 | shop.cash += orderTotal 91 | 92 | customer.owns ++= cart.items 93 | cart.items foreach (shop.stock remove _._1) 94 | true 95 | } 96 | 97 | def customerRemovesProductFromcart(productId: Int): Option[Product] = { 98 | if (!cart.items.isDefinedAt(productId)) 99 | return None 100 | cart.items.remove(productId) 101 | } 102 | 103 | // (no role implementations!) 104 | } 105 | 106 | 107 | // Test various scenarios. 108 | // (copy and paste of ShoppingCart4a tests) 109 | 110 | "Main success scenario" in new ShoppingCart { 111 | 112 | // Initial status (same for all tests...) 113 | shop.stock === Map(tires, wax, bmw) 114 | shop.cash === 100000 115 | customer.cash === 20000 116 | customer.owns === Map() 117 | 118 | val order = new PlaceOrder(shop, customer) 119 | 120 | // customer wants wax and tires 121 | order.customerMarksdesiredProductInshop(p1) 122 | order.customerMarksdesiredProductInshop(p2) 123 | 124 | order.customerRequestsToReviewOrder === Seq(wax, tires) 125 | 126 | val orderCompleted = order.customerPaysOrder === true 127 | 128 | shop.stock === Map(bmw) 129 | shop.cash === 100000 + 40 + 600 130 | customer.cash === 20000 - 40 - 600 131 | customer.owns === Map(tires, wax) 132 | } 133 | 134 | "Product out of stock" in new ShoppingCart { 135 | 136 | // Wax out of stock 137 | shop.stock.remove(p1) 138 | shop.stock === Map(tires, bmw) 139 | 140 | val order = new PlaceOrder(shop, customer) 141 | 142 | // customer wants wax 143 | val itemAdded = order.customerMarksdesiredProductInshop(p1) === None 144 | order.customerRequestsToReviewOrder === Seq() 145 | 146 | order.customerMarksdesiredProductInshop(p2) 147 | 148 | val orderCompleted = order.customerPaysOrder === true 149 | 150 | shop.stock === Map(bmw) 151 | shop.cash === 100000 + 600 152 | customer.cash === 20000 - 600 153 | customer.owns === Map(tires) 154 | } 155 | 156 | "customer has gold membership" in new ShoppingCart { 157 | 158 | // customer is gold member 159 | shop.goldMembers.add(customer) 160 | shop.goldMembers.contains(customer) === true 161 | 162 | val order = new PlaceOrder(shop, customer) 163 | 164 | order.customerMarksdesiredProductInshop(p1) 165 | 166 | val discountedWax = 1 -> Product("Wax", (40 * 0.5).toInt) 167 | order.customerRequestsToReviewOrder === Seq(discountedWax) 168 | 169 | val orderCompleted = order.customerPaysOrder === true 170 | 171 | shop.stock === Map(tires, bmw) 172 | shop.cash === 100000 + 20 173 | customer.cash === 20000 - 20 174 | customer.owns === Map(discountedWax) 175 | } 176 | 177 | "customer has too low credit" in new ShoppingCart { 178 | 179 | val order = new PlaceOrder(shop, customer) 180 | 181 | // customer wants a BMW 182 | val itemAdded = order.customerMarksdesiredProductInshop(p3) 183 | 184 | // Any product is added - shop doesn't yet know if customer can afford it 185 | itemAdded === Some(bmw._2) 186 | order.customerRequestsToReviewOrder === Seq(bmw) 187 | 188 | // customer tries to pay order 189 | val paymentStatus = order.customerPaysOrder 190 | 191 | // shop informs customer of too low credit 192 | paymentStatus === false 193 | 194 | // customer removes unaffordable BMW from cart 195 | order.customerRemovesProductFromcart(p3) 196 | 197 | // customer aborts shopping and no purchases are made 198 | shop.stock === Map(tires, wax, bmw) 199 | shop.cash === 100000 200 | customer.cash === 20000 201 | customer.owns === Map() 202 | } 203 | 204 | "All deviations in play" in new ShoppingCart { 205 | 206 | // Tires out of stock 207 | shop.stock.remove(p2) 208 | shop.stock === Map(wax, bmw) 209 | 210 | // We have a gold member 211 | shop.goldMembers.add(customer) 212 | 213 | val order = new PlaceOrder(shop, customer) 214 | 215 | // Let's get some tires 216 | val tiresItemAdded = order.customerMarksdesiredProductInshop(p2) 217 | 218 | // Product out of stock! 219 | shop.stock.contains(p2) === false 220 | 221 | // Nothing added to order yet 222 | tiresItemAdded === None 223 | order.customerRequestsToReviewOrder === Seq() 224 | 225 | // Let's buy the BMW instead. As a gold member that should be possible! 226 | val bmwItemAdded = order.customerMarksdesiredProductInshop(p3) 227 | 228 | // Discounted BMW is added to order 229 | val discountedBMW = Product("BMW", (50000 * 0.5).toInt) 230 | bmwItemAdded === Some(discountedBMW) 231 | order.customerRequestsToReviewOrder === Seq(p3 -> discountedBMW) 232 | 233 | // Ouch! We couldn't afford it. 234 | val paymentAttempt1 = order.customerPaysOrder === false 235 | 236 | // It's still 5000 too much for us, even with the membership discount 237 | discountedBMW.price - customer.cash === 5000 238 | 239 | // Ok, no new car today 240 | order.customerRemovesProductFromcart(p3) 241 | 242 | // Order is back to empty 243 | order.customerRequestsToReviewOrder === Seq() 244 | 245 | // Let's get some wax anyway... 246 | val waxItemAdded = order.customerMarksdesiredProductInshop(p1) 247 | 248 | // Did we get our membership discount on this one? 249 | val discountedWax = Product("Wax", (40 * 0.5).toInt) 250 | waxItemAdded === Some(discountedWax) 251 | 252 | // Now we can afford it! 253 | val paymentAttempt2 = order.customerPaysOrder === true 254 | 255 | // Not much shopping done Today. At least we got some cheap wax. 256 | shop.stock === Map(bmw) 257 | shop.cash === 100000 + 20 258 | customer.cash === 20000 - 20 259 | customer.owns === Map(p1 -> discountedWax) 260 | } 261 | } 262 | } -------------------------------------------------------------------------------- /coretest/src/test/Scala/scaladci/semantics/RoleContract.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package semantics 3 | import scala.language.reflectiveCalls 4 | import scaladci.util._ 5 | 6 | /* 7 | Role contracts (or "Role-object contracts") 8 | 9 | For an object to play a Role it needs to satisfy a Role contract that defines 10 | what methods the Role expect it to have before they can merge to a Role player. 11 | We call those methods in the object class "instance methods". 12 | 13 | When our Role players start to interact at runtime they will call these methods 14 | on each other. To maximise our ability to reason about the expected outcome of 15 | those interactions we therefore need to know what the instance methods do too 16 | to various extends depending on their nature (we don't need to lookup what 17 | `toString` does for instance). 18 | 19 | 20 | Types - precise but not a silver bullet always... 21 | 22 | One way to know the object's methods is of course to know the objects type. 23 | We can look at the method definition in the defining class (the objects type) 24 | and see what it does. 25 | 26 | But what if we get an object that is a subtype of the type we have defined 27 | in our Role contract? The method could have been overriden and now do nasty 28 | things that would give us nasty surprises once we run our program. 29 | 30 | Fortunately we can enforce an even more specific type equalling the expected 31 | type (avoiding subclasses thereof). But even that specific type might in turn 32 | depend on other classes that could have been subclasses and so on. That seems 33 | to suggest that we will never be able to reason with 100% certainty about 34 | the outcome of our program at runtime - unless we define all object classes 35 | ourselves. 36 | 37 | 38 | Structural types (duck typing) - flexible but unpredictable... 39 | 40 | When a Role is more flexible and wants to allow a broader set of objects 41 | to be able to play it, then a structural type (a la duck-typing) will come 42 | in handy. The role contract would then define what method signature(s) a 43 | Role expects. The backside of that coin is of course that we would know 44 | much less about what those methods do. 45 | 46 | 47 | Levels of predictability: 48 | 49 | 1) One and only type (enforced with type equality) 50 | 2) Type (could be any subtype too) 51 | 3) Structural type (duck-typing) 52 | 53 | */ 54 | 55 | class RoleContract extends DCIspecification { 56 | 57 | 58 | /** ****** Type **************************************************************/ 59 | 60 | 61 | "Can be a type" >> { 62 | 63 | @context 64 | case class Context(myRole: Data) { 65 | 66 | def trigger = myRole.foo 67 | 68 | role myRole { 69 | def foo = self.number // We know type `Data` (and that it has a number method) 70 | } 71 | } 72 | Context(Data(42)).trigger === 42 73 | } 74 | 75 | 76 | "Can naïvely trust a type" >> { 77 | 78 | class Data(i: Int) { 79 | def number = i 80 | } 81 | case class DodgySubClass(i: Int) extends Data(i) { 82 | override def number = -666 83 | } 84 | 85 | @context 86 | case class Context(myRole: Data) { 87 | // <- We feel falsely safe :-( 88 | 89 | def trigger = myRole.foo 90 | 91 | role myRole { 92 | def foo = self.number 93 | } 94 | } 95 | val devilInDisguise = DodgySubClass(42) 96 | Context(devilInDisguise).trigger === -666 // Not 42 !!! Auch 97 | } 98 | 99 | 100 | "Can rely more safely on a specific type" >> { 101 | 102 | class ExpectedType(i: Int) { 103 | def number = i 104 | } 105 | class DodgySubClass(i: Int) extends ExpectedType(i) { 106 | override def number = -666 107 | } 108 | 109 | { 110 | // Implicit type evidence enforces a strict type (no subtype allowed) 111 | @context 112 | case class Context[T](myRole: T)(implicit val ev: T =:= ExpectedType) { 113 | 114 | def trigger = myRole.foo 115 | 116 | role myRole { 117 | def foo = self.number 118 | } 119 | } 120 | 121 | // A dodgy subclass can't sneak in 122 | expectCompileError( 123 | """ 124 | val devilInDisguise = new DodgySubClass(42) 125 | Context(devilInDisguise).trigger === 42 126 | """, 127 | "Cannot prove that DodgySubClass =:= ExpectedType") 128 | 129 | // Only objects of our expected type (and no subtype) can be used: 130 | val expectedObject = new ExpectedType(42) 131 | Context(expectedObject).trigger === 42 132 | } 133 | } 134 | 135 | /** ****** Structural Type (duck typing) **************************************************/ 136 | 137 | 138 | "Can be a structural type (duck typing)" >> { 139 | 140 | @context 141 | case class Context(myRole: {def number: Int}) { 142 | 143 | def trigger = myRole.foo 144 | 145 | role myRole { 146 | // We know that the instance (of unknown type) has a `number` method returning Int 147 | def foo = self.number 148 | } 149 | } 150 | Context(Data(42)).trigger === 42 151 | 152 | 153 | case class NastyData(i: Int) { 154 | def number = { 155 | println("Firing missiles...") 156 | i 157 | } 158 | } 159 | 160 | @context 161 | case class NaiveContext(myRole: {def number: Int}) { 162 | 163 | def trigger = myRole.foo 164 | 165 | role myRole { 166 | // We know that the instance (of unknown type) has a `number` method returning Int 167 | // - but we don't know that it also fire off missiles!!! 168 | def foo = self.number 169 | } 170 | } 171 | NaiveContext(NastyData(42)).trigger === 42 // + world war III 172 | } 173 | 174 | 175 | "Can be a type alias for a structural type (duck typing)" >> { 176 | 177 | type WithNumberMethod = {def number: Int} 178 | 179 | { 180 | @context 181 | case class Context(myRole: WithNumberMethod) { 182 | 183 | def trigger = myRole.foo 184 | 185 | role myRole { 186 | // We know that the instance (of unknown type) has a `number` method returning Int 187 | def foo = self.number 188 | } 189 | } 190 | Context(Data(42)).trigger === 42 191 | } 192 | 193 | case class NastyData(i: Int) { 194 | def number = { 195 | println("Firing missiles...") 196 | i 197 | } 198 | } 199 | 200 | { 201 | @context 202 | case class NaiveContext(myRole: {def number: Int}) { 203 | 204 | def trigger = myRole.foo 205 | 206 | role myRole { 207 | // We know that the instance (of unknown type) has a `number` method returning Int 208 | // - but we don't know that it also fire off missiles!!! 209 | def foo = self.number 210 | } 211 | } 212 | NaiveContext(NastyData(42)).trigger === 42 // + world war III 213 | } 214 | } 215 | 216 | "Can require several instance methods with structural types (duck typing)" >> { 217 | 218 | case class Kid() { 219 | def age = 16 220 | def name = "John" 221 | } 222 | case class Adult() { 223 | def age = 32 224 | def name = "Alex" 225 | } 226 | 227 | @context 228 | case class Disco(visitor: { 229 | def age: Int 230 | def name: String}) { 231 | 232 | def letMeDance = visitor.canIGetIn 233 | 234 | role visitor { 235 | def canIGetIn = { 236 | if (self.age < 18) 237 | s"Sorry, ${self.name}, you're only ${self.age} years old. I can't let you in." 238 | else 239 | s"Welcome, ${self.name}. Shall I take your coat?" 240 | } 241 | } 242 | } 243 | Disco(Kid()).letMeDance === "Sorry, John, you're only 16 years old. I can't let you in." 244 | Disco(Adult()).letMeDance === "Welcome, Alex. Shall I take your coat?" 245 | } 246 | 247 | 248 | "Can't omit instance method defined in structural type" >> { 249 | 250 | trait Data { 251 | def foo: Boolean 252 | val i: Int 253 | } 254 | case class DataA(s: String) extends Data { 255 | def foo = true 256 | def text = s 257 | val i = 1 258 | } 259 | case class DataB(s: String, i: Int) extends Data { 260 | def foo = true 261 | def text = s 262 | def number = i 263 | } 264 | case class DataC(i: Int) extends Data { 265 | def foo = false 266 | def number = i 267 | } 268 | 269 | @context 270 | case class Context(myRole: Data {def text: String}) { 271 | 272 | def trigger = myRole.bar 273 | 274 | role myRole { 275 | def bar = { 276 | val result = if (self.foo) "Yes!" else "No!" 277 | val status = self.text + result 278 | status 279 | } 280 | } 281 | } 282 | 283 | Context(DataA("Will A fulfill the Role contract? ")).trigger === "Will A fulfill the Role contract? Yes!" 284 | Context(DataB("Will B fulfill the Role contract? ", 42)).trigger === "Will B fulfill the Role contract? Yes!" 285 | 286 | // This won't compile: 287 | // Context(DataC(911)).trigger === ... 288 | 289 | // Gives the following error message: 290 | 291 | // Type mismatch 292 | // expected: Data {def text: String} 293 | // actual: DataC 294 | } 295 | 296 | 297 | /** ****** Mix of types and duck typing **************************************************/ 298 | 299 | "Can be a mix of a type and a structural type" >> { 300 | 301 | class Data(i: Int) { 302 | def number = i 303 | } 304 | case class OtherData(i: Int) extends Data(i) { 305 | def text = "My number is: " 306 | } 307 | 308 | @context 309 | case class Context(myRole: Data {def text: String}) { 310 | // <- OtherData will satisfy this contract 311 | 312 | def trigger = myRole.foo 313 | 314 | role myRole { 315 | def foo = self.text + self.number // `Data` has a `number` method and there should also be some `text` method... 316 | } 317 | } 318 | Context(OtherData(42)).trigger === "My number is: 42" 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/ShoppingCart4b.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package examples 3 | import org.specs2.mutable.Specification 4 | 5 | /* 6 | Shopping cart example (version 4b) - only 1 customer role! 7 | 8 | The cart now has no role methods (would that be what we call a "methodless role"?) 9 | 10 | Can we talk about any Interactions having only the customer role left? We are definitely 11 | drifting away from our mental model... 12 | 13 | See discussion at: 14 | https://groups.google.com/forum/?fromgroups=#!topic/object-composition/JJiLWBsZWu0 15 | 16 | =========================================================================== 17 | USE CASE: Place Order [user-goal] 18 | 19 | Person browsing around finds product(s) in a web shop that he/she wants to buy. 20 | 21 | Primary actor.. Web customer ("Customer") 22 | Scope.......... Web shop ("Shop") 23 | Preconditions.. Shop presents product(s) to customer 24 | Trigger........ Customer wants to buy certain product(s) 25 | 26 | Main Success Scenario 27 | --------------------------------------------------------------------------- 28 | 1. Customer selects desired Product in Shop [can repeat] 29 | - System confirms Product availability 30 | - System adds Product to Order 31 | - UI shows updated contents of Cart to Customer 32 | 2. Customer requests to review Order 33 | - UI shows Cart with Products/prices to Customer 34 | 3. Customer pays Order 35 | - System confirms sufficient funds are available 36 | - System initiates transfer of funds 37 | - UI confirms purchase to Customer 38 | 39 | Deviations 40 | --------------------------------------------------------------------------- 41 | 1a. Product is out of stock: 42 | 1. UI informs Customer that Product is out of stock. 43 | 44 | 1b. Customer has gold membership: 45 | 1. System adds discounted Product to Order. 46 | 47 | 3a. Customer has insufficient funds to pay Order: 48 | 1. UI informs Customer of insufficient funds. 49 | a. Customer removes unaffordable Item(s) from Cart: 50 | 1. Go to step 3. 51 | =========================================================================== 52 | */ 53 | 54 | class ShoppingCart4b extends Specification { 55 | import ShoppingCartModel._ 56 | 57 | { 58 | @context 59 | class PlaceOrder(shop: Company, customer: Person) { 60 | // "Methodless role"? 61 | private val cart = Order(customer) 62 | 63 | // UC steps taken 64 | def customerSelecteddesiredProduct(productId: Int): Option[Product] = 65 | customer.selectdesiredProduct(productId) 66 | def customerRequestedToReviewOrder: Seq[(Int, Product)] = 67 | customer.reviewOrder 68 | def customerRequestedToPayOrder: Boolean = 69 | customer.payOrder 70 | 71 | // Deviation(s) 72 | def customerRemovedProductFromcart(productId: Int): Option[Product] = 73 | customer.removeProductFromcart(productId) 74 | 75 | // Only 1 role implementation! 76 | role customer { 77 | def selectdesiredProduct(productId: Int): Option[Product] = { 78 | if (!shop.stock.isDefinedAt(productId)) 79 | return None 80 | val product = shop.stock(productId) 81 | val discountedPrice = customer.getMemberPriceOf(product) 82 | val desiredProduct = product.copy(price = discountedPrice) 83 | cart.items.put(productId, desiredProduct) 84 | Some(desiredProduct) 85 | } 86 | def reviewOrder = cart.items.toIndexedSeq.sortBy(_._1) 87 | def removeProductFromcart(productId: Int): Option[Product] = { 88 | if (!cart.items.isDefinedAt(productId)) 89 | return None 90 | cart.items.remove(productId) 91 | } 92 | def payOrder: Boolean = { 93 | val orderTotal = cart.items.map(_._2.price).sum 94 | if (orderTotal > customer.cash) 95 | return false 96 | 97 | customer.cash -= orderTotal 98 | shop.cash += orderTotal 99 | 100 | // just for debugging... 101 | customer.owns ++= cart.items 102 | cart.items foreach (shop.stock remove _._1) 103 | true 104 | } 105 | 106 | def getMemberPriceOf(product: Product) = { 107 | val customerIsGoldMember = shop.goldMembers.contains(customer) 108 | val goldMemberReduction = 0.5 109 | val discountFactor = if (customerIsGoldMember) goldMemberReduction else 1 110 | (product.price * discountFactor).toInt 111 | } 112 | } 113 | } 114 | 115 | 116 | // Test various scenarios. 117 | // (copy and paste of ShoppingCart2/3/4 tests with trigger method names changed) 118 | 119 | "Main success scenario" in new ShoppingCart { 120 | 121 | // Initial status (same for all tests...) 122 | shop.stock === Map(tires, wax, bmw) 123 | shop.cash === 100000 124 | customer.cash === 20000 125 | customer.owns === Map() 126 | 127 | val order = new PlaceOrder(shop, customer) 128 | 129 | // customer wants wax and tires 130 | order.customerSelecteddesiredProduct(p1) 131 | order.customerSelecteddesiredProduct(p2) 132 | 133 | order.customerRequestedToReviewOrder === Seq(wax, tires) 134 | 135 | val orderCompleted = order.customerRequestedToPayOrder === true 136 | 137 | shop.stock === Map(bmw) 138 | shop.cash === 100000 + 40 + 600 139 | customer.cash === 20000 - 40 - 600 140 | customer.owns === Map(tires, wax) 141 | } 142 | 143 | "Product out of stock" in new ShoppingCart { 144 | 145 | // Wax out of stock 146 | shop.stock.remove(p1) 147 | shop.stock === Map(tires, bmw) 148 | 149 | val order = new PlaceOrder(shop, customer) 150 | 151 | // customer wants wax 152 | val itemAdded = order.customerSelecteddesiredProduct(p1) === None 153 | order.customerRequestedToReviewOrder === Seq() 154 | 155 | order.customerSelecteddesiredProduct(p2) 156 | 157 | val orderCompleted = order.customerRequestedToPayOrder === true 158 | 159 | shop.stock === Map(bmw) 160 | shop.cash === 100000 + 600 161 | customer.cash === 20000 - 600 162 | customer.owns === Map(tires) 163 | } 164 | 165 | "customer has gold membership" in new ShoppingCart { 166 | 167 | // customer is gold member 168 | shop.goldMembers.add(customer) 169 | shop.goldMembers.contains(customer) === true 170 | 171 | val order = new PlaceOrder(shop, customer) 172 | 173 | order.customerSelecteddesiredProduct(p1) 174 | 175 | val discountedWax = 1 -> Product("Wax", (40 * 0.5).toInt) 176 | order.customerRequestedToReviewOrder === Seq(discountedWax) 177 | 178 | val orderCompleted = order.customerRequestedToPayOrder === true 179 | 180 | shop.stock === Map(tires, bmw) 181 | shop.cash === 100000 + 20 182 | customer.cash === 20000 - 20 183 | customer.owns === Map(discountedWax) 184 | } 185 | 186 | "customer has too low credit" in new ShoppingCart { 187 | 188 | val order = new PlaceOrder(shop, customer) 189 | 190 | // customer wants a BMW 191 | val itemAdded = order.customerSelecteddesiredProduct(p3) 192 | 193 | // Any product is added - shop doesn't yet know if customer can afford it 194 | itemAdded === Some(bmw._2) 195 | order.customerRequestedToReviewOrder === Seq(bmw) 196 | 197 | // customer tries to pay order 198 | val paymentStatus = order.customerRequestedToPayOrder 199 | 200 | // shop informs customer of too low credit 201 | paymentStatus === false 202 | 203 | // customer removes unaffordable BMW from cart 204 | order.customerRemovedProductFromcart(p3) 205 | 206 | // customer aborts shopping and no purchases are made 207 | shop.stock === Map(tires, wax, bmw) 208 | shop.cash === 100000 209 | customer.cash === 20000 210 | customer.owns === Map() 211 | } 212 | 213 | "All deviations in play" in new ShoppingCart { 214 | 215 | // Tires out of stock 216 | shop.stock.remove(p2) 217 | shop.stock === Map(wax, bmw) 218 | 219 | // We have a gold member 220 | shop.goldMembers.add(customer) 221 | 222 | val order = new PlaceOrder(shop, customer) 223 | 224 | // Let's get some tires 225 | val tiresItemAdded = order.customerSelecteddesiredProduct(p2) 226 | 227 | // Product out of stock! 228 | shop.stock.contains(p2) === false 229 | 230 | // Nothing added to order yet 231 | tiresItemAdded === None 232 | order.customerRequestedToReviewOrder === Seq() 233 | 234 | // Let's buy the BMW instead. As a gold member that should be possible! 235 | val bmwItemAdded = order.customerSelecteddesiredProduct(p3) 236 | 237 | // Discounted BMW is added to order 238 | val discountedBMW = Product("BMW", (50000 * 0.5).toInt) 239 | bmwItemAdded === Some(discountedBMW) 240 | order.customerRequestedToReviewOrder === Seq(p3 -> discountedBMW) 241 | 242 | // Ouch! We couldn't afford it. 243 | val paymentAttempt1 = order.customerRequestedToPayOrder === false 244 | 245 | // It's still 5000 too much for us, even with the membership discount 246 | discountedBMW.price - customer.cash === 5000 247 | 248 | // Ok, no new car today 249 | order.customerRemovedProductFromcart(p3) 250 | 251 | // Order is back to empty 252 | order.customerRequestedToReviewOrder === Seq() 253 | 254 | // Let's get some wax anyway... 255 | val waxItemAdded = order.customerSelecteddesiredProduct(p1) 256 | 257 | // Did we get our membership discount on this one? 258 | val discountedWax = Product("Wax", (40 * 0.5).toInt) 259 | waxItemAdded === Some(discountedWax) 260 | 261 | // Now we can afford it! 262 | val paymentAttempt2 = order.customerRequestedToPayOrder === true 263 | 264 | // Not much shopping done Today. At least we got some cheap wax. 265 | shop.stock === Map(bmw) 266 | shop.cash === 100000 + 20 267 | customer.cash === 20000 - 20 268 | customer.owns === Map(p1 -> discountedWax) 269 | } 270 | } 271 | } -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/ShoppingCart4a.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package examples 3 | import org.specs2.mutable.Specification 4 | 5 | /* 6 | Shopping cart example (version 4a) - removing more roles 7 | 8 | Removed the desiredProduct role. It feels too technically motivated (to avoid passing 9 | the marked product id around) and not 100% as a justified role of a mental model. 10 | 11 | Absorbed the warehouse role responsibilities into the customer role. 12 | 13 | See discussion at: 14 | https://groups.google.com/forum/?fromgroups=#!topic/object-composition/JJiLWBsZWu0 15 | 16 | =========================================================================== 17 | USE CASE: Place Order [user-goal] 18 | 19 | Person browsing around finds product(s) in a web shop that he/she wants to buy. 20 | 21 | Primary actor.. Web customer ("Customer") 22 | Scope.......... Web shop ("Shop") 23 | Preconditions.. Shop presents product(s) to customer 24 | Trigger........ Customer wants to buy certain product(s) 25 | 26 | Main Success Scenario 27 | --------------------------------------------------------------------------- 28 | 1. Customer marks Desired Product in Shop 29 | - System confirms Product availability 30 | - System adds Item to Order (can repeat from step 1) 31 | - UI shows updated contents of Cart to Customer 32 | 2. Customer requests to review Order 33 | - UI presents Cart with Items and prices to Customer 34 | 3. Customer pays Order 35 | - System confirms sufficient funds are available 36 | - System initiates transfer of funds 37 | - UI confirms purchase to Customer 38 | 39 | Deviations 40 | --------------------------------------------------------------------------- 41 | 1a. Product is out of stock: 42 | 1. UI informs Customer that Product is out of stock. 43 | 44 | 1b. Customer has gold membership: 45 | 1. System adds discounted Product to Order. 46 | 47 | 3a. Customer has insufficient funds to pay Order: 48 | 1. UI informs Customer of insufficient funds. 49 | a. Customer removes unaffordable Item(s) from Cart: 50 | 1. Go to step 3. 51 | =========================================================================== 52 | */ 53 | 54 | class ShoppingCart4a extends Specification { 55 | import ShoppingCartModel._ 56 | 57 | { 58 | @context 59 | class PlaceOrder(shop: Company, customer: Person) { 60 | 61 | // UC steps 62 | def customerMarksdesiredProductInshop(productId: Int): Option[Product] = 63 | customer.markdesiredProductInshop(productId) 64 | def customerRequestsToReviewOrder: Seq[(Int, Product)] = 65 | customer.reviewOrder 66 | def customerPaysOrder: Boolean = 67 | customer.payOrder 68 | 69 | // Deviation(s) 70 | def customerRemovesProductFromcart(productId: Int): Option[Product] = 71 | customer.removeProductFromcart(productId) 72 | 73 | // Roles 74 | role customer { 75 | def markdesiredProductInshop(productId: Int): Option[Product] = { 76 | if (!shop.stock.isDefinedAt(productId)) 77 | return None 78 | val product = shop.stock(productId) 79 | val discountedPrice = customer.getMemberPriceOf(product) 80 | val desiredProduct = product.copy(price = discountedPrice) 81 | cart.addItem(productId, desiredProduct) 82 | Some(desiredProduct) 83 | } 84 | def reviewOrder = cart.getItems 85 | def removeProductFromcart(productId: Int) = cart.removeItem(productId: Int) 86 | def payOrder: Boolean = { 87 | val orderTotal = cart.total 88 | if (orderTotal > customer.cash) 89 | return false 90 | 91 | customer.cash -= orderTotal 92 | shop.cash += orderTotal 93 | 94 | customer.owns ++= cart.items 95 | cart.items foreach (shop.stock remove _._1) 96 | true 97 | } 98 | 99 | def getMemberPriceOf(product: Product) = { 100 | val customerIsGoldMember = shop.goldMembers.contains(customer) 101 | val goldMemberReduction = 0.5 102 | val discountFactor = if (customerIsGoldMember) goldMemberReduction else 1 103 | (product.price * discountFactor).toInt 104 | } 105 | } 106 | 107 | private val cart = Order(customer) 108 | 109 | role cart { 110 | def addItem(productId: Int, product: Product) { 111 | cart.items.put(productId, product) 112 | } 113 | def removeItem(productId: Int): Option[Product] = { 114 | if (!cart.items.isDefinedAt(productId)) 115 | return None 116 | cart.items.remove(productId) 117 | } 118 | def getItems = cart.items.toIndexedSeq.sortBy(_._1) 119 | def total = cart.items.map(_._2.price).sum 120 | } 121 | } 122 | 123 | 124 | // Test various scenarios. 125 | // (copy and paste of ShoppingCart2/3 tests) 126 | 127 | "Main success scenario" in new ShoppingCart { 128 | 129 | // Initial status (same for all tests...) 130 | shop.stock === Map(tires, wax, bmw) 131 | shop.cash === 100000 132 | customer.cash === 20000 133 | customer.owns === Map() 134 | 135 | val order = new PlaceOrder(shop, customer) 136 | 137 | // customer wants wax and tires 138 | order.customerMarksdesiredProductInshop(p1) 139 | order.customerMarksdesiredProductInshop(p2) 140 | 141 | order.customerRequestsToReviewOrder === Seq(wax, tires) 142 | 143 | val orderCompleted = order.customerPaysOrder === true 144 | 145 | shop.stock === Map(bmw) 146 | shop.cash === 100000 + 40 + 600 147 | customer.cash === 20000 - 40 - 600 148 | customer.owns === Map(tires, wax) 149 | } 150 | 151 | "Product out of stock" in new ShoppingCart { 152 | 153 | // Wax out of stock 154 | shop.stock.remove(p1) 155 | shop.stock === Map(tires, bmw) 156 | 157 | val order = new PlaceOrder(shop, customer) 158 | 159 | // customer wants wax 160 | val itemAdded = order.customerMarksdesiredProductInshop(p1) === None 161 | order.customerRequestsToReviewOrder === Seq() 162 | 163 | order.customerMarksdesiredProductInshop(p2) 164 | 165 | val orderCompleted = order.customerPaysOrder === true 166 | 167 | shop.stock === Map(bmw) 168 | shop.cash === 100000 + 600 169 | customer.cash === 20000 - 600 170 | customer.owns === Map(tires) 171 | } 172 | 173 | "customer has gold membership" in new ShoppingCart { 174 | 175 | // customer is gold member 176 | shop.goldMembers.add(customer) 177 | shop.goldMembers.contains(customer) === true 178 | 179 | val order = new PlaceOrder(shop, customer) 180 | 181 | order.customerMarksdesiredProductInshop(p1) 182 | 183 | val discountedWax = 1 -> Product("Wax", (40 * 0.5).toInt) 184 | order.customerRequestsToReviewOrder === Seq(discountedWax) 185 | 186 | val orderCompleted = order.customerPaysOrder === true 187 | 188 | shop.stock === Map(tires, bmw) 189 | shop.cash === 100000 + 20 190 | customer.cash === 20000 - 20 191 | customer.owns === Map(discountedWax) 192 | } 193 | 194 | "customer has too low credit" in new ShoppingCart { 195 | 196 | val order = new PlaceOrder(shop, customer) 197 | 198 | // customer wants a BMW 199 | val itemAdded = order.customerMarksdesiredProductInshop(p3) 200 | 201 | // Any product is added - shop doesn't yet know if customer can afford it 202 | itemAdded === Some(bmw._2) 203 | order.customerRequestsToReviewOrder === Seq(bmw) 204 | 205 | // customer tries to pay order 206 | val paymentStatus = order.customerPaysOrder 207 | 208 | // shop informs customer of too low credit 209 | paymentStatus === false 210 | 211 | // customer removes unaffordable BMW from cart 212 | order.customerRemovesProductFromcart(p3) 213 | 214 | // customer aborts shopping and no purchases are made 215 | shop.stock === Map(tires, wax, bmw) 216 | shop.cash === 100000 217 | customer.cash === 20000 218 | customer.owns === Map() 219 | } 220 | 221 | "All deviations in play" in new ShoppingCart { 222 | 223 | // Tires out of stock 224 | shop.stock.remove(p2) 225 | shop.stock === Map(wax, bmw) 226 | 227 | // We have a gold member 228 | shop.goldMembers.add(customer) 229 | 230 | val order = new PlaceOrder(shop, customer) 231 | 232 | // Let's get some tires 233 | val tiresItemAdded = order.customerMarksdesiredProductInshop(p2) 234 | 235 | // Product out of stock! 236 | shop.stock.contains(p2) === false 237 | 238 | // Nothing added to order yet 239 | tiresItemAdded === None 240 | order.customerRequestsToReviewOrder === Seq() 241 | 242 | // Let's buy the BMW instead. As a gold member that should be possible! 243 | val bmwItemAdded = order.customerMarksdesiredProductInshop(p3) 244 | 245 | // Discounted BMW is added to order 246 | val discountedBMW = Product("BMW", (50000 * 0.5).toInt) 247 | bmwItemAdded === Some(discountedBMW) 248 | order.customerRequestsToReviewOrder === Seq(p3 -> discountedBMW) 249 | 250 | // Ouch! We couldn't afford it. 251 | val paymentAttempt1 = order.customerPaysOrder === false 252 | 253 | // It's still 5000 too much for us, even with the membership discount 254 | discountedBMW.price - customer.cash === 5000 255 | 256 | // Ok, no new car today 257 | order.customerRemovesProductFromcart(p3) 258 | 259 | // Order is back to empty 260 | order.customerRequestsToReviewOrder === Seq() 261 | 262 | // Let's get some wax anyway... 263 | val waxItemAdded = order.customerMarksdesiredProductInshop(p1) 264 | 265 | // Did we get our membership discount on this one? 266 | val discountedWax = Product("Wax", (40 * 0.5).toInt) 267 | waxItemAdded === Some(discountedWax) 268 | 269 | // Now we can afford it! 270 | val paymentAttempt2 = order.customerPaysOrder === true 271 | 272 | // Not much shopping done Today. At least we got some cheap wax. 273 | shop.stock === Map(bmw) 274 | shop.cash === 100000 + 20 275 | customer.cash === 20000 - 20 276 | customer.owns === Map(p1 -> discountedWax) 277 | } 278 | } 279 | } -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/ShoppingCart3.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package examples 3 | import org.specs2.mutable.Specification 4 | 5 | /* 6 | Shopping cart example (version 3) - removing shop role 7 | 8 | Delegated most of the earlier shop role responsibilities to the customer role. We can 9 | view the shop as the system as a whole (maybe a context?) that we operate in - or the 10 | system that responds to various customer actions rather than a Role on its own. 11 | 12 | Now distinguishing between System and UI as different kinds of responses to the user 13 | inputs. When an item is checked with the warehouse we could see it as the System doing 14 | some technical background checks that doesn't involve the customer whereas the UI is 15 | what communicates with the customer. 16 | 17 | "Internally" the systems regards the cart as an Order. The customer primarily thinks 18 | "shopping cart" and might change to a mental model of "order" once he's about to pay? 19 | 20 | See discussion at: 21 | https://groups.google.com/forum/?fromgroups=#!topic/object-composition/JJiLWBsZWu0 22 | 23 | =========================================================================== 24 | USE CASE: Place Order [user-goal] 25 | 26 | Person browsing around finds product(s) in a web shop that he/she wants to buy. 27 | 28 | Primary actor.. Web customer ("Customer") 29 | Scope.......... Web shop ("Shop") 30 | Preconditions.. Shop presents product(s) to Customer 31 | Trigger........ Customer wants to buy certain product(s) 32 | 33 | Main Success Scenario 34 | --------------------------------------------------------------------------- 35 | 1. Customer marks Desired Product in Shop 36 | - System reserves Product in Warehouse 37 | - System adds Item to Order (can repeat from step 1) 38 | - UI shows updated contents of Cart to Customer 39 | 2. Customer requests to review Order 40 | - UI presents Cart with Items and prices to Customer 41 | 3. Customer pays Order 42 | - System confirms sufficient funds are available 43 | - System initiates transfer of funds 44 | - System informs Warehouse to ship Products 45 | - UI confirms purchase to Customer 46 | 47 | Deviations 48 | --------------------------------------------------------------------------- 49 | 1a. Product is out of stock: 50 | 1. UI informs Customer that Product is out of stock. 51 | 52 | 1b. Customer has gold membership: 53 | 1. System adds discounted Product to Order. 54 | 55 | 3a. Customer has insufficient funds to pay Order: 56 | 1. UI informs Customer of insufficient funds. 57 | a. Customer removes unaffordable Item(s) from Cart: 58 | 1. Go to step 3. 59 | =========================================================================== 60 | */ 61 | 62 | class ShoppingCart3 extends Specification { 63 | import ShoppingCartModel._ 64 | 65 | { 66 | @context 67 | class PlaceOrder(shop: Company, customer: Person) { 68 | 69 | // UC steps 70 | def customerMarksdesiredProductInshop(productId: Int): Option[Product] = { 71 | desiredProductId = productId 72 | customer.markdesiredProductInshop 73 | } 74 | def customerRequestsToReviewOrder: Seq[(Int, Product)] = 75 | customer.reviewOrder 76 | def customerPaysOrder: Boolean = 77 | customer.payOrder 78 | 79 | // Deviation(s) 80 | def customerRemovesProductFromcart(productId: Int): Option[Product] = 81 | customer.removeProductFromcart(productId) 82 | 83 | // Context data house-keeping 84 | private var desiredProductId: Int = _ 85 | 86 | // Roles 87 | private val warehouse = shop 88 | private val cart = Order(customer) 89 | 90 | role customer { 91 | def markdesiredProductInshop: Option[Product] = { 92 | if (!warehouse.hasdesiredProduct) 93 | return None 94 | val product = warehouse.reservedesiredProduct 95 | val discountedPrice = customer.getMemberPriceOf(product) 96 | val desiredProduct = product.copy(price = discountedPrice) 97 | cart.addItem(desiredProductId, desiredProduct) 98 | Some(desiredProduct) 99 | } 100 | def reviewOrder = cart.getItems 101 | def removeProductFromcart(productId: Int) = cart.removeItem(productId: Int) 102 | def payOrder: Boolean = { 103 | val orderTotal = cart.total 104 | if (orderTotal > customer.cash) 105 | return false 106 | 107 | customer.cash -= orderTotal 108 | shop.cash += orderTotal 109 | 110 | customer.owns ++= cart.items 111 | cart.items foreach (warehouse.stock remove _._1) 112 | true 113 | } 114 | 115 | def getMemberPriceOf(product: Product) = { 116 | val customerIsGoldMember = shop.goldMembers.contains(customer) 117 | val goldMemberReduction = 0.5 118 | val discountFactor = if (customerIsGoldMember) goldMemberReduction else 1 119 | (product.price * discountFactor).toInt 120 | } 121 | } 122 | 123 | role warehouse { 124 | def hasdesiredProduct = shop.stock.isDefinedAt(desiredProductId) 125 | def reservedesiredProduct = shop.stock(desiredProductId) // dummy reservation 126 | } 127 | 128 | role cart { 129 | def addItem(productId: Int, product: Product) { 130 | cart.items.put(productId, product) 131 | } 132 | def removeItem(productId: Int): Option[Product] = { 133 | if (!cart.items.isDefinedAt(productId)) 134 | return None 135 | cart.items.remove(productId) 136 | } 137 | def getItems = cart.items.toIndexedSeq.sortBy(_._1) 138 | def total = cart.items.map(_._2.price).sum 139 | } 140 | } 141 | 142 | 143 | // Test various scenarios. 144 | // (copy and paste of ShoppingCart2 tests) 145 | 146 | "Main success scenario" in new ShoppingCart { 147 | 148 | // Initial status (same for all tests...) 149 | shop.stock === Map(tires, wax, bmw) 150 | shop.cash === 100000 151 | customer.cash === 20000 152 | customer.owns === Map() 153 | 154 | val order = new PlaceOrder(shop, customer) 155 | 156 | // customer wants wax and tires 157 | order.customerMarksdesiredProductInshop(p1) 158 | order.customerMarksdesiredProductInshop(p2) 159 | 160 | order.customerRequestsToReviewOrder === Seq(wax, tires) 161 | 162 | val orderCompleted = order.customerPaysOrder === true 163 | 164 | shop.stock === Map(bmw) 165 | shop.cash === 100000 + 40 + 600 166 | customer.cash === 20000 - 40 - 600 167 | customer.owns === Map(tires, wax) 168 | } 169 | 170 | "Product out of stock" in new ShoppingCart { 171 | 172 | // Wax out of stock 173 | shop.stock.remove(p1) 174 | shop.stock === Map(tires, bmw) 175 | 176 | val order = new PlaceOrder(shop, customer) 177 | 178 | // customer wants wax 179 | val itemAdded = order.customerMarksdesiredProductInshop(p1) === None 180 | order.customerRequestsToReviewOrder === Seq() 181 | 182 | order.customerMarksdesiredProductInshop(p2) 183 | 184 | val orderCompleted = order.customerPaysOrder === true 185 | 186 | shop.stock === Map(bmw) 187 | shop.cash === 100000 + 600 188 | customer.cash === 20000 - 600 189 | customer.owns === Map(tires) 190 | } 191 | 192 | "customer has gold membership" in new ShoppingCart { 193 | 194 | // customer is gold member 195 | shop.goldMembers.add(customer) 196 | shop.goldMembers.contains(customer) === true 197 | 198 | val order = new PlaceOrder(shop, customer) 199 | 200 | order.customerMarksdesiredProductInshop(p1) 201 | 202 | val discountedWax = 1 -> Product("Wax", (40 * 0.5).toInt) 203 | order.customerRequestsToReviewOrder === Seq(discountedWax) 204 | 205 | val orderCompleted = order.customerPaysOrder === true 206 | 207 | shop.stock === Map(tires, bmw) 208 | shop.cash === 100000 + 20 209 | customer.cash === 20000 - 20 210 | customer.owns === Map(discountedWax) 211 | } 212 | 213 | "customer has too low credit" in new ShoppingCart { 214 | 215 | val order = new PlaceOrder(shop, customer) 216 | 217 | // customer wants a BMW 218 | val itemAdded = order.customerMarksdesiredProductInshop(p3) 219 | 220 | // Any product is added - shop doesn't yet know if customer can afford it 221 | itemAdded === Some(bmw._2) 222 | order.customerRequestsToReviewOrder === Seq(bmw) 223 | 224 | // customer tries to pay order 225 | val paymentStatus = order.customerPaysOrder 226 | 227 | // shop informs customer of too low credit 228 | paymentStatus === false 229 | 230 | // customer removes unaffordable BMW from cart 231 | order.customerRemovesProductFromcart(p3) 232 | 233 | // customer aborts shopping and no purchases are made 234 | shop.stock === Map(tires, wax, bmw) 235 | shop.cash === 100000 236 | customer.cash === 20000 237 | customer.owns === Map() 238 | } 239 | 240 | "All deviations in play" in new ShoppingCart { 241 | 242 | // Tires out of stock 243 | shop.stock.remove(p2) 244 | shop.stock === Map(wax, bmw) 245 | 246 | // We have a gold member 247 | shop.goldMembers.add(customer) 248 | 249 | val order = new PlaceOrder(shop, customer) 250 | 251 | // Let's get some tires 252 | val tiresItemAdded = order.customerMarksdesiredProductInshop(p2) 253 | 254 | // Product out of stock! 255 | shop.stock.contains(p2) === false 256 | 257 | // Nothing added to order yet 258 | tiresItemAdded === None 259 | order.customerRequestsToReviewOrder === Seq() 260 | 261 | // Let's buy the BMW instead. As a gold member that should be possible! 262 | val bmwItemAdded = order.customerMarksdesiredProductInshop(p3) 263 | 264 | // Discounted BMW is added to order 265 | val discountedBMW = Product("BMW", (50000 * 0.5).toInt) 266 | bmwItemAdded === Some(discountedBMW) 267 | order.customerRequestsToReviewOrder === Seq(p3 -> discountedBMW) 268 | 269 | // Ouch! We couldn't afford it. 270 | val paymentAttempt1 = order.customerPaysOrder === false 271 | 272 | // It's still 5000 too much for us, even with the membership discount 273 | discountedBMW.price - customer.cash === 5000 274 | 275 | // Ok, no new car today 276 | order.customerRemovesProductFromcart(p3) 277 | 278 | // Order is back to empty 279 | order.customerRequestsToReviewOrder === Seq() 280 | 281 | // Let's get some wax anyway... 282 | val waxItemAdded = order.customerMarksdesiredProductInshop(p1) 283 | 284 | // Did we get our membership discount on this one? 285 | val discountedWax = Product("Wax", (40 * 0.5).toInt) 286 | waxItemAdded === Some(discountedWax) 287 | 288 | // Now we can afford it! 289 | val paymentAttempt2 = order.customerPaysOrder === true 290 | 291 | // Not much shopping done Today. At least we got some cheap wax. 292 | shop.stock === Map(bmw) 293 | shop.cash === 100000 + 20 294 | customer.cash === 20000 - 20 295 | customer.owns === Map(p1 -> discountedWax) 296 | } 297 | } 298 | } -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/ShoppingCart1.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package examples 3 | import org.specs2.mutable.Specification 4 | import org.specs2.specification._ 5 | 6 | import scala.collection.mutable 7 | 8 | /* 9 | Shopping cart example, version 1 10 | 11 | Implementing a simple Place Order use case of an online shopping cart to explore 12 | how we could handle various execution paths (scenarios) within a single DCI Context. 13 | 14 | See discussion at: 15 | https://groups.google.com/forum/?fromgroups=#!topic/object-composition/JJiLWBsZWu0 16 | 17 | =========================================================================== 18 | USE CASE: Place Order [user-goal] 19 | 20 | Person browsing around finds product(s) in a web shop that he/she wants to buy. 21 | 22 | Primary actor.. Web customer ("Customer") 23 | Scope.......... Web shop ("Shop") 24 | Preconditions.. Shop presents product(s) to Customer 25 | Trigger........ Customer wants to buy certain product(s) 26 | 27 | Main Success Scenario 28 | --------------------------------------------------------------------------- 29 | 1. Customer marks desired Product in Shop. 30 | 2. Shop adds Item to Cart (can repeat from step 1). 31 | 3. Customer requests to review Order. 32 | 4. Shop presents Cart with Items and prices to Customer. 33 | 5. Customer pays Order. 34 | 6. Shop confirms purchase to Customer. 35 | 36 | Deviations 37 | --------------------------------------------------------------------------- 38 | 2a. Product is out of stock: 39 | 1. Shop informs Customer that Product is out of stock. 40 | 2. Go to step 1 (pick another Product). 41 | 42 | 4a. Customer has gold membership: 43 | 1. Shop presents Cart with Products and discounted prices to Customer. 44 | 2. Go to step 5. 45 | 46 | 5a. Customer has too low credit: 47 | 1. Customer removes unaffordable Item(s) from Cart. 48 | 2. Customer aborts Order. 49 | =========================================================================== 50 | */ 51 | 52 | // Domain model 53 | object ShoppingCartModel { 54 | case class Product(name: String, price: Int) 55 | case class Person(name: String, var cash: Int, owns: mutable.Map[Int, Product] = mutable.Map()) 56 | case class Company(name: String, var cash: Int, stock: mutable.Map[Int, Product], goldMembers: mutable.Set[Person]) 57 | case class Order(customer: Person, items: mutable.Map[Int, Product] = mutable.Map()) 58 | } 59 | 60 | // Setup for each test 61 | trait ShoppingCart extends Scope { 62 | import ShoppingCartModel._ 63 | val (p1, p2, p3) = (1, 2, 3) 64 | val (wax, tires, bmw) = (p1 -> Product("Wax", 40), p2 -> Product("Tires", 600), p3 -> Product("BMW", 50000)) 65 | val shop = Company("Don's Auto shop", 100000, mutable.Map(wax, tires, bmw), mutable.Set()) 66 | val customer = Person("Matthew", 20000) 67 | } 68 | 69 | // Define the Context and test various scenarios 70 | class ShoppingCart1 extends Specification { 71 | import ShoppingCartModel._ 72 | 73 | { 74 | @context 75 | class PlaceOrder(company: Company, person: Person) { 76 | 77 | // Trigger methods 78 | def addItem(productId: Int): Option[Product] = cart addItem productId 79 | def removeItem(productId: Int): Option[Product] = cart removeItem productId 80 | def getCurrentItems = cart.items.toIndexedSeq.sortBy(_._1) 81 | def pay = customer.payOrder 82 | 83 | // Roles 84 | private val customer = person 85 | private val shop = company 86 | private val warehouse = company 87 | private val cart = Order(person) 88 | 89 | role customer { 90 | def payOrder: Boolean = { 91 | // Sufficient funds? 92 | val orderTotal = cart.total 93 | if (orderTotal > customer.cash) 94 | return false 95 | 96 | // Transfer ownership of items 97 | customer.owns ++= cart.items 98 | cart.items foreach (warehouse.stock remove _._1) 99 | 100 | // Resolve payment 101 | customer.cash -= orderTotal 102 | shop receivePayment orderTotal 103 | true 104 | } 105 | def isGoldMember = shop.goldMembers contains customer 106 | def reduction = if (customer.isGoldMember) 0.5 else 1 107 | } 108 | 109 | role shop { 110 | def receivePayment(amount: Int) { shop.cash += amount } 111 | } 112 | 113 | role warehouse { 114 | def has(productId: Int) = shop.stock isDefinedAt productId 115 | } 116 | 117 | role cart { 118 | def addItem(productId: Int): Option[Product] = { 119 | // In stock? 120 | if (!warehouse.has(productId)) 121 | return None 122 | 123 | // Gold member price? 124 | val product = warehouse.stock(productId) 125 | val customerPrice = (product.price * customer.reduction).toInt 126 | 127 | // Add item with adjusted price to cart 128 | val revisedProduct = product.copy(price = customerPrice) 129 | cart.items.put(productId, revisedProduct) 130 | Some(revisedProduct) 131 | } 132 | 133 | def removeItem(productId: Int): Option[Product] = { 134 | if (!cart.items.isDefinedAt(productId)) 135 | return None 136 | cart.items.remove(productId) 137 | } 138 | 139 | def total = cart.items.map(_._2.price).sum 140 | } 141 | } 142 | 143 | // Test various scenarios 144 | 145 | "Main success scenario" in new ShoppingCart { 146 | 147 | // Initial status (same for all tests...) 148 | shop.stock === Map(tires, wax, bmw) 149 | shop.cash === 100000 150 | customer.cash === 20000 151 | customer.owns === Map() 152 | 153 | // hm... when is order created? When customer selects first product? 154 | val order = new PlaceOrder(shop, customer) 155 | 156 | // Step 1: customer selects product(s) in UI 157 | // Step 2: 2 items added to cart (step 1-2 repeated) 158 | order.addItem(p1) 159 | order.addItem(p2) 160 | 161 | // Step 3: customer requests to review order 162 | // Step 4: shop presents items in cart: 163 | order.getCurrentItems === Seq(wax, tires) 164 | 165 | // Step 5: customer requests to pay order 166 | val orderCompleted = order.pay 167 | 168 | // Step 6: Order completed? 169 | orderCompleted === true 170 | 171 | // Outcome 172 | shop.stock === Map(bmw) 173 | shop.cash === 100000 + 40 + 600 174 | customer.cash === 20000 - 40 - 600 175 | customer.owns === Map(tires, wax) 176 | } 177 | 178 | "Deviation 2a - Product out of stock" in new ShoppingCart { 179 | 180 | // Wax is out of stock! 181 | shop.stock.remove(p1) 182 | shop.stock === Map(tires, bmw) 183 | 184 | val order = new PlaceOrder(shop, customer) 185 | 186 | // customer wants wax 187 | val itemAdded = order.addItem(p1) 188 | 189 | // 2a. Product out of stock! 190 | shop.stock.contains(p1) === false 191 | 192 | // 2a.1. shop informs customer that Product is out of stock. 193 | itemAdded === None 194 | order.getCurrentItems === Seq() 195 | 196 | // 2a.2. customer picks tires instead 197 | order.addItem(p2) 198 | 199 | // Order completed 200 | val orderCompleted = order.pay === true 201 | 202 | // Outcome 203 | shop.stock === Map(bmw) 204 | shop.cash === 100000 + 600 205 | customer.cash === 20000 - 600 206 | customer.owns === Map(tires) 207 | } 208 | 209 | "Deviation 4a - customer has gold membership" in new ShoppingCart { 210 | 211 | // customer is gold member 212 | shop.goldMembers.add(customer) 213 | 214 | val order = new PlaceOrder(shop, customer) 215 | 216 | // customer orders wax 217 | order.addItem(p1) 218 | 219 | // 4a. customer has gold membership 220 | shop.goldMembers.contains(customer) === true 221 | 222 | // 4a.1. shop presents cart with wax at discounted price 223 | val discountedWax = 1 -> Product("Wax", (40 * 0.5).toInt) 224 | order.getCurrentItems === Seq(discountedWax) 225 | 226 | // Order completed 227 | val orderCompleted = order.pay === true 228 | 229 | // Outcome 230 | shop.stock === Map(tires, bmw) 231 | shop.cash === 100000 + 20 232 | customer.cash === 20000 - 20 233 | customer.owns === Map(discountedWax) 234 | } 235 | 236 | "Deviation 5a - customer has too low credit" in new ShoppingCart { 237 | 238 | val order = new PlaceOrder(shop, customer) 239 | 240 | // customer wants a BMW 241 | val itemAdded = order.addItem(p3) 242 | 243 | // Any product is added - shop doesn't yet know if customer can afford it 244 | itemAdded === Some(bmw._2) 245 | order.getCurrentItems === Seq(bmw) 246 | 247 | // 5. customer tries to pay order 248 | val paymentStatus = order.pay 249 | 250 | // 5a. shop informs customer of too low credit 251 | paymentStatus === false 252 | 253 | // 5a.1. customer removes unaffordable BMW from cart 254 | order.removeItem(p3) 255 | 256 | // customer aborts shopping 257 | shop.stock === Map(tires, wax, bmw) 258 | shop.cash === 100000 259 | customer.cash === 20000 260 | customer.owns === Map() 261 | } 262 | 263 | "All deviations in play" in new ShoppingCart { 264 | 265 | // Tires are out of stock 266 | shop.stock.remove(p2) 267 | shop.stock === Map(wax, bmw) 268 | 269 | // We have a gold member 270 | shop.goldMembers.add(customer) 271 | 272 | val order = new PlaceOrder(shop, customer) 273 | 274 | // Let's get some tires 275 | val tiresItemAdded = order.addItem(p2) 276 | 277 | // 2a. Product out of stock! 278 | shop.stock.contains(p2) === false 279 | 280 | // Nothing added to order yet 281 | tiresItemAdded === None 282 | order.getCurrentItems === Seq() 283 | 284 | // Let's buy the BMW instead. As a gold member that should be possible! 285 | val bmwItemAdded = order.addItem(p3) 286 | 287 | // Discounted BMW is added to order 288 | val discountedBMW = Product("BMW", (50000 * 0.5).toInt) 289 | bmwItemAdded === Some(discountedBMW) 290 | order.getCurrentItems === Seq(p3 -> discountedBMW) 291 | 292 | // Ouch! We couldn't afford it. 293 | val paymentAttempt1 = order.pay === false 294 | 295 | // It's still 5000 too much for us, even with the membership discount 296 | discountedBMW.price - customer.cash === 5000 297 | 298 | // Ok, no new car today 299 | order.removeItem(p3) 300 | 301 | // Order is back to empty 302 | order.getCurrentItems === Seq() 303 | 304 | // Let's get some wax anyway... 305 | val waxItemAdded = order.addItem(p1) 306 | 307 | // Did we get our membership discount on this one? 308 | val discountedWax = Product("Wax", (40 * 0.5).toInt) 309 | waxItemAdded === Some(discountedWax) 310 | 311 | // Now we can afford it! 312 | val paymentAttempt2 = order.pay === true 313 | 314 | // Not much shopping done Today. At least we got some cheap wax. 315 | shop.stock === Map(bmw) 316 | shop.cash === 100000 + 20 317 | customer.cash === 20000 - 20 318 | customer.owns === Map(p1 -> discountedWax) 319 | } 320 | } 321 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scala DCI 2 | 3 | The [Data Context Interaction (DCI)](http://en.wikipedia.org/wiki/Data,_context_and_interaction) 4 | paradigm by Trygve Reenskaug and James Coplien embodies true object-orientation where 5 | runtime Interactions between a network of objects in a particular Context 6 | is understood _and_ coded as first class citizens. ScalaDCI tries to get as close as Scala allows 7 | us to realize the goals of DCI by using macro source code transformation, also known as "injectionless DCI". 8 | 9 | DCI is a new paradigm that implies many layers of new thinking that are fundamentally different from 10 | how mainstream "object-oriented" coding is perceived Today. It's not just a "technique" or a "pattern". It 11 | is also about how we think about the domain and what the system is versus what the system does. And DCI 12 | offers new ways to be more explicit about the does-side in ways that enable us to reason about 13 | what the code will do at runtime. Something that the Gang of Four admitted most "object-oriented" systems 14 | don't offer with their deeply nested deferred hierarchies of classes organized in endless patterns that 15 | tell almost nothing about the path _objects_ instantiated from those classes will take at runtime. 16 | That unfortunately makes Todays programs almost impossible to reason about and therefore completely reliant on 17 | extensive test suites that desperately try to catch runtime surprises - surprises that DCI tries to avoid 18 | from the outset by taking a fundamentally more reasonable approach. 19 | 20 | At the moment extensive research and development is taken on by James Coplien to develop a truly 21 | object-oriented research language called "trygve", named after the inventor of DCI, Trygve Reenskaug. 22 | See more on the 23 | [official DCI site](http://www.fulloo.info) and on the 24 | [google list](https://groups.google.com/forum/#!forum/object-composition). 25 | 26 | 27 | To see how ScalaDCI works, let's take a very simple example of a Data class Account with some basic methods: 28 | ```scala 29 | case class Account(name: String, var balance: Int) { 30 | def increaseBalance(amount: Int) { balance += amount } 31 | def decreaseBalance(amount: Int) { balance -= amount } 32 | } 33 | ``` 34 | This is a primitive data class only knowing about its own data and how to manipulate that. 35 | The concept of a transfer between two accounts we can leave outside its responsibilities and instead 36 | delegate to a "Context" - the MoneyTransfer Context class. In this way we can keep the Account class 37 | very slim and avoid that it gradually takes on more and more responsibilities for each use case 38 | it participates in. 39 | 40 | Our Mental Model of a money transfer could be "Withdraw amount from a source account and deposit the 41 | amount in a destination account". Interacting concepts like our "source" 42 | and "destination" accounts we call "Roles" in DCI. And we can define how they can interact in our 43 | Context to accomplish a money transfer: 44 | ```Scala 45 | @context 46 | class MoneyTransfer(source: Account, destination: Account, amount: Int) { 47 | 48 | source.withdraw // Interactions start... 49 | 50 | role source { 51 | def withdraw() { 52 | source.decreaseBalance(amount) 53 | destination.deposit 54 | } 55 | } 56 | 57 | role destination { 58 | def deposit() { 59 | destination.increaseBalance(amount) 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | We want our source code to map as closely to our mental model as possible so that we can confidently and easily 66 | overview and reason about _how the objects will interact at runtime_! We want to expect no surprises at runtime. 67 | With DCI we have all runtime interactions right there! No need to look through endless convoluted abstractions, 68 | tiers, polymorphism etc to answer the reasonable question _where is it actually happening, goddammit?!_ 69 | 70 | At compile time, our @context macro annotation transforms the abstract syntax tree (AST) of our code to enable our 71 | _runtime data objects_ to "have" those extra Role Methods. Well, I'm half lying to you; the 72 | objects won't "get new methods". Instead we call Role-name prefixed Role methods that are 73 | lifted into Context scope which accomplishes what we intended in our source code. Our code gets transformed as 74 | though we had written this: 75 | 76 | ```Scala 77 | class MoneyTransfer(source: Account, destination: Account, amount: Int) { 78 | 79 | source_withdraw() 80 | 81 | private def source_withdraw() { 82 | source.decreaseBalance(amount) // Calling Data instance method 83 | destination_deposit() // Calling Role method 84 | } 85 | 86 | private def destination_deposit() { 87 | destination.increaseBalance(amount) 88 | } 89 | } 90 | ``` 91 | 92 | ## Role methods that "shadow" instance methods 93 | In ScalaDCI, role methods will always take precedence over instance methods of the object that will play the role. 94 | 95 | Generally we want to avoid defining a role method with a name that clashes with - or "shadows" - an instance method since this could cause confusion about the runtime behavior of our program. "Which method is called on the role-playing object?". 96 | 97 | Therefore ScalaDCI tries to determine the instance type to see if a role method shadows one of its methods, and if so throw a compile time error. 98 | 99 | But at runtime we might not be able to determine the instance type and thereby what instance method it has. In that case ScalaDCI falls back on always calling the role method. In this case no error is thrown. 100 | 101 | 102 | ## `self` reference to a Role Player 103 | As an alternative to using the Role name to reference a Role Player we can also use `self`: 104 | ```Scala 105 | role source { 106 | def withdraw { 107 | self.decreaseBalance(amount) 108 | destination.deposit 109 | } 110 | } 111 | 112 | role destination { 113 | def deposit { 114 | self.increaseBalance(amount) 115 | } 116 | } 117 | ``` 118 | or `this`: 119 | ```Scala 120 | role source { 121 | def withdraw { 122 | this.decreaseBalance(amount) 123 | destination.deposit 124 | } 125 | } 126 | 127 | role destination { 128 | def deposit { 129 | this.increaseBalance(amount) 130 | } 131 | } 132 | ``` 133 | 134 | 135 | ## Multiple roles 136 | We can "assign" or "bind" a domain object to several Roles in our Context by simply making 137 | more variables with role names pointing to that object: 138 | ```Scala 139 | @context 140 | class MyContext(someRole: MyData) { 141 | val otherRole = someRole 142 | val localRole = new DummyData() 143 | 144 | someRole.foo() // prints "Hello world" 145 | 146 | role someRole { 147 | def foo() { 148 | someRole.doMyDataStuff() 149 | otherRole.bar() 150 | } 151 | } 152 | 153 | role otherRole { 154 | def bar() { 155 | localRole.say("Hello") 156 | } 157 | } 158 | 159 | role localRole { 160 | def say(s: String) { 161 | println(s + " world") 162 | } 163 | } 164 | } 165 | ``` 166 | As you see in line 3, otherRole is simply a reference pointing to the MyData instance (named someRole). 167 | 168 | Inside each role definition we can still use `self`/`this`. 169 | 170 | We can add as many references/role definitions as we want. This is a way to 171 | allow different Roles of a Use Case each to have their own meaningful namespace for defining their 172 | role-specific behavior / role methods. 173 | 174 | ## How does it work? 175 | In order to have an intuitive syntax like 176 | 177 | ```scala 178 | role roleName { 179 | // role methods... 180 | } 181 | ``` 182 | 183 | for defining a Role and its Role methods we need to make a Scala 184 | contruct that is valid before our macro annotation can start transforming our code: 185 | 186 | ```scala 187 | object role extends Dynamic { 188 | def applyDynamic(obj: Any)(roleBody: => Unit) = roleBody 189 | } 190 | ``` 191 | 192 | Since the `role` object extends the `Dynamic` marker trait and we have defined an 193 | `applyDynamic` method, we can invoke methods with arbitrary method names on the 194 | `role` object. When the compiler find that we are trying to call a method on 195 | `role` that we haven't defined (it doesn't type check), it will rewrite our code 196 | so that it calls `applyDynamic`: 197 | 198 | ```scala 199 | role.foo(args) ~~> role.applyDynamic("foo")(args) 200 | role.bar(args) ~~> role.applyDynamic("bar")(args) 201 | ``` 202 | 203 | For the purpose of DCI we can presume to call a method on `role` that "happens" 204 | to have a Role name: 205 | 206 | ```scala 207 | role.source(args) ~~> role.applyDynamic("source")(args) 208 | role.destination(args) ~~> role.applyDynamic("destination")(args) 209 | ``` 210 | 211 | Scala allow us to replace the `.` with a space and the parentheses with curly 212 | braces: 213 | 214 | ```scala 215 | role source {args} ~~> role.applyDynamic("source")(args) 216 | role destination {args} ~~> role.applyDynamic("destination")(args) 217 | ``` 218 | 219 | You see where we're getting at. Now, the `args` signature in our 220 | `applyDynamic` method has a "by-name" parameter type of `=> Unit` that 221 | allow us to define a block of code that returns nothing: 222 | 223 | ```scala 224 | role source { 225 | doThis 226 | doThat 227 | } 228 | ~~> role.applyDynamic("source")(doThis; doThat) // pseudo code 229 | ``` 230 | 231 | The observant reader will note that "source" given the Dynamic invocation 232 | capability is merely a "free text" name that has no connection to the object 233 | that we have called "source": 234 | 235 | ```scala 236 | val source = new Account(...) // `source` is an object identifier 237 | role source {...} // "source" is a method name 238 | ``` 239 | 240 | In order to enforce that the method name "source" points to the object `source` 241 | our `@context` macro annotation checks that the method name has a corresponding 242 | identifier in the scope of the annotated Context. If it doesn't it won't compile 243 | and the programmer will be noticed of available identifier names (one could have 244 | misspelled the Role name for instance). 245 | 246 | 247 | ## Scala DCI demo application 248 | 249 | In the [Scala DCI Demo App](https://github.com/DCI/scaladci-demo) you can see an example of 250 | how to create a DCI project. 251 | 252 | 253 | ## Using Scala DCI in your project 254 | 255 | ScalaDCI is available for Scala 2.12.2 at [Sonatype](https://oss.sonatype.org/content/repositories/releases/org/scaladci/scaladci_2.12/0.5.6/). 256 | To start coding with DCI in Scala add the following to your SBT build file: 257 | 258 | libraryDependencies ++= Seq( 259 | "org.scaladci" %% "scaladci" % "0.5.6" 260 | ), 261 | addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full) 262 | 263 | 264 | ## Building Scala DCI 265 | ``` 266 | git clone https://github.com/DCI/scaladci.git 267 | 268 | ``` 269 | 270 | Have fun! 271 | 272 | 273 | ### DCI resources 274 | Discussions - [Object-composition](https://groups.google.com/forum/?fromgroups#!forum/object-composition)
275 | Website - [Full-OO](http://fulloo.info)
276 | Wiki - [DCI wiki](http://en.wikipedia.org/wiki/Data,_Context,_and_Interaction) 277 | 278 | ### Credits 279 | Trygve Renskaug and James O. Coplien for inventing and developing DCI. 280 | 281 | Scala DCI solution inspired by
282 | - Risto Välimäki's [post](https://groups.google.com/d/msg/object-composition/ulYGsCaJ0Mg/rF9wt1TV_MIJ) and
283 | - Rune Funch's [Marvin](http://fulloo.info/Examples/Marvin/Introduction/) DCI language 284 | and [Maroon](http://runefs.com/2013/02/14/using-moby-to-do-injectionless-dci-in-ruby/) for Ruby. 285 | -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/ShoppingCart2.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package examples 3 | import org.specs2.mutable.Specification 4 | 5 | /* 6 | Shopping cart example (version 2) - containing implicit logic leaking out 7 | 8 | UC now with primary user actions and secondary system responses - still naming 9 | system responsibilities with Role names... 10 | 11 | Added "desiredProduct" role to see if preventing passing around the product id 12 | of the marked product makes sense. 13 | 14 | New trigger method names prevent implicit logic leaking out of the Context. 15 | 16 | See discussion at: 17 | https://groups.google.com/forum/?fromgroups=#!topic/object-composition/JJiLWBsZWu0 18 | 19 | =========================================================================== 20 | USE CASE: Place Order [user-goal] 21 | 22 | Person browsing around finds product(s) in a web shop that he/she wants to buy. 23 | 24 | Primary actor.. Web Customer ("Customer") 25 | Scope.......... Web Shop ("Shop") 26 | Preconditions.. Shop presents Product(s) to Customer 27 | Trigger........ Customer wants to buy certain Product(s) 28 | 29 | Main Success Scenario 30 | --------------------------------------------------------------------------- 31 | 1. Customer marks Desired Product in Shop 32 | - Shop reserves Product in warehouse 33 | - Shop adds Item to Cart (can repeat from step 1) 34 | - Shop shows updated contents of Cart to Customer 35 | 2. Customer requests to review Order 36 | - Shop presents Cart with Items and prices to Customer 37 | 3. Customer pays Order 38 | - System confirms sufficient funds are available 39 | - System initiates transfer of funds 40 | - System informs Warehouse to ship Products 41 | - Shop confirms purchase to Customer 42 | 43 | Deviations 44 | --------------------------------------------------------------------------- 45 | 1a. Product is out of stock: 46 | 1. Shop informs customer that Product is out of stock. 47 | 48 | 1b. Customer has gold membership: 49 | 1. Shop adds discounted product to Cart. 50 | 51 | 3a. Customer has insufficient funds to pay Order: 52 | 1. Shop informs Customer of insufficient funds on credit card. 53 | a. Customer removes unaffordable Item(s) from Cart: 54 | 1. Go to step 3. 55 | =========================================================================== 56 | */ 57 | 58 | // (Same Data model as ShoppingCart1) 59 | 60 | class ShoppingCart2 extends Specification { 61 | import ShoppingCartModel._ 62 | 63 | { 64 | @context 65 | class PlaceOrder(company: Company, person: Person) { 66 | 67 | // Context house-keeping 68 | private var desiredProductId: Int = _ 69 | 70 | // Trigger methods matching the main success scenario steps 71 | def customerMarksDesiredProductInshop(productId: Int): Option[Product] = { 72 | desiredProductId = productId 73 | customer.markDesiredProductInshop 74 | } 75 | def customerRequestsToReviewOrder: Seq[(Int, Product)] = customer.reviewOrder 76 | def customerPaysOrder: Boolean = customer.payOrder 77 | def customerRemovesProductFromCart(productId: Int): Option[Product] = 78 | customer.removeProductFromCart(productId) 79 | 80 | 81 | // Roles 82 | private val customer = person 83 | private val shop = company 84 | private val warehouse = company 85 | private val cart = Order(customer) 86 | 87 | role customer { 88 | def markDesiredProductInshop = shop.addProductToOrder 89 | def reviewOrder = cart.getItems 90 | def removeProductFromCart(productId: Int) = cart.removeItem(productId: Int) 91 | def payOrder = shop.processOrder 92 | def availableFunds = customer.cash 93 | def withDrawFunds(amountToPay: Int) { customer.cash -= amountToPay } 94 | } 95 | 96 | role shop { 97 | def addProductToOrder: Option[Product] = { 98 | if (!warehouse.hasDesiredProduct) 99 | return None 100 | val product = warehouse.reserveDesiredProduct 101 | val discountedPrice = shop.discountPriceOf(product) 102 | val desiredProduct = product.copy(price = discountedPrice) 103 | cart.customerMarksDesiredProductInshop(desiredProductId, desiredProduct) 104 | Some(desiredProduct) 105 | } 106 | def processOrder: Boolean = { 107 | val orderTotal = cart.total 108 | if (orderTotal > customer.availableFunds) 109 | return false 110 | 111 | customer.withDrawFunds(orderTotal) 112 | shop.depositFunds(orderTotal) 113 | 114 | customer.owns ++= cart.items 115 | cart.items foreach (warehouse.stock remove _._1) 116 | true 117 | } 118 | def discountPriceOf(product: Product) = { 119 | val customerIsGoldMember = shop.goldMembers contains customer 120 | val discountFactor = if (customerIsGoldMember) goldMemberReduction else 1 121 | (product.price * discountFactor).toInt 122 | } 123 | def goldMemberReduction = 0.5 124 | def customerIsGoldMember = shop.goldMembers contains customer 125 | def depositFunds(amount: Int) { shop.cash += amount } 126 | } 127 | 128 | role warehouse { 129 | def hasDesiredProduct = shop.stock.isDefinedAt(desiredProductId) 130 | def reserveDesiredProduct = shop.stock(desiredProductId) // dummy reservation 131 | } 132 | 133 | role cart { 134 | def customerMarksDesiredProductInshop(productId: Int, product: Product) { 135 | cart.items.put(productId, product) 136 | } 137 | def removeItem(productId: Int): Option[Product] = { 138 | if (!cart.items.isDefinedAt(productId)) 139 | return None 140 | cart.items.remove(productId) 141 | } 142 | def getItems = cart.items.toIndexedSeq.sortBy(_._1) 143 | def total = cart.items.map(_._2.price).sum 144 | } 145 | } 146 | 147 | 148 | /* 149 | Test various scenarios. 150 | 151 | Basically copy and paste of ShoppingCart1 test with some 152 | trigger method names and UC steps changed/removed. 153 | 154 | Note how the more expressive trigger method names make 155 | commenting less needed. 156 | */ 157 | 158 | "Main success scenario" in new ShoppingCart { 159 | 160 | // Initial status (same for all tests...) 161 | shop.stock === Map(tires, wax, bmw) 162 | shop.cash === 100000 163 | customer.cash === 20000 164 | customer.owns === Map() 165 | 166 | val order = new PlaceOrder(shop, customer) 167 | 168 | // customer wants wax and tires 169 | order.customerMarksDesiredProductInshop(p1) 170 | order.customerMarksDesiredProductInshop(p2) 171 | 172 | order.customerRequestsToReviewOrder === Seq(wax, tires) 173 | 174 | val orderCompleted = order.customerPaysOrder === true 175 | 176 | shop.stock === Map(bmw) 177 | shop.cash === 100000 + 40 + 600 178 | customer.cash === 20000 - 40 - 600 179 | customer.owns === Map(tires, wax) 180 | } 181 | 182 | "Product out of stock" in new ShoppingCart { 183 | 184 | // Wax out of stock 185 | shop.stock.remove(p1) 186 | shop.stock === Map(tires, bmw) 187 | 188 | val order = new PlaceOrder(shop, customer) 189 | 190 | // customer wants wax 191 | val itemAdded = order.customerMarksDesiredProductInshop(p1) === None 192 | order.customerRequestsToReviewOrder === Seq() 193 | 194 | order.customerMarksDesiredProductInshop(p2) 195 | 196 | val orderCompleted = order.customerPaysOrder === true 197 | 198 | shop.stock === Map(bmw) 199 | shop.cash === 100000 + 600 200 | customer.cash === 20000 - 600 201 | customer.owns === Map(tires) 202 | } 203 | 204 | "customer has gold membership" in new ShoppingCart { 205 | 206 | // customer is gold member 207 | shop.goldMembers.add(customer) 208 | shop.goldMembers.contains(customer) === true 209 | 210 | val order = new PlaceOrder(shop, customer) 211 | 212 | order.customerMarksDesiredProductInshop(p1) 213 | 214 | val discountedWax = 1 -> Product("Wax", (40 * 0.5).toInt) 215 | order.customerRequestsToReviewOrder === Seq(discountedWax) 216 | 217 | val orderCompleted = order.customerPaysOrder === true 218 | 219 | shop.stock === Map(tires, bmw) 220 | shop.cash === 100000 + 20 221 | customer.cash === 20000 - 20 222 | customer.owns === Map(discountedWax) 223 | } 224 | 225 | "customer has too low credit" in new ShoppingCart { 226 | 227 | val order = new PlaceOrder(shop, customer) 228 | 229 | // customer wants a BMW 230 | val itemAdded = order.customerMarksDesiredProductInshop(p3) 231 | 232 | // Any product is added - shop doesn't yet know if customer can afford it 233 | itemAdded === Some(bmw._2) 234 | order.customerRequestsToReviewOrder === Seq(bmw) 235 | 236 | // customer tries to pay order 237 | val paymentStatus = order.customerPaysOrder 238 | 239 | // shop informs customer of too low credit 240 | paymentStatus === false 241 | 242 | // customer removes unaffordable BMW from cart 243 | order.customerRemovesProductFromCart(p3) 244 | 245 | // customer aborts shopping and no purchases are made 246 | shop.stock === Map(tires, wax, bmw) 247 | shop.cash === 100000 248 | customer.cash === 20000 249 | customer.owns === Map() 250 | } 251 | 252 | "All deviations in play" in new ShoppingCart { 253 | 254 | // Tires out of stock 255 | shop.stock.remove(p2) 256 | shop.stock === Map(wax, bmw) 257 | 258 | // We have a gold member 259 | shop.goldMembers.add(customer) 260 | 261 | val order = new PlaceOrder(shop, customer) 262 | 263 | // Let's get some tires 264 | val tiresItemAdded = order.customerMarksDesiredProductInshop(p2) 265 | 266 | // Product out of stock! 267 | shop.stock.contains(p2) === false 268 | 269 | // Nothing added to order yet 270 | tiresItemAdded === None 271 | order.customerRequestsToReviewOrder === Seq() 272 | 273 | // Let's buy the BMW instead. As a gold member that should be possible! 274 | val bmwItemAdded = order.customerMarksDesiredProductInshop(p3) 275 | 276 | // Discounted BMW is added to order 277 | val discountedBMW = Product("BMW", (50000 * 0.5).toInt) 278 | bmwItemAdded === Some(discountedBMW) 279 | order.customerRequestsToReviewOrder === Seq(p3 -> discountedBMW) 280 | 281 | // Ouch! We couldn't afford it. 282 | val paymentAttempt1 = order.customerPaysOrder === false 283 | 284 | // It's still 5000 too much for us, even with the membership discount 285 | discountedBMW.price - customer.cash === 5000 286 | 287 | // Ok, no new car today 288 | order.customerRemovesProductFromCart(p3) 289 | 290 | // Order is back to empty 291 | order.customerRequestsToReviewOrder === Seq() 292 | 293 | // Let's get some wax anyway... 294 | val waxItemAdded = order.customerMarksDesiredProductInshop(p1) 295 | 296 | // Did we get our membership discount on this one? 297 | val discountedWax = Product("Wax", (40 * 0.5).toInt) 298 | waxItemAdded === Some(discountedWax) 299 | 300 | // Now we can afford it! 301 | val paymentAttempt2 = order.customerPaysOrder === true 302 | 303 | // Not much shopping done Today. At least we got some cheap wax. 304 | shop.stock === Map(bmw) 305 | shop.cash === 100000 + 20 306 | customer.cash === 20000 - 20 307 | customer.owns === Map(p1 -> discountedWax) 308 | } 309 | } 310 | } -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/ShoppingCart6.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package examples 3 | import org.specs2.mutable.Specification 4 | 5 | /* 6 | Shopping cart example (version 6) - Distributed DCI model 7 | 8 | This version tries out a distributed model where each Role method calls another 9 | Role method until a part of the UC is accomplished. Trygve ReenSkaug describes this 10 | approach here: 11 | https://groups.google.com/forum/#!msg/object-composition/8Qe00Vt3MPc/4v9Wca3NSHIJ 12 | 13 | Compare this to version 5a (the DCI-ish mediator version) to see the difference 14 | between the two approaches! 15 | 16 | See discussion at: 17 | https://groups.google.com/forum/?fromgroups=#!topic/object-composition/JJiLWBsZWu0 18 | 19 | =========================================================================== 20 | USE CASE: Place Order [user-goal] 21 | 22 | Person browsing around finds product(s) in a web shop that he/she wants to buy. 23 | 24 | Primary actor.. Web customer ("Customer") 25 | Scope.......... Web shop ("Shop") 26 | Preconditions.. shop presents product(s) to customer 27 | Trigger........ customer wants to buy certain product(s) 28 | 29 | A "shopping cart" is a virtual/visual representation of a potential Order in the UI. 30 | We therefore loosely treat "Order" as synonymous to "cart". 31 | 32 | Main Success Scenario 33 | --------------------------------------------------------------------------- 34 | 1. Customer selects desired Product [can repeat] 35 | .1 Warehouse confirms Product availability 36 | .2 Customer Department provides eligible Customer discount factor for Product 37 | .3 System adds Product with qualified price to Cart 38 | .4 UI shows updated content of Cart to Customer 39 | 2. Customer requests to review Order 40 | .1 System collects Cart items 41 | .2 UI shows content of Cart to Customer 42 | 3. Ccustomer requests to pay Order 43 | .1 Payment Gateway confirms Customer has sufficient funds available 44 | .2 Payment Gateway initiates transfer of funds to Company 45 | .3 Wwarehouse prepare Products for shipment to Customer 46 | .4 UI confirms purchase to Customer 47 | 48 | Deviations 49 | --------------------------------------------------------------------------- 50 | 1a. Product is out of stock: 51 | 1. UI informs Customer that Product is out of stock. 52 | 53 | 1b. Customer has gold membership: 54 | 1. System adds discounted product to Cart. 55 | 56 | 3a. Customer has insufficient funds to pay Order: 57 | 1. UI informs Customer of insufficient funds available. 58 | a. Customer removes unaffordable Product from Cart 59 | 1. System updates content of Cart 60 | 1. UI shows updated content of Cart to Customer 61 | =========================================================================== 62 | */ 63 | 64 | class ShoppingCart6 extends Specification { 65 | import ShoppingCartModel._ 66 | 67 | { 68 | @context 69 | class PlaceOrder(company: Company, customer: Person) { 70 | 71 | // Context state ("Methodless roles"?) 72 | private var eligibleDiscountFactor = 1.0 73 | private var desiredProductId = 0 74 | 75 | // Trigger methods 76 | def processProductSelection(desiredProdId: Int): Option[Product] = { 77 | desiredProductId = desiredProdId 78 | 79 | // Step 1.1 Initiate first interaction... 80 | warehouse.confirmAvailability(desiredProdId) 81 | } 82 | 83 | def getOrderDetails: Seq[(Int, Product)] = cart.getItems 84 | 85 | def processPayment: Boolean = paymentGateway.confirmSufficientFunds 86 | 87 | def processProductRemoval(productId: Int): Option[Product] = cart.removeItem(productId) 88 | 89 | // Roles (in order of "appearance") 90 | private val warehouse = company 91 | private val customerDepartment = company 92 | private val paymentGateway = company 93 | private val companyAccount = company 94 | private val cart = Order(customer) 95 | 96 | role warehouse { 97 | def confirmAvailability(productId: Int): Option[Product] = { 98 | if (!warehouse.has(desiredProductId)) 99 | return None 100 | 101 | // Step 1.2 Second interaction... 102 | customerDepartment.calculateEligibleDiscountFactor 103 | } 104 | def has(productId: Int) = warehouse.stock.isDefinedAt(productId) 105 | def get(productId: Int) = warehouse.stock(productId) 106 | def shipProducts = { 107 | customer.owns ++= cart.items 108 | cart.items.foreach(i => warehouse.stock.remove(i._1)) 109 | true // dummy confirmation 110 | } 111 | } 112 | 113 | role customerDepartment { 114 | def calculateEligibleDiscountFactor = { 115 | if (customer.isGoldMember) eligibleDiscountFactor = 0.5 116 | 117 | // Step 1.3 Third interaction... 118 | cart.addItem 119 | } 120 | } 121 | 122 | role customer { 123 | def withdrawFunds(amountToPay: Int) { customer.cash -= amountToPay } 124 | def receiveProducts(products: Seq[(Int, Product)]) { customer.owns ++= products } 125 | def isGoldMember = customerDepartment.goldMembers.contains(customer) 126 | } 127 | 128 | role cart { 129 | def addItem = { 130 | val product = warehouse.get(desiredProductId) 131 | val qualifiedPrice = (product.price * eligibleDiscountFactor).toInt 132 | val qualifiedProduct = product.copy(price = qualifiedPrice) 133 | cart.items.put(desiredProductId, qualifiedProduct) 134 | Some(qualifiedProduct) 135 | } 136 | def removeItem(productId: Int): Option[Product] = { 137 | if (!cart.items.isDefinedAt(productId)) 138 | return None 139 | cart.items.remove(productId) 140 | } 141 | def getItems = cart.items.toIndexedSeq.sortBy(_._1) 142 | def total = cart.items.map(_._2.price).sum 143 | } 144 | 145 | role paymentGateway { 146 | // Step 3.1 147 | def confirmSufficientFunds: Boolean = { 148 | if (customer.cash < cart.total) return false 149 | 150 | // Step 3.2 151 | initiateOrderPayment 152 | } 153 | def initiateOrderPayment: Boolean = { 154 | val amount = cart.total 155 | customer.withdrawFunds(amount) 156 | companyAccount.depositFunds(amount) 157 | 158 | // Step 3.3 159 | warehouse.shipProducts 160 | } 161 | } 162 | 163 | role companyAccount { 164 | def depositFunds(amount: Int) { self.cash += amount } 165 | } 166 | } 167 | 168 | 169 | // Test various scenarios. 170 | // (copy and paste of ShoppingCart4a tests) 171 | 172 | "Main success scenario" in new ShoppingCart { 173 | 174 | // Initial status (same for all tests...) 175 | shop.stock === Map(tires, wax, bmw) 176 | shop.cash === 100000 177 | customer.cash === 20000 178 | customer.owns === Map() 179 | 180 | val order = new PlaceOrder(shop, customer) 181 | 182 | // customer wants wax and tires 183 | order.processProductSelection(p1) 184 | order.processProductSelection(p2) 185 | 186 | order.getOrderDetails === Seq(wax, tires) 187 | 188 | val orderCompleted = order.processPayment === true 189 | 190 | shop.stock === Map(bmw) 191 | shop.cash === 100000 + 40 + 600 192 | customer.cash === 20000 - 40 - 600 193 | customer.owns === Map(tires, wax) 194 | } 195 | 196 | "Product out of stock" in new ShoppingCart { 197 | 198 | // Wax out of stock 199 | shop.stock.remove(p1) 200 | shop.stock === Map(tires, bmw) 201 | 202 | val order = new PlaceOrder(shop, customer) 203 | 204 | // customer wants wax 205 | val itemAdded = order.processProductSelection(p1) === None 206 | order.getOrderDetails === Seq() 207 | 208 | order.processProductSelection(p2) 209 | 210 | val orderCompleted = order.processPayment === true 211 | 212 | shop.stock === Map(bmw) 213 | shop.cash === 100000 + 600 214 | customer.cash === 20000 - 600 215 | customer.owns === Map(tires) 216 | } 217 | 218 | "customer has gold membership" in new ShoppingCart { 219 | 220 | // customer is gold member 221 | shop.goldMembers.add(customer) 222 | shop.goldMembers.contains(customer) === true 223 | 224 | val order = new PlaceOrder(shop, customer) 225 | 226 | order.processProductSelection(p1) 227 | 228 | val discountedWax = 1 -> Product("Wax", (40 * 0.5).toInt) 229 | order.getOrderDetails === Seq(discountedWax) 230 | 231 | val orderCompleted = order.processPayment === true 232 | 233 | shop.stock === Map(tires, bmw) 234 | shop.cash === 100000 + 20 235 | customer.cash === 20000 - 20 236 | customer.owns === Map(discountedWax) 237 | } 238 | 239 | "customer has too low credit" in new ShoppingCart { 240 | 241 | val order = new PlaceOrder(shop, customer) 242 | 243 | // customer wants a BMW 244 | val itemAdded = order.processProductSelection(p3) 245 | 246 | // Any product is added - shop doesn't yet know if customer can afford it 247 | itemAdded === Some(bmw._2) 248 | order.getOrderDetails === Seq(bmw) 249 | 250 | // customer tries to pay order 251 | val paymentStatus = order.processPayment 252 | 253 | // shop informs customer of too low credit 254 | paymentStatus === false 255 | 256 | // customer removes unaffordable BMW from cart 257 | order.processProductRemoval(p3) 258 | 259 | // customer aborts shopping and no purchases are made 260 | shop.stock === Map(tires, wax, bmw) 261 | shop.cash === 100000 262 | customer.cash === 20000 263 | customer.owns === Map() 264 | } 265 | 266 | "All deviations in play" in new ShoppingCart { 267 | 268 | // Tires out of stock 269 | shop.stock.remove(p2) 270 | shop.stock === Map(wax, bmw) 271 | 272 | // We have a gold member 273 | shop.goldMembers.add(customer) 274 | 275 | val order = new PlaceOrder(shop, customer) 276 | 277 | // Let's get some tires 278 | val tiresItemAdded = order.processProductSelection(p2) 279 | 280 | // Product out of stock! 281 | shop.stock.contains(p2) === false 282 | 283 | // Nothing added to order yet 284 | tiresItemAdded === None 285 | order.getOrderDetails === Seq() 286 | 287 | // Let's buy the BMW instead. As a gold member that should be possible! 288 | val bmwItemAdded = order.processProductSelection(p3) 289 | 290 | // Discounted BMW is added to order 291 | val discountedBMW = Product("BMW", (50000 * 0.5).toInt) 292 | bmwItemAdded === Some(discountedBMW) 293 | order.getOrderDetails === Seq(p3 -> discountedBMW) 294 | 295 | // Ouch! We couldn't afford it. 296 | val paymentAttempt1 = order.processPayment === false 297 | 298 | // It's still 5000 too much for us, even with the membership discount 299 | discountedBMW.price - customer.cash === 5000 300 | 301 | // Ok, no new car today 302 | order.processProductRemoval(p3) 303 | 304 | // Order is back to empty 305 | order.getOrderDetails === Seq() 306 | 307 | // Let's get some wax anyway... 308 | val waxItemAdded = order.processProductSelection(p1) 309 | 310 | // Did we get our membership discount on this one? 311 | val discountedWax = Product("Wax", (40 * 0.5).toInt) 312 | waxItemAdded === Some(discountedWax) 313 | 314 | // Now we can afford it! 315 | val paymentAttempt2 = order.processPayment === true 316 | 317 | // Not much shopping done Today. At least we got some cheap wax. 318 | shop.stock === Map(bmw) 319 | shop.cash === 100000 + 20 320 | customer.cash === 20000 - 20 321 | customer.owns === Map(p1 -> discountedWax) 322 | } 323 | } 324 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /examples/src/test/scala/scaladci/examples/ShoppingCart5.scala: -------------------------------------------------------------------------------- 1 | package scaladci 2 | package examples 3 | import org.specs2.mutable.Specification 4 | 5 | /* 6 | Shopping cart example (version 5) - (Non-DCI?) Centralized Mediator model 7 | 8 | This version re-introduces more Roles reflecting the participants of a richer mental 9 | model drawn from both the user and application programmers mental models. 10 | 11 | Each UC step is a User action followed by a series of system actions with the last 12 | action returning some data or status to the UI. 13 | 14 | Each trigger method in the Context is named after the overall description of those 15 | system response actions rather than the User action/step that initiates them as we 16 | had in earlier versions. 17 | 18 | Each trigger method body contains all the system response actions as a centralized 19 | algorithm resembling the Mediator pattern. Trygve Reenskaug has a great explanation 20 | here of how this approach differs from DCI: 21 | https://groups.google.com/forum/#!msg/object-composition/8Qe00Vt3MPc/4v9Wca3NSHIJ 22 | 23 | Side note: Changed "marks" to "selects" in UC step 1 since the user might as well 24 | "click", "drag" "tell" etc - all UI words representing the same intention of selection. 25 | We have also already defined the shop as our scope. So we don't need to write where 26 | the customer is selecting ("customer selects desired product _in shop_"). 27 | 28 | See discussion at: 29 | https://groups.google.com/forum/?fromgroups=#!topic/object-composition/JJiLWBsZWu0 30 | 31 | =========================================================================== 32 | USE CASE: Place Order [user-goal] 33 | 34 | Person browsing around finds product(s) in a web shop that he/she wants to buy. 35 | 36 | Primary actor.. Web customer ("Customer") 37 | Scope.......... Web shop ("Shop") 38 | Preconditions.. Shop presents product(s) to customer 39 | Trigger........ Customer wants to buy certain product(s) 40 | 41 | A "shopping cart" is a virtual/visual representation of a potential Order in the UI. 42 | We therefore loosely treat "Order" as synonymous to "cart". 43 | 44 | Main Success Scenario 45 | --------------------------------------------------------------------------- 46 | 1. Customer selects desired Product [can repeat] 47 | - Warehouse confirms Product availability 48 | - Customer Department provides eligible Customer discount factor for Product 49 | - System adds Product with qualified price to Cart 50 | - UI shows updated content of Cart to Customer 51 | 2. Customer requests to review Order 52 | - System collects Cart items 53 | - UI shows content of Cart to Customer 54 | 3. Customer requests to pay Order 55 | - Payment Gateway confirms Customer has sufficient funds available 56 | - Payment Gateway initiates transfer of funds to Company 57 | - Warehouse prepare Products for shipment to Customer 58 | - UI confirms purchase to Customer 59 | 60 | Deviations 61 | --------------------------------------------------------------------------- 62 | 1a. Product is out of stock: 63 | 1. UI informs Customer that Product is out of stock. 64 | 65 | 1b. Customer has gold membership: 66 | 1. System adds discounted Product to Cart. 67 | 68 | 3a. Customer has insufficient funds to pay Order: 69 | 1. UI informs Customer of insufficient funds available. 70 | a. Customer removes unaffordable Product from cart 71 | 1. System updates content of Cart 72 | 1. UI shows updated content of Cart to Customer 73 | =========================================================================== 74 | */ 75 | 76 | class ShoppingCart5 extends Specification { 77 | import ShoppingCartModel._ 78 | 79 | { 80 | @context 81 | class PlaceOrder(company: Company, customer: Person) { 82 | 83 | // Trigger methods 84 | def processProductSelection(desiredProductId: Int): Option[Product] = { 85 | if (!warehouse.has(desiredProductId)) 86 | return None 87 | 88 | val discountFactor = customerDepartment.calculateEligibleDiscountFactor 89 | 90 | val product = warehouse.get(desiredProductId) 91 | val qualifiedPrice = (product.price * discountFactor).toInt 92 | val qualifiedProduct = product.copy(price = qualifiedPrice) 93 | 94 | cart.addItem(desiredProductId, qualifiedProduct) 95 | 96 | Some(qualifiedProduct) 97 | } 98 | 99 | def getOrderDetails: Seq[(Int, Product)] = cart.getItems 100 | 101 | def processPayment: Boolean = { 102 | if (!paymentGateway.confirmSufficientFunds) return false 103 | if (!paymentGateway.initiateOrderPayment) return false 104 | warehouse.shipProducts 105 | } 106 | 107 | def processProductRemoval(productId: Int): Option[Product] = { 108 | cart.removeItem(productId) 109 | } 110 | 111 | // Roles (in order of "appearance") 112 | private val warehouse = company 113 | private val customerDepartment = company 114 | private val paymentGateway = company 115 | private val companyAccount = company 116 | private val cart = Order(customer) 117 | 118 | role warehouse { 119 | def has(productId: Int) = warehouse.stock.isDefinedAt(productId) 120 | def get(productId: Int) = warehouse.stock(productId) 121 | def shipProducts = { 122 | customer.owns ++= cart.items 123 | cart.items.foreach(i => warehouse.stock.remove(i._1)) 124 | true // dummy delivery confirmation 125 | } 126 | } 127 | 128 | role customerDepartment { 129 | def calculateEligibleDiscountFactor = if (customer.isGoldMember) 0.5 else 1 130 | } 131 | 132 | role customer { 133 | def withdrawFunds(amountToPay: Int) { customer.cash -= amountToPay } 134 | def receiveProducts(products: Seq[(Int, Product)]) { customer.owns ++= products } 135 | def isGoldMember = customerDepartment.goldMembers.contains(customer) 136 | } 137 | 138 | role cart { 139 | def addItem(productId: Int, product: Product) { 140 | cart.items.put(productId, product) 141 | } 142 | def removeItem(productId: Int): Option[Product] = { 143 | if (!cart.items.isDefinedAt(productId)) 144 | return None 145 | cart.items.remove(productId) 146 | } 147 | def getItems = cart.items.toIndexedSeq.sortBy(_._1) 148 | def total = cart.items.map(_._2.price).sum 149 | } 150 | 151 | role paymentGateway { 152 | def confirmSufficientFunds = customer.cash >= cart.total 153 | def initiateOrderPayment = { 154 | val amount = cart.total 155 | customer.withdrawFunds(amount) 156 | companyAccount.depositFunds(amount) 157 | true // dummy transaction success 158 | } 159 | } 160 | 161 | role companyAccount { 162 | def depositFunds(amount: Int) { self.cash += amount } 163 | } 164 | } 165 | 166 | 167 | // Test various scenarios. 168 | // (copy and paste of ShoppingCart4 tests with trigger method name changes) 169 | 170 | "Main success scenario" in new ShoppingCart { 171 | 172 | // Initial status (same for all tests...) 173 | shop.stock === Map(tires, wax, bmw) 174 | shop.cash === 100000 175 | customer.cash === 20000 176 | customer.owns === Map() 177 | 178 | val order = new PlaceOrder(shop, customer) 179 | 180 | // customer wants wax and tires 181 | order.processProductSelection(p1) 182 | order.processProductSelection(p2) 183 | 184 | order.getOrderDetails === Seq(wax, tires) 185 | 186 | val orderCompleted = order.processPayment === true 187 | 188 | shop.stock === Map(bmw) 189 | shop.cash === 100000 + 40 + 600 190 | customer.cash === 20000 - 40 - 600 191 | customer.owns === Map(tires, wax) 192 | } 193 | 194 | "Product out of stock" in new ShoppingCart { 195 | 196 | // Wax out of stock 197 | shop.stock.remove(p1) 198 | shop.stock === Map(tires, bmw) 199 | 200 | val order = new PlaceOrder(shop, customer) 201 | 202 | // customer wants wax 203 | val itemAdded = order.processProductSelection(p1) === None 204 | order.getOrderDetails === Seq() 205 | 206 | order.processProductSelection(p2) 207 | 208 | val orderCompleted = order.processPayment === true 209 | 210 | shop.stock === Map(bmw) 211 | shop.cash === 100000 + 600 212 | customer.cash === 20000 - 600 213 | customer.owns === Map(tires) 214 | } 215 | 216 | "customer has gold membership" in new ShoppingCart { 217 | 218 | // customer is gold member 219 | shop.goldMembers.add(customer) 220 | shop.goldMembers.contains(customer) === true 221 | 222 | val order = new PlaceOrder(shop, customer) 223 | 224 | order.processProductSelection(p1) 225 | 226 | val discountedWax = 1 -> Product("Wax", (40 * 0.5).toInt) 227 | order.getOrderDetails === Seq(discountedWax) 228 | 229 | val orderCompleted = order.processPayment === true 230 | 231 | shop.stock === Map(tires, bmw) 232 | shop.cash === 100000 + 20 233 | customer.cash === 20000 - 20 234 | customer.owns === Map(discountedWax) 235 | } 236 | 237 | "customer has too low credit" in new ShoppingCart { 238 | 239 | val order = new PlaceOrder(shop, customer) 240 | 241 | // customer wants a BMW 242 | val itemAdded = order.processProductSelection(p3) 243 | 244 | // Any product is added - shop doesn't yet know if customer can afford it 245 | itemAdded === Some(bmw._2) 246 | order.getOrderDetails === Seq(bmw) 247 | 248 | // customer tries to pay order 249 | val paymentStatus = order.processPayment 250 | 251 | // shop informs customer of too low credit 252 | paymentStatus === false 253 | 254 | // customer removes unaffordable BMW from cart 255 | order.processProductRemoval(p3) 256 | 257 | // customer aborts shopping and no purchases are made 258 | shop.stock === Map(tires, wax, bmw) 259 | shop.cash === 100000 260 | customer.cash === 20000 261 | customer.owns === Map() 262 | } 263 | 264 | "All deviations in play" in new ShoppingCart { 265 | 266 | // Tires out of stock 267 | shop.stock.remove(p2) 268 | shop.stock === Map(wax, bmw) 269 | 270 | // We have a gold member 271 | shop.goldMembers.add(customer) 272 | 273 | val order = new PlaceOrder(shop, customer) 274 | 275 | // Let's get some tires 276 | val tiresItemAdded = order.processProductSelection(p2) 277 | 278 | // Product out of stock! 279 | shop.stock.contains(p2) === false 280 | 281 | // Nothing added to order yet 282 | tiresItemAdded === None 283 | order.getOrderDetails === Seq() 284 | 285 | // Let's buy the BMW instead. As a gold member that should be possible! 286 | val bmwItemAdded = order.processProductSelection(p3) 287 | 288 | // Discounted BMW is added to order 289 | val discountedBMW = Product("BMW", (50000 * 0.5).toInt) 290 | bmwItemAdded === Some(discountedBMW) 291 | order.getOrderDetails === Seq(p3 -> discountedBMW) 292 | 293 | // Ouch! We couldn't afford it. 294 | val paymentAttempt1 = order.processPayment === false 295 | 296 | // It's still 5000 too much for us, even with the membership discount 297 | discountedBMW.price - customer.cash === 5000 298 | 299 | // Ok, no new car today 300 | order.processProductRemoval(p3) 301 | 302 | // Order is back to empty 303 | order.getOrderDetails === Seq() 304 | 305 | // Let's get some wax anyway... 306 | val waxItemAdded = order.processProductSelection(p1) 307 | 308 | // Did we get our membership discount on this one? 309 | val discountedWax = Product("Wax", (40 * 0.5).toInt) 310 | waxItemAdded === Some(discountedWax) 311 | 312 | // Now we can afford it! 313 | val paymentAttempt2 = order.processPayment === true 314 | 315 | // Not much shopping done Today. At least we got some cheap wax. 316 | shop.stock === Map(bmw) 317 | shop.cash === 100000 + 20 318 | customer.cash === 20000 - 20 319 | customer.owns === Map(p1 -> discountedWax) 320 | } 321 | } 322 | } --------------------------------------------------------------------------------