2024年4月

如何将 ASP.NET Core MVC 项目的视图分离到另一个项目

在当下这个年代 SPA 已是主流,人们早已忘记了 MVC 以及 Razor 的故事。但是在某些场景下 SSR 还是有意想不到效果。比如某些静态页面,比如追求首屏加载速度的时候。最近在项目中回归传统效果还是不错。
有的时候我们希望将视图(Views)从主项目中分离出来,以提高项目的模块化程度。本文将介绍如何将视图分离到另一个 Razor 类库项目中。这在以前 .NET Framework 下是很常见的,但是 Core 下面的资料太少了,记录一下。

步骤 1:创建 Razor 类库项目

首先,我们需要创建一个新的 Razor 类库项目。在项目文件(.csproj)中,我们需要添加以下配置:

<Project Sdk="Microsoft.NET.Sdk.Razor">

	<PropertyGroup>
	 ...
		<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
		<PreserveCompilationContext>false</PreserveCompilationContext>
		<SuppressDependenciesWhenPacking>false</SuppressDependenciesWhenPacking>
		<PackageId>XXX</PackageId>
	</PropertyGroup>

	<ItemGroup>
		<FrameworkReference Include="Microsoft.AspNetCore.App" />
	</ItemGroup>

</Project>

步骤 2:复制视图到新项目

然后,我们需要将所有的视图文件从主项目复制到新的 Razor 类库项目中。

步骤 3:主项目引用新项目

接下来,我们需要在主项目中添加对新 Razor 类库项目的引用。这可以通过在主项目的项目文件中添加以下代码来实现:

<ItemGroup>
	<ProjectReference Include="path/to/your/razor/project.csproj" />
</ItemGroup>

步骤 4:添加视图的扫描路径

在主项目中,我们需要配置 Razor 视图引擎的视图位置格式,以便它能找到新项目中的视图。这可以通过以下代码来实现:

builder.Services.Configure<RazorViewEngineOptions>(options =>
{
    options.ViewLocationFormats.Add("/Widgets/{1}/{0}" + RazorViewEngine.ViewExtension);
    options.ViewLocationFormats.Add("/Widgets/Shared/{0}" + RazorViewEngine.ViewExtension);
});

步骤 5:调整静态资源的路径

最后,如果新项目中包含了静态资源(如 CSS、JavaScript、图片等),并且这些资源放在 wwwroot 文件夹下,那么这些资源会在编译后出现在主项目的 wwwroot/_content/{library project name} 文件夹下。因此,我们需要在 HTML 中使用以下的路径格式来引用这些静态资源:

<link href="~/_content/{library project name}/css/site.css" rel="stylesheet" />
<script src="~/_content/{library project name}/js/site.js"></script>

以上就是将 ASP.NET Core MVC 项目的视图分离到另一个项目的步骤。希望这篇文章能对你有所帮助!

关注我的公众号一起玩转技术

请大家动动小手,给我一个免费的 Star 吧~

这一章处理一下复制、粘贴、删除、画布归位、层次调整,通过右键菜单控制。

github源码

gitee源码

示例地址

复制粘贴

复制粘贴(通过快捷键)

image

  // 复制暂存
  pasteCache: Konva.Node[] = [];
  // 粘贴次数(用于定义新节点的偏移距离)
  pasteCount = 1;

  // 复制
  pasteStart() {
    this.pasteCache = this.render.selectionTool.selectingNodes.map((o) => {
      const copy = o.clone();
      // 恢复透明度、可交互
      copy.setAttrs({
        listening: true,
        opacity: copy.attrs.lastOpacity ?? 1,
      });
      // 清空状态
      copy.setAttrs({
        nodeMousedownPos: undefined,
        lastOpacity: undefined,
        lastZIndex: undefined,
        selectingZIndex: undefined,
      });
      return copy;
    });
    this.pasteCount = 1;
  }

  // 粘贴
  pasteEnd() {
    if (this.pasteCache.length > 0) {
      this.render.selectionTool.selectingClear();
      this.copy(this.pasteCache);
      this.pasteCount++;
    }
  }

快捷键处理:

    keydown: (e: GlobalEventHandlersEventMap['keydown']) => {
        if (e.ctrlKey) {
          if (e.code === Types.ShutcutKey.C) {
            this.render.copyTool.pasteStart() // 复制
          } else if (e.code === Types.ShutcutKey.V) {
            this.render.copyTool.pasteEnd() // 粘贴
          }
        }
      }
    }

逻辑比较简单,可以关注代码中的注释。

复制粘贴(右键)

