La belleza de los patrones de diseño 61-Patrón de estrategia (Parte 2): ¿Cómo implementar un pequeño programa que admita la clasificación de archivos de diferentes tamaños?

61 | Modo de estrategia (Parte 2): ¿Cómo implementar un pequeño programa que admita la clasificación de archivos de diferentes tamaños?

En la última clase, presentamos principalmente el principio y la implementación del patrón de estrategia, y cómo usar el patrón de estrategia para eliminar la lógica de juicio de bifurcación if-else o switch-case. Hoy, hablaremos sobre la intención del diseño y los escenarios de aplicación del patrón de estrategia en detalle con un ejemplo específico de "clasificación de archivos".

Además, en la explicación de hoy, le mostraré cómo se "crea" un patrón de diseño a través del análisis y la reconstrucción paso a paso. A través del estudio de hoy, encontrará que los principios e ideas de diseño son en realidad más universales e importantes que los patrones de diseño. Después de dominar los principios de diseño y las ideas del código, incluso podemos crear nuevos patrones de diseño por nosotros mismos .

Sin más preámbulos, ¡comencemos oficialmente el estudio de hoy!

Problemas y soluciones

Supongamos que existe tal requisito, y espero escribir un pequeño programa para realizar la función de ordenar un archivo. El archivo solo contiene números enteros y los números adyacentes están separados por comas. Si tuviera que escribir un programa tan pequeño, ¿cómo lo implementaría? Puede tomarlo como una pregunta de entrevista, pensarlo usted mismo y luego leer mi explicación a continuación.

Podría decir, ¿no es esto muy simple? Simplemente lea el contenido del archivo y divídalo en números uno por uno con comas, colóquelos en la matriz de memoria y luego escriba algún tipo de algoritmo de clasificación (como clasificación rápida) , O use directamente la función de clasificación proporcionada por el lenguaje de programación para ordenar la matriz y, finalmente, escriba los datos de la matriz en un archivo.

Pero, ¿y si el archivo es enorme? Por ejemplo, si hay un tamaño de 10 GB, porque la memoria es limitada (por ejemplo, solo 8 GB), no podemos cargar todos los datos del archivo en la memoria al mismo tiempo. En este momento, necesitamos usar un dispositivo externo algoritmo de clasificación (para obtener más detalles, consulte mis otros capítulos relacionados con "Clasificación" en una columna "La belleza de las estructuras de datos y los algoritmos").

Si el archivo es más grande, como 100 GB, para aprovechar la CPU multinúcleo, podemos optimizarlo sobre la base de la clasificación externa y agregar la función de clasificación simultánea de subprocesos múltiples, que es similar a la "versión independiente" de MapReduce.

Si el archivo es muy grande, como 1 TB de tamaño, incluso si se trata de una ordenación de subprocesos múltiples de una sola máquina, se considera muy lento. En este momento, podemos usar el marco MapReduce real para aprovechar las capacidades de procesamiento de múltiples máquinas para mejorar la eficiencia de la clasificación.

Implementación y análisis de código

La idea de la solución está terminada, no es difícil de entender. A continuación, echemos un vistazo a cómo traducir las ideas de solución en la implementación del código.

Primero lo implementaré de la manera más simple y directa. Publiqué el código específico a continuación, puedes echarle un vistazo primero. Debido a que estamos hablando de patrones de diseño, no de algoritmos, en la siguiente implementación de código, solo doy el código esqueleto relacionado con el patrón de diseño y no doy la implementación de código específica de cada algoritmo de clasificación. Si está interesado, puede implementarlo usted mismo.

public class Sorter {
  private static final long GB = 1000 * 1000 * 1000;

  public void sortFile(String filePath) {
    // 省略校验逻辑
    File file = new File(filePath);
    long fileSize = file.length();
    if (fileSize < 6 * GB) { // [0, 6GB)
      quickSort(filePath);
    } else if (fileSize < 10 * GB) { // [6GB, 10GB)
      externalSort(filePath);
    } else if (fileSize < 100 * GB) { // [10GB, 100GB)
      concurrentExternalSort(filePath);
    } else { // [100GB, ~)
      mapreduceSort(filePath);
    }
  }

