Go Language Design pattern Practice: combination (Composite)

Source: Internet
Author: User
This is a creation in Article, where the information may have evolved or changed.

About this series

This series is first about the go language practice. The actual use of the go language in the project also has a period of time, an experience is whether it is official documents, books or web materials, about the go language idioms (idiom) Introduction are relatively small, basic can only rely on the standard library source code to ponder their own, so I particularly want to have some collection and summary.

Then the series is about design patterns. Although the go language is not an object-oriented programming language, many object-oriented design patterns need to solve the problem that exists in the programming. No matter what language, always have to face and solve these problems, but the solution of the ideas and ways will be different. So I want to use the classic design pattern as a starting point to expand this series, after all, we are familiar with the design patterns, you can avoid out of nowhere to come up with some crappy application scenarios.

The specific topics in this series will be more flexible, including the topics of the program:

    1. Go language idiomatic method.
    2. The implementation of the design pattern. In particular, the introduction of closures, Ducktype, and other language features brought about by the changes.
    3. The discussion of design pattern thought. There will be some spit slots.

Gof's definition of combinatorial mode is that combining objects into a tree structure to represent a "partial whole" hierarchy, combining patterns makes the user consistent with the use of individual objects and composite objects .

I have the objection to this sentence, here first sell a Xiaoguanzi, we start from the actual example.

The example of the combination mode is a lot of people, such as file system (File/folder), GUI window (Frame/control), Menu (Menu/menu item), etc., I also give a menu example, but not the operating system menu, is the real menu, KFC's ...

Let's think of the KFC food as 菜单项 a set meal 菜单 . Menus and menu items have public properties: names, descriptions, prices, all can be purchased, and so, as Gof says, we need to use them consistently. Their hierarchies are reflected in a menu that contains multiple menu items or menus, and the price is the and of all children. Well, this example is not very appropriate, not very good to reflect the menu contains the menu, so I have defined a "value Lunch" menu, which contains a number of packages.

summed up in code, the final call code is this:

func main() {menu1 := NewMenu("培根鸡腿燕麦堡套餐", "供应时间:09:15--22:44")menu1.Add(NewMenuItem("主食", "培根鸡腿燕麦堡1个", 11.5))menu1.Add(NewMenuItem("小吃", "玉米沙拉1份", 5.0))menu1.Add(NewMenuItem("饮料", "九珍果汁饮料1杯", 6.5))menu2 := NewMenu("奥尔良烤鸡腿饭套餐", "供应时间:09:15--22:44")menu2.Add(NewMenuItem("主食", "新奥尔良烤鸡腿饭1份", 15.0))menu2.Add(NewMenuItem("小吃", "新奥尔良烤翅2块", 11.0))menu2.Add(NewMenuItem("饮料", "芙蓉荟蔬汤1份", 4.5))all := NewMenu("超值午餐", "周一至周五有售")all.Add(menu1)all.Add(menu2)all.Print()}

The resulting output is as follows:

超值午餐, 周一至周五有售, ¥53.50------------------------培根鸡腿燕麦堡套餐, 供应时间:09:15--22:44, ¥23.00------------------------  主食, ¥11.50    -- 培根鸡腿燕麦堡1个  小吃, ¥5.00    -- 玉米沙拉1份  饮料, ¥6.50    -- 九珍果汁饮料1杯奥尔良烤鸡腿饭套餐, 供应时间:09:15--22:44, ¥30.50------------------------  主食, ¥15.00    -- 新奥尔良烤鸡腿饭1份  小吃, ¥11.00    -- 新奥尔良烤翅2块  饮料, ¥4.50    -- 芙蓉荟蔬汤1份

Object-oriented implementation

First of all: The go language is not an object-oriented language, in fact there are only structs and no classes or objects. But for the sake of convenience, I will use this term to represent the definition of a struct and use 对象 this term to represent a struct instance.

By convention, the classical object-oriented analysis is used first. First, we need to define the abstract base class for menus and menu items so that the user can rely on the interface only, thus achieving consistency in use.

There is no inheritance in the go language, so we define the abstract base class as an interface, which is followed by menus and menu items for specific functions:

type MenuComponent interface {Name() stringDescription() stringPrice() float32Print()Add(MenuComponent)Remove(int)Child(int) MenuComponent}

The implementation of the menu item:

type MenuItem struct {name        stringdescription stringprice       float32}func NewMenuItem(name, description string, price float32) MenuComponent {return &MenuItem{name:        name,description: description,price:       price,}}func (m *MenuItem) Name() string {return m.name}func (m *MenuItem) Description() string {return m.description}func (m *MenuItem) Price() float32 {return m.price}func (m *MenuItem) Print() {fmt.Printf("  %s, ¥%.2f\n", m.name, m.price)fmt.Printf("    -- %s\n", m.description)}func (m *MenuItem) Add(MenuComponent) {panic("not implement")}func (m *MenuItem) Remove(int) {panic("not implement")}func (m *MenuItem) Child(int) MenuComponent {panic("not implement")}