image

  /**
   * 复制粘贴
   * @param nodes 节点数组
   * @param skip 跳过检查
   * @returns 复制的元素
   */
  copy(nodes: Konva.Node[]) {
    const arr: Konva.Node[] = [];

    for (const node of nodes) {
      if (node instanceof Konva.Transformer) {
        // 复制已选择
        const backup = [...this.render.selectionTool.selectingNodes];
        this.render.selectionTool.selectingClear();
        this.copy(backup);
      } else {
        // 复制未选择
        const copy = node.clone();
        // 使新节点产生偏移
        copy.setAttrs({
          x: copy.x() + this.render.toStageValue(this.render.bgSize) * this.pasteCount,
          y: copy.y() + this.render.toStageValue(this.render.bgSize) * this.pasteCount,
        });
        // 插入新节点
        this.render.layer.add(copy);
        // 选中复制内容
        this.render.selectionTool.select([...this.render.selectionTool.selectingNodes, copy]);
      }
    }

    return arr;
  }

逻辑比较简单,可以关注代码中的注释。

删除

image

处理方法:

  // 移除元素
  remove(nodes: Konva.Node[]) {
    for (const node of nodes) {
      if (node instanceof Konva.Transformer) {
        // 移除已选择的节点
        this.remove(this.selectionTool.selectingNodes);
        // 清除选择
        this.selectionTool.selectingClear();
      } else {
        // 移除未选择的节点
        node.remove();
      }
    }
  }

事件处理:

      keydown: (e: GlobalEventHandlersEventMap['keydown']) => {
        if (e.ctrlKey) {
          // 略
        } else if (e.code === Types.ShutcutKey.删除) {
          this.render.remove(this.render.selectionTool.selectingNodes)
        }
      }

画布归位

逻辑比较简单,恢复画布比例和偏移量:

  // 恢复位置大小
  positionZoomReset() {
    this.render.stage.setAttrs({
      scale: { x: 1, y: 1 }
    })

    this.positionReset()
  }

  // 恢复位置
  positionReset() {
    this.render.stage.setAttrs({
      x: this.render.rulerSize,
      y: this.render.rulerSize
    })

    // 更新背景
    this.render.draws[Draws.BgDraw.name].draw()
    // 更新比例尺
    this.render.draws[Draws.RulerDraw.name].draw()
    // 更新参考线
    this.render.draws[Draws.RefLineDraw.name].draw()
  }

稍微说明一下,初始位置需要考虑比例尺的大小。

层次调整

关于层次的调整,相对比较晦涩。

image

一些辅助方法

获取需要处理的节点,主要是处理 transformer 内部的节点:

  // 获取移动节点
  getNodes(nodes: Konva.Node[]) {
    const targets: Konva.Node[] = []
    for (const node of nodes) {
      if (node instanceof Konva.Transformer) {
        // 已选择的节点
        targets.push(...this.render.selectionTool.selectingNodes)
      } else {
        // 未选择的节点
        targets.push(node)
      }
    }
    return targets
  }

获得计算所需的最大、最小 zIndex:

  // 最大 zIndex
  getMaxZIndex() {
    return Math.max(
      ...this.render.layer
        .getChildren((node) => {
          return !this.render.ignore(node)
        })
        .map((o) => o.zIndex())
    )
  }

  // 最小 zIndex
  getMinZIndex() {
    return Math.min(
      ...this.render.layer
        .getChildren((node) => {
          return !this.render.ignore(node)
        })
        .map((o) => o.zIndex())
    )
  }

记录选择之前的 zIndex

由于被选择的节点会被临时置顶,会影响节点层次的调整,所以选择之前需要记录一下选择之前的 zIndex:

  // 更新 zIndex 缓存
  updateLastZindex(nodes: Konva.Node[]) {
    for (const node of nodes) {
      node.setAttrs({
        lastZIndex: node.zIndex()
      })
    }
  }

处理 transformer 的置顶影响

通过 transformer 选择的时候,所选节点的层次已经被置顶。

所以调整时需要有个步骤:

  • 记录已经被 transformer 影响的每个节点的 zIndex(其实就是记录置顶状态)
  • 调整节点的层次
  • 恢复被 transformer 选择的节点的 zIndex(其实就是恢复置顶状态)

举例子:

现在有节点:

A/1 B/2 C/3 D/4 E/5 F/6 G/7

记录选择 C D E 之前的 lastZIndex:C/3 D/4 E/5

选择后,“临时置顶” C D E:

A/1 B/2 F/3 G/4 C/5 D/6 E/7

此时置底了 C D E,由于上面记录了选择之前的 lastZIndex,直接计算 lastZIndex,变成 C/1 D/2 E/3