  private void quickSort(String filePath) {
    // 快速排序
  }

  private void externalSort(String filePath) {
    // 外部排序
  }

  private void concurrentExternalSort(String filePath) {
    // 多线程外部排序
  }

  private void mapreduceSort(String filePath) {
    // 利用MapReduce多机排序
  }
}

public class SortingTool {
  public static void main(String[] args) {
    Sorter sorter = new Sorter();
    sorter.sortFile(args[0]);
  }
}

En la parte de "Especificaciones de codificación", dijimos que el número de líneas de la función no debe ser demasiado, y es mejor no exceder el tamaño de una pantalla. Por lo tanto, para evitar que la función sortFile() sea demasiado larga, separamos cada algoritmo de clasificación de la función sortFile() y lo dividimos en 4 funciones de clasificación independientes.

Si solo desarrolla una herramienta simple, la implementación del código anterior es suficiente. Después de todo, no hay muchos códigos y no hay muchos requisitos para modificaciones y expansiones posteriores. No importa cómo los escriba, el código no será inmantenible. Sin embargo, si estamos desarrollando un proyecto a gran escala y el archivo de clasificación es solo uno de los módulos funcionales, entonces tenemos que trabajar duro en el diseño y la calidad del código. Solo cuando cada pequeño módulo funcional está bien escrito, el código de todo el proyecto no puede ser malo.

En el código de ahora, no proporcionamos la implementación del código de cada algoritmo de clasificación. De hecho, si lo implementa usted mismo, encontrará que la lógica de implementación de cada algoritmo de clasificación es más complicada y la cantidad de líneas de código es mayor. La implementación del código de todos los algoritmos de clasificación se acumula en una clase de Sorter, lo que generará una gran cantidad de código en esta clase. En la parte de "Estándares de codificación", también mencionamos que demasiado código en una clase también afectará la legibilidad y la capacidad de mantenimiento. Además, todos los algoritmos de clasificación están diseñados como funciones privadas de Sorter, lo que también afectará la reutilización del código.

Optimización y refactorización de código

Siempre que hayamos dominado los principios e ideas de diseño de los que hablamos antes, deberíamos poder saber cómo resolver los problemas anteriores incluso si no podemos pensar en ningún patrón de diseño para usar para la refactorización, es decir, para dividir algunos códigos en la clase Sorter Salga y conviértase en una subcategoría independiente con más responsabilidades individuales. De hecho, la división es un método común para manejar demasiadas clases o códigos de función y la complejidad del código. De acuerdo con esta solución, refactorizamos el código. El código después de la refactorización se ve así:

public interface ISortAlg {
  void sort(String filePath);
}

public class QuickSort implements ISortAlg {
  @Override
  public void sort(String filePath) {
    //...
  }
}

public class ExternalSort implements ISortAlg {
  @Override
  public void sort(String filePath) {
    //...
  }
}

public class ConcurrentExternalSort implements ISortAlg {
  @Override
  public void sort(String filePath) {
    //...
  }
}

public class MapReduceSort implements ISortAlg {
  @Override
  public void sort(String filePath) {
    //...
  }
}

public class Sorter {
  private static final long GB = 1000 * 1000 * 1000;

  public void sortFile(String filePath) {
    // 省略校验逻辑
    File file = new File(filePath);
    long fileSize = file.length();
    ISortAlg sortAlg;
    if (fileSize < 6 * GB) { // [0, 6GB)
      sortAlg = new QuickSort();
    } else if (fileSize < 10 * GB) { // [6GB, 10GB)
      sortAlg = new ExternalSort();
    } else if (fileSize < 100 * GB) { // [10GB, 100GB)
      sortAlg = new ConcurrentExternalSort();
    } else { // [100GB, ~)
      sortAlg = new MapReduceSort();
    }
    sortAlg.sort(filePath);
  }
}

