Geek time: la belleza de los patrones de diseño Modo singleton (medio): ¿Por qué no recomiendo usar el modo singleton? Cuales son las alternativas?

Aunque el singleton es un patrón de diseño muy utilizado, lo usamos a menudo en el desarrollo real, sin embargo, algunas personas piensan que singleton es un anti-patrón y no se recomienda. Entonces, hoy, hablaré sobre estos temas en detalle en respuesta a esta declaración: ¿Cuáles son los problemas con el patrón de diseño singleton? ¿Por qué se llama antipatrón? Si no usa un singleton, ¿cómo representar la clase globalmente única? ¿Cuáles son las soluciones alternativas?

¿Cuáles son los problemas con los singleton?

En la mayoría de los casos, usamos singletons en nuestros proyectos, que se usan para representar algunas clases únicas a nivel mundial, como clases de información de configuración, clases de grupo de conexiones y clases de generador de ID. El modo singleton es simple de escribir y fácil de usar. En el código, no necesitamos crear un objeto, simplemente llamarlo directamente a través de un método como IdGenerator.getInstance (). GetId (). Sin embargo, este método de uso es un poco similar al código duro, lo que trae muchos problemas. A continuación, echemos un vistazo a los problemas específicos.

1. Singleton no es compatible con las funciones de programación orientada a objetos

Sabemos que las cuatro características principales de la programación orientada a objetos son encapsulación, abstracción, herencia y polimorfismo. El patrón de diseño singleton no admite abstracción, herencia ni polimorfismo. ¿Por qué dices eso? Todavía usamos el ejemplo de IdGenerator para explicar.


public class Order {
    
    
  public void create(...) {
    
    
    //...
    long id = IdGenerator.getInstance().getId();
    //...
  }
}

public class User {
    
    
  public void create(...) {
    
    
    // ...
    long id = IdGenerator.getInstance().getId();
    //...
  }
}

El uso de IdGenerator viola el principio de diseño basado en interfaces en lugar de implementación, y también viola las características abstractas de OOP como se entiende en un sentido amplio. Si algún día en el futuro, esperamos adoptar diferentes algoritmos de generación de ID para diferentes negocios. Por ejemplo, el ID de pedido y el ID de usuario se generan utilizando diferentes generadores de ID. Para hacer frente a este cambio en la demanda, necesitamos modificar todos los lugares donde se usa la clase IdGenerator, de modo que los cambios de código sean relativamente grandes.


public class Order {
    
    
  public void create(...) {
    
    
    //...
    long id = IdGenerator.getInstance().getId();
    // 需要将上面一行代码,替换为下面一行代码
    long id = OrderIdGenerator.getIntance().getId();
    //...
  }
}

public class User {
    
    
  public void create(...) {
    
    
    // ...
    long id = IdGenerator.getInstance().getId();
    // 需要将上面一行代码,替换为下面一行代码
    long id = UserIdGenerator.getIntance().getId();
  }
}

Además, el soporte singleton para herencia y polimorfismo no es amigable. La razón por la que utilizo la palabra "hostil" en lugar de "completamente sin soporte" aquí es porque teóricamente, una clase singleton también se puede heredar y se puede lograr polimorfismo, pero la implementación será muy extraña. Esto conduce a una legibilidad deficiente del código. Las personas que no comprenden la intención del diseño encontrarán un diseño así inexplicable. Por lo tanto, una vez que elige diseñar una clase en una clase singleton, significa renunciar a las dos poderosas características orientadas a objetos de herencia y polimorfismo, lo que equivale a perder la escalabilidad que puede hacer frente a futuros cambios en los requisitos. .

2. Singleton ocultará las dependencias entre clases

Sabemos que la legibilidad del código es muy importante. Al leer el código, esperamos ver las dependencias entre clases de un vistazo y averiguar de qué clases externas depende esta clase.

Las dependencias entre clases declaradas a través de constructores, paso de parámetros, etc. pueden identificarse fácilmente mirando la definición de la función. Sin embargo, la clase singleton no necesita crearse explícitamente y no necesita depender del paso de parámetros, y se puede llamar directamente en la función. Si el código es más complicado, esta relación de llamada estará muy oculta. Al leer el código, debemos verificar cuidadosamente la implementación del código de cada función para saber en qué clases singleton se basa esta clase.

3. Singleton no es compatible con la escalabilidad del código.