在 selectingClear 的时候,会根据 lastZIndex 让 zIndex 的调整生效:

逐步变化:

0、A/1 B/2 F/3 G/4 C/5 D/6 E/7 改变 C/5 -> C/1
1、C/1 A/2 B/3 F/4 G/5 D/6 E/7 改变 D/6 -> D/2
2、C/1 D/2 A/3 B/4 F/5 G/6 E/7 改变 E/7 -> E/3
3、C/1 D/2 E/3 A/4 B/5 F/6 G/7 完成调整

因为 transformer 的存在,调整完还要恢复原来的“临时置顶”:

A/1 B/2 F/3 G/4 C/5 D/6 E/7

下面是记录选择之前的 zIndex 状态、恢复调整之后的 zIndex 状态的方法:

  // 记录选择期间的 zIndex
  updateSelectingZIndex(nodes: Konva.Node[]) {
    for (const node of nodes) {
      node.setAttrs({
        selectingZIndex: node.zIndex()
      })
    }
  }

  // 恢复选择期间的 zIndex
  resetSelectingZIndex(nodes: Konva.Node[]) {
    nodes.sort((a, b) => a.zIndex() - b.zIndex())
    for (const node of nodes) {
      node.zIndex(node.attrs.selectingZIndex)
    }
  }

关于 zIndex 的调整

主要分两种情况:已选的节点、未选的节点

  • 已选:如上面所说,调整之余,还要处理 transformer 的置顶影响
  • 未选:直接调整即可
  // 上移
  up(nodes: Konva.Node[]) {
    // 最大zIndex
    const maxZIndex = this.getMaxZIndex()

    const sorted = this.getNodes(nodes).sort((a, b) => b.zIndex() - a.zIndex())

    // 上移
    let lastNode: Konva.Node | null = null

    if (this.render.selectionTool.selectingNodes.length > 0) {
      this.updateSelectingZIndex(sorted)

      for (const node of sorted) {
        if (
          node.attrs.lastZIndex < maxZIndex &&
          (lastNode === null || node.attrs.lastZIndex < lastNode.attrs.lastZIndex - 1)
        ) {
          node.setAttrs({
            lastZIndex: node.attrs.lastZIndex + 1
          })
        }
        lastNode = node
      }

      this.resetSelectingZIndex(sorted)
    } else {
      // 直接调整
      for (const node of sorted) {
        if (
          node.zIndex() < maxZIndex &&
          (lastNode === null || node.zIndex() < lastNode.zIndex() - 1)
        ) {
          node.zIndex(node.zIndex() + 1)
        }
        lastNode = node
      }

      this.updateLastZindex(sorted)
    }
  }

直接举例子(忽略 transformer 的置顶影响):

现在有节点:

A/1 B/2 C/3 D/4 E/5 F/6 G/7,上移 D F

执行一次:

移动F,A/1 B/2 C/3 D/4 E/5 G/6 F/7

移动D,A/1 B/2 C/3 E/4 D/5 G/6 F/7

再执行一次:

移动F,已经到头了,不变,A/1 B/2 C/3 E/4 D/5 G/6 F/7

移动D,A/1 B/2 C/3 E/4 G/5 D/6 F/7

再执行一次:

移动F,已经到尾了,不变,A/1 B/2 C/3 E/4 G/5 D/6 F/7

移动D,已经贴着 F 了,为了保持 D F 的相对顺序,也不变,A/1 B/2 C/3 E/4 G/5 D/6 F/7

结束

  // 下移
  down(nodes: Konva.Node[]) {
    // 最小 zIndex
    const minZIndex = this.getMinZIndex()

    const sorted = this.getNodes(nodes).sort((a, b) => a.zIndex() - b.zIndex())

    // 下移
    let lastNode: Konva.Node | null = null

    if (this.render.selectionTool.selectingNodes.length > 0) {
      this.updateSelectingZIndex(sorted)

      for (const node of sorted) {
        if (
          node.attrs.lastZIndex > minZIndex &&
          (lastNode === null || node.attrs.lastZIndex > lastNode.attrs.lastZIndex + 1)
        ) {
          node.setAttrs({
            lastZIndex: node.attrs.lastZIndex - 1
          })
        }
        lastNode = node
      }

      this.resetSelectingZIndex(sorted)
    } else {
      // 直接调整
      for (const node of sorted) {
        if (
          node.zIndex() > minZIndex &&
          (lastNode === null || node.zIndex() > lastNode.zIndex() + 1)
        ) {
          node.zIndex(node.zIndex() - 1)
        }
        lastNode = node
      }

      this.updateLastZindex(sorted)
    }
  }

