站长信息
jeffery.xu
jeffery.xu

软件工程师

欢迎访问我的个人笔记网站!我是一名热爱技术的开发者,专注于Web开发和技术分享。

811495111@qq.com
18521510875
筛选

个人笔记

开闭原则(Open/Closed Principle, OCP)
设计模式
开闭原则(Open/Closed Principle, OCP)是面向对象设计的核心原则之一,由 Bertrand Meyer 提出。其核心思想是:软件实体(类、模块、函数等)应当对扩展开放,对修改关闭。这意味着在添加新功能时,应该通过扩展现有代码而非修改它来实现。

核心动机

  • 减少风险:修改现有代码可能引入新的错误,影响原有功能。
  • 提高可维护性:代码无需频繁修改,降低维护成本。
  • 促进复用:通过抽象和多态,可复用现有设计框架。

实现方式

  1. 抽象化:通过接口或抽象类定义稳定的契约。
  2. 多态:利用子类实现行为的扩展。
  3. 依赖注入:通过依赖倒置,使高层模块不依赖具体实现。

C# 示例:报表生成器

假设你正在开发一个报表生成器,初始需求是生成 PDF 报表。随着业务发展,需要支持 Excel 和 CSV 格式。
public class ReportGenerator
{
    public void GenerateReport(string format, ReportData data)
    {
        if (format == "PDF")
        {
            // 生成 PDF 报表的具体实现
            Console.WriteLine("生成 PDF 报表");
        }
        else if (format == "Excel")
        {
            // 生成 Excel 报表的具体实现
            Console.WriteLine("生成 Excel 报表");
        }
        // 问题:每次新增格式都需要修改此方法
    }
}

遵循开闭原则的重构

通过抽象化和多态,将报表生成逻辑封装到独立的类中:
// 定义报表生成器接口(抽象)
public interface IReportGenerator
{
    void Generate(ReportData data);
}

// 具体实现:PDF 报表生成器
public class PdfReportGenerator : IReportGenerator
{
    public void Generate(ReportData data)
    {
        Console.WriteLine("生成 PDF 报表");
    }
}

// 具体实现:Excel 报表生成器
public class ExcelReportGenerator : IReportGenerator
{
    public void Generate(ReportData data)
    {
        Console.WriteLine("生成 Excel 报表");
    }
}

// 新增格式:CSV 报表生成器(无需修改原有代码)
public class CsvReportGenerator : IReportGenerator
{
    public void Generate(ReportData data)
    {
        Console.WriteLine("生成 CSV 报表");
    }
}

// 报表服务:依赖抽象接口
public class ReportService
{
    private readonly IReportGenerator _generator;

    public ReportService(IReportGenerator generator)
    {
        _generator = generator; // 通过构造函数注入依赖
    }

    public void CreateReport(ReportData data)
    {
        _generator.Generate(data);
    }
}

// 使用示例
public class Program
{
    public static void Main()
    {
        var data = new ReportData();
        
        // 需要 PDF 报表时
        var pdfService = new ReportService(new PdfReportGenerator());
        pdfService.CreateReport(data);

        // 需要 Excel 报表时
        var excelService = new ReportService(new ExcelReportGenerator());
        excelService.CreateReport(data);

        // 需要 CSV 报表时(扩展无需修改原有代码)
        var csvService = new ReportService(new CsvReportGenerator());
        csvService.CreateReport(data);
    }
}

关键改进点

  1. 抽象化:通过 IReportGenerator 接口定义稳定的报表生成契约。
  2. 多态:每个报表格式(PDF/Excel/CSV)都实现该接口,行为由具体子类决定。
  3. 依赖注入ReportService 依赖接口而非具体实现,支持动态切换报表生成器。

新增需求:添加 HTML 报表

