Home > Blockchain >  Better code, one method one responsibility
Better code, one method one responsibility

Time:11-08

Let's say you're writing an algorithm for some collection, for example method 'contains' for some List. The code could look something like this (this is very simple, just for the sake of the example):

def betterContains(list: List[String], element: String): Boolean = {
   if (list.isEmpty) false 
   else if (list.head == element) true
   else betterContains(list.tail, element)
}

Imagine a more complex algorithm, examples would be searching for elements in trees, graphs, etc. And for some reason you add code for logging:

def betterContains(list: List[String], element: String): Boolean = {
   if (list.isEmpty) {
      log.info("The element was not found in the list.")
      false
   } 
   else if (list.head == element) {
      log.info("Yes! found it!")
      true
   }
   else {
      log.info(s"Still searching, ${list.tail.size} elements pending")
      betterContains(list.tail, element)
   }
}

Then, let's say you are adding code for saving some progress data in a text file. At the end, you will have one method that is doing 3 things:

  • search for a element in a list
  • logging info to the console
  • adding data (related to the progress) to a text file

If the developer decides to use a new log library he will have to make changes to the method implementation. Also if he decides to change the way the data is being saved in the text file, again he will have to make changes to the method implementation.

Is there any approach to avoid this? I mean, I only want to make changes to the method if I found a better way (a better algorithm) to find the element in the list. It seems to me that the algorithm does not meet the single responsibility principle, it is doing more than one thing.

CodePudding user response:

It seems to me that the algorithm does not meet the single responsibility principle, it is doing more than one thing.

Right. This is one of the reasons why for logging, auditing, security checks, performance monitoring, exception handling, caching, transaction management, persistence, validation etc. i.e. for different kinds of additional orthogonal behavior people use instrumentation of their code

What are the possible AOP use cases?

Instrumentation can be runtime (runtime reflection, runtime annotations, aspect-oriented programming, java agents, bytecode manipulation), compile-time (macros, compile-time annotation processors, compiler plugins), pre-compile-time/build-time (source generation, Scalameta/SemanticDB, sbt source generators, boilerplate templating) etc.

For example you can instrument your code at compile time with a macro annotation logging branching ("if-else")

import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

@compileTimeOnly("""enable macro annotations: scalacOptions  = "-Ymacro-annotations" """)
class logBranching extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro LogBranchingMacro.impl
}

object LogBranchingMacro {
  def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
    import c.universe._

    val printlnT = q"_root_.scala.Predef.println"
    def freshName(prefix: String) = TermName(c.freshName(prefix))

    annottees match {
      case q"$mods def $tname[..$tparams](...$paramss): $tpt = $expr" :: Nil =>

        val branchTransformer = new Transformer {
          override def transform(tree: Tree): Tree = tree match {
            case q"if ($cond) $thenExpr else $elseExpr" =>
              val condStr = showCode(cond)
              val cond2   = freshName("cond")
              val left2   = freshName("left")
              val right2  = freshName("right")

              val (optLeft1, optRight1, cond1, explanation) = cond match {
                case q"$left == $right" =>
                  (
                    Some(this.transform(left)),
                    Some(this.transform(right)),
                    q"$left2 == $right2",
                    q""" ", i.e. "   $left2   "=="   $right2 """,
                  )
                case _ =>
                  (
                    None,
                    None,
                    this.transform(cond),
                    q""" "" """
                  )
              }

              val backups = (cond, optLeft1, optRight1) match {
                case (q"$_ == $_", Some(left1), Some(right1)) =>
                  Seq(
                    q"val $left2  = $left1",
                    q"val $right2 = $right1",
                  )
                case _ => Seq()
              }

              val thenExpr1 = this.transform(thenExpr)
              val elseExpr1 = this.transform(elseExpr)

              q"""
                ..$backups
                val $cond2  = $cond1
                $printlnT("checking condition: "   $condStr   $explanation   ", result is "   $cond2)
                if ($cond2) $thenExpr1 else $elseExpr1
              """

            case _ => super.transform(tree)
          }
        }

        val expr1 = branchTransformer.transform(expr)
        q"$mods def $tname[..$tparams](...$paramss): $tpt = $expr1"

      case _ => c.abort(c.enclosingPosition, "@logBranching can annotate methods only")
    }
  }
}
// in a different subproject

@logBranching
def betterContains(list: List[String], element: String): Boolean = {
  if (list.isEmpty) false
  else if (list.head == element) true
  else betterContains(list.tail, element)
}

  //   scalacOptions  = "-Ymacro-debug-lite"
//scalac: def betterContains(list: List[String], element: String): Boolean = {
//  val cond$macro$1 = list.isEmpty;
//  _root_.scala.Predef.println("checking condition: ".$plus("list.isEmpty").$plus("").$plus(", result is ").$plus(cond$macro$1));
//  if (cond$macro$1)
//    false
//  else
//    {
//      val left$macro$5 = list.head;
//      val right$macro$6 = element;
//      val cond$macro$4 = left$macro$5.$eq$eq(right$macro$6);
//      _root_.scala.Predef.println("checking condition: ".$plus("list.head.==(element)").$plus(", i.e. ".$plus(left$macro$5).$plus("==").$plus(right$macro$6)).$plus(", result is ").$plus(cond$macro$4));
//      if (cond$macro$4)
//        true
//      else
//        betterContains(list.tail, element)
//    }
//}
betterContains(List("a", "b", "c"), "c")

//checking condition: list.isEmpty, result is false
//checking condition: list.head.==(element), i.e. a==c, result is false
//checking condition: list.isEmpty, result is false
//checking condition: list.head.==(element), i.e. b==c, result is false
//checking condition: list.isEmpty, result is false
//checking condition: list.head.==(element), i.e. c==c, result is true

For example Scastie instrument user-entered code with Scalameta

https://github.com/scalacenter/scastie/tree/master/instrumentation/src/main/scala/com.olegych.scastie.instrumentation

Another approach to add additional behavior in functional programming is effects, e.g. monads. Read about logging monad, Writer monad, logging with free monads etc.

CodePudding user response:

To this opinion-based question, I will give opinion-based answer.

Is there any approach to avoid this?

There is a solution, for sure by algorithm. But you should share us the following codes:

  • The way data is being saved initially(You have shared us in second snippet)
  • The way the data is being saved in the text file, finally(YOU HAVE NOT SHARED THIS)
  • The way developer uses a new log library(YOU HAVE NOT SHARED THIS)

You have not clarified the following parts of your question:

"If the developer decides to use a new log library..."

"Also if he decides to change the way the data is being saved in the text file..."

  • Related