Sabemos que una clase singleton solo puede tener una instancia de objeto. Si un día en el futuro, necesitamos crear dos o más instancias en el código, entonces necesitamos hacer cambios relativamente grandes en el código. Se podría decir, ¿habrá tal demanda? Dado que las clases singleton se utilizan para representar clases globales en la mayoría de los casos, ¿cómo es que se necesitan dos o más instancias?

De hecho, esta demanda no es infrecuente. Tomemos el grupo de conexiones de la base de datos como ejemplo para explicar.

En la etapa inicial del diseño del sistema, pensamos que solo debería haber un grupo de conexiones de base de datos en el sistema, de modo que podamos controlar el consumo de los recursos de conexión de la base de datos. Por lo tanto, diseñamos la clase de grupo de conexiones de base de datos como una clase singleton. Pero luego descubrimos que algunas sentencias SQL en el sistema se ejecutan muy lentamente. Cuando se ejecutan estas instrucciones SQL, ocupan los recursos de conexión de la base de datos durante mucho tiempo, lo que provoca que otras solicitudes SQL no respondan. Para resolver este problema, esperamos aislar el SQL lento de otro SQL para su ejecución. Para lograr este objetivo, podemos crear dos grupos de conexiones de base de datos en el sistema. Slow SQL tiene un grupo de conexiones de base de datos exclusivo y otros SQL tienen un grupo de conexiones de base de datos exclusivo. De esta manera, podemos evitar que el SQL lento afecte la ejecución de otros SQL.

Si diseñamos el pool de conexiones de la base de datos como una clase singleton, obviamente no se podrá adaptar a tales cambios en los requisitos, es decir, la clase singleton afectará la escalabilidad y flexibilidad del código en algunos casos. Por lo tanto, es mejor no diseñar grupos de recursos como grupos de conexiones de bases de datos y grupos de subprocesos como singletons. De hecho, algunos grupos de conexiones de bases de datos de código abierto y grupos de subprocesos no están diseñados como singletons.

4. Singleton no es compatible con la capacidad de prueba del código

El uso del modo singleton afectará la capacidad de prueba del código. Si la clase singleton se basa en un recurso externo relativamente pesado, como DB, cuando escribimos pruebas unitarias, esperamos reemplazarlo burlándose. El uso codificado de clases singleton hace que sea imposible implementar un reemplazo simulado.

Además, si la clase singleton contiene variables miembro (como la variable miembro id en IdGenerator), en realidad es equivalente a una especie de variable global, compartida por todos los códigos. Si esta variable global es una variable global variable, es decir, sus variables miembro se pueden modificar, entonces cuando escribimos pruebas unitarias, también debemos prestar atención a diferentes casos de prueba, modificar la clase singleton El valor de la misma variable miembro causa el problema de la influencia mutua de los resultados de la prueba.

5. Singleton no admite constructor parametrizado

Los singleton no admiten constructores parametrizados. Por ejemplo, cuando creamos un objeto singleton de un grupo de conexiones, no podemos especificar el tamaño del grupo de conexiones mediante parámetros. En respuesta a este problema, veamos qué soluciones están disponibles.

La primera solución es: después de crear la instancia, llame a la función init () para pasar parámetros. Cabe señalar que cuando usamos esta clase singleton, debemos llamar al método init () antes de llamar al método getInstance (), de lo contrario, el código lanzará una excepción. La implementación del código específico es la siguiente:


public class Singleton {
    
    
  private static Singleton instance = null;
  private final int paramA;
  private final int paramB;

  private Singleton(int paramA, int paramB) {
    
    
    this.paramA = paramA;
    this.paramB = paramB;
  }

  public static Singleton getInstance() {
    
    
    if (instance == null) {
    
    
       throw new RuntimeException("Run init() first.");
    }
    return instance;
  }

  public synchronized static Singleton init(int paramA, int paramB) {
    
    
    if (instance != null){
    
    
       throw new RuntimeException("Singleton has been created!");
    }
    instance = new Singleton(paramA, paramB);
    return instance;
  }
}

Singleton.init(10, 50); // 先init,再使用
Singleton singleton = Singleton.getInstance();

La segunda solución es poner los parámetros en el método getIntance (). La implementación del código específico es la siguiente:


public class Singleton {
    
    
  private static Singleton instance = null;
  private final int paramA;
  private final int paramB;

  private Singleton(int paramA, int paramB) {
    
    
    this.paramA = paramA;
    this.paramB = paramB;
  }

  public synchronized static Singleton getInstance(int paramA, int paramB) {
    
    
    if (instance == null) {
    
    
      instance = new Singleton(paramA, paramB);
    }
    return instance;
  }
}

