interface design principles using Golang

Keywords: Java Programming Go

Interface interface

Interface is one of the basic features of GO language. It can be understood as a type of specification or convention. It's different from java and C ා in that it doesn't need to show that it implements an interface. It doesn't inherit or subclass or "implements" keywords. It just implicitly implements the methods in the interface in the form of conventions. Therefore, the interface in Golang makes coding more flexible and extensible.

How to understand the interface in go? Just remember the following three points.

  1. interface is a collection of method declarations
  2. An object of any type implements all the methods declared in the interface interface, indicating that the type implements the interface.
  3. Interface can be used as a data type. Any object that implements the interface can assign a value to the corresponding interface type variable.
  1. interface can be implemented by any object, and a type / object can also implement multiple interfaces
  2. Method cannot be overloaded, such as eat(), eat(s string) cannot exist at the same time

Sample code

package main

import "fmt"

type Phone interface {
    call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}

type ApplePhone struct {
}

func (iPhone ApplePhone) call() {
    fmt.Println("I am Apple Phone, I can call you!")
}

func main() {
    var phone Phone
    phone = new(NokiaPhone)
    phone.call()

    phone = new(ApplePhone)
    phone.call()
}

The above shows the syntax of the interface, and the polymorphism in the main function.
The same phone's abstract interface points to different entity objects and calls the call() method. The printing effect is different, so it reflects the polymorphism.

Open close principle in object oriented

Flat module design

So as an interface data type, what is the significance of its existence? In fact, it is to satisfy some object-oriented programming ideas. We know that the highest goal of software design is high cohesion and low coupling. So one of the design principles is called the opening and closing principle. What is the opening and closing principle? Let's take an example:

package main

import "fmt"

//We're going to write a class, Banker
type Banker struct {
}

//Deposit business
func (this *Banker) Save() {
    fmt.Println( "Deposit business...")
}

//Transfer business
func (this *Banker) Transfer() {
    fmt.Println( "Conducted transfer business...")
}

//Payment business
func (this *Banker) Pay() {
    fmt.Println( "Paid...")
}

func main() {
    banker := &Banker{}

    banker.Save()
    banker.Transfer()
    banker.Pay()
}

The code is very simple. It's just a bank clerk. He may have many businesses, such as Save() deposit, Transfer() transfer, Pay() payment, etc. So if only these methods are available for this salesman module, but with our program writing becoming more and more complex, the bank salesman may need to add more methods, which will lead to the salesman module becoming more and more bloated.

Such a design will lead to that when we add a new business to the Banker, we will directly modify the original Banker code, so the function of the Banker module will be more and more, and the probability of problems will be more and more. If at this time the Banker has 99 businesses, now we need to add the 100th business, which may be caused by carelessness once, so the previous 99 businesses are also one Because all businesses are in the same Banker class, their coupling is too high, the responsibilities of bankers are not single, and the cost of code maintenance increases exponentially with the complexity of the business.

Opening and closing design principle

So, if we have interfaces, interfaces, we can abstract one layer, make an abstract Banker module, and then provide an abstract method. According to this abstract module, implement payment bank (implement payment method) and transfer bank (implement transfer method)
As follows:

Then you can still handle the needs of the program. Then, when we want to add additional functions to the Banker, we used to modify the content of the Banker directly. Now we can define a stock Banker (implement the stock method) into the system separately. Moreover, the success or failure of the implementation of the stock Banker will not affect the previous stable system. It is single and independent.

Therefore, when we add a function to a system, it is not done by modifying the code, but by adding the code. That is the core idea of the opening and closing principle. Therefore, in order to meet the above requirements, it is necessary to provide an abstract interface.

The golang code is implemented as follows:

package main

import "fmt"

//Abstract bank clerk
type AbstractBanker interface{
    DoBusi()    //Abstract processing business interface
}

//Deposit agent
type SaveBanker struct {
    //AbstractBanker
}

