python 视角下的 6 大程序设计原则

news2024/11/15 11:43:02

众所周知,python 是面向对象的语言。

但大多数人学习 python 只是为了写出“能够实现某些任务的自动化脚本”,因此,python 更令人熟知的是它脚本语言的身份。

那么,更近一步,如果使用 python 实现并维护一个大的项目,进行软件开发,简单的串并联脚本就难以满足日常繁重的维护需求。

此时需要借鉴其他编程语言的精化,比如说 JAVA 语言中的面向对象的思想。通过将核心功能抽象化并原子化,赋予 python 脚本更高的可维护性。

这篇文章是我读 秦小波 老师的 《设计模式之禅》有感。虽然书里全篇都是Java语言,看不懂,但书中提到的 6 大程序设计原则还是让我受益匪浅。下面我将分别介绍这 6 大原则,并将尝试给出正反例帮助大家理解。

文章目录

  • 单一职责原则
  • 里氏替换原则
  • 依赖倒置原则
  • 接口隔离原则
  • 迪米特原则
  • 开闭原则

单一职责原则

简单理解就是,一个类、或者一个函数,只能承担一项职责。
这一设计思想指导我们,在设计业务逻辑时,要尽可能的剥离不相干的业务,降低业务逻辑的颗粒度,进而实现“原子函数”、“原子类”。

如何有效的鉴别“不相干”的业务,成为了履行单一职责原则的核心难点。一种常见的解释是:类的变更仅由一件业务决定。

一个违反了该原则的案例可以是:

class Tiger:
    def walk(self):
        print('walk')
        
    def swim(self):
        print('swim')

可以看到,老虎类包含了两个方法,走和游泳。然而,游泳的业务和走是相对独立的。所以说, Tiger 类将受到两种变更因素的影响,违背了单一职责的原则。

一种解决方案是将二者剥离,如下:

class Walk_animal():
    def walk(self):
        print('walk')


class Swim_animal():
    def swim(self):
        print('swim')


class Tiger(Walk_animal, Swim_animal):
    pass


a_tiger = Tiger()
a_tiger.swim()

Tiger 类通过继承父类,获得游泳和走的能力,这种设计很好的遵守了单一职责原则,但凭空增加了两个类,因此在大多数业务实践中,我们也不会这样做。

秦小波 老师的 《设计模式之禅》中是这样解决的:Java 环境下,面向对象的精髓是面向接口编程。

我们通过增加接口来规范类的功能,虽然 python 原生不支持接口,但我们可以使用抽象类来实现接口类。相关教程:python–接口类与抽象类 - 知乎 (zhihu.com)

from abc import abstractmethod, ABCMeta


class Walk_animal(metaclass=ABCMeta):
    @abstractmethod
    def walk(self): pass


class Swim_animal(metaclass=ABCMeta):
    @abstractmethod
    def swim(self): pass


class Tiger(Walk_animal, Swim_animal):
    def walk(self):
        print('walk')

    def swim(self):
        print('swim')


a_tiger = Tiger()
a_tiger.swim()

这种实现方式看似更复杂,但在大型项目中,可以提高代码的可读性和可维护性。

里氏替换原则

该原则规定了继承操作的要点,通俗解释的话就是,父类能够使用的地方,将父类替换为子类,程序也能正常运行。

具体来说:

  1. 子类必须完全使用父类的方法;
  2. 子类可以有自己的个性;
  3. 覆写方法时,子类对应的参数类型范围只能比父类窄;
  4. 覆写方法时,子类对应的输出结果的类型只能比父类窄;

可以看到,里氏替换原则关注的焦点是覆写父类方法。因为正常的继承操作是一定满足上述要求的,如下:

class Walk_animal():
    def walk(self):
        print('walk')


class Swim_animal():
    def swim(self):
        print('swim')


class Tiger(Walk_animal, Swim_animal):
    pass

那么一种违反里氏替换原则的案例可以是:

class do_plus:
    def plus(self, addon):
        return addon + 1

class math(do_plus):
    def plus(self, addon):
        return addon + 2

a = do_plus()
assert a.plus(1) == 2

b = math()
assert b.plus(1) == 2

会报错:

Traceback (most recent call last):
  File "H:/123123123/main.py", line 65, in <module>
    assert b.plus(1) == 2
