Learning from bottom to bottom: ask questions -> analyze problems -> solve problems -> summarize

Demand scenarios

The business requirements

Rotary draw, three types of prizes (physical prizes, points, gold coins), the total number of prizes is limited, the total number of daily limitCopy the code

Source link

Code implementation

type IPrize interface {
   Award()
   CheckLimit() error
}

type Prize struct {
   Id int
   Number int
   Percent time.Time
   Name string
   TotalLimit int
}

func (dom *Prize) CheckLimit(a) error {
   if dom.TotalLimit == 0 {
      return errors.New("Total prize limit has been reached.")}return nil
}

type Gold struct {
   Prize
}

func (dom *Gold) Award(a) {
   // Call the gold system to issue gold
}

type Physical struct {
   Prize
}

func (dom *Physical) Award(a) {
   // Call the prize system to distribute physical prizes
}

type Point struct {
   Prize
}

func (dom *Point) Award(a) {
   // Call the points system to distribute physical prizes
}


type PrizePool struct {
   Prizes []IPrize
}


func (dom *PrizePool) Lottery(a) (IPrize, error) {

   prize := dom.getPrizeByPercent(dom.Prizes)

  iferr := prize.CheckLimit(); err ! =nil {
     return nil, err
  }
 
   prize.Award()

   return prize, nil
}

func (dom *PrizePool) getPrizeByPercent(prizes []IPrize) IPrize {
   // simulate a random draw
   return prizes[3]}Copy the code

Examine the code

1. The IPrize class defines the abstract method Award, and the subclasses realize their own different Award logic; The Prize class abstracts the CheckLimit method for subclasses to call; 3. The caller calls the Prize class for logical verification and award;Copy the code

It looks perfect so far. Well done! Prefect! But is that really the case? Prepare to be overrun by requirements changes!

New demand

Added award types: Thank you for participating, thank you for participating award types do not need to verify the number of limitsCopy the code

Code implementation

type Thanks struct {
   Prize
}

func (dom *Thanks) Award(a) {
   // Call the points system to distribute physical prizes} caller:func (dom *PrizePool) Lottery(a) (IPrize, error) {

   prize := dom.getPrizeByPercent(dom.Prizes)

   _, isThankPrize := prize.(*Thanks)
   if! isThankPrize {iferr := prize.CheckLimit(); err ! =nil {
         return nil, err
      }
   }

   prize.Award()

   return prize, nil} code analysis: determine whether the prize drawn is a thank you participation prize, if it is a thank you participation prize, skip the prize limit checkCopy the code

Consider: add another result for a prize type that does not require validation of a limited number of items

Advantages and disadvantages of inheritance

Advantages: 1. A subclass has all the methods and attributes of its parent class, thus reducing the amount of work required to create a class. 2. Improved code reuse. 3, improve the extensibility of the code, subclasses not only have all the functions of the parent class, but also can add their own functions. Disadvantages: 1. Inheritance is intrusive. As long as you inherit, you must have all the attributes and methods of the parent class, breaking the encapsulation properties of the class. 2. Reduced code flexibility. Because when you inherit, the parent class has a constraint on the child class. 3. Enhanced coupling. When you need to make code changes to the parent class, you must consider the impact on the child class. Sometimes even a small change in code may require refactoring of the interrupt program.Copy the code

How to make good use of inheritance in code? The way to do that is to follow Richter’s substitution principle

The solution of Go language is to eliminate inheritance directly and use combination instead of inheritance. “Combination is better than inheritance” is also the mainstream view now

Richter’s substitution principle

Definition: Wherever a parent class occurs, it can be replaced with a subclass without any errors or exceptions. But the other way around, where there are subclasses, you can’t replace them with their parent.

