[JAVA] Cohesion / Coupling / Cyclomatic complexity

Overview

Here are some tips for writing code that is easy to maintain / test.

By the way, the language is written in Java (please read other languages)

Also, you may understand that the degree of coupling is wrong. In that case, please point out. (Because it is different from other things written by books and sites ... (´ ・ ω ・ `))

Description of each

Cohesion

Also known as module strength. A measure of how much code is focused on the division of responsibility for the class. ** Classes that exist only for one role (all methods exist only for that role) are highly cohesive. ** ** On the contrary, the classes that have no commonality in each method and play various roles have a low degree of cohesion.

What's wrong with low cohesion

Classes with low cohesion tend to have many member variables

When the degree of cohesion is low, the number of member variables tends to increase. Since member variables are the state of the class itself, if there are many member variables, the patterns of states that the instance can take will increase, the complexity will increase, and maintainability will be significantly reduced. It also makes testing harder and more patterns.

Highly cohesive class example: String class

This class exists just to "handle strings" and has no other role. String class

Specific example

Consider a class called Price. That class only has methods that handle price data.

public class Price {
   private final int value;
   public Price(int value) {
      this.value = value;
   }
   public Price() {
      this(0);
   }

   /**Add price*/
   public Price add(Price other) {
       return new Price(this.value + other.value);
   }

   /**Spending the price*/
   public Price multiply(int amount) {
       return new Price(this.value * amount);
   }

   /**Discount*/
   public Price discount(double percentage) {
      return new Price(this.value * percentage);
   }

   /**Format and return the price*/
   public String format() {
       return NumberFormat.getCurrencyInstance().format(this.value);
   }

   public void print() {
       System.out.println(this.format());
   }
}

//User side
Price price = new Price(1000);
price.print(); // ¥1,000
System.out.println(price.format()); // ¥1,000
System.out.println(price.add(new Price(500)).format());  // ¥1,500
System.out.println(price.discount(0.9).format()); // ¥900
//Supplement:Price is immutable and the initially set value does not change
//Value when calling the last discount=Calculation is done at 1000

To increase the degree of cohesion

Coupling

Simply put, "** how much the class or method of the user and the user knows (how much depends on) **".

The higher the degree of coupling, the less maintainable it is and the more difficult it is to test.

Type of coupling

The degree of coupling increases in the following order.

Message join

It only calls methods with no arguments and has no return value. The ideal shape.

public class Main {
   private final Price price;
   
   public Main(Price price) {
      this.price = price;
   }

   public void output() {
      this.price.print();
   }
}

Price price = new Price(1000);
Main main = new Main(price);
main.output(); // ¥1,Is displayed as 000

In the above, the Main class and the Price class do not share data, and Main # output () does not depend on the implementation ofprint ().

The Price class doesn't know anything about the Main class, nor does the Main class care about whatPrice # print ()is doing inside. ** For example, even if you change the implementation of the print () method to print a string to a file, even if you don't actually do anything, Main # output doesn't care. ** **

Such joins are also called ** message passing **, and interactions between such objects are called ** messaging **. It was named "messaging" because it completely delegates the work to the other object, and it's just like telling someone "this is finally done".

Data join

Only pass simple data.

//Data join example

public class NameFormatter {
    public String format(String firstName , String familyName) {
        return new StringBuilder().add(familyName).add(" ").add(firstName).toString();
    }
}
public class Main {
   private final String firstName;
   private final String familyName;

   public Main(String firstName, String familyName) {
      this.firstName = firstName;
      this.familyName = familyName;
   }

   public void printName() {
      NameFormatter formatter = new NameFormatter();
      System.out.println(formatter.format(this.firstName, this.familyName)); 
   }
}

Main main = new Main("Kazuki", "Oda");
main.printName(); //Kazutaka Oda

The combination with NameFormatter inMain # printName ()above is a data combination. In data binding, ** share only "necessary information" **. The "information" referred to here corresponds to the following.

The data join will always have the same output for the same input. Also, since only the necessary information is passed to NameFormatter,printName ()just uses the value finally returned from NameFormatter # format, and what kind of implementation is done internally. You don't have to know if you are there.

In general, data joins pass data as an argument [immutable](https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%9F%E3%83%A5%E3%83] % BC% E3% 82% BF% E3% 83% 96% E3% 83% AB) or Primitive Type AA% E3% 83% 9F% E3% 83% 86% E3% 82% A3% E3% 83% 96% E5% 9E% 8B) Or refers to a join when passing an interface (I think). However, even in the case of passing a reference type object, it may correspond to data binding (for example, only the necessary information is disclosed by the called method).

Stamp combination

Pass a structure of data (that is, an instance with multiple member variables) as an argument.

public class Item {
    //In the body that there is a constructor, getter and setter respectively
    private String name;
    private String group;
}

public class CashRegister {
    public String format(Item item, Price price) {
        return item.getName() + ":" + price.format();
    }
}

CashRegister regi = new CashRegister();
System.out.println(regi.format(new Item("Smash Bra", "game"), new Price(6800))); //Smash Bra:¥6,800

In the case of stamp binding, data that the callee does not use can be referenced in the structure (that is, the callee also knows unnecessary information). In the above case, group is not used, but if you want to do it, you can refer to any of them in CashRegister # format.

It doesn't matter if it's just a reference, but in some cases it is possible to rewrite the value of the data in the called method. So ** In the case of stamp join, when the caller wants to use it correctly, there are cases where it is necessary to know (know) to some extent what data is handled in the method of the callee. ** **

The above is a data join with the caller if CashRegister is set as follows. However, the caller and Item, and the caller and Price are stamped together </ font>

public class CashRegister {
    public String format(String item, int price) {
        return item + ":" + price;
    }
}

CashRegister regi = new CashRegister();
Item item = new Item("Smash Bra", "game");
Price price = new Price(6800);
System.out.println(regi.format(item.getName(), price.format())); //Smash Bra:¥6,800

Thus, stamp binding cannot be easily eliminated as long as the class is used. By using the interface, it is possible to reduce the degree of coupling to data binding or message binding, but it will be longer, so I will omit it.

  • Personally, in the case of a medium-sized project, it is more realistic to allow stamp combination normally than to run on object-oriented fundamentalism.

Control join

A case where the return value changes depending on the content of the given argument.

public class Item {
//In the body that there is a constructor, getter and setter respectively
   private String name;
   private Price unitPrice;
}

public class SaleService {
   public Price fixPrice(Item item, int amount, boolean discount) {
       Price newPrice = item.getUnitPrice().multiply(amount);
       if (discount == false) {
           return newPrice;
       }
       return newPrice.discount(0.90);
   }
}

Item item = new Item("Smash Bra", new Price(6800));
SaleService service = new SaleService();
System.out.prinln(service.fixPrice(item, 2, false)); // ¥13,600
System.out.prinln(service.fixPrice(item, 2, true)); // ¥12,240

In this control combination, the return value differs by the number of patterns even if a similar value is entered as an argument depending on the patterns that can be taken. In other words, ** the patterns that can be taken depend on the arguments. ** **

This control coupling can be reduced to less than the stamp coupling by using polymorphism. (However, I personally think that control coupling can be tolerated if it is about 3 to 4 patterns)

Outer join

Outer join refers to the state in which multiple modules refer to one global resource. From here on, if you feel free to allow this join, it will be very difficult to test.

The global resources refer to the following, for example.

  • Global static variables
  • Editable held in Singleton data
  • File
  • Database resources
  • Return value from another server (Web API, etc.)

For these, the value changed in other instances etc. affects the entire class referenced by other. Also, if the structure of those resources changes, it will be necessary to modify all the calling classes.

** In order to write testable code, it is important (I think) how to reduce the number of bindings after this outer join. ** **

Common join

A state in which various classes refer to multiple data of global resources. It will be more chaotic than the outer join.

Content combination

A join that knows even the unpublished methods and variable names of other classes and directly references / calls them. In content combination, if the internal implementation (implementation that is not published) of the dependent class changes, it will be directly affected, so even if the variable name of the dependency is changed, it will be affected.

  • If reflection is used, there is a risk of a crash.

To reduce the degree of coupling

By keeping the following in mind, the degree of coupling can be kept low.

  • Do not refer to data directly as much as possible
  • Do not use setter / getter as much as possible (especially setter)
  • Try to create an interface
  • In particular, classes that cooperate with the outside are externalized and interfaced.
  • Dependency inversion principle
  • Use Dependency Injection
  • There are various DI libraries such as Guice
  • More effective when combined with factory methods
  • Aware of Demeter's Law

Cyclomatic complexity (cyclomatic complexity)

Simply put, the number of "if statements / for statements / while statements / switch statements / try statements / catch statements" and so on. If there are none of them, it will be 1, and as the number of branches increases, it will increase by 1. For example, the cyclomatic complexity of the following method is 4.

public void hogehoge(int test) {
   if (test > 5 ) {
       // doSomething1
   } else if (test < 1) {
       // doSomething2
   }
   for (int i = 0; i < 5; i++) {
       // doSomething3
   }
}

When the degree of cyclic reference is high

Of course, there are more test patterns. In addition, readability is reduced and maintenance becomes difficult.

How to reduce cyclical reference

  • Split method
  • Make the nest shallow (try to return early)
  • Observe the DRY principle
  • Divide processing by polymorphism
  • Utilize Stream API etc.

Summary

  • Higher cohesion is better
  • Low coupling is better
  • Low cyclomatic complexity is better

Recommended Posts

Cohesion / Coupling / Cyclomatic complexity
About cyclomatic complexity