DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
Securing Your Software Supply Chain with JFrog and Azure
Register Today

Trending

  • A Comprehensive Guide To Testing and Debugging AWS Lambda Functions
  • How To Use Geo-Partitioning to Comply With Data Regulations and Deliver Low Latency Globally
  • Manifold vs. Lombok: Enhancing Java With Property Support
  • Hiding Data in Cassandra

Trending

  • A Comprehensive Guide To Testing and Debugging AWS Lambda Functions
  • How To Use Geo-Partitioning to Comply With Data Regulations and Deliver Low Latency Globally
  • Manifold vs. Lombok: Enhancing Java With Property Support
  • Hiding Data in Cassandra
  1. DZone
  2. Coding
  3. Languages
  4. Using Scala traits as modules, or the "Thin Cake" Pattern

Using Scala traits as modules, or the "Thin Cake" Pattern

Adam Warski user avatar by
Adam Warski
·
Mar. 01, 14 · Interview
Like (0)
Save
Tweet
Share
15.10K Views

Join the DZone community and get the full member experience.

Join For Free

I would like to describe a pure-Scala approach to modularity that we are successfully using in a couple of our Scala projects.

But let’s start with how we do Dependency Injection (see also my other blogs). Each class can have dependencies in the form of constructor parameters, e.g.:

class WheatField
class Mill(wheatField: wheatField)
class CowPasture
class DiaryFarm(cowPasture: CowPasture)
class Bakery(mill: Mill, dairyFarm: DairyFarm)

At the “end of the world”, there is a main class which runs the application and where the whole object graph is created:

object BakeMeCake extends App {
     // creating the object graph
     lazy val wheatField = new WheatField()
     lazy val mill = new Mill(wheatField)     
     lazy val cowPasture = new CowPasture()
     lazy val diaryFarm = new DiaryFarm(cowPasture)
     lazy val bakery = new Bakery(mill, dairyFarm)
 
     // using the object graph
     val cake = bakery.bakeCake()
     me.eat(cake)
}

The wiring can be done manually, or e.g. using MacWire.

Note that we can do scoping using Scala constructs: a lazy val corresponds to a singleton object (in the constructed object graph), a def to a dependent-scoped object (a new instance will be created for each usage).

Thin Cake pattern

What if the object graph, and at the same time the main class, becomes large? The answer is simple: we have to break it into pieces, which will be the “modules”. Each module is a Scala trait, and contains some part of the object graph.

For example:

trait CropModule {
     lazy val wheatField = new WheatField()
     lazy val mill = new Mill(wheatField)     
} 
 
trait LivestockModule {
     lazy val cowPasture = new CowPasture()
     lazy val diaryFarm = new DiaryFarm(cowPasture)
}

The main object then becomes a composition of traits. This is exactly what also happens in the Cake Pattern. However here we are using only one element of it, hence the “Think Cake” Pattern name.

object BakeMeCake extends CropModule with LivestockModule {
     lazy val bakery = new Bakery(mill, dairyFarm)
 
     val cake = bakery.bakeCake()
     me.eat(cake) 
}

If you have ever used Google Guice, you may see a similarity: trait-modules directly correspond to Guice modules. However, here we gain the additional type-safety and compile-time checking that dependency requirements for all classes are met.

Of course, the module trait can contain more than just new object instantiations, however you have to be cautious not to put too much logic in there – at some point you probably need to extract a class. Typical code that also goes into modules is e.g. new actor creation code and setting up caches.

Dependencies

What if our trait modules have inter-module dependencies? There are two ways we can deal with that problem.

The first is abstract members. If there’s an instance of a class that is needed in our module, we can simply define it as an abstract member of the trait-module. This abstract member has to be then implemented in some other module with which our module gets composed in the end. Using a consistent naming convention helps here. The fact that all abstract dependencies are defined at some point is checked by the compiler.

The second way is composition. If we e.g. want to create a bigger module out of three smaller modules, we can simply extend the other module-traits, and due to the way inheritance works we can use all of the objects defined there.

Putting the two methods together we get for example:

// composition: bakery depends on crop and livestock modules
trait BakeryModule extends CropModule with LivestockModule {
     lazy val bakery = new Bakery(mill, dairyFarm)
}   
 
// abstract member: we need a bakery
trait CafeModule {
     lazy val espressoMachine = new EspressoMachine()
     lazy val cafe = new Cafe(bakery, espressoMachine)
 
     def bakery: Bakery
}
 
// the abstract bakery member is implemented in another module
object CafeApp extends CafeModule with BakeryModule {
     cafe.orderCoffeeAndCroissant()
}

Multiple implementations

Taking this idea a bit further, in some situations we might have trait-module-interfaces and several trait-module-implementions. The interface would contain only abstract members, and the implementations would wire the appropriate classes. If other modules depend only on the trait-module-interface, when we do the final composition we can use any implementation.

This isn’t perfect, however. The implementation must be known statically, when writing the code – we cannot dynamically decide which implementations we want to use. If we want to dynamically choose an implementation for only one trait-interface, that’s not a problem – we can use a simple “if”. But every additional combination causes an exponential increase in the cases we have to cover. For example:

trait MillModule {
     def mill: Mill
}
 
trait CornMillModule extends MillModule { 
     lazy val cornField = new CornField()
     lazy val mill = new CornMill(cornField)
}
 
trait WheatMillModule extends MillModule { 
     lazy val wheatField = new WheatField()
     lazy val mill = new WheatMill(wheatField)
}
 
val modules = if (config.cornPreferred) {
     new BakeryModule with CornMillModule
} else {
     new BakeryModule with WheatMillModule
}

Can it be any better?

Sure! There’s always something to improve :). One of the problems was already mentioned – you cannot choose which trait-module to use dynamically (run-time configuration).

Another area that could get improved is the relation between trait-modules and packages. A good approach is to have a single trait-module per package (or per package tree). That way you logically group code that implements some functionality in a single package, and specify how the classes that form the implementations should be used in the trait-module. But why then do you have to define both the package and trait-module? Maybe they can be merged together somehow? Increasing the role of packages is also an idea I’ve been exploring in the Veripacks project.

It may also be good to restrict the visibility of some of the defined objects. Following the “one public class per package” rule, here we might have “one public object per trait-module”. However, if we are creating bigger trait-modules out of smaller ones, the bigger module has no way to restrict the visibility of the objects in the module it composes of. In fact, the smaller modules would have to know the maximum scope of their visibility and use an appropriate private[package name] modifier (supposing the bigger module is in a parent package).

Summing up

Overall, we found this solution to be a simple, clear way to structure our code and create the object graph. It uses only native Scala constructs, does not depend on any frameworks or libraries, and provides compile-time checking that everything is defined properly.

Bon Appetit!

Scala (programming language) Trait (computer programming) Object (computer science) Dependency injection

Published at DZone with permission of Adam Warski, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Trending

  • A Comprehensive Guide To Testing and Debugging AWS Lambda Functions
  • How To Use Geo-Partitioning to Comply With Data Regulations and Deliver Low Latency Globally
  • Manifold vs. Lombok: Enhancing Java With Property Support
  • Hiding Data in Cassandra

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com

Let's be friends: