如何将整数和字符串集合的常见方法重构为一个共同的基类?

huangapple go评论96阅读模式
英文:

How to refactor common methods of sets of int and string into a common base?

问题

考虑以下定义了两种类型IntSetStringSet的程序,分别用于存储intstring类型的集合。

这些类型的Add()AddRange()Contains()ContainsAny()Length()方法基本相同(只有参数类型不同)。

我可以定义独立的函数Add()AddRange()等,而不使用方法接收器和interface{}参数来处理IntSetStringSet,但我希望这些方法与集合保持耦合。

如果我使用组合,那么基本结构体无法访问子结构体的map[...]bool

重构上述五个方法以消除代码重复的正确方法是什么?

程序:

package main

import (
	"fmt"
	"sort"
	"strconv"
	"strings"
)


type IntSet map[int]bool
type StringSet map[string]bool

func NewStringSet(vs []string) StringSet {
	ss := StringSet{}
	for _, v := range vs {
		ss.Add(v)
	}
	return ss
}

func (ss StringSet) Add(v string) bool {
	_, found := ss[v]
	ss[v] = true
	return !found
}

func (ss StringSet) AddRange(vs []string) {
	for _, v := range vs {
		ss[v] = true
	}
}

func (ss StringSet) Contains(v string) bool {
	_, found := ss[v]
	return found
}

func (ss StringSet) ContainsAny(vs []string) bool {
	for _, v := range vs {
		if _, found := ss[v]; found {
			return true
		}
	}
	return false
}

func (ss StringSet) Length() int {
	return len(ss)
}

func (ss StringSet) Stringify() string {
	vs := make([]string, len(ss))
	i := 0
	for v := range ss {
		vs[i] = v
		i++
	}
	return strings.Join(vs, ",")
}

func NewIntSet(vs []int) IntSet {
	is := IntSet{}
	for _, v := range vs {
		is.Add(v)
	}
	return is
}

func (is IntSet) Add(v int) bool {
	_, found := is[v]
	is[v] = true
	return !found
}

func (is IntSet) AddRange(vs []int) {
	for _, v := range vs {
		is[v] = true
	}
}

func (is IntSet) Contains(v int) bool {
	_, found := is[v]
	return found
}

func (is IntSet) ContainsAny(vs []int) bool {
	for _, v := range vs {
		if _, found := is[v]; found {
			return true
		}
	}
	return false
}

func (is IntSet) Length() int {
	return len(is)
}

func (is IntSet) Stringify() string {
	vs := make([]int, 0)
	for v := range is {
		vs = append(vs, v)
	}
	sort.Ints(vs)
	ws := make([]string, 0)
	for _, v := range vs {
		s := strconv.Itoa(v)
		ws = append(ws, s)
	}
	return strings.Join(ws, ",")
}
英文:

Consider the following program that defines two types IntSet and StringSet for sets containing ints and strings respectively.

The Add(), AddRange(), Contains(), ContainsAny, and Length() of these types are basically the same (only the argument types differ).

I could define standalone functions Add(), AddRange(), ... without method receivers and with interface{} argument for IntSet or StringSet, but I would like these methods to stay coupled with the sets.

If I use composition, then the base struct can't access the map[...]bool of the child struct.

What's the proper way of refactoring the five methods above to remove code duplication?

Program:

package main
import (
"fmt"
"sort"
"strconv"
"strings"
)
type IntSet map[int]bool
type StringSet map[string]bool
func NewStringSet(vs []string) StringSet {
ss := StringSet{}
for _, v := range vs {
ss.Add(v)
}
return ss
}
func (ss StringSet) Add(v string) bool {
_, found := ss[v]
ss[v] = true
return !found
}
func (ss StringSet) AddRange(vs []string) {
for _, v := range vs {
ss[v] = true
}
}
func (ss StringSet) Contains(v string) bool {
_, found := ss[v]
return found
}
func (ss StringSet) ContainsAny(vs []string) bool {
for _, v := range vs {
if _, found := ss[v]; found {
return true
}
}
return false
}
func (ss StringSet) Length() int {
return len(ss)
}
func (ss StringSet) Stringify() string {
vs := make([]string, len(ss))
i := 0
for v := range ss {
vs[i] = v
i++
}
return strings.Join(vs, ",")
}
func NewIntSet(vs []int) IntSet {
is := IntSet{}
for _, v := range vs {
is.Add(v)
}
return is
}
func (is IntSet) Add(v int) bool {
_, found := is[v]
is[v] = true
return !found
}
func (is IntSet) AddRange(vs []int) {
for _, v := range vs {
is[v] = true
}
}
func (is IntSet) Contains(v int) bool {
_, found := is[v]
return found
}
func (is IntSet) ContainsAny(vs []int) bool {
for _, v := range vs {
if _, found := is[v]; found {
return true
}
}
return false
}
func (is IntSet) Length() int {
return len(is)
}
func (is IntSet) Stringify() string {
vs := make([]int, 0)
for v := range is {
vs = append(vs, v)
}
sort.Ints(vs)
ws := make([]string, 0)
for v := range vs {
s := strconv.Itoa(v)
ws = append(ws, s)
}
return strings.Join(ws, ",")
}

