La belleza de los patrones de diseño 53-Patrones combinados: ¿Cómo diseñar e implementar una estructura de árbol de directorios del sistema de archivos que admita el recorrido recursivo?

53 | Modo de combinación: ¿Cómo diseñar e implementar una estructura de árbol de directorios del sistema de archivos que admita el recorrido recursivo?

El patrón de diseño estructural está casi terminado y quedan dos menos utilizados: el patrón de combinación y el patrón de peso mosca. Hoy, hablemos del patrón de diseño compuesto.

El modo de composición es completamente diferente de la "relación de composición (para ensamblar dos clases a través de la composición)" en el diseño orientado a objetos del que hablamos antes. El "modo de combinación" mencionado aquí se utiliza principalmente para procesar datos estructurados en árbol. Los "datos" aquí pueden entenderse simplemente como una colección de objetos, que explicaremos en detalle más adelante.

Debido a la particularidad de sus escenarios de aplicación, los datos deben representarse en una estructura de árbol, lo que también hace que este modo no sea tan utilizado en el desarrollo de proyectos reales. Sin embargo, una vez que los datos satisfacen la estructura de árbol, la aplicación de este patrón puede desempeñar un papel importante y hacer que el código sea muy conciso.

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

El principio y la realización del modo compuesto

En el libro "Patrones de diseño" de GoF, el patrón compuesto se define de la siguiente manera:

Componga objetos en una estructura de árbol para representar jerarquías de parte-todo. Composite le permite al cliente tratar objetos individuales y composiciones de objetos de manera uniforme.

Traducido al chino es: organizar un grupo de objetos (Componer) en una estructura de árbol para representar una jerarquía "parte-todo". La composición le permite al cliente (en muchos libros de patrones de diseño, "cliente" se refiere al usuario del código) unificar la lógica de procesamiento de objetos individuales y objetos compuestos.

A continuación, para el modo de combinación, déjame darte un ejemplo para explicártelo.

Supongamos que tenemos tal requisito: diseñar una clase para representar el directorio en el sistema de archivos, que puede realizar convenientemente las siguientes funciones:

  • Agregue y elimine dinámicamente subdirectorios o archivos en un directorio determinado;
  • Cuente la cantidad de archivos en el directorio especificado;
  • Cuente el tamaño total de los archivos en el directorio especificado.

Doy el código esqueleto de esta clase aquí, como se muestra a continuación. La lógica central no se ha realizado, puede intentar completarla usted mismo y luego llegar a mi explicación. En la siguiente implementación de código, usamos la clase FileSystemNode para representar archivos y directorios de manera uniforme, y los distinguimos a través del atributo isFile.

public class FileSystemNode {
  private String path;
  private boolean isFile;
  private List<FileSystemNode> subNodes = new ArrayList<>();

  public FileSystemNode(String path, boolean isFile) {
    this.path = path;
    this.isFile = isFile;
  }

  public int countNumOfFiles() {
    // TODO:...
  }

  public long countSizeOfFiles() {
    // TODO:...
  }

  public String getPath() {
    return path;
  }

  public void addSubNode(FileSystemNode fileOrDir) {
    subNodes.add(fileOrDir);
  }

  public void removeSubNode(FileSystemNode fileOrDir) {
    int size = subNodes.size();
    int i = 0;
    for (; i < size; ++i) {
      if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) {
        break;
      }
    }
    if (i < size) {
      subNodes.remove(i);
    }
  }
}

De hecho, si ha leído mi columna "La belleza de las estructuras de datos y los algoritmos", no es difícil completar las dos funciones countNumOfFiles() y countSizeOfFiles() De hecho, este es el recorrido recursivo en el algoritmo del árbol. Para los archivos, devolvemos directamente el número de archivos (devuelve 1) o el tamaño. Para los directorios, recorremos cada subdirectorio o archivo en el directorio, calculamos recursivamente su número o tamaño y luego los sumamos, que es el número y el tamaño de los archivos en este directorio.

Publiqué la implementación del código de las dos funciones a continuación, puede consultarlo.

  public int countNumOfFiles() {
    if (isFile) {
      return 1;
    }
    int numOfFiles = 0;
    for (FileSystemNode fileOrDir : subNodes) {
      numOfFiles += fileOrDir.countNumOfFiles();
    }
    return numOfFiles;
  }

  public long countSizeOfFiles() {
    if (isFile) {
      File file = new File(path);
      if (!file.exists()) return 0;
      return file.length();
    }
    long sizeofFiles = 0;
    for (FileSystemNode fileOrDir : subNodes) {
      sizeofFiles += fileOrDir.countSizeOfFiles();
    }
    return sizeofFiles;
  }

