[PYTHON] Codes I often see while working at SIer and how to fix them-Demeter's law violation

I'm thinking of putting together anti-patterns that I often see while working. This time it is a violation of Demeter's law. The code is Scala, but it's basically the same for Ruby, Python, and Java.

The topics are below.

Code that violates Demeter's law

Demeter's law is that you should not use anything other than "directly instantiated" or "passed as an argument".

class Profile(name: String, age: Int) {
  def showProfile(): Unit = println(s"my name is $name (age: $age)")
}

class Applicant(id: String, val profile: Profile)

object Order {
  //Violates Demeter's Law
  def showApplicant(applicant: Applicant): Unit = {
    applicant.profile.showProfile()
  }
}

ʻApplicantis passed as an argument, but in the method, It violates because theshowProfile ()method is executed after getting theprofile in ʻapplicant.profile.

ʻApplicant.profile.showProfile ()because I want to do it I think you should hesitate to ** publish properties with getters ** withval like class Applicant (id: String, val profile: Profile) `.

The form you want to call is as follows.

  def showApplicant(applicant: Applicant): Unit = {
    applicant.showProfile()
  }

Here, the following is described.

Inheritance solution

class Profile(name: String, age: Int) {
  def showProfile(): Unit = println(s"my name is $name (age: $age)")
}

//Applicant inherits Profile
class Applicant(id: String, name: String, age: Int) extends Profile(name, age)

object Order {
  def showApplicant(applicant: Applicant): Unit = {
    applicant.showProfile()
  }
}

↑ It feels strange, so fix it with the transfer method.

Transfer method (delegation) solution

class Profile(name: String, age: Int) {
  def showProfile(): Unit = println(s"my name is $name (age: $age)")
}

//Since the profile no longer needs to be published by getters
//I removed the val.
class Applicant(id: String, profile: Profile) {
  //Transfer method
  def showProfile(): Unit = profile.showProfile()
}

object Order {
  def showApplicant(applicant: Applicant): Unit = {
    applicant.showProfile()
  }
}

About the test

Now consider testing the ʻOrder.showApplicant ()` method modified by the ** transfer method **.

object SimpleTest extends App {
  //Create the required objects
  val profile = new Profile("taro", 22)
  val applicant = new Applicant("001", profile)
  
  //Execute the method under test
  Order.showApplicant(applicant)
}

In the above example, it is easy to create an instance of ʻApplicant, but for example, consider the case where ʻApplicant aggregates various classes and instantiation is awkward.

The important thing is that ** the test target is the ʻOrder.showApplicant ()` method, which is a transfer method, so it's good if you can confirm that you called it. That means **.

object ComplexTest extends App {
  //Complex instantiation
  val profile1 = ...
  val profile2 = ...
  val profile3 = ...
  val applicant = new Applicant("001", profile1, profile2, profile3, ...)

  //I just want to execute the method under test,
  //Instance generation is a pain.
  Order.showApplicant(applicant)
}

Refactoring to make testing easier

As long as the ʻOrder.showApplicant () method can be executed, the content of the argument ʻapplicant can be anything. So, sandwich the interface so that ʻOrder.showApplicant ()` depends on abstraction.

//Keep abstract
trait IApplicant {
  def showProfile(): Unit
}

class Applicant(id: String, profile: Profile) extends IApplicant {
  def showProfile(): Unit = profile.showProfile()
}

object Order {
  //The applicant type is now IApplicant and depends on abstraction.
  def showApplicant(applicant: IApplicant): Unit = {
    applicant.showProfile()
  }
}

The ʻapplicant required for the argument of the ʻOrder.showApplicant () method can be generated as follows, which makes the test a little easier.

object RefactorComplexTest extends App {
  //Complex instantiation
  val applicant = new IApplicant {
    override def showProfile(): Unit = println("Come on. suitable. Come on")
  }
  
  Order.showApplicant(applicant)
}

If you continue to violate Demeter's law as shown below, it will be difficult to test, so I personally think that if you violate Demeter's law, you should fix it.

You can test it by using a mock, but I personally think that it is better to review the design before using the mock.

  def showApplicant(applicant: Applicant): Unit = {
    //It is difficult to test if you continue to violate Demeter's law
    applicant.profile.showProfile()
  }

Recommended Posts

Codes I often see while working at SIer and how to fix them-Demeter's law violation
Codes I often see while working at SIer and how to fix them-Demeter's law violation
How to deal with the error "Failed to load module" canberra-gtk-module "that appears when you run OpenCV
How to deal with errors when installing whitenoise and deploying to Heroku
How to deal with errors when installing Python and pip with choco
How to build Python and Jupyter execution environment with VS Code
Convert facial images with PULSE to high image quality so that you can see pores and texture