func (sb *SaveBanker) DoBusi() {
    fmt.Println("Deposit made")
}

//Operator of transfer
type TransferBanker struct {
    //AbstractBanker
}

func (tb *TransferBanker) DoBusi() {
    fmt.Println("Transfer made")
}

//Paid salesman
type PayBanker struct {
    //AbstractBanker
}

func (pb *PayBanker) DoBusi() {
    fmt.Println("Payment made")
}

func main() {
    //Deposit
    sb := &SaveBanker{}
    sb.DoBusi()

    //Transfer
    tb := &TransferBanker{}
    tb.DoBusi()
    
    //Make payment
    pb := &PayBanker{}
    pb.DoBusi()

}

Of course, we can also design a small framework according to AbstractBanker

//Implementation architecture layer (business encapsulation based on abstract layer - encapsulation for interface interface)
func BankerBusiness(banker AbstractBanker) {
    //Call down through interface, (polymorphism)
    banker.DoBusi()
}

In main, business calls can be implemented as follows:

func main() {
    //Deposit
    BankerBusiness(&SaveBanker{})
    //Deposit
    BankerBusiness(&TransferBanker{})
    //Deposit
    BankerBusiness(&PayBanker{})
}

Definition of opening and closing principle:
A software entity such as classes, modules, and functions should be open to extension and closed to modification.
In short, when modifying requirements, we should try to implement changes through extension rather than by modifying existing code.

Meaning of interface

Well, now that the interface has been basically understood, where is the meaning of the interface? Presumably now you have a preliminary understanding. In fact, the greatest significance of the interface is to realize the idea of polymorphism, that is, we can design the API interface according to the interface type, so the adaptability of this API interface can not only adapt to all the modules implemented at present, but also Adapt to the future implementation of the module to call. Calling the future may be the most significant point of an interface, which is why an architect is so valuable, because a good architect can design a framework for an interface, which will still be applicable in many years to come.

The principle of dependency inversion in object-oriented

Module design with high coupling

package main

import "fmt"

// ===> Mercedes Benz<===
type Benz struct {
  //...
}

func (this *Benz) Run() {
    fmt.Println("Benz is running...")
}

// ===> BMW<===
type BMW struct {
  //...
}

func (this *BMW) Run() {
    fmt.Println("BMW is running ...")
}

//===>Driver Zhang San<===
type Zhang3 struct {
    //...
}

func (zhang3 *Zhang3) DriveBenZ(benz *Benz) {
    fmt.Println("zhang3 Drive Benz")
    benz.Run()
}

func (zhang3 *Zhang3) DriveBMW(bmw *BMW) {
    fmt.Println("zhang3 drive BMW")
    bmw.Run()
}

//===>Driver Li Si<===
type Li4 struct {
    //...
}

func (li4 *Li4) DriveBenZ(benz *Benz) {
    fmt.Println("li4 Drive Benz")
    benz.Run()
}

func (li4 *Li4) DriveBMW(bmw *BMW) {
    fmt.Println("li4 drive BMW")
    bmw.Run()
}

func main() {
    //Business 1 x 3-car Benz
    benz := &Benz{}
    zhang3 := &Zhang3{}
    zhang3.DriveBenZ(benz)

    //Business 2 Li Si Kai BMW
    bmw := &BMW{}
    li4 := &Li4{}
    li4.DriveBMW(bmw)
}

Let's look at the dependency relationship between the above code and each module in the figure. In fact, we don't use any
The interface layer code of interface layer. Obviously, in the end, our two businesses Zhang Sanjiao Benz, Li sixkai BMW, Zhang Sanjiao Benz and Li sixkai BMW are all implemented in the program. But the problem of this design is that small scale is no problem, but once the program needs to be expanded, for example, I want to add a Toyota Motor or driver Wang Wu, then the dependency between modules and modules will increase exponentially, and it is more and more difficult to maintain and smooth like spider web.

