Skip to content

Instantly share code, notes, and snippets.

@aappddeevv
Last active June 17, 2024 14:36
Show Gist options
  • Select an option

  • Save aappddeevv/8509607 to your computer and use it in GitHub Desktop.

Select an option

Save aappddeevv/8509607 to your computer and use it in GitHub Desktop.
scala, cake pattern, service, DAO, Reader, scalaz, spring, dependency inejection (DI)

#The Problem We just described standard design issues you have when you start creating layers of services, DAOs and other components to implement an application. That gist is here.

#Working through Layers If you compose services and DAOs the normal way, you typically get imperative style objects. For example, imagine the following:

  /**
   * The service layer. Most service layers define the unit of work. Many
   * times a unit of work in the service layer is the same as that implemented
   * in the DAO layer, but that's because some internet examples are too small
   * to show the nuanances. Since this layer defines units of work, this is where
   * the transaction strategy is also implemented. Spring implements this with
   * annotations and proxies/byte-code enhancers. We'll use explicit coding because we are not
   * using proxies/byte-code enhancers like spring uses.
   *
   * Classic cake pattern. Any implementation must define
   * a service val (or directly create an object) to satisfy
   * this abstract type.
   */
  trait UserServiceComponent {

    val service: UserService

    trait UserService {
      def updateName(User: UserId, newName: String): Either[String, User]
      def findById(name: UserId): Option[User]
    }
  }

  /**
   * This is the usual "interface" declaration that has the underlying technology
   * parameterized. The component is parameterized on a context that allows
   * concrete classes access to a specific execution environment called the context.
   * The context will be implementation specific.
   *
   * To improve composability, methods return functions (a Reader is a wrapper
   * around a function) instead of just raw values. If we did not do this, the imperative
   * nature of the DAO would not allow us to compose a sequence of DAO calls because
   * queries are modeled as Strings versus directly in the scala type system. We could
   * also put this context concept at the UserDao trait level (and make UserDao
   * a class) so that it becomes a constructor argument but then a new UserDao must
   * be constructed each time you need to use it. You may want this pattern or not
   * so choose where you place that context based on your application needs and design.
   * Based on the way we have defined this, the ExecutionContext could change on a per
   * call basis and this make it appropriate to use when we need a transaction, session,
   * entity manager or some other dependency.
   *
   * We know that most likely our implementations need a technology-specific context
   * to execute under--an environment. So we provide that as an abstract. We use the
   * Reader to access that context if needed. We could abstract this further so its
   * not even dependent on Reader but that makes it hard to read.
   *
   * A type parameter could also be used but that sometimes makes inheritance
   * more restricted. By using a abstract type member (the over all object is know
   * an existential type) we also allow ourselves the ability to combine the context
   * members across all components that are instantiated together to allow the context
   * to satisfy multiple component context needs.
   *
   * We could also abstract the Reader away if we wanted to to say a Keisli object,
   * anything that lifts a function (A => M[B]) into an environment and can be
   * mapped on in some way.
   *
   */
  trait UserDaoComponent {

    val userDao: UserDao

    trait UserDao {
      /**
       * Find a user by their id.
       */
      def findById(user: UserId): Option[User]

      /**
       * Update the user properties, whatever has changed.
       */
      def update(user: User): Unit
    }
  }

  /**
   * The simple application auditing component.
   */
  trait AuditDaoComponent {

    /**
     * We define a def here. If you want singleton behavior,
     * define a private member _auditDao, instantiate that
     * then return it in the def. If you want a new auditDao
     * each time, create a new one each time this def is called.
     * DAOs traditionally do not hold any state so it which you
     * choose is not too critical for most applications but the
     * option is demonstated here by using a def instead of a val.
     */
    def auditDao: AuditDao

    trait AuditDao {
      /**
       * Send the changes to an audit log somewhere.
       */
      def auditChange(user: User, changedProperties: Seq[String]): Unit
    }
  }

Here's where you get stuck. This is not well parameterized. In spring, you would use the @Transactional and the container injection (with our without java config) to configure your objects. In other words, you would provide specific technology choices in the form of annotations like @Transactional or @Autowired. Since scala and the cake pattern does not use byte code generation or proxies, you have to be a bit more explicit with the technology choices.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment