As mentioned in the previous article, Go Design Patterns (2)- Object-oriented Analysis and Design, the most important thing in designing is to keep the right extension points. How do you design the right extension points?

This article will cover some classic design principles. You’ve probably heard these design principles before, but you probably haven’t thought about why they were distilled or what they do. I will try to find an example of an internal design principle that illustrates its importance. It is much more effective to feel principles through examples than through dry words.

Here it needs to be explained that design principle is an idea, and design mode is the embodiment of this idea. So when we really understand this idea, we can get twice the result with half the effort when designing.

The principles to be elaborated in this paper are as follows:

  1. Single responsibility principle
  2. Open – close principle
  3. Richter’s substitution principle
  4. Interface Isolation Principle
  5. Rely on the inversion principle
  6. Demeter’s rule

Single responsibility principle

To understand the principle

Single Responsibility principle (SRP) : A class is responsible for only one responsibility or function. Don’t design large, comprehensive classes. Design small, single-purpose classes. The principle of single responsibility is to achieve high cohesion and low coupling of code, and improve code reusability, readability, and maintainability.

The implementation of

Different application scenarios, requirement backgrounds at different stages, different business levels, and whether the responsibilities of the same class are single may have different results. In fact, some of the side judgments are more instructive and actionable. For example, the following situations may indicate that the design does not meet the single responsibility principle:

  • Too many lines of code, functions, or attributes in a class;
  • A class depends on too many other classes, or depends on too many other classes;
  • Too many private methods;
  • It can be difficult to name a class properly;
  • A large number of methods in a class operate on a few properties of the class.

The instance

Suppose we want to make a tetris Game to play on mobile phones, the Game class can be designed as follows:

type Game struct {
   x int64
   y int64
}
func (game *Game) Show(a) {
   fmt.Println(game.x, game.y)
}
func (game *Game) Move(a) {
   game.x--
   game.y++
}
Copy the code

The display and movement of the Game are placed in the class Game. Later requirements changed, not only to display on the phone, but also need to display on the computer, and there are two versus mode, these changes are mainly related to display.

In this case, it is best to split Show and Move into two functions, so that you can reuse the logic of Move, and any future changes to Show will not affect the class in which Move is located.

However, since the Game has different responsibilities at the beginning, there are many places in the system that use the same Game variable to call Show and Move, and changing and testing these places can be a waste of time.

Open – close principle

To understand the principle

Open to Extension, Modify Closed (OCP) : Adding new functionality should be done by extending the existing code (adding modules, classes, methods, properties, etc.) rather than modifying the existing code (modifying modules, classes, methods, properties, etc.).

  • First, the open closed principle does not mean that changes are completely eliminated, but that new features are developed at the minimum cost of changing the code.
  • Second, the same code change may be considered a “change” under coarse-code granularity. At a fine code granularity, it might be considered “extended.”

The implementation of

We should always have the consciousness of expansion, abstraction and encapsulation. In writing the code, we should spend more time to think about, what this code possible future demand changes, how to design the code structure, keep good extension points in advance, to demand changes in the future, without changes to the code structure, minimize code changes, insert the new code and flexibly to the extension point.

Many design principles, ideas, and patterns are designed to make code more extensible. In particular, most of the 23 classic design patterns are summed up to solve the problem of code extensibility, and are guided by the open and closed principle. The most common approaches to code extensibility are polymorphism, dependency injection, interface-based programming rather than implementation, and most design patterns (e.g., decorators, policies, templates, chains of responsibility, and state).

The instance

Suppose we want to make an API interface to monitor alarms. If TPS or Error exceeds the specified value, relevant personnel will be notified in different ways (email or phone) according to different emergencies. According to Go Design Pattern (2)- Object-oriented Analysis and Design, we first find the class.

The business implementation process is as follows:

  1. Obtaining abnormal indicators
  2. Obtain abnormal data and compare with abnormal indicators
  3. Inform relevant personnel

So, we can set up three classes, AlertRules for alarm rules, Notification for Notification, and Alert for comparison.

// Store alarm rules
type AlertRules struct{}func (alertRules *AlertRules) GetMaxTPS(api string) int64 {
   if api == "test" {
      return 10
   }
   return 100
}
func (alertRules *AlertRules) GetMaxError(api string) int64 {
   if api == "test" {
      return 10
   }
   return 100
}