AssertionError

可见,math 的覆写没有通过父类的测试案例。上述代码改为:

class do_plus:
    def plus(self, addon):
        return addon + 1

class math(do_plus):
    def plus_2(self, addon):
        return addon + 2

a = do_plus()
assert a.plus(1) == 2

b = math()
assert b.plus(1) == 2

通过了测试案例,同时子类也有了自己的个性,很好的满足了里氏替换原则的前两条。

事实上,第一条,子类必须完全使用父类的方法 更多是针对接口类的继承,接口类中的抽象方法是一定要继承的。

另一方面,后两条,则可以通过下面一段代码展示:

from typing import Union

class do_plus:
    def plus(self, addon: int) -> float:
        return float(addon) + 1

class math(do_plus):
    def plus(self, addon: Union[int, float]) -> Union[int, float]:
        return addon + 1


a = do_plus()
a_results = a.plus(1)
assert type(a_results) == float

b = math()
b_results = b.plus(1)
assert type(b_results) == float

运行后,报错信息如下:

Traceback (most recent call last):
  File "H:/123123123/main.py", line 70, in <module>
    assert type(b_results) == float
AssertionError

可以看到,math 拓宽了 do_plus 类的应用范围,输入类型增加了浮点型。这点是正确的,符合里氏替换原则的第三条。但输出时的类型也拓宽了,因此没有通过原有测试案例的类型检查。

解决办法就是约束输出的变量类型,如下:

from typing import Union

class do_plus:
    def plus(self, addon: int) -> float:
        return float(addon) + 1

class math(do_plus):
    def plus(self, addon: Union[int, float]) -> float:
        return float(addon) + 1


a = do_plus()
a_results = a.plus(1)
assert type(a_results) == float

b = math()
b_results = b.plus(1)
assert type(b_results) == float

总的来说,检验里氏替换原则的最简单方法就是,子类能够通过所有情况下的父类的测试案例。

依赖倒置原则

该原则在面向对象的语境下指:

  1. 高层模块不应该依赖于低层模块,二者均应该依赖其抽象。
  2. 抽象不应该依赖细节,细节应该依赖于抽象。

所谓抽象就是指接口类,细节就是实例化的接口类。

依赖倒置原则指,面向对象编程的核心是面向接口编程,即不同类之间的依赖耦合在接口层上就已经实现。

一种不满足依赖倒置原则的情况可以是:

class ase_optimizer():
    def __init__(self):
        self.name = 'ASE'
    def ase_check(self):
        print('ase check')

class xtb_optimizer():
    def __init__(self):
        self.name = 'xTB'
    def xtb_check(self):
        print('xtb check')

class checker():
    def __init__(self, optimizer):
        self.optimizer = optimizer
    def check(self):
        if self.optimizer.name == 'ASE':
            self.optimizer.ase_check()
        elif self.optimizer.name == 'xTB':
            self.optimizer.xtb_check()

a = checker(ase_optimizer())
a.check()

这段代码中,高层模块 checker 通过调用低层模块 optimizer 完成业务逻辑,没有用到接口类,三个类均为细节,因此我们说,这是以细节为基础搭建起来的架构。

一个很显然的弊端在于:

        if self.optimizer.name == 'ASE':
            self.optimizer.ase_check()
        elif self.optimizer.name == 'xTB':
            self.optimizer.xtb_check()

如果需要增加优化器,我们要修改 checker 类,如下:

        if self.optimizer.name == 'ASE':
            self.optimizer.ase_check()
        elif self.optimizer.name == 'xTB':
            self.optimizer.xtb_check()
        elif self.optimizer.name == 'gauss':
            self.optimizer.gauss_check()

显然,低层业务逻辑的变动,惊动了高层业务逻辑,因此我们说,违反了依赖倒置原则。

一种改进方法如下:

from abc import abstractmethod, ABCMeta


class opt_check(metaclass=ABCMeta):
    @abstractmethod
    def check(self): pass


class ase_optimizer(opt_check):
    def __init__(self):
        self.name = 'ASE'

    def check(self):
        print('ase check')


class xtb_optimizer(opt_check):
    def __init__(self):
        self.name = 'xTB'

    def check(self):
        print('xtb check')