Please note that there are two points.

    1. Newmenuitem () creates a MenuItem, but returns an abstract interface MenuComponent. (polymorphic in object-oriented)
    2. Because MenuItem is a leaf node, it is not possible to provide an implementation of the three methods of the Add () Remove () child (), so the call will be panic.

The following is the implementation of the menu:

type Menu struct {name        stringdescription stringchildren    []MenuComponent}func NewMenu(name, description string) MenuComponent {return &Menu{name:        name,description: description,}}func (m *Menu) Name() string {return m.name}func (m *Menu) Description() string {return m.description}func (m *Menu) Price() (price float32) {for _, v := range m.children {price += v.Price()}return}func (m *Menu) Print() {fmt.Printf("%s, %s, ¥%.2f\n", m.name, m.description, m.Price())fmt.Println("------------------------")for _, v := range m.children {v.Print()}fmt.Println()}func (m *Menu) Add(c MenuComponent) {m.children = append(m.children, c)}func (m *Menu) Remove(idx int) {m.children = append(m.children[:idx], m.children[idx+1:]...)}func (m *Menu) Child(idx int) MenuComponent {return m.children[idx]}

Price()it counts the post-addition of all the subkeys and outputs information of Price all the Print() subkeys after outputting their own information. Also note Remove() the implementation (remove an item from slice).

OK, now consider the following 3 questions for this implementation.

    1. MenuItemAnd Menu in the name, description these two properties and methods, repeat write two times obvious redundancy. If you use any other object-oriented language, both properties and methods should be implemented in the base class. But go does not inherit, this is really the pit father.
    2. Are we really implementing user-consistent access here? Obviously not, when the user gets one MenuComponent , still need to know its type to be able to use correctly, if do not judge in MenuItem use Add() and so on the method that does not realize will produce panic. Similarly, we can abstract folders/files into the "File System node", can read the name, can calculate the occupied space, but once we want to add a child node to the "File System node", still have to determine whether it is a folder.
    3. Then 2nd continues to ponder: what is the essential reason for a consistent access phenomenon? One view: Menu and MenuItem some are essentially (IS-A) the same thing ( MenuComponent ), so you can access them consistently; another view: Menu and MenuItem is two different things, just happen to have some of the same properties, so they can be consistent access.

Replace inheritance with a combination

As mentioned earlier, the go language does not inherit, and the name and description that originally belong to the base class cannot be implemented in the base class. In fact, as long as the conversion of ideas, the problem is very easy to solve with the combination. If we think Menu and in MenuItem essence is two different things, just happen to have (HAS-A) some of the same properties, then the same attributes are drawn out, and then separately combined into the two, the problem is solved.

First look at the extracted properties:

type MenuDesc struct {name        stringdescription string}func (m *MenuDesc) Name() string {return m.name}func (m *MenuDesc) Description() string {return m.description}

Rewrite MenuItem :

type MenuItem struct {MenuDescprice float32}func NewMenuItem(name, description string, price float32) MenuComponent {return &MenuItem{MenuDesc: MenuDesc{name:        name,description: description,},price: price,}}// ... 方法略 ...

Rewrite Menu :

type Menu struct {MenuDescchildren []MenuComponent}func NewMenu(name, description string) MenuComponent {return &Menu{MenuDesc: MenuDesc{name:        name,description: description,},}}// ... 方法略 ...

The use of combinations in the go language helps to express the intent of data structures . Especially when a more complex object is dealing with several things at the same time, it is very clear and elegant to split the objects into separate parts and then combine them together. For example MenuItem , above is the description + price, Menu is the description + sub-menu.

In fact Menu , a better approach is to take children and Add() Remove() Child() also extract the package after the combination, such Menu a function at a glance.

type MenuGroup struct {children []MenuComponent}func (m *Menu) Add(c MenuComponent) {m.children = append(m.children, c)}func (m *Menu) Remove(idx int) {m.children = append(m.children[:idx], m.children[idx+1:]...)}func (m *Menu) Child(idx int) MenuComponent {return m.children[idx]}type Menu struct {MenuDescMenuGroup}func NewMenu(name, description string) MenuComponent {return &Menu{MenuDesc: MenuDesc{name:        name,description: description,},}}

The way of thinking in the Go language

The following is the focus of this article. Using Go language development project 2 months, the biggest feeling is: Learning go language must change the way of thinking, change success is fun, can not be changed in time will find themselves everywhere.

Let's implement the KFC menu in a real go way. First of all, please repeat three times: no inheritance, no inheritance, no inheritance, no base class, no base class, no base class, interface is just a collection of function signatures, interface is just a collection of function signatures, interface is only a collection of function signatures; struct does not depend on interfaces, structs do not depend on interfaces. Structs do not depend on interfaces.

Well, unlike before, now we do not define the interface to implement specifically, because the struct does not depend on the interface, so we directly implement the specific function. First MenuDesc and MenuItem , note that NewMenuItem the return value type is now *MenuItem .