Richter's substitution principle imposes rules on inheritance, which is mainly reflected in four aspects: 1. A subclass must implement the abstract method of the parent class, but cannot override the non-abstract (realized) method of the parent class. 2. Subclasses can add their own special methods. 3. When a subclass overrides or implements a method of its parent class, the method's preconditions (that is, its parameters) are looser than the input parameters of the parent method. 4. When a subclass's method implements an abstract method of the parent class, the method's postcondition (that is, the method's return value) is stricter than the parent class's. There are articles on the Internet that explain these four aspects in great detail, which are not covered hereCopy the code

Any place where a base class can occur, you can replace it with subclasses without side effects. But the problem is, when there is no specific subclasses of no side effect when replacement base class (such as thank you to participate in the prize does not need to validate limit), we in the calling code will have to these specific classes are class, thus leading to the calling code with specific types of implementation code “coupling” and difficult to extend and maintain, This is a typical symptom of a violation of Richter’s substitution principle and is often overlooked.

Consider: Pause for five minutes and re-examine the code. How can you refactor to comply with The Richter substitution principle?

Interface separation principle

A user should not rely on methods it does not use.

Interface separation principle is used to deal with a bloated bloated interface or base class and interface or class contains a lot of multiple methods, use the party at the time of use, will find himself is not necessary to implement a method, but because in the presence of inheritance, you have to achieve a, or is empty methods, or throw an exception to show that he does not support.

The interface separation principle guides the division of interfaces into smaller granularity so that users only need to implement the interfaces they need, rather than having to implement bloated interfaces or tasks handed down by bloated base classes because of inheritance (ThanksPrize is a ThanksPrize system that has capabilities they don’t need because of inheritance).

Consider: How small is the smaller granularity?

Reengineering objectives

  1. Eliminate unreasonable inheritance – thank you for inheriting the validation quantity limit method for participating prize types
  2. Eliminate the determination of specific types in the code and the coupling between the caller code and the target code

Source link

type IPrizeLimiter interface {
   CheckLimit() error
}

type IPrize interface {
   Award()
}

type Prize struct {
   Id int
   Number int
   Percent time.Time
   Name string
   TotalLimit int
}

type PrizeLimiter struct {
   Prize
}

func (dom *PrizeLimiter) CheckLimit(a) error {
   if dom.TotalLimit == 0 {
      return errors.New("Total prize limit has been reached.")}return nil
}

type Gold struct {
   PrizeLimiter
}

func (dom *Gold) Award(a) {
   // Call the gold system to issue gold
}

type Physical struct {
   PrizeLimiter
}

func (dom *Physical) Award(a) {
   // Call the prize system to distribute physical prizes
}

type Point struct {
   PrizeLimiter
}

func (dom *Point) Award(a) {
   // Call the points system to distribute physical prizes
}

type Thanks struct {
   Prize
}

func (dom *Thanks) Award(a) {
   // Call the prize system to distribute thanks for participating
}


/ / the caller
func (dom *PrizePool) Lottery(a) (IPrize, error) {

   prize := dom.getPrizeByPercent(dom.Prizes)

   p, ok := prize.(IPrizeLimiter)
   if ok {
      iferr := p.CheckLimit(); err ! =nil {
         return nil, err
      }
   }

   prize.Award()

   return prize, nil
}


Copy the code

At this point, we have eliminated irrational inheritance, optimized the caller code, and now the caller code has some resistance to the impact of changing requirements.

The code analysis

1. The checkLimit method is abstracted into an independent IPrizeLimiter interface; 2. IPrizeLimiter interface is realized for the prize type as required; 3. Call the policy to judge the interface, rather than a specific prize type; This is the dependency inversion principleCopy the code

Consider: add another result for a prize type that doesn’t need to validate a number of limits, or change the number of points to no number of limits

Dependency inversion principle

1. High-level modules should not depend on low-level modules, but both should depend on their abstractions; 2, abstraction should not rely on details, details should rely on abstraction;Copy the code

A high-level module should not depend on a low-level module, that is, a high-level module should hold a reference to an abstract class or interface, rather than a reference to a concrete implementation class.

1. The caller PrizePool is the upper level, while physical prizes, points, coins and other prizes are the lower level; 2. When PrizePool determines the type of Thanks, the upper-level module depends on the lower-level module. 3. When PrizePool determines the type of IPrizeLimiter, the high-level module depends on the low-level abstraction. 4. The prize type realizes the IPrizeLimiter interface, which belongs to the low-level module dependent abstraction;Copy the code

Any problem in computer science can be solved by adding an indirect intermediate layer, such as insufficient performance and caching.

Abstractions should not depend on details, that is, an interface or abstract class should not hold a reference to a concrete implementation class, but rather a reference to an abstract class that class inherits or implements. Details should depend on abstractions, that is, the implementation class should also hold a reference to an abstract class or interface, not a reference to a concrete implementation class.

The IPrize and IPrizeLimiter interfaces are independent of their respective implementation classes. PrizePool interfaces depend on the IPrize and IPrizeLimiter interfaces. Adding an implementation class does not affect the interface or the callers. Abstractions (abstract classes or interfaces) should not depend on details (concrete implementation classes).

type IPrizeLimiter interface {
   Check(prize IPrize) error  // The IPrizeLimiter interface relies on the IPrize interface, which is abstraction dependent on abstraction
}

type IPrizeLimiter interface {
   Check(thanks Prize) error  // The IPrizeLimiter interface relies on the Thanks concrete implementation class, which is the abstraction that depends on the details
}
Copy the code
_isthankprize := prize.(*Thanks) if! isThankPrize { if err := prize.CheckLimit(); err ! P, ok := prize.(IPrizeLimiter) if ok {if err := p.checklimit (); err ! = nil { return nil, err } }Copy the code

conclusion

1, using the magnitude of substitution principle, the principle of interface separation to eliminate the wrong inheritance relationships: handle the relations between classes, and lays the foundation for combination 2, using the dependency inversion principle, eliminating dependence on specific details: simplify the caller code 3, understanding, using abstract isolate changes 4, combination is superior to the inheritance (principle) : reduce the system complexityCopy the code

Question to consider: in real development, do I need to abstract IPrizeLimiter from the interface separation principle at the very beginning, before the thank you for participating prize type?

When no thank you to participate in the prize type code is in line with the substitution principle on the Richter scale, subclass to replace the parent class without any side effects, is also likely to thank participation award type this demand will never appear, excessive design is also a kind of waste, the code should be constantly as demand changes and reconstruction, the needs of different phase reconstruct the suitable code logic, When there is a need for the thank you participation prize type, this is the right time to refactor.