class checker():
    def __init__(self, optimizer):
        self.optimizer = optimizer

    def check(self):
        self.optimizer.check()


a = checker(ase_optimizer())
a.check()

可以看到,业务逻辑大大简化,此时如果需要新增逻辑,如下所示:

from abc import abstractmethod, ABCMeta


class opt_check(metaclass=ABCMeta):
    @abstractmethod
    def check(self): pass


class ase_optimizer(opt_check):
    def __init__(self):
        self.name = 'ASE'

    def check(self):
        print('ase check')


class xtb_optimizer(opt_check):
    def __init__(self):
        self.name = 'xTB'

    def check(self):
        print('xtb check')


class gauss_optimizer(opt_check):
    def __init__(self):
        self.name = 'gauss'

    def check(self):
        print('gauss check')


class checker():
    def __init__(self, optimizer):
        self.optimizer = optimizer

    def check(self):
        self.optimizer.check()


a = checker(ase_optimizer())
a.check()

可以看到,虽然我们新增了一个低层类,但在高层 checker 上完全看不到影响。

这就是面向接口编程的奇妙之处:我们通过接口,预先指定了方法 check,因此高层在调用时,不需要了解具体的低层实现,只要知道低层,有这个 check 方法就可以完成业务逻辑的搭建。

这样带来的好处是:

  1. 利于并行开发。比如说10个人开发10个子类,如果按照第一种实现方式,每一个人完成开发后都需要改动高层类,这会带来潜在的风险。
  2. 高层类和低层类相对解耦,可以分别进行测试。

然而,做到这一步并没有完全实现依赖倒置原则。因为该原则第一条指出,高层模块也要依赖于抽象。

我们上面的改动只是实现了,低层模块依赖于抽象(接口),高层模块并没有依赖接口。我们只是利用接口简化了部分业务逻辑。

事实上,这依然是一种短视的行为。

因为我们无法预知,现在所谓的高层模块会不会在未来,也变成了庞大业务逻辑中的一个低层模块。

真正满足依赖倒置原则的实现如下:

from abc import abstractmethod, ABCMeta


class opt_check(metaclass=ABCMeta):
    @abstractmethod
    def opt_check(self): pass


class chk_check(metaclass=ABCMeta):
    @abstractmethod
    def __init__(self, optimizer: opt_check): pass

    @abstractmethod
    def chk_check(self): pass


class ase_optimizer(opt_check):
    def opt_check(self):
        print('ase check')


class checker(chk_check):
    def __init__(self, optimizer: opt_check):
        self.optimizer = optimizer

    def chk_check(self):
        self.optimizer.opt_check()


class auto_checker():
    def check(self, a_checker: checker):
        a_checker.chk_check()


a = auto_checker()
a.check(a_checker=checker(optimizer=ase_optimizer()))

我们首先通过两个接口 opt_checkchk_check ,搭建了清晰的业务逻辑框架。

from abc import abstractmethod, ABCMeta


class opt_check(metaclass=ABCMeta):
    @abstractmethod
    def opt_check(self): pass


class chk_check(metaclass=ABCMeta):
    @abstractmethod
    def __init__(self, optimizer: opt_check): pass

    @abstractmethod
    def chk_check(self): pass

剩余部分只是对抽象的具体实现,即细节

class ase_optimizer(opt_check):
    def opt_check(self):
        print('ase check')


class checker(chk_check):
    def __init__(self, optimizer: opt_check):
        self.optimizer = optimizer

    def chk_check(self):
        self.optimizer.opt_check()

可以看到,细节实现时,按照抽象的指示,填补空缺即可。此外,更高层调用高层时,依然满足依赖倒置原则:

class auto_checker():
    def check(self, a_checker: checker):
        a_checker.chk_check()

因为 checker 模块已经被定死了。

至此,我们完全实现了依赖倒置原则,搭建了以 抽象 为框架的业务逻辑。这部分是完全独立的,可以看作业务实现的草稿,更像是论文写作的大纲。

总的来说,依赖倒置原则对编程人员的架构能力要求很高,面向接口编程也因此成为了面向对象编程的精髓。

接口隔离原则

