Go设计模式(3)-设计原则(gof设计模式按目的分为哪几类)
ccwgpt 2024-10-10 04:51 36 浏览 0 评论
上一篇文章Go设计模式(2)-面向对象分析与设计里讲过,做设计最重要的是保留合适的扩展点。如何才能设计出合适的扩展点呢?
这篇文章会讲解一下经典的设计原则。这些设计原则大家可能都听过,但可能没有想过为什么会提炼出这些原则,它们有什么作用。对内一个设计原则,我会尽量找到一个实例,说明它的重要性。通过实例来感受原则,比起只看枯燥的文字有效的多。
在这里需要说明一点,设计原则是一种思想,设计模式是这种思想的具象化。所以当我们真正领悟到这种思想后,设计的时候会事半功倍。
本文要阐述的原则如下:
- 单一职责原则
- 开放-封闭原则
- 里氏替换原则
- 接口隔离原则
- 依赖倒转原则
- 迪米特法则
单一职责原则
理解原则
单一职责原则(SRP):一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。
实施
不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:
- 类中的代码行数、函数或者属性过多;
- 类依赖的其他类过多,或者依赖类的其他类过多;
- 私有方法过多;
- 比较难给类起一个合适的名字;
- 类中大量的方法都是集中操作类中的某几个属性。
实例
假设我们要做一个在手机上玩的俄罗斯方块游戏,Game类可以设计如下:
type Game struct {
x int64
y int64
}
func (game *Game) Show() {
fmt.Println(game.x, game.y)
}
func (game *Game) Move() {
game.x--
game.y++
}
游戏的显示和移动都放在类Game里。后面需求变更了,不但要在手机上显示,还需要在电脑上显示,而且还有两人对战模式,这些更改主要和显示有关。
这时最好将Show和Move拆分到两个函数,这样不但可以复用Move的逻辑,而且今后无论如何更改Show,都不会影响Move所在的类。
但因为一开始Game职责不单一,整个系统中很多位置使用同一个Game变量调用Show和Move,对这些位置的改动和测试是十分浪费时间的。
开放-封闭原则
理解原则
对扩展开放、修改关闭(OCP):添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。
- 第一点,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。
- 第二点,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。
实施
我们要时刻具备扩展意识、抽象意识、封装意识。在写代码的时候,我们要多花点时间思考一下,这段代码未来可能有哪些需求变更,如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,将新的代码灵活地插入到扩展点上。
很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是23种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。
实例
假设我们要做一个API接口监控告警,如果TPS或Error超过指定值,则根据不同的紧急情况通过不同方式(邮箱、电话)通知相关人员。根据Go设计模式(2)-面向对象分析与设计里讲的方案,我们先找出类。
业务实现流程为:
- 获取异常指标
- 获取异常数据,和异常指标进行比较
- 通知相关人员
所以,我们可以设置三个类,AlertRules存放报警规则,Notification用来通知,Alert用来比较。
//存储报警规则
type AlertRules struct {
}
func (alertRules *AlertRules) GetMaxTPS(api string) int64 {
if api == "test" {
return 10
}
return 100
}
func (alertRules *AlertRules) GetMaxError(api string) int64 {
if api == "test" {
return 10
}
return 100
}
const (
SERVRE = "SERVRE"
URGENT = "URGENT"
)
//通知类
type Notification struct {
}
func (notification *Notification) Notify(notifyLevel string) bool {
if notifyLevel == SERVRE {
fmt.Println("打电话")
} else if notifyLevel == URGENT {
fmt.Println("发短信")
} else {
fmt.Println("发邮件")
}
return true
}
//检查类
type Alert struct {
alertRules *AlertRules
notification *Notification
}
func CreateAlert(a *AlertRules, n *Notification) *Alert {
return &Alert{
alertRules: a,
notification: n,
}
}
func (alert *Alert) Check(api string, tps int64, errCount int64) bool {
if tps > alert.alertRules.GetMaxTPS(api) {
alert.notification.Notify(URGENT)
}
if errCount > alert.alertRules.GetMaxError(api) {
alert.notification.Notify(SERVRE)
}
return true
}
func main() {
alert := CreateAlert(new(AlertRules), new(Notification))
alert.Check("test", 20, 20)
}
虽然程序比较简陋,但是是面向对象的,而且能跑。
对于这个需求,有很多可能的变动点,最可能变的是增加新的报警指标。现在新需求来了,如果每秒内接口超时量超过指定值,也需要报警,我们需要怎么做?
如果在原有代码上修改,我们需要
- AlertRules上添加新的规则
- Check函数增加新的入参timeoutCount
- Check函数中增加新的判断逻辑if timeoutCount > alert.alertRules.GetMaxTimeoutCount(api) {
alert.notification.Notify(SERVRE)
}
这会导致一些问题,一是Check可能在多个地方被引用,所以这些位置都需要进行修改,二是更改了Check逻辑,需要重新做这部分的测试。如果说我们做第一版没有预料到这些变化,但现在我们找到了可能的变更点,我们是否有好的方案能够做好扩展,让下次改动量最小?
我们把Alert中Check做的事情拆散,放到对应的类里,这些类都实现了AlertHandler接口。
//优化
type ApiStatInfo struct {
api string
tps int64
errCount int64
timeoutCount int64
}
type AlertHandler interface {
Check(apiStatInfo ApiStatInfo) bool
}
type TPSAlertHandler struct {
alertRules *AlertRules
notification *Notification
}
func CreateTPSAlertHandler(a *AlertRules, n *Notification) *TPSAlertHandler {
return &TPSAlertHandler{
alertRules: a,
notification: n,
}
}
func (tPSAlertHandler *TPSAlertHandler) Check(apiStatInfo ApiStatInfo) bool {
if apiStatInfo.tps > tPSAlertHandler.alertRules.GetMaxTPS(apiStatInfo.api) {
tPSAlertHandler.notification.Notify(URGENT)
}
return true
}
type ErrAlertHandler struct {
alertRules *AlertRules
notification *Notification
}
func CreateErrAlertHandler(a *AlertRules, n *Notification) *ErrAlertHandler {
return &ErrAlertHandler{
alertRules: a,
notification: n,
}
}
func (errAlertHandler *ErrAlertHandler) Check(apiStatInfo ApiStatInfo) bool {
if apiStatInfo.errCount > errAlertHandler.alertRules.GetMaxError(apiStatInfo.api) {
errAlertHandler.notification.Notify(SERVRE)
}
return true
}
type TimeOutAlertHandler struct {
alertRules *AlertRules
notification *Notification
}
func CreateTimeOutAlertHandler(a *AlertRules, n *Notification) *TimeOutAlertHandler {
return &TimeOutAlertHandler{
alertRules: a,
notification: n,
}
}
func (timeOutAlertHandler *TimeOutAlertHandler) Check(apiStatInfo ApiStatInfo) bool {
if apiStatInfo.timeoutCount > timeOutAlertHandler.alertRules.GetMaxTimeOut(apiStatInfo.api) {
timeOutAlertHandler.notification.Notify(SERVRE)
}
return true
}
Alert类增加成员变量handlers []AlertHandler,并添加如下函数
//版本2
func (alert *Alert) AddHanler(alertHandler AlertHandler) {
alert.handlers = append(alert.handlers, alertHandler)
}
func (alert *Alert) CheckNew(apiStatInfo ApiStatInfo) bool {
for _, h := range alert.handlers {
h.Check(apiStatInfo)
}
return true
}
调用方式如下:
func main() {
alert := CreateAlert(new(AlertRules), new(Notification))
alert.Check("test", 20, 20)
//版本2,alert其实已经不需要有成员变量AlertRules和Notification了
a := new(AlertRules)
n := new(Notification)
alert.AddHanler(CreateTPSAlertHandler(a, n))
alert.AddHanler(CreateErrAlertHandler(a, n))
alert.AddHanler(CreateTimeOutAlertHandler(a, n))
apiStatInfo := ApiStatInfo{
api: "test",
timeoutCount: 20,
errCount: 20,
tps: 20,
}
alert.CheckNew(apiStatInfo)
}
这样今后无论增加多少报警指标,只需要创建新的Handler类,放入到alert中即可。代码改动量极小,而且不需要重复测试。
系统还有许多改动点,大家可以自己尝试去改动一下,所有代码位置:https://github.com/shidawuhen/asap/blob/master/controller/design/3principle.go
里式替换原则
理解原则
里氏替换原则(LSP):子类对象能够替换程序(program)中父类对象出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
多态与里氏替换原则的区别:多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
实施
里式替换原则不仅仅是说子类可以替换父类,它有更深层的含义。
子类在设计的时候,要遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。这里的行为约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。所以我们可以通过几个点判断是否违反里氏替换原则:
- 子类违背父类声明要实现的功能:如排序函数,父类按照金额排序,子类按照时间排序
- 子类违背父类对输入、输出、异常的约定
- 子类违背父类注释中所罗列的任何特殊说明
实例
里氏替换原则可以提高代码可扩展性。假设我们需要做一个发送信息的功能,最初只需要发送站内信。
type Message struct {
}
func (message *Message) Send() {
fmt.Println("message send")
}
func LetDo(notify *Message) {
notify.Send()
}
func main() {
LetDo(new(Message))
}
实现完成后,许多地方都调用LetDo发送信息。后面想用SMS替换站内信,处理起来就很麻烦了。所以最好的方案是使用里氏替换原则,丝毫不影响新的通知方法接入。
//里氏替换原则
type Notify interface {
Send()
}
type Message struct {
}
func (message *Message) Send() {
fmt.Println("message send")
}
type SMS struct {
}
func (sms *SMS) Send() {
fmt.Println("sms send")
}
func LetDo(notify Notify) {
notify.Send()
}
func main() {
//里氏替换原则
LetDo(new(Message))
}
接口隔离原则
理解原则
接口隔离原则(ISP):客户端不应该强迫依赖它不需要的接口
接口隔离原则与单一职责原则的区别:单一职责原则针对的是模块、类、接口的设计。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
实施
如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。如果把“接口”理解为单个API接口或函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。如果把“接口”理解为OOP中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。
实例
假设项目用到三个外部系统:Redis、MySQL、Kafka。其中Redis和Kafaka支持配置热更新。MySQL和Redis有显示监控功能。对于这个需求,我们需要怎么设计接口?
一种方式是将所有功能放到一个接口中,另一种方式是将这两个功能放到不同的接口中。下面的代码按照接口隔离原则编写:
//接口隔离原则
type Updater interface {
Update() bool
}
type Shower interface {
Show() string
}
type RedisConfig struct {
}
func (redisConfig *RedisConfig) Connect() {
fmt.Println("I am Redis")
}
func (redisConfig *RedisConfig) Update() bool {
fmt.Println("Redis Update")
return true
}
func (redisConfig *RedisConfig) Show() string {
fmt.Println("Redis Show")
return "Redis Show"
}
type MySQLConfig struct {
}
func (mySQLConfig *MySQLConfig) Connect() {
fmt.Println("I am MySQL")
}
func (mySQLConfig *MySQLConfig) Show() string {
fmt.Println("MySQL Show")
return "MySQL Show"
}
type KafkaConfig struct {
}
func (kafkaConfig *KafkaConfig) Connect() {
fmt.Println("I am Kafka")
}
func (kafkaConfig *KafkaConfig) Update() bool {
fmt.Println("Kafka Update")
return true
}
func ScheduleUpdater(updater Updater) bool {
return updater.Update()
}
func ServerShow(shower Shower) string {
return shower.Show()
}
func main() {
//接口隔离原则
fmt.Println("接口隔离原则")
ScheduleUpdater(new(RedisConfig))
ScheduleUpdater(new(KafkaConfig))
ServerShow(new(RedisConfig))
ServerShow(new(MySQLConfig))
}
这种方案比起将Update和Show放在一个interface中有如下好处:
- 不需要做无用功。MySQL不需要写热更新函数,Kafka不需要写监控显示函数
- 复用性、扩展性好。如果接入新的系统,只需要监控显示函数,只需要实现Shower接口,就能复用ServerShow的功能。
依赖倒转原则
理解原则
依赖倒转原则(DIP):高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。
实施
在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。核心思想是:要面向接口编程,不要面向实现编程。
实践
这个可以直接用里式替换中的例子来讲解。LetDo就使用了依赖倒转原则,提高了代码的扩展性,可以灵活地替换依赖的类。
迪米特法则
理解原则
迪米特法则(LOD):不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口
实施
迪米特法则主要用来实现高内聚低耦合。
高内聚:就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中
松耦合:在代码中,类与类之间的依赖关系简单清晰
减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。
实践
假设我们要做一个搜索引擎爬取网页的功能,功能点为
- 发起请求
- 下载网页
- 分析网页
所以我们设置三个类NetworkTransporter负责底层网络、用于获取数据,HtmlDownloader下载网页,Document用于分析网页。下面是符合迪米特法则的代码
//迪米特法则
type Transporter interface {
Send(address string, data string) bool
}
type NetworkTransporter struct {
}
func (networkTransporter *NetworkTransporter) Send(address string, data string) bool {
fmt.Println("NetworkTransporter Send")
return true
}
type HtmlDownloader struct {
transPorter Transporter
}
func CreateHtmlDownloader(t Transporter) *HtmlDownloader {
return &HtmlDownloader{transPorter: t}
}
func (htmlDownloader *HtmlDownloader) DownloadHtml() string {
htmlDownloader.transPorter.Send("123", "test")
return "htmDownloader"
}
type Document struct {
html string
}
func (document *Document) SetHtml(html string) {
document.html = html
}
func (document *Document) Analyse() {
fmt.Println("document analyse " + document.html)
}
func main() {
//迪米特法则
fmt.Println("迪米特法则")
htmlDownloader := CreateHtmlDownloader(new(NetworkTransporter))
html := htmlDownloader.DownloadHtml()
doc := new(Document)
doc.SetHtml(html)
doc.Analyse()
}
这种写法可以对应迪米特法则的两部分
- 不该有直接依赖关系的类之间,不要有依赖。Document不需要依赖HtmlDownloader,Document作用是分析网页,怎么得到网页是不需要关心的。这样做的好处是无论HtmlDownloader怎么变动,Document都不需要关心。
- 有依赖关系的类之间,尽量只依赖必要的接口。HtmlDownloader下载网页必须依赖NetworkTransporter,此处使用接口是为将来如果有更好的底层网络功能,可以迅速替换。当然,此处有点过渡设计的感觉,主要为了契合一下迪米特法则。具体是否需要这么设计,还是根据具体情况来判断。
总结
终于写完了这6个原则,不过对我的好处也很明显,重新梳理知识结构,对原则的理解也更深了一步。宏观上看,这些原则都是为了实现可复用、可扩展、高内聚、低耦合的目的。现在大家在掌握了Go面向对象语法、如何做面向对象分析与设计、面向对象设计原则的基础上,可以做一些面向对象的事情了。
原则是道,设计模式是术,后面会写一些设计模式相关的内容。
本文所有代码位置为:https://github.com/shidawuhen/asap/blob/master/controller/design/3principle.go
资料
- 设计模式-golang实现之七大设计原则https://blog.csdn.net/liuyonglun/article/details/103768269
- 设计模式之美https://time.geekbang.org/column/intro/100039001
最后
大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)
我的个人博客为:https://shidawuhen.github.io/
技术
- Go设计模式(2)-面向对象分析与设计
- 支付接入常规问题
- HTTP2.0基础教程
- Go设计模式(1)
- MySQL开发规范
- HTTPS配置实战
- Go通道实现原理
- Go定时器实现原理
- HTTPS连接过程
- 限流实现2
- 秒杀系统
- 分布式系统与一致性协议
- 微服务之服务框架和注册中心
- Beego框架使用
- 浅谈微服务
- TCP性能优化
- 限流实现1
- Redis实现分布式锁
- Golang源码BUG追查
- 事务原子性、一致性、持久性的实现原理
- CDN请求过程详解
- 常用缓存技巧
- 如何高效对接第三方支付
- Gin框架简洁版
- InnoDB锁与事务简析
- 算法总结
读书笔记
- 原则
- 资治通鉴
- 敏捷革命
- 如何锻炼自己的记忆力
- 简单的逻辑学-读后感
- 热风-读后感
- 论语-读后感
- 孙子兵法-读后感
思考
- 服务端团队假期值班方案
- 项目流程管理
- 对项目管理的一些看法
- 对产品经理的一些思考
- 关于程序员职业发展的思考
- 关于代码review的思考
- Markdown编辑器推荐-typora
- 上一篇:项目流程管理(项目流程管理的作用)
- 下一篇:【源码分享】给你一个开源文库网站
相关推荐
- 团队管理“布阵术”:3招让你的团队战斗力爆表!
-
为何古代军队能够以一当十?为何现代企业有的团队高效似“特种部队”,有的却松散若“游击队”?**答案正隐匿于“布阵术”之中!**今时今日,让我们从古代兵法里萃取3个核心要义,助您塑造一支战斗力爆棚的...
- 知情人士回应字节大模型团队架构调整
-
【知情人士回应字节大模型团队架构调整】财联社2月21日电,针对原谷歌DeepMind副总裁吴永辉加入字节跳动后引发的团队调整问题,知情人士回应称:吴永辉博士主要负责AI基础研究探索工作,偏基础研究;A...
- 豆包大模型团队开源RLHF框架,训练吞吐量最高提升20倍
-
强化学习(RL)对大模型复杂推理能力提升有关键作用,但其复杂的计算流程对训练和部署也带来了巨大挑战。近日,字节跳动豆包大模型团队与香港大学联合提出HybridFlow。这是一个灵活高效的RL/RL...
- 创业团队如何设计股权架构及分配(创业团队如何设计股权架构及分配方案)
-
创业团队的股权架构设计,决定了公司在随后发展中呈现出的股权布局。如果最初的股权架构就存在先天不足,公司就很难顺利、稳定地成长起来。因此,创业之初,对股权设计应慎之又慎,避免留下巨大隐患和风险。两个人如...
- 消息称吴永辉入职后引发字节大模型团队架构大调整
-
2月21日,有消息称前谷歌大佬吴永辉加入字节跳动,并担任大模型团队Seed基础研究负责人后,引发了字节跳动大模型团队架构大调整。多名原本向朱文佳汇报的算法和技术负责人开始转向吴永辉汇报。简单来说,就是...
- 31页组织效能提升模型,经营管理团队搭建框架与权责定位
-
分享职场干货,提升能力!为职场精英打造个人知识体系,升职加薪!31页组织效能提升模型如何拿到分享的源文件:请您关注本头条号,然后私信本头条号“文米”2个字,按照操作流程,专人负责发送源文件给您。...
- 异形柱结构(异形柱结构技术规程)
-
下列关于混凝土异形柱结构设计的说法,其中何项正确?(A)混凝土异形柱框架结构可用于所有非抗震和抗震设防地区的一般居住建筑。(B)抗震设防烈度为6度时,对标准设防类(丙类)采用异形柱结构的建筑可不进行地...
- 职场干货:金字塔原理(金字塔原理实战篇)
-
金字塔原理的适用范围:金字塔原理适用于所有需要构建清晰逻辑框架的文章。第一篇:表达的逻辑。如何利用金字塔原理构建基本的金字塔结构受众(包括读者、听众、观众或学员)最容易理解的顺序:先了解主要的、抽象的...
- 底部剪力法(底部剪力法的基本原理)
-
某四层钢筋混凝土框架结构,计算简图如图1所示。抗震设防类别为丙类,抗震设防烈度为8度(0.2g),Ⅱ类场地,设计地震分组为第一组,第一自振周期T1=0.55s。一至四层的楼层侧向刚度依次为:K1=1...
- 结构等效重力荷载代表值(等效重力荷载系数)
-
某五层钢筋混凝土框架结构办公楼,房屋高度25.45m。抗震设防烈度8度,设防类别丙类,设计基本地震加速度0.2g,设计地震分组第二组,场地类别为Ⅱ类,混凝土强度等级C30。该结构平面和竖向均规则。假定...
- 体系结构已成昭告后世善莫大焉(体系构架是什么意思)
-
实践先行也理论已初步完成框架结构留余后人后世子孙俗话说前人栽树后人乘凉在夏商周大明大清民国共和前人栽树下吾之辈已完成结构体系又俗话说青出于蓝而胜于蓝各个时期任务不同吾辈探索框架结构体系经历有限肯定发展...
- 框架柱抗震构造要求(框架柱抗震设计)
-
某现浇钢筋混凝土框架-剪力墙结构高层办公楼,抗震设防烈度为8度(0.2g),场地类别为Ⅱ类,抗震等级:框架二级,剪力墙一级,混凝土强度等级:框架柱及剪力墙C50,框架梁及楼板C35,纵向钢筋及箍筋均采...
- 梁的刚度、挠度控制(钢梁挠度过大会引起什么原因)
-
某办公楼为现浇钢筋混凝土框架结构,r0=1.0,混凝土强度等级C35,纵向钢筋采用HRB400,箍筋采用HPB300。其二层(中间楼层)的局部平面图和次梁L-1的计算简图如图1~3(Z)所示,其中,K...
- 死要面子!有钱做大玻璃窗,却没有钱做“柱和梁”,不怕房塌吗?
-
活久见,有钱做2层落地大玻璃窗,却没有钱做“柱子和圈梁”,这样的农村自建房,安全吗?最近刷到个魔幻施工现场,如下图,这栋5开间的农村自建房,居然做了2个全景落地窗仔细观察,这2个落地窗还是飘窗,为了追...
- 不是承重墙,物业也不让拆?话说装修就一定要拆墙才行么
-
最近发现好多朋友装修时总想拆墙“爆改”空间,别以为只要避开承重墙就能随便砸!我家楼上邻居去年装修,拆了阳台矮墙想扩客厅,结果物业直接上门叫停。后来才知道,这种配重墙拆了会让阳台承重失衡,整栋楼都可能变...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- MVC框架 (46)
- spring框架 (46)
- 框架图 (58)
- bootstrap框架 (43)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- jpa框架 (47)
- laravel框架 (46)
- express框架 (43)
- scrapy框架 (52)
- beego框架 (42)
- java框架spring (43)
- grpc框架 (55)
- 前端框架bootstrap (42)
- orm框架有哪些 (43)
- ppt框架 (48)
- 内联框架 (52)
- winform框架 (46)
- gui框架 (44)
- cad怎么画框架 (58)
- ps怎么画框架 (47)
- ssm框架实现登录注册 (49)
- oracle字符串长度 (48)
- oracle提交事务 (47)