直接举例子(忽略 transformer 的置顶影响):

现在有节点:

A/1 B/2 C/3 D/4 E/5 F/6 G/7,下移 B D

执行一次:

移动B,B/1 A/2 C/3 D/4 E/5 F/6 G/7

移动D,B/1 A/2 D/3 C/4 E/5 F/6 G/7

再执行一次:

移动B,已经到头了,不变,B/1 A/2 D/3 C/4 E/5 F/6 G/7

移动D,B/1 D/2 A/3 C/4 E/5 F/6 G/7

再执行一次:

移动B,已经到头了,不变,B/1 D/2 A/3 C/4 E/5 F/6 G/7

移动D,已经贴着 B 了,为了保持 B D 的相对顺序,也不变,B/1 D/2 A/3 C/4 E/5 F/6 G/7

结束

  // 置顶
  top(nodes: Konva.Node[]) {
    // 最大 zIndex
    let maxZIndex = this.getMaxZIndex()

    const sorted = this.getNodes(nodes).sort((a, b) => b.zIndex() - a.zIndex())

    if (this.render.selectionTool.selectingNodes.length > 0) {
      // 先选中再调整
      this.updateSelectingZIndex(sorted)

      // 置顶
      for (const node of sorted) {
        node.setAttrs({
          lastZIndex: maxZIndex--
        })
      }

      this.resetSelectingZIndex(sorted)
    } else {
      // 直接调整

      for (const node of sorted) {
        node.zIndex(maxZIndex)
      }

      this.updateLastZindex(sorted)
    }
  }

从高到低,逐个移动,每次移动递减 1

  // 置底
  bottom(nodes: Konva.Node[]) {
    // 最小 zIndex
    let minZIndex = this.getMinZIndex()

    const sorted = this.getNodes(nodes).sort((a, b) => a.zIndex() - b.zIndex())

    if (this.render.selectionTool.selectingNodes.length > 0) {
      // 先选中再调整
      this.updateSelectingZIndex(sorted)

      // 置底
      for (const node of sorted) {
        node.setAttrs({
          lastZIndex: minZIndex++
        })
      }

      this.resetSelectingZIndex(sorted)
    } else {
      // 直接调整

      for (const node of sorted) {
        node.zIndex(minZIndex)
      }

      this.updateLastZindex(sorted)
    }
  }

从低到高,逐个移动,每次移动递增 1

调整 zIndex 的思路比较个性化,所以晦涩。要符合 konva 的 zIndex 特定,且达到目的,算法可以自行调整。

右键菜单

事件处理

      mousedown: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>) => {
        this.state.lastPos = this.render.stage.getPointerPosition()

        if (e.evt.button === Types.MouseButton.左键) {
          if (!this.state.menuIsMousedown) {
            // 没有按下菜单,清除菜单
            this.state.target = null
            this.draw()
          }
        } else if (e.evt.button === Types.MouseButton.右键) {
          // 右键按下
          this.state.right = true
        }
      },
      mousemove: () => {
        if (this.state.target && this.state.right) {
          // 拖动画布时(右键),清除菜单
          this.state.target = null
          this.draw()
        }
      },
      mouseup: () => {
        this.state.right = false
      },
      contextmenu: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['contextmenu']>) => {
        const pos = this.render.stage.getPointerPosition()
        if (pos && this.state.lastPos) {
          // 右键目标
          if (pos.x === this.state.lastPos.x || pos.y === this.state.lastPos.y) {
            this.state.target = e.target
          } else {
            this.state.target = null
          }
          this.draw()
        }
      },
      wheel: () => {
        // 画布缩放时,清除菜单
        this.state.target = null
        this.draw()
      }