Después de la división, el código de cada clase no será demasiado, la lógica de cada clase no será demasiado complicada y se mejorará la legibilidad y la capacidad de mantenimiento del código. Además, diseñamos el algoritmo de clasificación como una clase independiente, desvinculada de la lógica comercial específica (lógica if-else en el código) e hicimos que el algoritmo de clasificación fuera reutilizable. Este paso es en realidad el primer paso del patrón de estrategia, que consiste en separar la definición de la estrategia.

De hecho, el código anterior se puede seguir optimizando. Cada clase de clasificación no tiene estado y no necesitamos recrear un nuevo objeto cada vez que lo usamos. Por lo tanto, podemos usar el patrón de fábrica para encapsular la creación de objetos. De acuerdo con esta idea, refactorizamos el código. El código después de la refactorización se ve así:

public class SortAlgFactory {
  private static final Map<String, ISortAlg> algs = new HashMap<>();

  static {
    algs.put("QuickSort", new QuickSort());
    algs.put("ExternalSort", new ExternalSort());
    algs.put("ConcurrentExternalSort", new ConcurrentExternalSort());
    algs.put("MapReduceSort", new MapReduceSort());
  }

  public static ISortAlg getSortAlg(String type) {
    if (type == null || type.isEmpty()) {
      throw new IllegalArgumentException("type should not be empty.");
    }
    return algs.get(type);
  }
}

public class Sorter {
  private static final long GB = 1000 * 1000 * 1000;

  public void sortFile(String filePath) {
    // 省略校验逻辑
    File file = new File(filePath);
    long fileSize = file.length();
    ISortAlg sortAlg;
    if (fileSize < 6 * GB) { // [0, 6GB)
      sortAlg = SortAlgFactory.getSortAlg("QuickSort");
    } else if (fileSize < 10 * GB) { // [6GB, 10GB)
      sortAlg = SortAlgFactory.getSortAlg("ExternalSort");
    } else if (fileSize < 100 * GB) { // [10GB, 100GB)
      sortAlg = SortAlgFactory.getSortAlg("ConcurrentExternalSort");
    } else { // [100GB, ~)
      sortAlg = SortAlgFactory.getSortAlg("MapReduceSort");
    }
    sortAlg.sort(filePath);
  }
}

Después de las dos refactorizaciones anteriores, el código actual en realidad se ajusta a la estructura de código del patrón de estrategia. Desacoplamos la definición, creación y uso de estrategias a través de patrones de estrategia, para que cada parte no sea demasiado complicada. Sin embargo, la función sortFile() en la clase Sorter todavía tiene un montón de lógica if-else. No hay muchas ramificaciones lógicas if-else aquí, y no son complicadas, por lo que no hay problema para escribir de esta manera. Pero si realmente desea eliminar el juicio de la rama if-else, hay una manera. Le daré el código directamente, y puede entenderlo de un vistazo. De hecho, esto también se basa en el método de tabla de búsqueda, donde "algs" es "table".

public class Sorter {
  private static final long GB = 1000 * 1000 * 1000;
  private static final List<AlgRange> algs = new ArrayList<>();
  static {
    algs.add(new AlgRange(0, 6*GB, SortAlgFactory.getSortAlg("QuickSort")));
    algs.add(new AlgRange(6*GB, 10*GB, SortAlgFactory.getSortAlg("ExternalSort")));
    algs.add(new AlgRange(10*GB, 100*GB, SortAlgFactory.getSortAlg("ConcurrentExternalSort")));
    algs.add(new AlgRange(100*GB, Long.MAX_VALUE, SortAlgFactory.getSortAlg("MapReduceSort")));
  }

  public void sortFile(String filePath) {
    // 省略校验逻辑
    File file = new File(filePath);
    long fileSize = file.length();
    ISortAlg sortAlg = null;
    for (AlgRange algRange : algs) {
      if (algRange.inRange(fileSize)) {
        sortAlg = algRange.getAlg();
        break;
      }
    }
    sortAlg.sort(filePath);
  }

  private static class AlgRange {
    private long start;
    private long end;
    private ISortAlg alg;