Singleton singleton = Singleton.getInstance(10, 50);

No sé si ha notado que la implementación del código anterior es un poco problemática. Si ejecutamos el método getInstance () dos veces de la siguiente manera, los paramA y paramB obtenidos de singleton1 y signleton2 son 10 y 50. En otras palabras, los segundos parámetros (20, 30) no funcionaron y el proceso de construcción no dio un aviso, lo que engañaría a los usuarios. ¿Cómo resolver este problema? Déjelo a su propio pensamiento, puede hablar sobre sus ideas de solución en el área de mensajes.


Singleton singleton1 = Singleton.getInstance(10, 50);
Singleton singleton2 = Singleton.getInstance(20, 30);

La tercera solución es poner los parámetros en otra variable global. La implementación del código específico es la siguiente. Config es una variable global que almacena los valores de paramA y paramB. El valor dentro se puede definir mediante constantes estáticas como el siguiente código, o se puede cargar desde el archivo de configuración. De hecho, este método es el más recomendado.


public class Config {
    
    
  public static final int PARAM_A = 123;
  public static final int PARAM_B = 245;
}

public class Singleton {
    
    
  private static Singleton instance = null;
  private final int paramA;
  private final int paramB;

  private Singleton() {
    
    
    this.paramA = Config.PARAM_A;
    this.paramB = Config.PARAM_B;
  }

  public synchronized static Singleton getInstance() {
    
    
    if (instance == null) {
    
    
      instance = new Singleton();
    }
    return instance;
  }
}

¿Cuáles son las soluciones alternativas?

Acabamos de mencionar muchos problemas con los singleton. Se podría decir que, incluso si hay tantos problemas con los singleton, no es necesario. Mi empresa tiene el requisito de una clase única a nivel mundial. Si no uso un singleton, ¿cómo puedo asegurarme de que los objetos de esta clase sean únicos a nivel mundial?

Para garantizar la unicidad global, además de usar singletons, también podemos usar métodos estáticos para lograrlo. Esta es también una idea de implementación que se usa a menudo en el desarrollo de proyectos. Por ejemplo, el ejemplo del generador de incremento de ID único mencionado en la lección anterior se puede implementar con un método estático, que se ve así:


// 静态方法实现方式
public class IdGenerator {
    
    
  private static AtomicLong id = new AtomicLong(0);
  
  public static long getId() {
    
     
    return id.incrementAndGet();
  }
}
// 使用举例
long id = IdGenerator.getId();

Sin embargo, la realización de métodos estáticos no resuelve los problemas que mencionamos anteriormente. De hecho, es más inflexible que los singleton, por ejemplo, no puede soportar la carga diferida. Veamos si hay otra forma. De hecho, hay otra forma de usar un singleton además de la forma en que lo usamos antes. El código específico es el siguiente:


// 1. 老的使用方式
public demofunction() {
    
    
  //...
  long id = IdGenerator.getInstance().getId();
  //...
}

// 2. 新的使用方式:依赖注入
public demofunction(IdGenerator idGenerator) {
    
    
  long id = idGenerator.getId();
}
// 外部调用demofunction()的时候,传入idGenerator
IdGenerator idGenerator = IdGenerator.getInsance();
demofunction(idGenerator);

En base a la nueva forma de uso, pasamos el objeto generado por el singleton como parámetro a la función (también se puede pasar a la variable miembro de la clase a través del constructor), que puede resolver el problema de las dependencias ocultas entre las clases singleton. Sin embargo, no se pueden resolver otros problemas con singletons, como características OOP poco amigables, escalabilidad y capacidad de prueba.

Por lo tanto, si queremos resolver estos problemas por completo, es posible que tengamos que encontrar otras formas de implementar clases globalmente únicas desde la raíz. De hecho, la unicidad global de los objetos de clase se puede garantizar de muchas formas diferentes. Podemos hacer cumplir la garantía a través del modelo singleton, o mediante el modelo de fábrica, contenedor IOC (como el contenedor Spring IOC) para asegurarnos de eso, pero también a través de los propios programadores (asegúrese de no crear dos clases al escribir código Objeto). Esto es similar a la JVM responsable del lanzamiento de objetos de memoria en Java, mientras que el programador es responsable de ello en C ++. La verdad es la misma.

Para la explicación detallada del modelo de fábrica alternativo y contenedor IOC, lo explicaremos en los siguientes capítulos.

Supongo que te gusta

Origin blog.csdn.net/zhujiangtaotaise/article/details/110443138
Recomendado
Clasificación