逻辑说明都在注释里了,主要处理的是右键菜单出现的位置,以及出现和消失的时机,最后是右键的目标。

  override draw() {
    this.clear()

    if (this.state.target) {
      // 菜单数组
      const menus: Array<{
        name: string
        action: (e: Konva.KonvaEventObject<MouseEvent>) => void
      }> = []

      if (this.state.target === this.render.stage) {
        // 空白处
        menus.push({
          name: '恢复位置',
          action: () => {
            this.render.positionTool.positionReset()
          }
        })
        menus.push({
          name: '恢复大小位置',
          action: () => {
            this.render.positionTool.positionZoomReset()
          }
        })
      } else {
        // 未选择:真实节点,即素材的容器 group 
        // 已选择:transformer
        const target = this.state.target.parent

        // 目标
        menus.push({
          name: '复制',
          action: () => {
            if (target) {
              this.render.copyTool.copy([target])
            }
          }
        })
        menus.push({
          name: '删除',
          action: () => {
            if (target) {
              this.render.remove([target])
            }
          }
        })
        menus.push({
          name: '置顶',
          action: () => {
            if (target) {
              this.render.zIndexTool.top([target])
            }
          }
        })
        menus.push({
          name: '上一层',
          action: () => {
            if (target) {
              this.render.zIndexTool.up([target])
            }
          }
        })
        menus.push({
          name: '下一层',
          action: () => {
            if (target) {
              this.render.zIndexTool.down([target])
            }
          }
        })
        menus.push({
          name: '置底',
          action: () => {
            if (target) {
              this.render.zIndexTool.bottom([target])
            }
          }
        })
      }

      // stage 状态
      const stageState = this.render.getStageState()

      // 绘制右键菜单
      const group = new Konva.Group({
        name: 'contextmenu',
        width: stageState.width,
        height: stageState.height
      })

      let top = 0
      // 菜单每项高度
      const lineHeight = 30

      const pos = this.render.stage.getPointerPosition()
      if (pos) {
        for (const menu of menus) {
          // 框
          const rect = new Konva.Rect({
            x: this.render.toStageValue(pos.x - stageState.x),
            y: this.render.toStageValue(pos.y + top - stageState.y),
            width: this.render.toStageValue(100),
            height: this.render.toStageValue(lineHeight),
            fill: '#fff',
            stroke: '#999',
            strokeWidth: this.render.toStageValue(1),
            name: 'contextmenu'
          })
          // 标题
          const text = new Konva.Text({
            x: this.render.toStageValue(pos.x - stageState.x),
            y: this.render.toStageValue(pos.y + top - stageState.y),
            text: menu.name,
            name: 'contextmenu',
            listening: false,
            fontSize: this.render.toStageValue(16),
            fill: '#333',
            width: this.render.toStageValue(100),
            height: this.render.toStageValue(lineHeight),
            align: 'center',
            verticalAlign: 'middle'
          })
          group.add(rect)
          group.add(text)

          // 菜单事件
          rect.on('click', (e) => {
            if (e.evt.button === Types.MouseButton.左键) {
              // 触发事件
              menu.action(e)

              // 移除菜单
              this.group.removeChildren()
              this.state.target = null
            }

            e.evt.preventDefault()
            e.evt.stopPropagation()
          })
          rect.on('mousedown', (e) => {
            if (e.evt.button === Types.MouseButton.左键) {
              this.state.menuIsMousedown = true
              // 按下效果
              rect.fill('#dfdfdf')
            }

            e.evt.preventDefault()
            e.evt.stopPropagation()
          })
          rect.on('mouseup', (e) => {
            if (e.evt.button === Types.MouseButton.左键) {
              this.state.menuIsMousedown = false
            }
          })
          rect.on('mouseenter', (e) => {
            if (this.state.menuIsMousedown) {
              rect.fill('#dfdfdf')
            } else {
              // hover in
              rect.fill('#efefef')
            }

            e.evt.preventDefault()
            e.evt.stopPropagation()
          })
          rect.on('mouseout', () => {
            // hover out
            rect.fill('#fff')
          })
          rect.on('contextmenu', (e) => {
            e.evt.preventDefault()
            e.evt.stopPropagation()
          })

          top += lineHeight - 1
        }
      }

      this.group.add(group)
    }
  }

逻辑也不复杂,根据右键的目标分配相应的菜单项

空白处:恢复位置、大小

节点:复制、删除、上移、下移、置顶、置底

绘制右键菜单

右键的目标有二种情况:空白处、单个/多选节点。

接下来,计划实现下面这些功能:

  • 实时预览窗
  • 导出、导入
  • 对齐效果
  • 等等。。。

是不是值得更多的 Star 呢?勾勾手指~

源码

gitee源码

示例地址


本文以钢筋计数为例,讲解一下如何使用ImageJ软件进行计数,这里只介绍两种方法:

  • 多点工具法
  • 阀值分割法

image

钢筋计数是我接触的第一个视觉项目,虽然项目最后不了了之,但作为我机器视觉的开荒项目还是很有纪念意义的。

多点工具法

多点工具法适用于数目不多的情况,讲究大力出奇迹,纯手动计数。
右键点工具选择
Multi-point Tool
激活工具:
image

双击点工具,设置点的颜色、形状并勾选
Label points

image

手动点击目标即可计数,
按住
Alt
键点击则可取消该点


image