Dependency inversion design for abstract layer


                       . So, we first define the modules and interfaces of the abstract layer, and then we need to design the interface. Then we implement the modules of each implementation layer in turn according to the abstract layer. When we write the code of the implementation layer, in fact, we only need to refer to the corresponding implementation of the abstract layer. The implementation of each module has nothing to do with other modules , which also conforms to the opening and closing principles described above. In this way, each module only depends on the interface of the object, but it has nothing to do with other modules, and the dependency is single. The system is easy to expand and maintain.
                                 .
We call this kind of design principle dependency reversal principle

Let's take a look at the modified code:

package main

import "fmt"

// =====> abstraction layer >========
type Car interface {
    Run()
}

type Driver interface {
    Drive(car Car)
}

// =====> implementation layer >========
type BenZ struct {
    //...
}

func (benz * BenZ) Run() {
    fmt.Println("Benz is running...")
}

type Bmw struct {
    //...
}

func (bmw * Bmw) Run() {
    fmt.Println("Bmw is running...")
}

type Zhang_3 struct {
    //...
}

func (zhang3 *Zhang_3) Drive(car Car) {
    fmt.Println("Zhang3 drive car")
    car.Run()
}

type Li_4 struct {
    //...
}

func (li4 *Li_4) Drive(car Car) {
    fmt.Println("li4 drive car")
    car.Run()
}


// =====> business logic layer========
func main() {
    //Zhang 3 drives BMW
    var bmw Car
    bmw = &Bmw{}

    var zhang3 Driver
    zhang3 = &Zhang_3{}

    zhang3.Drive(bmw)

    //Li 4 drives Mercedes Benz
    var benz Car
    benz = &BenZ{}

    var li4 Driver
    li4 = &Li_4{}

    li4.Drive(benz)
}

Dependency reversal small case

Simulated assembly of 2 computers

  • Abstraction layer
    There are the display method of the video Card, the storage method of the Memory, and the calculation method of the processor CPU

  • Implementation layer
    Intel Intel Intel, products (graphics card, memory, CPU), Kingston, products (memory 3), NVIDIA, products (graphics card)

  • Logic layer
    Assemble an Intel series computer and run it. 2. Assemble an Intel CPU, Kingston memory, NVIDIA graphics card and run it

package main
import "fmt"

//------Abstraction layer-----
type Card interface{
    Display()
}

type Memory interface {
    Storage()
}

type CPU interface {
    Calculate()
}

type Computer struct {
    cpu CPU
    mem Memory
    card Card
}

func NewComputer(cpu CPU, mem Memory, card Card) *Computer{
    return &Computer{
        cpu:cpu,
        mem:mem,
        card:card,
    }
}

func (this *Computer) DoWork() {
    this.cpu.Calculate()
    this.mem.Storage()
    this.card.Display()
}

//------Implementation layer-----
//intel
type IntelCPU struct {
    CPU
}

func (this *IntelCPU) Calculate() {
    fmt.Println("Intel CPU It's counting...")
}

type IntelMemory struct {
    Memory
}

func (this *IntelMemory) Storage() {
    fmt.Println("Intel Memory It's starting to store...")
}

type IntelCard struct {
    Card
}

func (this *IntelCard) Display() {
    fmt.Println("Intel Card Start showing...")
}

//kingston
type KingstonMemory struct {
    Memory
}

func (this *KingstonMemory) Storage() {
    fmt.Println("Kingston memory storage...")
}

//nvidia
type NvidiaCard struct {
    Card
}

func (this *NvidiaCard) Display() {
    fmt.Println("Nvidia card display...")
}
//------Business logic layer-----
func main() {
    //intel series computers
    com1 := NewComputer(&IntelCPU{}, &IntelMemory{}, &IntelCard{})
    com1.DoWork()

    //Miscellany
    com2 := NewComputer(&IntelCPU{}, &KingstonMemory{}, &NvidiaCard{})
    com2.DoWork()
}

Posted by maxonon on Sun, 14 Jun 2020 22:21:01 -0700