答案1

得分: 1

只保留重复的代码。从维护开销的角度来看,五种方法不是问题。

无论如何,这里有一个使用泛型的示例,也可以在Go2 playground上运行:

package main

import (
	"fmt"
)

type Set[T comparable] map[T]bool

func NewSet[T comparable](vs []T) Set[T] {
	ss := Set[T]{}
	for _, v := range vs {
		ss.Add(v)
	}
	return ss
}

func (s Set[T]) Add(v T) bool {
	_, found := s[v]
	s[v] = true
	return !found
}

func (s Set[T]) AddRange(vs []T) {
	for _, v := range vs {
		s[v] = true
	}
}

func (s Set[T]) Contains(v T) bool {
	_, found := s[v]
	return found
}

func (s Set[T]) ContainsAny(vs []T) bool {
	for _, v := range vs {
		if _, found := s[v]; found {
			return true
		}
	}
	return false
}

func (s Set[T]) Length() int {
	return len(s)
}

func (s Set[T]) Stringify() string {
	vs := make([]interface{}, len(s))
	i := 0
	for v := range s {
		vs[i] = v
		i++
	}
	return fmt.Sprintf("%v", vs)
}

func main() {
	sset := NewSet([]string{"foo", "bar"})
	sset.Add("baz")
	fmt.Println(sset.Stringify()) // [foo bar baz]

	iset := NewSet([]int{12, 13, 14})
	iset.Add(20)
	fmt.Println(iset.Stringify()) // [12 13 14 20]
}

特别说明:

  • Set 的类型参数中使用的约束必须是 comparable,因为映射键必须支持比较运算符(==!=)。
  • 所有接收器中必须显式重复类型参数,但约束不需要重复。因此,在所有方法中都有 func (s Set[T]) ... 的形式。
  • Stringify() 的实现有些麻烦,因为类型参数 T comparable 不支持字符串操作。它只是一个 comparable。因此,我简单地使用了 []interface{}fmt.Sprintf,这样就完成了任务。
英文:

Just keep the duplicate code. Five methods are a non-issue in terms of maintenance overhead.

Anyway here's an obligatory example with generics, which also works on the Go2 playground:

package main
import (
"fmt"
)
type Set[T comparable] map[T]bool
func NewSet[T comparable](vs []T) Set[T] {
ss := Set[T]{}
for _, v := range vs {
ss.Add(v)
}
return ss
}
func (s Set[T]) Add(v T) bool {
_, found := s[v]
s[v] = true
return !found
}
func (s Set[T]) AddRange(vs []T) {
for _, v := range vs {
s[v] = true
}
}
func (s Set[T]) Contains(v T) bool {
_, found := s[v]
return found
}
func (s Set[T]) ContainsAny(vs []T) bool {
for _, v := range vs {
if _, found := s[v]; found {
return true
}
}
return false
}
func (s Set[T]) Length() int {
return len(s)
}
func (s Set[T]) Stringify() string {
vs := make([]interface{}, len(s))
i := 0
for v := range s {
vs[i] = v
i++
}
return fmt.Sprintf("%v", vs)
}
func main() {
sset := NewSet([]string{"foo", "bar"})
sset.Add("baz")
fmt.Println(sset.Stringify()) // [foo bar baz]
iset := NewSet([]int{12, 13, 14})
iset.Add(20)
fmt.Println(iset.Stringify()) // [12 13 14 20]
}

In particular:

  • the constraint used in Set's type parameter must be comparable, as map keys must support comparison operators (==, !=)
  • the type param must be repeated explicitly in all receivers, but the constraint need not be repeated. So you have func (s Set[T]) ... in all methods
  • the implementation of Stringify() is pesky because the type parameter T comparable doesn't support string operations. It's just a comparable. So above I naively use []interface{} and fmt.Sprintf, which just does the job

huangapple
  • 本文由 发表于 2021年11月9日 20:37:32
  • 转载请务必保留本文链接:https://go.coder-hub.com/69898275.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定