在面向接口的程序设计理念下,我们需要先确定业务逻辑框架,再去实现细节。因此,在设计接口时,其颗粒度要尽可能的小,不然抽象类的实例会实现一些没有必要的功能。

from abc import abstractmethod, ABCMeta


class teach(metaclass=ABCMeta):
    @abstractmethod
    def teach_eng(self): pass

    @abstractmethod
    def teach_math(self): pass


class teacher(teach):
    def teach_eng(self):
        print('teach english')

    def teach_math(self):
        print('teach math')


class eng_class():
    def start_class(self, eng_teacher: teacher):
        eng_teacher.teach_eng()


class math_class():
    def start_class(self, math_teacher: teacher):
        math_teacher.teach_math()

可以看到,实际业务逻辑中,英语课并不需要老师教数学。显然,我们 teacher 接口的设计是失败的。

将其拆分为两个接口,如下:

from abc import abstractmethod, ABCMeta


class eng_teach(metaclass=ABCMeta):
    @abstractmethod
    def teach_eng(self): pass

class math_teach(metaclass=ABCMeta):
    @abstractmethod
    def teach_math(self): pass


class eng_teacher(eng_teach):
    def teach_eng(self):
        print('teach english')


class math_teacher(math_teach):
    def teach_math(self):
        print('teach math')


class eng_class():
    def start_class(self, eng_teacher: eng_teacher):
        eng_teacher.teach_eng()


class math_class():
    def start_class(self, math_teacher: math_teacher):
        math_teacher.teach_math()

上述为两个接口的简易拆分,值得注意的是,接口并不是颗粒度越小越好,因为这另一方面会无端增加接口数量。

事实上,在复杂业务逻辑的实践中,我们应该把相似功能的捆绑在一起,在保证接口纯洁性的同时,尽量降低接口数量。

下图为一种接口设计:

在这里插入图片描述
原博客

该设计中,高层模块 A 调用了低层模块 B,而低层模块 B 通过接口 I1, I2 实现了方法 1,2,3

通过整合相似功能,这种设计很好的体现了接口隔离原则。

总的来说,接口隔离原则理念上和最开始的单一职责原则类似,都在讲原子化。二者的区别在于,单一职责原则的对象是类,是实例。接口隔离原则面向的是接口,因为“面向接口编程”的理念被单独列出。

迪米特原则

该原则指出:一个对象应该对其他对象了解的越少越好。

一个违反了迪米特原则的例子如下:

class products():
    def __init__(self, year):
        self.year = year


class employee():
    def query_products(self, query_year: int):
        print(f'{query_year} has products 100')


class Boss():
    def query(self, a_employee: employee, products_year: int):
        prod = products(year=products_year)
        a_employee.query_products(query_year=prod.year)


b = Boss()
employee_A = employee()
b.query(a_employee=employee_A, products_year=2000)

程序设计的本意是,老板通过员工获知产品信息。

老板和员工是直接朋友,然而,老板方法里冒出了 products 类,并与其产生了交流。

修改后如下所示:

class products():
    def __init__(self, year):
        self.year = year


class employee():
    def query_products(self, query_year: int):
        prod = products(year=query_year)
        print(f'{prod.year} has products 100')


class Boss():
    def query(self, a_employee: employee, products_year: int):
        a_employee.query_products(query_year=products_year)


b = Boss()
employee_A = employee()
b.query(a_employee=employee_A, products_year=2000)

老板告知员工查询哪一年,员工据此查库存,符合我们的设计逻辑。

总的来说,迪米特法则的理念是类间解耦,弱耦合。但这会增加大量的跳转类。比如上例中,employee 就是一个跳转类,事实上,查库存的操作完全可以有老板类自己完成。所以在采用这一原则时,需要反复权衡,既要让结构清晰,又要做到高内聚低耦合。

开闭原则

开闭原则是面向对象编程的极致追求。它要求我们,软件开发过程中对扩展开放,对修改关闭。

也就是说,已经写好的代码,不能再动。如果有新的业务需求,只要在原有基础上拓展即可,因此对程序架构能力提出了极高的要求。

实现开闭原则的重要途径就是面向接口编程,如依赖倒置原则那一节所举的例子,在良好定义接口的前提下,我们可以轻松完成拓展,但又不会影响现有业务逻辑,最大程度上降低了增量开发带来的风险。

