Patrones de diseño en la práctica GoF: Patrón Singleton

Este artículo es compartido por la comunidad HUAWEI CLOUD " [Implementación de Go] Práctica 23 Patrones de diseño de GoF: Patrón Singleton ", autor: Yuan Runzi.

Brevemente

GoF define el patrón singleton de la siguiente manera:

Asegúrese de que una clase solo tenga una instancia y proporcione un punto de acceso global a ella.

Es decir, garantizar que una clase tenga solo una instancia y proporcionarle un punto de acceso global .

En programación, algunos objetos generalmente solo necesitan una instancia compartida, como un grupo de subprocesos, un caché global, un grupo de objetos, etc. La forma más fácil y directa de implementar instancias compartidas es usar variables globales . Sin embargo, el uso de variables globales trae algunos problemas, tales como:

  1. Los programas cliente pueden crear instancias homogéneas, por lo que no hay garantía de que haya una sola instancia compartida en todo el sistema.
  2. Es difícil controlar el acceso de los objetos, por ejemplo, es difícil agregar una función de "contar el número de visitas", y la escalabilidad es baja.
  3. Exponer los detalles de implementación al programa cliente profundiza el acoplamiento y es propenso a modificaciones repentinas.

Para un escenario globalmente único, es mejor usar el patrón singleton para implementarlo. El patrón singleton impide que los programas cliente creen instancias del mismo tipo y puede ampliar o modificar la funcionalidad en un punto de acceso global sin afectar a los programas cliente .

Sin embargo, no todos los únicos globales se aplican al patrón singleton. Por ejemplo el siguiente escenario:

Considere una situación en la que necesita contar una llamada API, hay dos indicadores, la cantidad de llamadas exitosas y la cantidad de llamadas fallidas. Ambas métricas son globalmente únicas, por lo que alguien podría modelar esto como dos singletons SuccessApiMetric y FailApiMetric. De acuerdo con esta idea, a medida que aumenta el número de indicadores, encontrará que habrá más y más definiciones de clase en el código, y se volverá cada vez más inflado. Este es también el escenario de uso indebido más común del patrón singleton. Una mejor manera es diseñar los dos indicadores como dos instancias de éxito de ApiMetic y falla de ApiMetic bajo un objeto ApiMetric.

Entonces, ¿cómo saber si un objeto debe modelarse como singleton? A menudo, los objetos que se modelan como singletons tienen un significado de " punto central ", como un grupo de subprocesos que es el centro que administra todos los subprocesos. Entonces, al juzgar si un objeto es adecuado para el patrón singleton, primero piénselo, ¿es un punto central ?

estructura UML

Código

De acuerdo con la definición del patrón singleton, hay dos puntos clave de implementación:

  1. Restrinja a la persona que llama de instanciar el objeto directamente ;
  2. Proporciona un método de acceso global único para los elementos únicos de este objeto .

Para C++/Java, simplemente haga que el constructor del objeto sea privado y proporcione un método estático para acceder a la única instancia del objeto. Pero el lenguaje Go no tiene el concepto de constructor, ni método estático, por lo que debe encontrar otra salida.

Podemos usar las reglas de acceso del paquete de lenguaje Go para lograr esto, y diseñar el objeto singleton para que la primera letra en minúsculas, de modo que su alcance de acceso solo pueda limitarse al paquete actual, simulando el constructor privado de C++/Java; luego , en el actual Implementar una función de acceso en mayúsculas en el paquete, que es equivalente al rol del método estático.

Ejemplo

En un sistema de aplicación distribuido simple (proyecto de código de ejemplo), definimos una red de módulo de red para simular la función de reenvío de paquetes de red. El diseño de la red también es muy simple. El mapeo de Endpoint a Socket se mantiene a través de una tabla hash. Cuando se reenvía el mensaje, se dirige al Socket a través del Endpoint, y luego se llama al método Receive del Socket para completar el reenvío.

Dado que solo se requiere un objeto de red para todo el sistema, y ​​tiene la semántica de un punto central en el modelo de dominio , lo implementamos naturalmente usando el patrón singleton. El modo Singleton se puede dividir aproximadamente en dos categorías, "modo hambriento" y "modo perezoso". El primero es para completar la creación de instancias del objeto singleton durante la inicialización del sistema; el último es para retrasar la creación de instancias cuando se llama, ahorrando así memoria hasta cierto punto.

Realización del "Modo Hombre Hambriento"

// demo/network/network.go
package network

// 1、设计为小写字母开头,表示只在network包内可见,限制客户端程序的实例化
type network struct {
	sockets sync.Mapvar instancevar instance
}

// 2、定义一个包内可见的实例对象,也即单例
var instance = &network{sockets: sync.Map{}}

// 3、定义一个全局可见的唯一访问方法
func Instance() *network {
	return instance
}