const (
   SERVRE = "SERVRE"
   URGENT = "URGENT"
)

/ / notice
type Notification struct{}func (notification *Notification) Notify(notifyLevel string) bool {
   if notifyLevel == SERVRE {
      fmt.Println("Make a call")}else if notifyLevel == URGENT {
      fmt.Println("Texting")}else {
      fmt.Println("Send an email")}return true
}

/ / check
type Alert struct {
   alertRules   *AlertRules
   notification *Notification
}

func CreateAlert(a *AlertRules, n *Notification) *Alert {
   return &Alert{
      alertRules:   a,
      notification: n,
   }
}
func (alert *Alert) Check(api string, tps int64, errCount int64) bool {
   if tps > alert.alertRules.GetMaxTPS(api) {
      alert.notification.Notify(URGENT)
   }
   if errCount > alert.alertRules.GetMaxError(api) {
      alert.notification.Notify(SERVRE)
   }
   return true
}
func main(a) {
   alert := CreateAlert(new(AlertRules), new(Notification))
   alert.Check("test".20.20)}Copy the code

Although the program is relatively simple, but is object-oriented, and can run.

There are many possible points of change for this requirement, the most likely being the addition of new alarm indicators. Now the new requirement comes, if the interface timeout per second exceeds the specified value, also need to alarm, what do we need to do?

If we were to modify the original code, we need to

  1. Added new rules on AlertRules

  2. The Check function adds a new entry parameter, timeoutCount

  3. Added new logic to Check function

    if timeoutCount > alert.alertRules.GetMaxTimeoutCount(api) {
       alert.notification.Notify(SERVRE)
    }
    Copy the code

This can cause some problems. First, Check may be referenced in multiple places, so these locations need to be modified, and second, the Check logic has been changed and this part of the test needs to be redone. If we did the first release and didn’t anticipate these changes, but now we find the possible change points, do we have a good plan to scale and minimize the change next time?

We split up the things that Check does in Alert and put them in the corresponding classes that implement the AlertHandler interface.

/ / optimization
type ApiStatInfo struct {
   api          string
   tps          int64
   errCount     int64
   timeoutCount int64
}

type AlertHandler interface {
   Check(apiStatInfo ApiStatInfo) bool
}

type TPSAlertHandler struct {
   alertRules   *AlertRules
   notification *Notification
}

func CreateTPSAlertHandler(a *AlertRules, n *Notification) *TPSAlertHandler {
   return &TPSAlertHandler{
      alertRules:   a,
      notification: n,
   }
}

func (tPSAlertHandler *TPSAlertHandler) Check(apiStatInfo ApiStatInfo) bool {
   if apiStatInfo.tps > tPSAlertHandler.alertRules.GetMaxTPS(apiStatInfo.api) {
      tPSAlertHandler.notification.Notify(URGENT)
   }
   return true
}

type ErrAlertHandler struct {
   alertRules   *AlertRules
   notification *Notification
}

func CreateErrAlertHandler(a *AlertRules, n *Notification) *ErrAlertHandler {
   return &ErrAlertHandler{
      alertRules:   a,
      notification: n,
   }
}

func (errAlertHandler *ErrAlertHandler) Check(apiStatInfo ApiStatInfo) bool {
   if apiStatInfo.errCount > errAlertHandler.alertRules.GetMaxError(apiStatInfo.api) {
      errAlertHandler.notification.Notify(SERVRE)
   }
   return true
}

type TimeOutAlertHandler struct {
   alertRules   *AlertRules
   notification *Notification
}

func CreateTimeOutAlertHandler(a *AlertRules, n *Notification) *TimeOutAlertHandler {
   return &TimeOutAlertHandler{
      alertRules:   a,
      notification: n,
   }
}

func (timeOutAlertHandler *TimeOutAlertHandler) Check(apiStatInfo ApiStatInfo) bool {
   if apiStatInfo.timeoutCount > timeOutAlertHandler.alertRules.GetMaxTimeOut(apiStatInfo.api) {
      timeOutAlertHandler.notification.Notify(SERVRE)
   }
   return true
}
Copy the code