根据最后一个计数可得总共100根,也可以通过
Analyze
->
Measure
查看计数结果和标注点的坐标:
image

如果想把标注点保存在图片中,可以
利用
Image
->
Overlay
->
Flatten
创建原始数据的一个副本

,最后保存后的副本图片会存有标注点。

阀值分割法

阀值分割法适用于数目较多的情况,全自动化计数,相应的图片预处理会比较麻烦。

二值化

先打开图片,执行以下预处理操作:

  • 点击
    Image
    ->
    Type
    ->
    8-bit
    ,将图片转为灰度图
  • 点击
    Image
    ->
    Adjust
    ->
    Threshold
    ,调节阀值
  • 点击
    Apply
    即可得到二值化后的图片

image

软件会自动给一个合适的阀值,适当调节阀值
不要让横截面完全断开
即可,横截面粘连和细小干扰项留到后面处理。

填充分割

二值化后,部分横截面存在空隙或粘连现象,可以通过以下操作进行处理:

  • 选择
    Process
    ->
    Binary
    ->
    Fill Holes
    填补截面空隙
  • 选择
    Process
    ->
    Binary
    ->
    Erode
    腐蚀边缘毛刺
  • 通过
    Process
    ->
    Binary
    ->
    Watershed
    打断重叠部分

image

第二步的腐蚀操作是为了去除毛刺,降低第三步打断的难度,
大约腐蚀两次左右即可

自动计数

先随便选取一个截面大概测试一下面积,这一步很重要,
可以根据这个面积值过滤掉一些细小的干扰项

测量结果如下,截面面积大概在2000个像素左右:
image

选择
Analyze
->
Analyze Particles
打开窗口,设置
Size

Show

image

  • Size
    :1000-Infinity——指分析颗粒面积大于1000(单位是pixel),一直到无穷大的颗粒。
  • Circularity:0.00-1.00——指圆度,1.00为标准圆,一般不需要设置。
  • Show:Overlay Masks——在原图显示结果并标记,可以试试其它几种输出效果。
  • 勾选
    Add to Manager
    ——方便后面把ROI显示到原图。

点击
OK
弹出计数结果,总计100根钢筋:
image

image

显示结果

打开原图,选择
Analyze
->
Tools
->
ROI Manager

image

点击右下角
Show All
在原图上显示结果:
image

总结

总的来说,多点计数适合数目小的目标计数,而阀值分割法适合数目多的目标计数。如果图片质量比较差的话,
使用阈值分割法会比较麻烦

参考资料

单例模式的写法总的来说分为两类:饿汉式和饱汉式,他们都依赖C++的一个知识点:static的使用。

具体的写法有很多种,
首先给出最推荐的写法
。这个写法是所谓的饱汉式(即:延时初始化,再使用的时候才去初始化)

classSingleton
{
public:static Singleton&getInstance() {staticSingleton _instance;return_instance;
}
Singleton(
const Singleton& other) = delete;
Singleton(Singleton
&& other) = delete;
Singleton
& operator=(const Singleton& other) = delete;
Singleton
&operator=(Singleton&& other) = delete;private:
Singleton()
= default;~Singleton() = default;
};

也看到过有人这样写,我觉得没有必要。

classSingleton
{
public:static Singleton*getInstance() {staticSingleton _instance;return &_instance;
}
Singleton(
const Singleton& other) = delete;
Singleton(Singleton
&& other) = delete;
Singleton
& operator=(const Singleton& other) = delete;
Singleton
&operator=(Singleton&& other) = delete;private:
Singleton()
= default;~Singleton() = default;
};

下面给出其他的写法

写法一:静态成员饿汉式

存在的问题
:no local static(函数外的static)变量在不同的编译单元中的初始化顺序未定义,即static Singleton& getInstance()和static Singleton _instance的顺序未知。

classSingleton
{
public:static Singleton&getInstance() {return_instance;
}
Singleton(
const Singleton& other) = delete;
Singleton(Singleton
&& other) = delete;
Singleton
& operator=(const Singleton& other) = delete;
Singleton
& operator=(Singleton&& other) = delete;private:staticSingleton _instance;
Singleton()
= default;~Singleton() = default;
};
Singleton Singleton::_instance;

写法二:指针饱汉式

存在的问题:
1.不是线程安全(改进:加锁)、2.内存泄漏(改进:使用智能指针或者依赖静态的嵌套类的析构函数)