Puramente desde la perspectiva de la realización de la función, no hay ningún problema con el código anterior y la función que queremos se ha realizado. Sin embargo, si estamos desarrollando un sistema a gran escala, desde la perspectiva de la escalabilidad (los archivos o directorios pueden corresponder a diferentes operaciones), el modelado empresarial (archivos y directorios son dos conceptos en los negocios), la legibilidad del código (archivos y directorios desde la perspectiva de tratamiento diferenciado de los directorios es más acorde con la percepción que la gente tiene del negocio), sería mejor diseñar archivos y directorios de manera diferente, definiéndolos como dos clases, Archivo y Directorio.

De acuerdo con esta idea de diseño, refactorizamos el código. El código después de la refactorización se ve así:

public abstract class FileSystemNode {
  protected String path;

  public FileSystemNode(String path) {
    this.path = path;
  }

  public abstract int countNumOfFiles();
  public abstract long countSizeOfFiles();

  public String getPath() {
    return path;
  }
}

public class File extends FileSystemNode {
  public File(String path) {
    super(path);
  }

  @Override
  public int countNumOfFiles() {
    return 1;
  }

  @Override
  public long countSizeOfFiles() {
    java.io.File file = new java.io.File(path);
    if (!file.exists()) return 0;
    return file.length();
  }
}

public class Directory extends FileSystemNode {
  private List<FileSystemNode> subNodes = new ArrayList<>();

  public Directory(String path) {
    super(path);
  }

  @Override
  public int countNumOfFiles() {
    int numOfFiles = 0;
    for (FileSystemNode fileOrDir : subNodes) {
      numOfFiles += fileOrDir.countNumOfFiles();
    }
    return numOfFiles;
  }

  @Override
  public long countSizeOfFiles() {
    long sizeofFiles = 0;
    for (FileSystemNode fileOrDir : subNodes) {
      sizeofFiles += fileOrDir.countSizeOfFiles();
    }
    return sizeofFiles;
  }

  public void addSubNode(FileSystemNode fileOrDir) {
    subNodes.add(fileOrDir);
  }

  public void removeSubNode(FileSystemNode fileOrDir) {
    int size = subNodes.size();
    int i = 0;
    for (; i < size; ++i) {
      if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) {
        break;
      }
    }
    if (i < size) {
      subNodes.remove(i);
    }
  }
}

Las clases de archivos y directorios están diseñadas, veamos cómo usarlas para representar la estructura de árbol de directorios en un sistema de archivos. Un ejemplo de código específico es el siguiente:

public class Demo {
  public static void main(String[] args) {
    /**
     * /
     * /wz/
     * /wz/a.txt
     * /wz/b.txt
     * /wz/movies/
     * /wz/movies/c.avi
     * /xzg/
     * /xzg/docs/
     * /xzg/docs/d.txt
     */
    Directory fileSystemTree = new Directory("/");
    Directory node_wz = new Directory("/wz/");
    Directory node_xzg = new Directory("/xzg/");
    fileSystemTree.addSubNode(node_wz);
    fileSystemTree.addSubNode(node_xzg);

    File node_wz_a = new File("/wz/a.txt");
    File node_wz_b = new File("/wz/b.txt");
    Directory node_wz_movies = new Directory("/wz/movies/");
    node_wz.addSubNode(node_wz_a);
    node_wz.addSubNode(node_wz_b);
    node_wz.addSubNode(node_wz_movies);

    File node_wz_movies_c = new File("/wz/movies/c.avi");
    node_wz_movies.addSubNode(node_wz_movies_c);

    Directory node_xzg_docs = new Directory("/xzg/docs/");
    node_xzg.addSubNode(node_xzg_docs);

    File node_xzg_docs_d = new File("/xzg/docs/d.txt");
    node_xzg_docs.addSubNode(node_xzg_docs_d);

    System.out.println("/ files num:" + fileSystemTree.countNumOfFiles());
    System.out.println("/wz/ files num:" + node_wz.countNumOfFiles());
  }
}

Echemos un vistazo a la definición del modo de combinación en comparación con este ejemplo: "Organizar un conjunto de objetos (archivos y directorios) en una estructura de árbol para representar una jerarquía 'parte-todo' (la anidación de directorios y subdirectorios) Establecer estructura). El modo compuesto permite al cliente unificar la lógica de procesamiento (recorrido recursivo) de un solo objeto (archivo) y un objeto compuesto (directorio).

De hecho, la idea de diseño del modo de combinación que acabamos de mencionar no es tanto un modo de diseño como una abstracción de estructuras de datos y algoritmos para escenarios comerciales. Entre ellos, los datos se pueden expresar como una estructura de datos, como un árbol, y los requisitos comerciales se pueden cumplir a través de un algoritmo transversal recursivo en el árbol.

Ejemplos de escenarios de aplicación del modo de combinación

Acabamos de hablar sobre el ejemplo del sistema de archivos.Para el modo de combinación, daré otro ejemplo aquí. Después de comprender estos dos ejemplos, básicamente has dominado el modo de combinación. En proyectos reales, si encuentra un escenario empresarial similar que se puede expresar en una estructura de árbol, solo necesita diseñarlo "siguiendo la calabaza".