func (n *network) Listen(endpoint Endpoint, socket Socket) error {
	if _, ok := n.sockets.Load(endpoint); ok {
		return ErrEndpointAlreadyListened
	}
	n.sockets.Store(endpoint, socket)
	return nil
}

func (n *network) Send(packet *Packet) error {
	record, rOk := n.sockets.Load(packet.Dest())
	socket, sOk := record.(Socket)
	if !rOk || !sOk {
		return ErrConnectionRefuse
	}
	go socket.Receive(packet)
	return nil
}

Luego, el cliente puede hacer referencia al singleton a través de network.Instance():

// demo/sidecar/flowctrl_sidecar.go
package sidecar

type FlowCtrlSidecar struct {...}

// 通过 network.Instance() 直接引用单例
func (f *FlowCtrlSidecar) Listen(endpoint network.Endpoint) error {
	return network.Instance().Listen(endpoint, f)
}
...

Implementación del "Modo Lazy Man"

Como todos sabemos, el "modo perezoso" traerá problemas de seguridad de subprocesos, que pueden optimizarse mediante un bloqueo ordinario o un bloqueo de verificación doble más eficiente . Independientemente del método, es para garantizar que el singleton solo se inicialice una vez .

type network struct {...}

// 单例
var instance *network
// 定义互斥锁
var mutex = sync.Mutex{}

// 普通加锁,缺点是每次调用 Instance() 都需要加锁
func Instance() *network {
	mutex.Lock()
	if instance == nil {
		instance = &network{sockets: sync.Map{}}
	}
	mutex.Unlock()
	return instance
}

// 双重检验后加锁,实例化后无需加锁
func Instance() *network {
	if instance == nil {
        mutex.Lock()
        if instance == nil {
           instance = &network{sockets: sync.Map{}}
        }
        mutex.Unlock()
	}
	return instance
}

Para el "modo perezoso", el lenguaje Go tiene una forma más elegante de implementarlo, que es usar sync.Once. Tiene un método Do, que se declara como func (o *Once) Do(f func()), donde el parámetro de entrada es el tipo de método de func(), y Go garantiza que el método solo se llamará una vez. Usando esta función, podemos lograr que el singleton solo se inicialice una vez.

type network struct {...}
// 单例
var instance *network
// 定义 once 对象
var once = sync.Once{}

// 通过once对象确保instance只被初始化一次
func Instance() *network {
	once.Do(func() {
        // 只会被调用一次
		instance = &network{sockets: sync.Map{}}
	})
	return instance
}

expandir

proporcionar varias instancias

Aunque el patrón singleton por definición significa que cada objeto solo puede tener una instancia, no debemos estar limitados por la definición, sino también entenderlo desde la motivación del patrón en sí. Una de las principales motivaciones del patrón singleton es restringir que el programa cliente cree instancias de objetos. Realmente no importa cuántas instancias haya. Se puede modelar y diseñar de acuerdo con la escena específica.

Por ejemplo, en el módulo de red anterior, ahora hay un nuevo requisito para dividir la red en Internet y una red de área local. Entonces, podemos diseñarlo así:

type network struct {...}

// 定义互联网单例
var inetInstance = &network{sockets: sync.Map{}}
// 定义局域网单例
var lanInstance = &network{sockets: sync.Map{}}
// 定义互联网全局可见的唯一访问方法
func Internet() *network {
	return inetInstance
}
// 定义局域网全局可见的唯一访问方法
func Lan() *network {
	return lanInstance
}

Aunque hay dos instancias de la estructura de red en el ejemplo anterior, es esencialmente un modo singleton porque restringe la creación de instancias del cliente y proporciona un método de acceso global único para cada singleton.

Proporciona múltiples implementaciones.

El patrón singleton también puede lograr polimorfismo. Si predice que el singleton puede expandirse en el futuro, entonces puede diseñarlo como una interfaz abstracta y dejar que el cliente confíe en la abstracción, de modo que no sea necesario cambiar el programa cliente. en la futura expansión .

Por ejemplo, podemos diseñar la red como una interfaz abstracta:

// network 抽象接口
type network interface {
	Listen(endpoint Endpoint, socket Socket) error
	Send(packet *Packet) error
}

// network 的实现1
type networkImpl1 struct {
	sockets sync.Map
}
func (n *networkImpl1) Listen(endpoint Endpoint, socket Socket) error {...}
func (n *networkImpl1) Send(packet *Packet) error {...}

// networkImpl1 实现的单例
var instance = &networkImpl1{sockets: sync.Map{}}

// 定义全局可见的唯一访问方法,注意返回值时network抽象接口!
func Instance() network {
	return instance
}

// 客户端使用示例
func client() {
    packet := network.NewPacket(srcEndpoint, destEndpoint, payload)
    network.Instance().Send(packet)
}

Si es necesario agregar una nueva implementación de networkImpl2 en el futuro, solo necesitamos modificar la lógica de inicialización de la instancia y no es necesario cambiar el programa cliente:

// 新增network 的实现2
type networkImpl2 struct {...}
func (n *networkImpl2) Listen(endpoint Endpoint, socket Socket) error {...}
func (n *networkImpl2) Send(packet *Packet) error {...}

// 将单例 instance 修改为 networkImpl2 实现
var instance = &networkImpl2{...}

// 单例全局访问方法无需改动
func Instance() network {
	return instance
}

// 客户端使用也无需改动
func client() {
    packet := network.NewPacket(srcEndpoint, destEndpoint, payload)
    network.Instance().Send(packet)
}

A veces, es posible que también necesitemos decidir qué implementación de singleton usar leyendo la configuración. Luego, podemos mantener todas las implementaciones a través del mapa y luego seleccionar la implementación correspondiente de acuerdo con la configuración específica:

// network 抽象接口
type network interface {
	Listen(endpoint Endpoint, socket Socket) error
	Send(packet *Packet) error
}

// network 具体实现
type networkImpl1 struct {...}
type networkImpl2 struct {...}
type networkImpl3 struct {...}
type networkImpl4 struct {...}

// 单例 map
var instances = make(map[string]network)

// 初始化所有的单例
func init() {
	instances["impl1"] = &networkImpl1{...}
	instances["impl2"] = &networkImpl2{...}
	instances["impl3"] = &networkImpl3{...}
	instances["impl4"] = &networkImpl4{...}
}

// 全局单例访问方法,通过读取配置决定使用哪种实现
func Instance() network {
    impl := readConf()
    instance, ok := instances[impl]
    if !ok {
        panic("instance not found")
    }
    return instance
}

Escenarios de aplicación típicos

  1. registro _ Cada servicio generalmente requiere un objeto de registro global para registrar los registros generados por el servicio.
  2. Configuración global . Para alguna configuración global, los clientes pueden utilizarla definiendo un singleton.
  3. Generación de números de serie únicos . La generación de números de serie únicos requiere necesariamente que todo el sistema solo pueda tener una instancia de generación, lo cual es muy adecuado para usar el patrón singleton.
  4. Grupo de subprocesos, grupo de objetos, grupo de conexiones, etc. La esencia del grupo xxx es compartir , y también es un escenario común del modo singleton.
  5. caché mundial

Ventajas y desventajas

ventaja

En la situación correcta, usar el patrón singleton tiene las siguientes ventajas :

  1. Todo el sistema tiene solo una o unas pocas instancias, lo que efectivamente ahorra memoria y gastos generales de creación de objetos.
  2. A través del punto de acceso global, las funciones se pueden ampliar fácilmente, como agregar estadísticas sobre el número de visitas.
  3. Oculte los detalles de implementación del cliente para evitar modificaciones repentinas.

defecto

Aunque el patrón singleton tiene muchas ventajas sobre las variables globales, es esencialmente una "variable global", y no se pueden evitar algunas desventajas de las variables globales :

  1. Acoplamiento implícito de llamadas a funciones . Por lo general, esperamos saber qué hace la función, de qué depende y qué devuelve de su declaración. Usar el patrón singleton significa que la instancia se puede usar en una función sin pasar parámetros a través de la función. También se trata de hacer que las dependencias/acoplamientos sean implícitos, lo que no conduce a una mejor comprensión del código.
  2. No es amigable con las pruebas . Por lo general, para probar un método/función, no necesitamos conocer su implementación específica. Pero si se usa un objeto singleton en un método/función, debemos considerar el cambio del estado singleton, es decir, debemos considerar la implementación específica del método/función.
  3. Problemas de concurrencia . Compartir significa que puede haber problemas de concurrencia. No solo debemos considerar los problemas de concurrencia durante la fase de inicialización, sino también prestarles atención después de la inicialización. Por lo tanto, en escenarios de alta simultaneidad, el modo singleton también puede tener conflictos de bloqueo.

Aunque el patrón singleton es simple y fácil de usar, también es el patrón de diseño más fácil de abusar. No es una "bala de plata". En el uso real, debe usarse con precaución de acuerdo con los escenarios comerciales específicos.

Enlaces a otros esquemas

El patrón de método de fábrica y el patrón de fábrica abstracto a menudo se implementan en el patrón singleton, porque la clase de fábrica generalmente no tiene estado y solo se necesita una instancia globalmente, lo que puede evitar de manera efectiva la creación y destrucción frecuente de objetos.

Previous: [Implementación de Go] Practique los 23 patrones de diseño de GoF: Principios SOLID

Sistema de aplicación distribuida simple (proyecto de código de ejemplo): https://github.com/ruanrunxue/Practice-Design-Pattern–Go-Implementation

Haga clic en Seguir para conocer las nuevas tecnologías de HUAWEI CLOUD por primera vez~

Supongo que te gusta

Origin blog.csdn.net/devcloud/article/details/124018178
Recomendado
Clasificación