Handlers []AlertHandler and add the following functions

/ / version 2
func (alert *Alert) AddHanler(alertHandler AlertHandler) {
   alert.handlers = append(alert.handlers, alertHandler)
}
func (alert *Alert) CheckNew(apiStatInfo ApiStatInfo) bool {
   for _, h := range alert.handlers {
      h.Check(apiStatInfo)
   }
   return true
}
Copy the code

Call as follows:

func main(a) {
   alert := CreateAlert(new(AlertRules), new(Notification))
   alert.Check("test".20.20)
   // Version 2, alert does not require the member variables AlertRules and Notification
   a := new(AlertRules)
   n := new(Notification)
   alert.AddHanler(CreateTPSAlertHandler(a, n))
   alert.AddHanler(CreateErrAlertHandler(a, n))
   alert.AddHanler(CreateTimeOutAlertHandler(a, n))
   apiStatInfo := ApiStatInfo{
      api:          "test",
      timeoutCount: 20,
      errCount:     20,
      tps:          20,
   }
   alert.CheckNew(apiStatInfo)
}
Copy the code

In this way, no matter how many alarm indicators are added in the future, we just need to create a new Handler class and put it in the alert. Code changes are minimal and do not require repeated testing.

There are many changes in the system, you can try to change it yourself, all the code location: github.com/shidawuhen/…

Omega substitution principle

To understand the principle

Richter’s Substitution principle (LSP) : A subclass object can replace a superclass object anywhere in the program, and ensure that the logical behavior of the original program remains unchanged and the correctness of the program is not damaged.

The difference between polymorphism and Richter’s substitution principle: Polymorphism is a feature of object-oriented programming and a syntax of object-oriented programming languages. It’s an idea of code implementation. The interior substitution is a design principle, which is used to guide how to design the subclass in the inheritance relationship. The design of the subclass should ensure that when replacing the parent class, the logic of the original program is not changed and the correctness of the original program is not damaged.

The implementation of

The Li substitution principle doesn’t just say that a subclass can replace a parent class.

Subclasses are designed to follow the behavior conventions (or agreements) of their parent classes. The parent class defines the behavior convention of a function, and the subclass can change the internal implementation logic of a function, but not the original behavior convention of the function. The behavior conventions here include: function declaration to implement the function; Conventions for inputs, outputs, and exceptions; Even any special instructions listed in the notes. So we can judge whether we violate The Richter’s substitution principle by following several points:

  • Subclasses violate the functionality that the parent class claims to implement: sorting functions, such as sorting by amount, and subclasses by time
  • A subclass violates its parent’s convention on inputs, outputs, and exceptions
  • Subclasses violate any special instructions listed in the parent class comments

The instance

Richter’s substitution principle can improve code extensibility. Suppose we need to make a message sending function, initially only need to send in-station messages.

type Message struct{}func (message *Message) Send(a) {
   fmt.Println("message send")}func LetDo(notify *Message) {
	notify.Send()
}
func main(a) {
	LetDo(new(Message))
}
Copy the code

After the implementation is complete, LetDo is called in many places to send messages. If you want to use SMS to replace the message inside the station, it is very troublesome to deal with it. Therefore, the best solution is to use the Richter substitution principle, does not affect the new notification method access.

// Richter's substitution principle
type Notify interface {
	Send()
}
type Message struct{}func (message *Message) Send(a) {
	fmt.Println("message send")}type SMS struct{}func (sms *SMS) Send(a) {
	fmt.Println("sms send")}func LetDo(notify Notify) {
	notify.Send()
}

func main(a) {
	// Richter's substitution principle
	LetDo(new(Message))
}
Copy the code

Interface Isolation Principle

To understand the principle

Interface Isolation Principle (ISP) : A client should not be forced to rely on interfaces it does not need

The difference between the interface isolation principle and the single responsibility principle: The single responsibility principle applies to the design of modules, classes, and interfaces. The interface isolation principle provides a criterion for determining whether an interface has a single responsibility: indirectly by how callers use the interface. If the caller uses only part of the interface or part of the function of the interface, the design of the interface is not responsible enough.

The implementation of