Supongamos que estamos desarrollando un sistema OA (sistema de automatización de oficinas). La estructura organizativa de la empresa contiene dos tipos de datos, departamento y empleado. Entre ellos, un departamento puede contener subdepartamentos y empleados. La estructura de la tabla en la base de datos es la siguiente:

inserte la descripción de la imagen aquí

Esperamos construir un diagrama de estructura de personal de toda la empresa (departamento, subdepartamento, afiliación de empleados) en la memoria y proporcionar una interfaz para calcular el costo salarial del departamento (la suma salarial de todos los empleados que pertenecen a este departamento).

Los departamentos contienen subdepartamentos y empleados, que es una estructura anidada que se puede representar como una estructura de datos de árbol. La necesidad de calcular el gasto salarial de cada departamento también se puede realizar a través del algoritmo transversal en el árbol. Entonces, desde este punto de vista, este escenario de aplicación puede diseñarse e implementarse utilizando el patrón compuesto.

La estructura del código de este ejemplo es muy similar a la del ejemplo anterior. Pegué la implementación del código directamente debajo, para que puedas compararlo. Entre ellos, HumanResource es la clase padre abstraída de la clase Department (Departamento) y la clase Employee (Employee), con el fin de unificar la lógica de procesamiento del salario. El código en la demostración es responsable de leer los datos de la base de datos y construir el organigrama en la memoria.

public abstract class HumanResource {
  protected long id;
  protected double salary;

  public HumanResource(long id) {
    this.id = id;
  }

  public long getId() {
    return id;
  }

  public abstract double calculateSalary();
}

public class Employee extends HumanResource {
  public Employee(long id, double salary) {
    super(id);
    this.salary = salary;
  }

  @Override
  public double calculateSalary() {
    return salary;
  }
}

public class Department extends HumanResource {
  private List<HumanResource> subNodes = new ArrayList<>();

  public Department(long id) {
    super(id);
  }

  @Override
  public double calculateSalary() {
    double totalSalary = 0;
    for (HumanResource hr : subNodes) {
      totalSalary += hr.calculateSalary();
    }
    this.salary = totalSalary;
    return totalSalary;
  }

  public void addSubNode(HumanResource hr) {
    subNodes.add(hr);
  }
}

// 构建组织架构的代码
public class Demo {
  private static final long ORGANIZATION_ROOT_ID = 1001;
  private DepartmentRepo departmentRepo; // 依赖注入
  private EmployeeRepo employeeRepo; // 依赖注入

  public void buildOrganization() {
    Department rootDepartment = new Department(ORGANIZATION_ROOT_ID);
    buildOrganization(rootDepartment);
  }

  private void buildOrganization(Department department) {
    List<Long> subDepartmentIds = departmentRepo.getSubDepartmentIds(department.getId());
    for (Long subDepartmentId : subDepartmentIds) {
      Department subDepartment = new Department(subDepartmentId);
      department.addSubNode(subDepartment);
      buildOrganization(subDepartment);
    }
    List<Long> employeeIds = employeeRepo.getDepartmentEmployeeIds(department.getId());
    for (Long employeeId : employeeIds) {
      double salary = employeeRepo.getEmployeeSalary(employeeId);
      department.addSubNode(new Employee(employeeId, salary));
    }
  }
}

Comparemos la definición del patrón de composición con este ejemplo: "Organice un conjunto de objetos (empleados y departamentos) en una estructura de árbol para representar una jerarquía 'parte-todo' (la estructura anidada de departamentos y subdepartamentos) ). El modo de composición permite al cliente unificar la lógica de procesamiento (recorrido recursivo) de un solo objeto (empleado) y un objeto compuesto (departamento)".

revisión clave

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

La idea de diseño del modo de combinación no es tanto un modo de diseño como una abstracción de estructuras de datos y algoritmos para escenarios comerciales. Entre ellos, los datos se pueden expresar como una estructura de datos, como un árbol, y los requisitos comerciales se pueden cumplir a través de un algoritmo transversal recursivo en el árbol.

El modo de combinación organiza un grupo de objetos en una estructura de árbol, trata tanto los objetos individuales como los objetos compuestos como nodos en el árbol para unificar la lógica de procesamiento y utiliza las características de la estructura de árbol para procesar cada subárbol de forma recursiva, simplificando a su vez el código. La premisa de usar el modo compuesto es que su escenario empresarial debe poder expresarse en una estructura de árbol. Por lo tanto, los escenarios de aplicación del modo de combinación son relativamente limitados y no es un modo de diseño muy utilizado.

discusión en clase

En el ejemplo del sistema de archivos, las dos funciones countNumOfFiles() y countSizeOfFiles() no se implementan de manera eficiente, porque cada vez que se las llama, el subárbol debe atravesarse nuevamente. ¿Hay alguna forma de mejorar la eficiencia de ejecución de estas dos funciones (nota: el sistema de archivos también implica la eliminación y adición frecuentes de archivos, es decir, correspondientes a las funciones addSubNode() y removeSubNode() en la clase Directory)?

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/130498796
Recomendado
Clasificación