classSingleton
{
public:static Singleton*getInstance() {if(_instance)
_instance
= newSingleton();return_instance;
}
Singleton(
const Singleton& other) = delete;
Singleton(Singleton
&& other) = delete;
Singleton
& operator=(const Singleton& other) = delete;
Singleton
& operator=(Singleton&& other) = delete;private:static Singleton *_instance;
Singleton()
= default;~Singleton() = default;
};
Singleton
* Singleton::_instance = nullptr;

题外话:

写法二中的指针到底需不需要释放?

如果对象没有被释放,在
程序运行期间
可能会存在内存泄露问题。

有人可能会说,在程序结束时,操作系统会进行必要的清理工作,包括释放进程的所有堆栈等信息,即使存在内存泄露,操作系统也会收回的;且对于单例来讲,进程运行期间仅有一个,对于现代计算机而言,占用的内存貌似也不会太大。而且该实例有可能根本就没有进行内存的申请操作,这种情况下不释放实例所占内存,对进程的运行也不会造成影响。

且选用单例模式设计初衷就是整个程序运行期间都只有一个实例。

但我觉得还是要在合适的地方加上释放(那在哪儿合适呢,在程序关闭前?),养成良好的习惯,并且对于大型项目来说,会有内存检测工具,避免报内存泄漏,增加检查成本。

当然,如果采用前面的最佳模式,就无需考虑这个问题了,只需要写好这个类的析构函数就可以了。

大家好,我是老猫。今天和大家分享一下程序员日常的绘图思路,以及一些老猫日常使用的绘图工具。

为什么要画图?

我们在进行系统设计的时候,为了更加具象地呈现系统的轮廓以及各个组件或者系统之间的关系和边界以及工作流程。我们就会画逻辑架构图,模块图、流程图、时序图等等。

在日常开发中,软件设计图是一种非常好的表达方式,尤其在技术评审的时候,一副好的设计图可能比干巴巴的文字更能说明问题。正所谓“一图胜千言”。

软件工程中的绘图

不知道大家有没有听说过“4+1”模型。其实在很早的时候,大概1995年的时候,Philippe Kruchten在IEEE Software上就发表了“The 4+1 View Model of Architecture”的论文。这篇论文引起了极大的关注,最终被RUP采纳。

“4+1”咱们来看一下下图。

经典4+1

分别解释一下:

1、场景视图:主要用于系统参与者和功能之间的关系,老猫理解一般由用例图组成。

2、逻辑视图:描述软件拆解之后的组件关系、组件约束和边界,反映系统整体组成和系统构建,通常由组件图和类图自称。

3、物理视图:描述系统软件到物理硬件的映射关系,主要指软件的部署架构图,这里老猫的理解是可能运维人员更需要关注。

4、处理流程:主要描述软件组件之间的通信时序以及输入输出,反映系统功能流程和数据流程,这里咱们可以用时序图以及流程图来表示。

5、开发视图:描述系统的业务模块划分以及内部的组成设计,反映系统的开发实施过程。

老猫之前写过一篇文章【
新接手一个业务系统,我是这么熟悉的
】。这篇文章的写作思路,主要也是从上面这些点切入去写的。

开发人员如何绘制技术评审的图?

工作这么多年之后,老猫发现,写代码的时候其实是最安逸的时候,只要事先方案设计得好,流程绘制得精准,模型设计得合理。戴上耳机写代码就是一种享受。因为写代码的时候只要对着设计稿去撸就好了。

那么咱们在做技术方案设计的时候应该从哪些点去切入进行画设计图呢?老猫日常的绘图思路主要是从整体到局部,从概要到细节,到最终的模型落地。

例如关于设计支付系统,咱们先把各个系统之间关系以及功能模块梳理清楚,让参与评审的人能够对支付系统的架构有个整体的认知。具体如下:

支付整体架构

通过上面的的图,可以表示清楚系统和系统之间的层级关系,可以让评审人一目了然地知道,你当前所设计的系统在整个架构领域属于那一块。另外的话也能够让人清晰地感知每个系统的主要的功能是什么。

接下来的设计就是开始局部设计。局部设计的话一般需要理清楚功能点,以及整体的业务流程。我们挑一个下单支付的流程,简单绘制一下业务流程。具体如下:

下单支付交易流程

当然上面的电商下单支付交易流程还是比较粗的,看起来还是不够细,另外的也没有达到对着流程图就能进行开发的地步。当然上述的流程图可能并没有涉及到相关的泳道,老猫只是粗略地展现一下大概的意思。关于实际的场景中,大家还是需要根据自己的业务逻辑进行梳理绘制。

再细化一些,那就是加上泳道,然后体现出不同的系统的内部流程。如下图:

泳道流程