If “interface” is understood as a set of interfaces, it can be the interface of a microservice, or the interface of a class library, etc. If part of the interface is used only by part of the callers, we need to isolate that part of the interface and give it to that part of the callers without forcing other callers to rely on the part of the interface that is not used. If the “interface” is understood as a single API interface or function, and some callers need only some of the functions in the function, then we need to break the function into more fine-grained functions so that the caller only depends on the fine-grained function it needs. If “interface” is understood as an interface in OOP, it can also be understood as an interface syntax in object-oriented programming languages. The interface design should be as simple as possible, so that the implementation class and the caller of the interface do not rely on interface functions that are not needed.

The instance

Suppose the project uses three external systems: Redis, MySQL, and Kafka. Redis and Kafaka support hot updates. MySQL and Redis have display monitoring capabilities. How do we design the interface for this requirement?

One way is to put everything in one interface, the other way is to put the two functions in different interfaces. The following code is written according to the interface isolation principle:

// Interface isolation principle
type Updater interface {
   Update() bool
}

type Shower interface {
   Show() string
}

type RedisConfig struct{}func (redisConfig *RedisConfig) Connect(a) {
   fmt.Println("I am Redis")}func (redisConfig *RedisConfig) Update(a) bool {
   fmt.Println("Redis Update")
   return true
}

func (redisConfig *RedisConfig) Show(a) string {
   fmt.Println("Redis Show")
   return "Redis Show"
}

type MySQLConfig struct{}func (mySQLConfig *MySQLConfig) Connect(a) {
   fmt.Println("I am MySQL")}func (mySQLConfig *MySQLConfig) Show(a) string {
   fmt.Println("MySQL Show")
   return "MySQL Show"
}

type KafkaConfig struct{}func (kafkaConfig *KafkaConfig) Connect(a) {
   fmt.Println("I am Kafka")}func (kafkaConfig *KafkaConfig) Update(a) bool {
   fmt.Println("Kafka Update")
   return true
}

func ScheduleUpdater(updater Updater) bool {
   return updater.Update()
}
func ServerShow(shower Shower) string {
   return shower.Show()
}

func main(a) {
   // Interface isolation principle
   fmt.Println("Interface Isolation Principle")
   ScheduleUpdater(new(RedisConfig))
   ScheduleUpdater(new(KafkaConfig))
   ServerShow(new(RedisConfig))
   ServerShow(new(MySQLConfig))
}
Copy the code

This approach has the following advantages over placing Update and Show on the same interface:

  1. No need to reinvent the wheel. MySQL does not need to write hot update functions, Kafka does not need to write monitor display functions
  2. Good reusability and expansibility. If a new system is added, you only need to monitor the display function, implement the Shower interface, and reuse the ServerShow function.

Rely on the inversion principle

To understand the principle

Dependency reversal principle (DIP) : High-level modules do not depend on low-level modules. High-level and low-level modules should depend on each other through abstractions. In addition, abstractions should not rely on details. Details should rely on abstractions.

The implementation of

Use interfaces and abstract classes for variable type declarations, parameter type declarations, method return type declarations, and data type conversions, rather than concrete classes, when passing parameters in program code or in relational relationships. The core idea is: program to the interface, not to the implementation.

practice

This can be explained directly by using the example of the Li substitution. LetDo uses the principle of dependency reversal to improve the expansibility of the code and flexibly replace the dependent classes.

Demeter’s rule

To understand the principle

Demeter’s Law (LOD) : No dependencies between classes that should not have direct dependencies; Try to rely on only the necessary interfaces between classes that have dependencies

The implementation of

Demeter’s rule is mainly used to achieve high cohesion and low coupling.

High cohesion: Similar functions should be placed in the same class, and unrelated functions should not be placed in the same class

Loose coupling: In code, the dependencies between classes are simple and clear

Reduce coupling between classes and make them as independent as possible. Each class should know less about the rest of the system. Once a change occurs, there are fewer classes that need to know about it.

practice

Suppose we want to do a search engine crawling web page function, function point is

  1. The initiating
  2. Download the web page
  3. Analysis of web page

Therefore, we set up three classes NetworkTransporter responsible for the underlying network, used to obtain data, HtmlDownloader to download web pages, Document used to analyze web pages. Here’s the code that follows Demeter’s rule