开闭原则并没有像前5条原则一样,有着清晰的定义和指导。它更多的是一种口号,是架构师的终极目标。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/23315.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于SpringBoot的CSGO赛事管理系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SpringBoot 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目…

为研发效能而生|一场与 Serverless 的博弈

2022 年 11 月 3 日&#xff0c;第三届云原生编程挑战赛即将迎来终极答辩&#xff0c;18 支战队、32 位云原生开发者入围决赛&#xff0c;精彩即将开启。 云原生编程挑战赛项目组特别策划了《登顶之路》系列选手访谈&#xff0c;期待通过参赛选手的故事&#xff0c;看到更加生…

第1章 计算机系统概述

1.1 操作系统的基本概念 1.1.1 操作系统的概念 操作系统是计算机系统中最基本的系统软件。 操作系统&#xff08;Operating System&#xff0c;OS&#xff09;是指控制和管理整个计算机系统的硬件与软件资源&#xff0c;合理地组织、调度计算机的工作与资源的分配&#xff0c;进…

锐捷端口安全实验配置

端口安全分为IPMAC绑定、仅IP绑定、仅MAC绑定 配置端口安全是注意事项 如果设置了IPMAC绑定或者仅IP绑定&#xff0c;该交换机还会动态学习下联用户的MAC地址 如果要让IPMAC绑定或者仅IP绑定的用户生效&#xff0c;需要先让端口安全学习到用户的MAC地址&#xff0c;负责绑定不生…

如何参与一个开源项目!

今天教大家如何给开源项目提交pr&#xff0c;成为一名开源贡献者。pr是 Pull Request 的缩写&#xff0c;当你在github上发现一个不错的开源项目&#xff0c;你可以将其fork到自己的仓库&#xff0c;然后再改动一写代码&#xff0c;再提交上去&#xff0c;如果项目管理员觉得你…

【建议收藏】回收站数据恢复如何操作?3个方案帮你恢复删除的文件

在使用电脑时&#xff0c;我们经常会清理不需要的文件数据。电脑回收站被清空了&#xff0c;但是里面有我们重要的数据&#xff0c;回收站数据恢复如何操作&#xff1f;不如试试下面的3个方案&#xff0c;一起来了解一下回收站数据恢复吧&#xff01; 一、注册表恢复回收站数据…

电脑视频转换成mp4格式,视频格式转换器转换

怎么把电脑视频转换成mp4格式&#xff1f;使用视频转换器&#xff0c;可以转换来自各种设备的音视频格式&#xff0c;包括相机、手机、视频播放器、电视、平板电脑等。因此&#xff0c;音视频爱好者都可以使用它在各种设备上播放或在社交平台上分享。 主要人群及作用&#xff1…

BHQ-2 NHS,916753-62-3作为各种荧光共振能量转移DNA检测探针中淬灭部分

英文名称&#xff1a;BHQ-2 NHS CAS&#xff1a;916753-62-3 外观&#xff1a;深紫色粉末 分子式&#xff1a;C29H29N7O8 分子量&#xff1a;603.59 储存条件&#xff1a;-20C&#xff0c;在黑暗中 结构式&#xff1a; 凯新生物产品简介&#xff1a; 黑洞猝灭剂-2&#…

pytorch初学笔记(九):神经网络基本结构之卷积层

目录 一、torch.nn.CONV2D 1.1 参数介绍 1.2 stride 和 padding 的可视化 1.3 输入、输出通道数 1.3.1 多通道输入 1.3.2 多通道输出 二、卷积操作练习 2.1 数据集准备 2.2 自定义神经网络 2.3 卷积操作控制台输出结果 2.4 tensorboard可视化 三、完整代码 一、torc…

NestJS 使用体验 | 不如 Spring Boot

本博客站点已全量迁移至 DevDengChao 的博客 https://blog.dengchao.fun , 后续的新内容将优先在自建博客站进行发布, 欢迎大家访问. 文章目录前言正文开发体验运行体验总结相关内容推广前言 公司里近期在尝试部署一些业务到阿里云的函数计算上, 受之前迁移已有的 Spring Boot…

TestStand-调试VI