type MenuDesc struct {name        stringdescription string}func (m *MenuDesc) Name() string {return m.name}func (m *MenuDesc) Description() string {return m.description}type MenuItem struct {MenuDescprice float32}func NewMenuItem(name, description string, price float32) *MenuItem {return &MenuItem{MenuDesc: MenuDesc{name:        name,description: description,},price: price,}}func (m *MenuItem) Price() float32 {return m.price}func (m *MenuItem) Print() {fmt.Printf("  %s, ¥%.2f\n", m.name, m.price)fmt.Printf("    -- %s\n", m.description)}

Next up is MenuGroup . We know that it MenuGroup is a set of menus/menu items whose children type is indeterminate, so we know that we need to define an interface here. And because MenuGroup the logic is to do the children increment, delete, read operation, the properties of the children property does not have any constraints and requirements, so here we temporarily define the interface as an empty interface interface{} .

type MenuComponent interface {}type MenuGroup struct {children []MenuComponent}func (m *Menu) Add(c MenuComponent) {m.children = append(m.children, c)}func (m *Menu) Remove(idx int) {m.children = append(m.children[:idx], m.children[idx+1:]...)}func (m *Menu) Child(idx int) MenuComponent {return m.children[idx]}

The last is Menu the implementation:

type Menu struct {MenuDescMenuGroup}func NewMenu(name, description string) *Menu {return &Menu{MenuDesc: MenuDesc{name:        name,description: description,},}}func (m *Menu) Price() (price float32) {for _, v := range m.children {price += v.Price()}return}func (m *Menu) Print() {fmt.Printf("%s, %s, ¥%.2f\n", m.name, m.description, m.Price())fmt.Println("------------------------")for _, v := range m.children {v.Print()}fmt.Println()}

In the Menu process of implementation, we find that Menu children there are actually two constraints on it: there is a need for Price() methods and Print() methods. The MenuComponent changes are then made:

type MenuComponent interface {Price() float32Print()}

Finally observe MenuItem and Menu , they all conform to MenuComponent the constraints, so both can become Menu children , the combination mode is done!

Comparison and thinking

Before and after two code differences are actually very small:

    1. The second implementation has a simple interface, with only two functions.
    2. The new function returns a different type of value.

From the perspective of thinking, the difference is very big but also some subtle:

    1. The first implementation of the interface is a template, is the blueprint of the struct, its properties derived from the system components in advance of the comprehensive analysis of induction; the second implementation in the interface is a constraint declaration, whose properties are derived from the user's requirements for the user.
    2. The first implementation considers a children MenuComponent specific object, which has a series of methods that can be called, except that the function of its method behaves differently because of the subclass coverage, while the second implementation considers that it children can be MenuComponent any unrelated object, and the only requirement is that they "happen" Implements the constraints specified by the interface.

Note that the first implementation, MenuComponent medium Add() , Remove() and Child() three methods, but not necessarily available, can be used by the type of the specific object, and the second implementation does not have these unsafe methods, because the new function returns a specific type, So the methods that can be called are safe.

In addition, Menu removing a child from the, the available methods only Price() and Print() , as can be completely safe call. What if you want to MenuComponent add a subkey to the Menu case? Very simple:

if m, ok := all.Child(1).(*Menu); ok {m.Add(NewMenuItem("玩具", "Hello Kitty", 5.0))}

Clearly, if a child is a Menu , then we can Add() manipulate it.

Further, here we are not so strong in the type requirements, do not need it must be Menu , just need to provide a combination MenuComponent of functions, so you can extract such an interface:

type Group interface {Add(c MenuComponent)Remove(idx int)Child(idx int) MenuComponent}

The code for the previous add subkey is changed to this:

if m, ok := all.Child(1).(Group); ok {m.Add(NewMenuItem("玩具", "Hello Kitty", 5.0))}

Consider the "buy" operation, the object-oriented implementation, the type of purchase is MenuComponent , so the purchase operation can also be applied to Menu and MenuItem . If you look at the way of thinking in the go language, the only thing that can be purchased is to have it Price() , so the parameters of the purchase operation are the interface:

type Product interface {    Price() float32}

The purchase operation can be applied not only to Menu and but MenuItem also to any object that provides the price. We can add any product, whether it is a toy or membership card or coupon, as long as there is a Price() way to be purchased.

Summarize

Finally, I would like to summarize my thoughts and welcome you to discuss or criticize:

    1. In composite mode, consistent access is a pseudo-requirement. Consistent access is not a requirement that we need to meet at design time, but a natural effect when different entities have the same attributes. In the example above, we created two different types of menu and MenuItem, but since they have the same properties, we can fetch the price in the same way, take the description, and join the menu as a child.
    2. The polymorphism of Go language is not reflected in the object creation phase, but in the object use stage, reasonable use of "small interface" can significantly reduce the system coupling degree.

PS. This article covers three complete code, I put on the play.golang.org: (Need FQ)

    • Object-oriented implementation: Http://play.golang.org/p/2DzGhVYseY
    • Use combination: http://play.golang.org/p/KuH2Vu7f9k
    • The way of thinking in the Go language: http://play.golang.org/p/TGjI3CDHD4
Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.