若需新增 HTML 报表,只需:
  1. 创建 HtmlReportGenerator 类实现 IReportGenerator
  2. 在调用处注入新的生成器,无需修改现有类。
  3. public class HtmlReportGenerator : IReportGenerator
    {
        public void Generate(ReportData data)
        {
            Console.WriteLine("生成 HTML 报表");
        }
    }
    
    // 使用时直接注入新实现
    var htmlService = new ReportService(new HtmlReportGenerator());
    htmlService.CreateReport(data);

    开闭原则的其他应用场景

    1. 插件系统:通过接口定义插件规范,新插件只需实现接口。
    2. 策略模式:将算法封装为策略类,运行时动态切换。
    3. 事件驱动架构:通过事件和监听器实现功能扩展。

    注意事项

    • 过度抽象风险:不要为未来可能的需求过度设计,遵循 YAGNI(You Aren't Gonna Need It)原则。
    • 平衡点:对可能变化的部分应用 OCP,对稳定部分无需过度抽象。
    通过开闭原则,代码可以优雅地应对变化,同时保持稳定性和可维护性。
bundler压缩出现问题
编程技巧

问题出在函数声明的位置和压缩工具处理函数声明的方式上。
在JavaScript中,函数声明提升是很常见的,但是在压缩过程中,当函数声明放在代码中间而不是顶部时,可能会导致问题。

采用将 togglePanel 改为函数表达式的方法

document.addEventListener('DOMContentLoaded', function () {
    // 获取可折叠面板的标题元素
    const collapsibleHeader = document.querySelector('.collapsible-header');
    const collapseElement = document.getElementById('relatedNotesCollapse');
    const toggleIcon = document.querySelector('.toggle-icon');
    const mainContent = document.querySelector('.col-md-8');
    const sidePanel = document.querySelector('.col-md-4');
    const noteContent = document.querySelector('.note-content-detail');
    const floatBtn = document.querySelector('.float-toggle-btn');

    if (!collapsibleHeader || !collapseElement || !toggleIcon || !mainContent || !sidePanel) {
        // 如果找不到必要的元素,就退出
        return;
    }

    // 切换面板状态的函数(使用函数表达式而不是函数声明)
    const togglePanel = function() {
        const isExpanded = !sidePanel.classList.contains('collapsed');

        // 只在非移动设备上更新状态
        if (window.innerWidth >= 768) {
            // 更新本地存储中的状态
            localStorage.setItem('relatedNotesCollapsed', isExpanded);

            if (isExpanded) {
                // 收缩
                sidePanel.classList.add('collapsed');
                mainContent.classList.add('expanded');

                // 隐藏内容区域
                collapseElement.classList.remove('show');

                // 显示悬浮按钮
                if (floatBtn) {
                    floatBtn.classList.add('visible');
                }
            } else {
                // 展开
                sidePanel.classList.remove('collapsed');
                mainContent.classList.remove('expanded');

                // 显示内容区域
                collapseElement.classList.add('show');

                // 隐藏悬浮按钮
                if (floatBtn) {
                    floatBtn.classList.remove('visible');
                }
            }
        }
    };

    // 从本地存储中获取面板状态(如果有)
    const isCollapsed = localStorage.getItem('relatedNotesCollapsed') === 'true';

    // 初始化面板状态
    if (isCollapsed && window.innerWidth >= 768) {
        // 只在非移动设备上应用收缩状态
        // 收缩
        sidePanel.classList.add('collapsed');
        mainContent.classList.add('expanded');

        // 隐藏内容区域
        collapseElement.classList.remove('show');

        // 显示悬浮按钮
        if (floatBtn) {
            floatBtn.classList.add('visible');
        }
    }

    // 添加点击事件监听器,仅在非移动设备下启用
    collapsibleHeader.addEventListener('click', function (e) {
        // 在移动设备下阻止叉号图标的点击事件
        if (window.innerWidth < 768 && e.target.classList.contains('toggle-icon')) {
            e.stopPropagation();  // 阻止事件传播
            return;  // 在移动设备上不处理叉号图标点击
        }

        // 非移动设备下正常切换面板
        if (window.innerWidth >= 768) {
            togglePanel();
        }
    });

    // 特别为移动设备处理叉号图标
    if (toggleIcon) {
        toggleIcon.addEventListener('click', function (e) {
            // 在移动设备下阻止点击事件
            if (window.innerWidth < 768) {
                e.stopPropagation();
                e.preventDefault();
            }
        });
    }

    // 如果存在悬浮按钮,添加点击事件
    if (floatBtn) {
        floatBtn.addEventListener('click', function () {
            togglePanel();
        });
    }

    // 窗口大小改变时的处理
    window.addEventListener('resize', function () {
        if (window.innerWidth < 768) {
            // 在移动设备视图下恢复正常布局
            sidePanel.classList.remove('collapsed');
            mainContent.classList.remove('expanded');

            // 显示相关笔记内容
            collapseElement.classList.add('show');

            // 隐藏悬浮按钮
            if (floatBtn) {
                floatBtn.classList.remove('visible');
            }

            // 确保叉号图标在移动设备上不执行收缩功能
            if (toggleIcon) {
                toggleIcon.style.pointerEvents = 'none';
                toggleIcon.style.display = 'none';
            }
        } else {
            // 恢复图标功能
            if (toggleIcon) {
                toggleIcon.style.pointerEvents = 'auto';
                toggleIcon.style.display = '';
            }

            // 在桌面视图下,如果状态是收缩的,保持收缩
            if (localStorage.getItem('relatedNotesCollapsed') === 'true') {
                sidePanel.classList.add('collapsed');
                mainContent.classList.add('expanded');
                collapseElement.classList.remove('show');
                if (floatBtn) {
                    floatBtn.classList.add('visible');
                }
            }
        }
    });

    // 初始检查是否为移动设备,禁用叉号图标功能
    if (window.innerWidth < 768 && toggleIcon) {
        toggleIcon.style.pointerEvents = 'none';
        toggleIcon.style.display = 'none';
    }
});
解决弹出层乱码问题
编程技巧

是因为这里选了GB936出的错,改了以后好了

单例模式
软考

单例模式是一种对象创建型模式,使用单例模式,可以保证为一个类只生成唯一的实例对象。也就是说,在整个程序空间中,该类只存在一个实例对象。GoF对单例模式的定义是:保证一个类、只有一个实例存在,同时提供能对该实例加以访问的全局访问方法。

为什么使用单例模式

在应用系统开发中,我们常常有以下需求:

-在多个线程之间,比如初始化一次socket资源;比如servlet环境,共享同一个资源或者操作同一个对象

-在整个程序空间使用全局变量,共享资源

-大规模系统中,为了性能的考虑,需要节省对象的创建时间等等。

因为Singleton模式可以保证为一个类只生成唯一的实例对象,所以这些情况,Singleton模式就派上用场了。

实现单例步骤常用步骤

a)构造函数私有化