文章目录调试VI调试VI 在LabVIEW PASS/FAIL TEST步骤中放置一个断点。 ExecuteRun MainSequence。执行在LabVIEW PASS/FAIL处暂停测试步骤。 3.完成以下步骤来调试LabVIEW PASS/FAIL TEST VI步骤。 a.在TestStand的调试工具栏上单击step into&#xff08;步进&#xff09;…

System V IPC+消息队列

多进程与多线程 使用有名管道实现双向通信时&#xff0c;由于读管道是阻塞读的&#xff0c;为了不让“读操作”阻塞“写操作”&#xff0c;使用了父子进程来多线操作&#xff0c; 1&#xff09;父进程这条线&#xff1a;读管道1 2&#xff09;子进程这条线&#xff1a;写管道2…

【二叉树的顺序结构:堆 堆排序 TopK]

努力提升自己&#xff0c;永远比仰望别人更有意义 目录 1 二叉树的顺序结构 2 堆的概念及结构 3 堆的实现 3.1 堆向下调整算法 3.2 堆向上调整算法 3.3堆的插入 3.4 堆的删除 3.5 堆的代码实现 4 堆的应用 4.1 堆排序 4.2 TOP-K问题 总结&#xff1a; 1 二叉树的顺序结…

分享几招教会你怎么给图片加边框

大家平时分享图片的时候&#xff0c;会不会喜欢给照片加点装饰呢&#xff1f;比如加些边框、文字或者水印之类的。我就喜欢给图片加上一些边框&#xff0c;感觉加了边框的照片像裱在相框中的感觉似的&#xff0c;非常有趣。那么你知道如何给图片加边框吗&#xff1f;不知道的话…

【Nginx】01-什么是Nginx?Nginx技术的功能及其特性介绍

目录1. 介绍1.1 常见服务器的对比1&#xff09;IIS2&#xff09;Tomcat3&#xff09;Apache4&#xff09;Lighttpd1.2 Nginx的优点(1) 速度更快、并发更高(2) 配置简单、扩展性强(3) 高可靠性(4) 热部署(5) 成本低、BSD许可证2. Nginx常用功能2.1 基本HTTP服务2.2 高级HTTP服务…

华为数通2022年11月 HCIP-Datacom-H12-821 第二章

142.以下关于状态检测防火墙的描述&#xff0c;正确是哪一项&#xff1f; A.状态检测防火墙需要对每个进入防火墙的数据包进行规则匹配 B.因为UDP协议为面向无连接的协议&#xff0c;因此状态检测型防火墙无法对UDP报文进行状态表的匹配 C.状态检测型防火墙只需要对该连接的第一…

性能测试-CPU性能分析,IO密集导致系统负载高

目录 IO密集导致系统负载高 使用top命令-观察服务器资源状态 使用vmstat命令-观察服务器资源状态 使用pidstat命令-观察服务器资源状态 使用iostat命令-观察服务器资源状态 IO密集导致系统负载高 stress-ng -i 10 --hdd 1 --timeout 100-i :有多少个工作者进行&#…

函数的极限:如何通过 δ 和 ϵ 来定义一个连续的函数

连续的定义 维基百科给出的定义&#xff1a; 连续函数&#xff08;英语&#xff1a;Continuous function&#xff09;是指函数在数学上的属性为连续。直观上来说&#xff0c;连续的函数就是当输入值的变化足够小的时候&#xff0c;输出的变化也会随之足够小的函数。 所以不要直…

51单总线控制SV-5W语音播报模块

单总线控制SV-5W语音播报模块SV-5W语音播报模块SV-5W语音播报模块简介工作模式说明模块配置接线驱动部分代码效果展示SV-5W语音播报模块 SV-5W语音播报模块简介 DY-SV5W是一款智能语音模块&#xff0c;集成IO分段触发&#xff0c;UART串口控制&#xff0c;ONE_line单总线串口控…

macOS monterey 12.6.1安装homebrew + nginx + php + mysql

效果图 主要步骤 安装homebrew使用brew安装nginxphpmysql详细步骤 参考“Homebrew国内如何自动安装&#xff08;国内地址&#xff09;&#xff08;Mac & Linux&#xff09;”安装brew&#xff0c; 命令&#xff1a; /bin/zsh -c "$(curl -fsSL https://gitee.com/cu…