当绘制到这里的时候,其实咱们距离最终的写代码还差一点了,那就是细致的时序逻辑。还有对应的流程图中的每个模型的数据库详细设计。再细致一点到实际的唤起收银台支付。那么咱们这边用时序图表示出来。这里主要把支付宝的对接流程展示给大家看一下。

时序流程

到此,基本业务流程差不多已经很清楚了,系统之间的交互时序也比较清晰地表现出来了。那么接下来的话其实就是模型设计了,那么日常模型设计的话,当然我们可以把每个模型之间的关系先表示出来。

数据库设计

当然这里的话其实字段上可以写粗一点,在细节一点的模型完全可以用表格的形式表现出来。可以用word文档中的表,也可以直接用excel直接写。不过还有一款数据库设计工具软件。在下面详细和大家介绍。

完成上面这些设计,基本就差不多了,顶多的话,可能就是具体的接口设计,包括接口的请求入参以及返回参数的设计,当然还有类型的设计。这里的话就不展开说了,当然关于接口的设计也有不错的工具,咱们还是向下继续看。

日常一些绘图工具推荐

UML图绘制工具

日常工作中,绘制流程图,老猫主要用两个工具,一款是draw.io,另外一款是wps。下面咱们分别来介绍一下。

draw.io

drawio主要如下:

draw.io

这款工具,老猫觉得还是比较轻量的,除了有客户端之外,还有网页版,使用起来相当方便,而且用起来也简单,也没有什么学习成本。在线绘制的话,链接地址:
https://app.diagrams.net/

大家可以打开试试。

Draw.io的特点包括:

多种图形元素:提供丰富的图形元素库,包括形状、符号、箭头等,用户可以根据需要自由选择和组合这些元素。

丰富的模板库:内置大量模板,涵盖多个领域的常见图表和图形,如组织结构图、UML图、网络拓扑图等,便于用户快速创建符合自己需求的图形。

实时协作:支持多人实时协作,允许用户邀请他人加入绘图工作,实现实时编辑和交流,提高团队协作效率。

云端存储和同步:文件可以保存在云端,支持与多种云存储服务集成,方便文件备份和同步。

导入导出多种格式:支持导入和导出多种文件格式,方便用户在不同平台间使用和分享图表。

wps

WPS,是金山软件公司自主研发的一款办公软件品牌。相信大家在写文档的时候都用过。wps十分强大,当然相对于draw.io来说的话也更重。但是里面内容丰富呀。

wps

wps除了我们日常知道的office软件之外,其实还有绘图工具也在里面,上图中我们看到还包括流程图以及思维导图等等。说到流程图的话其实和draw.io的差别不是很大,但是有个明显的优势是wps内部的流程图模版非常多。大家可以选择自己喜欢的风格,然后在模版上画出自己风格的业务流程图。如下:

模版

如果想用内部精美的模版当然是要开通会员的。

数据库设计工具

关于数据库建模工具,老猫这里自己用得是一款开源的设计工具,感觉做的还是相当可以的。叫做pdman。官网地址:
http://www.pdman.cn/#/
大家有兴趣的话可以了解一下,本人还是非常喜欢这款工具的,两个字“好用”。

具体工作台的页面如下:

数据库设计工具

这款工具强大的点,不但能进行基本的数据库设计,同时也可以逆向生成SQL,甚至直接创建表。一般把表字段写好,对应的SQL也就出来了,非常省事儿。
目前好像支持mysql数据库以及oracle数据库生成。

接口设计工具-APIFOX

聊到接口设计的话,大家用得比较多的是什么呢?当然最基本的很多朋友会用到word文档。写写也挺好的。老猫觉得用word其实挺费劲的。比较推荐大家使用:
https://apifox.com/

官网是这么介绍的:

Apifox 是接口管理、开发、测试全流程集成工具,定位 Postman + Swagger + Mock + JMeter。通过一套系统、一份数据,解决多个系统之间的数据同步问题。只要定义好接口文档,接口调试、数据 Mock、接口测试就可以直接使用,无需再次定义;接口文档和接口开发调试使用同一个工具,接口调试完成后即可保证和接口文档定义完全一致。高效、及时、准确!

大家可以感受一下这款工具的强大:

接口设计工具

关于怎么用的,老猫在此不多做赘述,推荐大家去试试。老猫在怎么说其功能强大,可能大家也感受不到,所以最好的方式还是自己去试试。

总结

工欲上其事必先利器,以上是老猫日常系统设计过程中的设计思路以及期间使用的相关工具。希望能够给大家带来一点帮助。当然,如果大家有更好的设计软件或者是软件设计方面的思路,也欢迎大家能够在评论区留言。