b)提供一个全局的静态方法(全局访问点)

c)在类中定义一个静态指针,指向本类的变量的静态变量指针

项目中添加 Bundler 来压缩 CSS 和 JS
编程技巧

添加 BuildBundlerMinifier NuGet 包

在项目根目录创建 bundleconfig.json 文件,用于配置要压缩和捆绑的文件

[
  {
    "outputFileName": "wwwroot/css/site.min.css",
    "inputFiles": [
      "wwwroot/css/site.css"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    }
  },
  {
    "outputFileName": "wwwroot/js/site.min.js",
    "inputFiles": [
      "wwwroot/js/site.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    }
  },
  {
    "outputFileName": "wwwroot/js/page-loader.min.js",
    "inputFiles": [
      "wwwroot/js/page-loader.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    }
  },
  {
    "outputFileName": "wwwroot/js/back-to-top.min.js",
    "inputFiles": [
      "wwwroot/js/back-to-top.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    }
  },
  {
    "outputFileName": "wwwroot/js/collapsible-panel.min.js",
    "inputFiles": [
      "wwwroot/js/collapsible-panel.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    }
  },
  {
    "outputFileName": "wwwroot/js/bundle.min.js",
    "inputFiles": [
      "wwwroot/js/site.js",
      "wwwroot/js/page-loader.js",
      "wwwroot/js/back-to-top.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    }
  }
]
@{
    // 获取当前页面路径
    var currentPage = ViewContext.RouteData.Values["page"]?.ToString() ?? "";
    var isPublicNotes = currentPage.StartsWith("/PublicNotes");
    var isPrivacy = currentPage.StartsWith("/Privacy");
    
    // 判断是否为开发环境
    var isDevelopment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development";
    var cssFile = isDevelopment ? "~/css/site.css" : "~/css/site.min.css";
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - 个人研习知识记录笔记</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/lib/bootstrap-icons/bootstrap-icons.min.css" />
    <link rel="stylesheet" href="@cssFile" asp-append-version="true" />
    <link rel="stylesheet" href="~/WebNote.styles.css" asp-append-version="true" />
    @await RenderSectionAsync("Styles", required: false)
</head>
<body>
    <!-- 添加全局加载指示器 -->
    <div id="page-loader" class="page-loader">
        <div class="loader-container">
            <div class="spinner-border text-primary" role="status">
                <span class="visually-hidden">加载中...</span>
            </div>
            <p class="mt-3">页面加载中,请稍候...</p>
        </div>
    </div>
    <!-- 现有布局内容,包装在一个容器内以控制显示 -->
    <div id="page-content" class="page-content d-none">
        <!-- 页面内容 -->
    </div>
    
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    
    @if (isDevelopment)
    {
        <script src="~/js/site.js" asp-append-version="true"></script>
        <script src="~/js/page-loader.js" asp-append-version="true"></script>
        <script src="~/js/back-to-top.js" asp-append-version="true"></script>
    }
    else
    {
        <!-- 在生产环境中使用捆绑和压缩的JS文件 -->
        <script src="~/js/bundle.min.js" asp-append-version="true"></script>
    }
    
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
右下角回到顶部按钮开发
编程技巧
  1. css
    * 回到顶部按钮样式 */
    .back-to-top {
        position: fixed;
        bottom: 25px;
        right: 0; /* 修改为0,确保紧贴页面右边缘 */
        display: none;
        width: 40px;
        height: 40px;
        border-radius: 50% 0 0 50%; /* 修改为半圆形,左边圆形,右边直线 */
        background-color: #007bff;
        color: white;
        border: none; /* 移除按钮边框 */
        padding: 0; /* 移除按钮内边距 */
        text-align: center;
        font-size: 20px;
        cursor: pointer;
        z-index: 1000;
        transition: all 0.3s ease;
        box-shadow: 0 2px 5px rgba(0,0,0,0.2);
    }
    
        .back-to-top:hover {
            background-color: #0056b3;
            width: 45px; /* 悬停时稍微扩大按钮 */
        }
    
        .back-to-top:focus {
            outline: none; /* 移除焦点轮廓 */
        }
    
        .back-to-top i {
            line-height: 40px;
            margin-left: -3px; /* 微调图标位置 */
        }
  2. js
    // 回到顶部功能
    document.addEventListener('DOMContentLoaded', function () {
        // 创建回到顶部按钮元素(使用button元素替代a元素)
        var backToTopButton = document.createElement('button');
        backToTopButton.className = 'back-to-top';
        backToTopButton.id = 'back-to-top';
        backToTopButton.type = 'button';
        backToTopButton.setAttribute('aria-label', '回到顶部');
        backToTopButton.innerHTML = '<i class="bi bi-arrow-up"></i>';
        document.body.appendChild(backToTopButton);
    
        // 控制按钮显示与隐藏
        window.addEventListener('scroll', function () {
            if (window.pageYOffset > 300) { // 当页面滚动超过300px时显示
                backToTopButton.style.display = 'block';
            } else {
                backToTopButton.style.display = 'none';
            }
        });
    
        // 点击按钮回到顶部
        backToTopButton.addEventListener('click', function () {
            // 平滑滚动回顶部
            window.scrollTo({
                top: 0,
                behavior: 'smooth'
            });
        });
    });
TLS 1.3支持
编程技巧

.net Framework可参照上图

.net core从 3.0以后支持TLS 1.3,.net 5以后也支持

.net 中启用缓存一周
编程技巧

// 添加到 Program.cs 中
app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        // 缓存图片、CSS和JavaScript文件一周
        if (ctx.File.Name.EndsWith(".jpg") || ctx.File.Name.EndsWith(".css") || ctx.File.Name.EndsWith(".js"))
        {
            ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=604800");
           }
       }
   });

新框架接口记录1-NHS企微展示
新框架

基本逻辑:NHS问卷完成后先查询是否和医药代表有关系,没有的话返回url链接

public dynamic QueryRelationExist(WebParameterContext webContext)
{
    var response = new ServerContext<dynamic>() { ResponseMessage = new ResponseMessage() { Success = true } };
    ActionReturnResponseMessage = () =>
    {
        try
        {
            var Datas = GetStoreCodeUserCodeByID(webContext);
            if (Datas == null)
            {
                return response;
            }
            string url = SystemConfig.GetValue("SanXiaFriendRelationQuery"); //"https://nhssanxia-test.nestlechinese.com/member-mini/apis/representative/exists";
            var friendresult = false;
            foreach (var Data in ((IEnumerable<dynamic>)Datas).ToList())
            {
                dynamic data = DynamicExtension.EmptyDynamic;
                data.userCode = Data.userCode;
                data.unionid = webContext.RequestBody.unionid;
                var content = HttpTools.SIMHttpPostSign(url, JSON.ToJSON(data), "查询医务业务代表好友关系");
                var result = JSON.ToDynamic(content);
                if (result.code == 20000)
                {
                    if (result.data == 1)
                    {
                        friendresult = true;
                        break;
                    }
                }
            }
            if (friendresult)
            {
                return response;
            }
            else
            {
                string QRurl = SystemConfig.GetValue("SanXiaQRCodeRepresent"); //"https://nhssanxia-test.nestlechinese.com/member-mini/apis/representative/qrcode";

                var returnValue = new List<dynamic>();
                foreach (var Data in ((IEnumerable<dynamic>)Datas).ToList())
                {
                    dynamic data = DynamicExtension.EmptyDynamic;
                    data.userCode = Data.userCode;
                    data.storeCode = Data.storeCode;
                    var content = HttpTools.SIMHttpPostSign(QRurl, JSON.ToJSON(data), "医务/业务代表企微渠道码");
                    var result = JSON.ToDynamic(content);
                    if (result.code == 20000)
                    {
                        returnValue.Add(result.data);

                    }

                }
                response.ResponseMessage.ErrorCode = 200;
                response.ResponseMessage.ErrorMessage = "获取成功";
                response.Status = returnValue;
                return response;
            }
        }
        catch (Exception ex)
        {
            return new ServerContext<dynamic>()
            {
                ResponseMessage = new ResponseMessage()
                {
                    Success = false,
                    ErrorCode = 500,
                    ErrorMessage = ex.Message
                }
            };
        }
    };
    return webContext;
}
单一职责原则
软考

修改一个类的原因只能有一个。

      这条原则的主要目的是减少复杂度。 你不需要费尽心机地去构思如何仅用 200 行代码来实现复杂设计, 实际上完全可以使用十几个清晰的方法。
      当程序规模不断扩大、 变更不断增加后, 真实问题才会逐渐显现出来。 到了某个时候, 类会变得过于庞大, 以至于你无法记住其细节。 查找代码将变得非常缓慢, 你必须浏览整个类, 甚至整个程序才能找到需要的东西。 程序中实体的数量会让你的大脑堆栈过载,你会感觉自己对代码失去了控制。
      还有一点: 如果类负责的东西太多, 那么当其中任何一件事发生改变时, 你都必须对类进行修改。 而在进行修改时, 你就有可能改动类中自己并不希望改动的部分。如果你开始感觉在同时关注程序特定方面的内容时有些困难
的话, 请回忆单一职责原则并考虑现在是否应将某些类分割为几个部分。