// Demeter's rule
type Transporter interface {
   Send(address string, data string) bool
}
type NetworkTransporter struct{}func (networkTransporter *NetworkTransporter) Send(address string, data string) bool {
   fmt.Println("NetworkTransporter Send")
   return true
}

type HtmlDownloader struct {
   transPorter Transporter
}

func CreateHtmlDownloader(t Transporter) *HtmlDownloader {
   return &HtmlDownloader{transPorter: t}
}

func (htmlDownloader *HtmlDownloader) DownloadHtml(a) string {
   htmlDownloader.transPorter.Send("123"."test")
   return "htmDownloader"
}

type Document struct {
   html string
}

func (document *Document) SetHtml(html string) {
   document.html = html
}

func (document *Document) Analyse(a) {
   fmt.Println("document analyse " + document.html)
}

func main(a) {
   // Demeter's rule
   fmt.Println(Demeter's rule)
   htmlDownloader := CreateHtmlDownloader(new(NetworkTransporter))
   html := htmlDownloader.DownloadHtml()
   doc := new(Document)
   doc.SetHtml(html)
   doc.Analyse()
}
Copy the code

This way of writing it corresponds to two parts of Demeter’s rule

  1. Don’t have dependencies between classes that shouldn’t have direct dependencies. Document does not need to rely on HtmlDownloader, Document function is to analyze the web page, how to get the web page is not concerned. The advantage of this is that no matter how HtmlDownloader changes, the Document doesn’t need to be concerned.
  2. Try to rely on only the necessary interfaces between classes that have dependencies. HtmlDownloader must rely on NetworkTransporter to download web pages. The interface used here is for the future if there is a better underlying network function, it can be quickly replaced. Of course, there is a kind of transition design here, mainly to fit Demeter’s law. Whether specific need such design, or according to the specific situation to judge.

conclusion

Finally finished writing these six principles, but the benefits for me are also obvious, reorganize the knowledge structure, the understanding of the principles is also deeper. From a macro point of view, these principles are all for the purpose of reusable, extensible, high cohesion, low coupling. Now we have mastered the Go object-oriented grammar, how to do object-oriented analysis and design, object-oriented design principles on the basis of, you can do some object-oriented things.

Principles are tao, design patterns are art, and I’ll write about design patterns later.

All code locations for this article are: github.com/shidawuhen/…

data

  1. Design Patterns – Golang implements seven design principles
  2. The beauty of design patterns

The last

If you like my article, you can follow my public account (Programmer Malatang)

My personal blog is shidawuhen.github. IO /

Review of previous articles:

technology

  1. Go Design Patterns (3)- Design principles
  2. Go Design Pattern (2)- Object-oriented analysis and design
  3. Payment access general issues
  4. HTTP2.0 basics tutorial
  5. Go Design Pattern (1)- Syntax
  6. MySQL development specification
  7. HTTPS Configuration Combat
  8. Implementation principle of Go channel
  9. Implementation principle of Go timer
  10. HTTPS Connection Process
  11. Current limiting 2
  12. Seconds kill system
  13. Distributed systems and consistency protocols
  14. Service framework and registry for microservices
  15. Beego framework use
  16. Discussion on Micro-service
  17. TCP Performance Optimization
  18. Current limiting implementation 1
  19. Redis implements distributed locking
  20. Golang source BUG tracking
  21. The implementation principle of atomicity, consistency and persistence of transactions
  22. CDN request process details
  23. Common Cache tips
  24. How to effectively connect with third-party payment
  25. Gin framework concise version
  26. InnoDB locks and transactions
  27. Algorithm is summarized

Reading notes

  1. The principle of
  2. History As A Mirror
  3. Agile revolution
  4. How to exercise your memory
  5. Simple Logic – After reading
  6. Hot Wind – After reading
  7. Analects of Confucius – After reading
  8. Sun Tzu’s Art of War – Reflections from reading

thinking

  1. Service team holiday shift plan
  2. Project process management
  3. Some thoughts on project management
  4. Some thoughts on product manager
  5. Thinking about programmer career development
  6. Thinking about code review
  7. Markdown editor recommends – Typora