    public AlgRange(long start, long end, ISortAlg alg) {
      this.start = start;
      this.end = end;
      this.alg = alg;
    }

    public ISortAlg getAlg() {
      return alg;
    }

    public boolean inRange(long size) {
      return size >= start && size < end;
    }
  }
}

La implementación del código actual es aún más elegante. Aislamos las partes variables en secciones de código estático en la clase de fábrica de estrategias y la clase Sorter. Al agregar un nuevo algoritmo de clasificación, solo necesitamos modificar los segmentos de código estático en la clase de fábrica de estrategias y la clase de clasificación, y no es necesario modificar otros códigos, para que los cambios de código se minimicen y centralicen.

Se podría decir que aun así, cuando añadimos un nuevo algoritmo de ordenación, todavía tenemos que modificar el código, que no cumple completamente con el principio de abrir-cerrar. ¿Hay alguna manera de que podamos satisfacer completamente el principio de apertura y cierre?

Para el lenguaje Java, podemos evitar la modificación de la clase de fábrica de políticas a través de la reflexión. Específicamente, hacemos esto: usamos un archivo de configuración o una anotación personalizada para marcar qué clases de estrategia están disponibles; la clase de fábrica de estrategia lee el archivo de configuración o busca las clases de estrategia marcadas por la anotación, y luego carga dinámicamente estas clases de estrategia a través de reflexión, Crear un objeto de estrategia; cuando agregamos una nueva estrategia, solo necesitamos agregar la clase de estrategia recién agregada al archivo de configuración o marcarla con una anotación. ¿Recuerdas las preguntas de discusión en clase de la última clase? También podemos usar este método para resolver.

Para Sorter, podemos evitar la modificación por el mismo método. Ponemos la correspondencia entre el rango de tamaño del archivo y el algoritmo en el archivo de configuración. Al agregar un nuevo algoritmo de clasificación, solo necesitamos cambiar el archivo de configuración, no el código.

revisión clave

Bueno, eso es todo por el contenido de hoy. Resumamos y revisemos juntos, en qué necesitas concentrarte.

Cuando se trata del juicio de la rama if-else, algunas personas piensan que es un código incorrecto. Si el juicio de bifurcación if-else no es complicado y no hay muchos códigos, no hay problema.Después de todo, el juicio de bifurcación if-else es una sintaxis proporcionada por casi todos los lenguajes de programación, y hay una razón para su existencia. Siguiendo el principio KISS, el mejor diseño es lo más simple posible. Es una especie de sobrediseño tener que usar el modo de estrategia para crear más de n categorías.

Cuando se trata del modo de estrategia, algunas personas piensan que su función es evitar la lógica de juicio de rama if-else. De hecho, esta comprensión es muy unilateral. El papel principal del patrón de estrategia es desacoplar la definición, creación y uso de estrategias, y controlar la complejidad del código, para que cada parte no sea demasiado complicada y la cantidad de código sea demasiado. Además, para código complejo, el patrón de estrategia también puede satisfacer el principio abierto-cerrado.Al agregar una nueva estrategia, minimiza y centraliza los cambios de código y reduce el riesgo de introducir errores.

De hecho, los principios e ideas de diseño son más universales e importantes que los patrones de diseño. Después de dominar los principios de diseño y las ideas del código, podemos entender más claramente por qué necesitamos usar un determinado patrón de diseño y podemos aplicar el patrón de diseño de manera más adecuada.

discusión en clase

  1. En el desarrollo de proyectos anteriores, ¿ha utilizado alguna vez el patrón de estrategia? ¿Lo está utilizando para resolver algún problema?
  2. ¿Puede decirme bajo qué circunstancias es necesario que eliminemos la lógica de bifurcación if-else o switch-case en el código?

Bienvenido a dejar un mensaje y compartir sus pensamientos conmigo. Si obtienes algo, puedes compartir este artículo con tus amigos.

Supongo que te gusta

Origin blog.csdn.net/fegus/article/details/130519140
Recomendado
Clasificación