当前位置: 首页 > news >正文

学会这10个设计原则,离架构师又进了一步!!!

213df257944f9b2aeda7c0ce6ab01923.gif

做软件开发多年,CRUD仿佛已经形成一种惯性,深入骨髓,按照常规的结构拆分:表现层业务逻辑层数据持久层,一个功能只需要个把小时代码就撸完了。

再结合CTRL+CCTRL+V 绝世秘籍,一个个功能点便如同雨后春笋般被快速克隆实现。

是不是有种雄霸天下的感觉,管他什么业务场景,大爷我一梭到底,天下无敌!!!

3f88a4c5b6235df17a2ac2646ca37ad1.png

可现实真的是这样?

答案不言而喻!!!

初入软件行业,很多人都会经历这个阶段。时间久了,很多人便产生困惑,能力并没有随着工作年限得到同比提升,焦虑失眠,如何改变现状?

悟性高的人,很快能从一堆乱麻中找到线索,并不断的提升自己的能力。

什么能力?

当然是软件架构能力,一名优秀的软件架构师,要具备复杂的业务系统的吞吐设计能力抽象能力扩展能力稳定性

如何培养这样能力?

ef0cf272a8d57801b4b1b8c2b776fddd.png

我将常用的软件架构原则,做了汇总,目录如下:

5320fd66d3b7c50d9041c18703f159c0.png

当然这些原则有些是相互辅助,有些是相互矛盾的。实际项目开发中,要根据具体业务场景,灵活应对。千万不能教条主义,生搬硬套

单一职责

我们在编码的时候,为了省事,总是喜欢在一个类中添加各种各样的功能。未来业务迭代时,再不断的修改这个类,导致后续的维护成本很高,耦合性大。牵一发而动全身。

为了解决这个问题,我们在架构设计时通常会考虑单一职责

定义:

单一职责(SRP:Single Responsibility Principle),面向对象五个基本原则(SOLID)之一。每个功能只有一个职责,这样发生变化的原因也会只有一个。通过缩小职责范围,尽量减少错误的发生。

单一职责原则和一个类只干一件事之间,最大的差别就是,将变化纳入了考量。

代码要求:

一个接口、类、方法只负责一项职责,简单清晰。

优点:

降低了类的复杂度,提高类的可读性、可维护性。进而提升系统的可维护性,降低变更引起的风险

示例:

有一个用户服务接口UserService,提供了用户注册、登录、查询个人信息的方法,主要还是围绕用户相关的服务,看似合理。

public interface UserService{
    // 注册接口
    Object register(Object param);
    // 登录接口
    Object login(Object param);
    // 查询用户信息
    Object queryUserInfoById(Long uid);
}

过了几天,业务方提了一个需求,用户可以参加项目。简单的做法是在UserService类中增加一个joinProject()方法

又过了几天,业务方又提了一个需求,统计一个用户参加过多少个项目,我们是不是又在UserService类中增加一个countProject()方法。

这样导致的后果是,UserService类的职责越来越重,类会不断膨胀,内部的实现会越来越复杂。既要负责用户相关还有负责项目相关,后续任何一块业务变动,都会导致这个类的修改。

两类不同的需求,都改到同一个类。正确做法是,把不同的需求引起的变动拆分开,单独构建一个ProjectService类,专门负责项目相关的功能

public interface ProjectService{
    // 加入一个项目
    void addProject (Object param);
    // 统计一个用户参加过多少个项目
    void countProject(Object param);
}

这样带来的好处是,用户相关的需求只要改动UserService。如果是项目管理的需求,只需要改动ProjectService。二者各自变动的理由就少了很多。

开闭原则

开闭原则(OCP:Open-Closed Principle),主要指一个类、方法、模块 等 对扩展开放,对修改关闭。简单来讲,一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。

个人感觉,开闭原则在所有的原则中最重要,像我们耳熟能详的23种设计模式,大部分都是遵循开闭原则,来解决代码的扩展性问题。

实现思路:

采用抽象构建框架主体,用实现扩展细节。不同的业务采用不用的子类,尽量避免修改已有代码。

优点:

  • 可复用性好。在软件完成以后,仍然可以对软件进行扩展,加入新的功能,非常灵活。因此,这个软件系统就可以通过不断地增加新的组件,来满足不断变化的需求。

  • 可维护性好。它的底层抽象相对固定,不用担心软件系统中原有组件的稳定性,这就使变化中的软件系统有一定的稳定性和延续性。

示例:

比如有这样一个业务场景,我们的电商支付平台,需要接入一些支付渠道,项目刚启动时由于时间紧张,我们只接入微信支付,那么我们的代码这样写:

class WeixinPay {
    public Object pay(Object requestParam) {
        // 请求微信完成支付
        // 省略。。。。
        return new Object();
    }
}

随着业务扩展,后期开始逐步接入一些其他的支付渠道,比如支付宝云闪付红包支付零钱包支付积分支付等,要如何迭代?

class PayGateway {
    public Object pay(Object requestParam) {

        if(微信支付){
            // 请求微信完成支付
            // 省略。。。。
        }esle if(支付宝){
            // 请求支付宝完成支付
            // 省略。。。。
        }esle if(云闪付){
            // 请求云闪付完成支付
            // 省略。。。。
        }
         // 其他,不同渠道的个性化参数的抽取,转换,适配
         // 可能有些渠道一次支付需要多次接口请求,获取一些前置准备参数
         // 省略。。。。
        return new Object();
    }
}

所有的业务逻辑都集中到一个方法中,每一个支付渠道本身的业务逻辑又相当复杂,随着更多支付渠道的接入,pay方法中的代码逻辑会越来越重,维护性只会越来越差。每一次改动都要回归测试所有的支付渠道,劳民伤财。那么有没有什么好的设计原则,来解决这个问题。我们可以尝试按开闭原则重新编排代码

首先定义一个支付渠道的抽象接口类,把所有的支付渠道的骨架抽象出来。设计一系列的插入点,并对若干插入点流程关联。

关于插入点,用过OpenResty的同学都知道,通过set_by_lua、rewrite_by_lua、body_filter_by_lua 等不同阶段来处理请求在对应阶段的逻辑,有效的避免各种衍生问题。

abstract class AbstractPayChannel {
    public Object pay(Object requestParam) {
        // 抽象方法
    }
}

逐个实现不同支付渠道的子类,如:AliayPayChannelWeixinPayChannel,每个渠道都是独立的,后期如果做渠道升级维护,只需修改对应的子类即可,降低修改代码的影响面。

class AliayPayChannel extends  AbstractPayChannel{
    public Object pay(Object requestParam) {
        // 根据请求参数,如果选择支付宝支付,处理后续流程
        // 支付宝处理
    }
}
class WeixinPayChannel extends  AbstractPayChannel{
    public Object pay(Object requestParam) {
        // 根据请求参数,如果选择微信支付,处理后续流程
        // 微信处理
    }
}

总调度入口,遍历所有的支付渠道,根据requestParam里的参数,判断当前渠道是否处理本次请求。

当然,也有可能采用组合支付的方式,比如,红包支付+微信支付,可以通过上下文参数,传递一些中间态的数据。

class PayGateway {

    List<AbstractPayChannel> payChannelList;

    public Object pay(Object requestParam) {
        for(AbstractPayChannel channel:payChannelList){
            channel.pay(requestParam);
        }
    }
}

里氏替换

里氏替换原则(LSP:Liskov Substitution Principle):所有引用基类的地方必须能透明地使用其子类的对象

简单来讲,子类可以扩展父类的功能,但不能改变父类原有的功能(如:不能改变父类的入参,返回),跟面向对象编程的多态性类似。

多态是面向对象编程语言的一种语法,是一种代码实现的思路。而里氏替换是一种设计原则,是用来指导继承关系中子类如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。

实现思路:

  • 子类可以实现父类的抽象方法

  • 子类中可以增加自己特有的方法。

  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。

  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

接口隔离

接口隔离原则(ISP:Interface Segregation Principle)要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含调用方感兴趣的方法,而不应该强迫调用方依赖它不需要的接口。

实现思路:

  • 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。

  • 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。

  • 结合业务,因地制宜。每个项目或产品都有特定的环境因素,环境不同,接口拆分的标准就不同,需要我们有较强的业务 sense

  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

示例:

用户中心封装了一套UserService接口,给上层调用(业务端以及管理后台)提供用户基础服务。

public interface UserService{
    // 注册接口
    Object register(Object param);
    // 登录接口
    Object login(Object param);
    // 查询用户信息
    Object queryUserInfoById(Long uid);
}

但随着业务衍化,我们需要提供一个删除用户功能,常规的做法是直接在UserService接口中增加一个deleteById方法,比较简单。

但这样会带来一个安全隐患,如果该方法被普通权限的业务方误调用,容易导致误删用户,引发灾难。

如何避免这个问题,我们可以采用接口隔离的原则

定义一个全新的接口服务,并提供deleteById方法,BopsUserService接口只提供给Bops管理后台系统使用。

public interface BopsUserService{
    // 删除用户
    Object deleteById(Long uid);
}

总结一下,在设计微服务接口时,如果其中一些方法只限于部分调用者使用,我们可以将其拆分出来,独立封装,而不是强迫所有的调用方都能看到它。

依赖倒置

软件设计中的细节具有多变性,但是抽象相对稳定,为了利用好这个特性,我们引入了依赖倒置原则。

依赖倒置原则(DIP:Dependence Inversion Principle):高层模块不应直接依赖低层模块,二者应依赖于抽象;抽象不应该依赖实现细节;而实现细节应该依赖于抽象。

依赖倒置原则的主要思想是要面向接口编程,不要面向具体实现编程。

示例:

定义一个消息发送接口MessageSender,具体的实例Bean注入到Handler,触发完成消息的发送。

interface MessageSender {
  void send(Message message);
}

class Handler {

  @Resource
  private MessageSender sender;
  
  void execute() {
     sender.send(message);
  }
}

假如消息的发送采用Kafka消息中间件,我们需要定义一个KafkaMessageSender实现类来实现具体的发送逻辑。

class KafkaMessageSender implements MessageSender {
  private KafkaProducer producer;
  
  public void send(final Message message) {
     producer.send(new KafkaRecord<>("topic", message));
  }
}

这样实现的好处,将高层模块与低层实现解耦开来。假如,后期公司升级消息中间件框架,采用Pulsar,我们只需要定义一个PulsarMessageSender类即可,借助Spring容器的@Resource会自动将其Bean实例依赖注入。

优点:

  • 降低类间的耦合性

  • 提高系统的稳定性

  • 降低并行开发引起的风险

  • 提高代码的可读性和可维护性

最后,要玩溜依赖倒置原则,必须要熟悉控制反转依赖注入,如果你是java后端,这两个词语你一定不陌生,Spring框架核心设计就是依赖这两个原则。

简单原则

复杂系统的终极架构思路就是化繁为简,此简单非彼简单,简单意味着灵活性的无限扩展,接下来我们来了解下这个简单原则。

简单原则(KISS:Keep It Simple and Stupid)。翻译过来,保持简单,保持愚蠢。

我们深入剖析下这个 “简单”:

1、简单不等于简单设计或简单编程。软件开发中,为了赶时间进度,很多技术方案简化甚至没有技术方案,认为后面再找时间重构,编码时,风格随意,追求本次项目快速落地,导致欠下一大堆技术债。长此以往,项目维护成本越来越高。

保持简单并不是只能做简单设计或简单编程,而是做设计或编程时要努力以最终产出简单为目标,过程可能非常复杂也没关系。

2、简单不等于数量少。这两者没有必然联系,代码行少或者引入不熟悉的开源框架,看似简单,但可能引入更复杂的问题。

如何写出“简单”的代码?

  • 不要长期进行打补丁式的编码

  • 不要炫耀编程技巧

  • 不要简单编程

  • 不要过早优化

  • 要定期做 Code Review

  • 要选择合适的编码规范

  • 要适时重构

  • 要有目标地逐渐优化

最少原则

最少原则也称迪米特法则(LoD:Law of Demeter)。迪米特法则定义只与你的直接朋友交谈,不跟“陌生人”说话。

如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。

核心思路:

  • 一个类只应该与它直接相关的类通信

  • 每一个类应该知道自己需要的最少知识

示例:

现在的软件采用分层架构,比如常见的Web --> Service --> Dao 三层结构。如果中间的Service层没有什么业务逻辑,但是按照迪米特法则保持层之间的密切联系,也要定义一个类,纯粹用于Web层Dao层之间的调用转发。

这样传递效率势必低下,而且存在大量代码冗余。面对此问题,我们需灵活应对,早期可以允许Web层直接调用Dao。后面随着业务复杂度的提高,我们可以慢慢将Controller中的重业务逻辑收拢沉淀到Service层中。随着架构的衍化,清晰的分层开始慢慢沉淀下来。

写在最后,迪米特法则关心局部简化,这样很容易忽视整体的简化。

表达原则

代码的可维护性也是考验工程师能力的一个重要标准。试问一个人写的代码,每次code review时都是一堆问题,你会觉得他靠谱吗?

这时候我们就需要引入一个表达原则

表达原则(Program Intently and Expressively,简称 PIE),起源于敏捷编程,是指编程时应该有清晰的编程意图,并通过代码明确地表达出来。

表达原则的核心思想:代码即文档,通过代码清晰地表达我们的真实意图。

那么如何提高代码的可读性?

1、优化代码表现形式

无论是变量名、类名还是方法名,要命名合理,要能清晰准确的表达含义。再配合一定的中文注释,基本不用看设计文档就能快速的熟悉项目代码,理解原作者的意图。

2、改进控制流和逻辑

控制嵌套代码的深度,比如if else的深度最好不要超多三层。外层最好提前做否定式判断,提前终止操作或返回。这样的代码逻辑清晰。下面示例便是正确的处理:

public List<User> getStudents(int uid) {
    List<User> result = new ArrayList<>();
    User user = getUserByUid(uid);
    if (null == user) {
        System.out.println("获取员工信息失败");
        return result;
    }
    
    Manager manager = user.getManager();
    if (null == manager) {
        System.out.println("获取领导信息失败");
        return result;
    }

    List<User> users = manager.getUsers();
    if (null == users || users.size() == 0) {
        System.out.println("获取员工列表失败");
        return result;
    }

    for (User user1 : users) {
        if (user1.getAge() > 35 && "MALE".equals(user1.getSex())) {
            result.add(user1);
        }
    }
    return result;
}

分离原则

天下大事,分久必合合久必分。面对复杂的问题,考虑人脑的处理能力有限,有效的解决方案,就是大事化小,小事化了,将复杂问题拆分为若干个小问题,通过解决小问题进而解决大问题。

分离的核心思路:

1、架构视角

结合业务场景对整个系统内若干组件进行边界划分,如,层与层(MVC)、模块与模块、服务与服务等。像现在流行的DDD领域驱动设计指导的微服务就是一种很好的拆解方式,通过水平分离的策略达到服务与服务之间的分离。

4ac14ca7dd59625f4e961eb6187c6a5e.png

架构设计视角下的关注点分离更重视组件之间的分离,并通过一定的通信策略来保证架构内各个组件间的相互引用。

2、编码视角

编码视角主要侧重于某个具体类或方法间的边界划分。比如Stream流的filtermaplimit,数据集在不同阶段按照不同的逻辑处理,并将输出内容作为下一个方法的输入,当所有的流程处理完后,最后汇总结果。

一些不错分层案例:

1、MVC模型

2、网络 OSI 七层模型

一个好的架构一定具有不错的分层,各层之间通过定义好的规范通讯 ,一旦系统中的某一部分发生了改变,并不会影响其他部分(前提,系统容错做的足够好)。

契约原则

天下事无规矩不成方圆,软件架构也是一样道理。动辄千日的大项目,如何分工协作,保证大家的工作能有条不紊的向前推进,靠的就是契约原则。

契约式原则(DbC:Design by Contract)。软件设计时应该为软件组件定义一种精确和可验证的接口规范,这种规范要包括使用的预置条件、后置条件和不变条件,用来扩展普通抽象数据类型的定义。

契约原则关注重点:

  • API 必须要保证输入是接收者期望的输入参数

  • API 必须要保证输出结果的正确性

  • API 必须要保持处理过程中的一致性。如果一个API被二次修改后,整个集群的服务器都要重新部署,保证服务能力状态的一致。

如何做好 API 接口设计?

1、接口职责分离。设计 API 的时候,应该尽量让每一个 API 只做一个职责的事情,保证API的简单和稳定性。避免相互干扰。

2、 API 命名。通过命名基本能猜出接口的功能,另外尽量使用小写英文

3、接口具有幂等性。当一个操作执行多次所产生的影响与一次执行的影响相同

4、安全策略。如果API是外部使用,要考虑黑客攻击、接口滥用,比如采用限流策略。

5、版本管理。API发布后不可能一成不变,很可能因为升级导致新、旧版本的兼容性问题,解决办法就是对API 进行版本控制和管理。

推荐阅读

ed8eb758238d9ca12c7c94879b5338c2.png

01

编程原则

来自代码大师Max Kanat-Alexander的建议

ee05100846415f20a541a08a28eecc95.png

作者:[美]马克斯·卡纳特-亚历山大(Max Kanat-Alexander)

译者:李光毅

编程大师向你展示如何让简约设计的思想回归到计算机编程中

推荐语:在本书中,富有传奇色彩的编程大师马克斯·卡纳特-亚历山大(Max Kanat-Alexander)将会向你展示如何让简约设计的思想回归到计算机编程中。马克斯会解释程序员为何会感到力不从心,以及应该如何持续改善。世界上存在太多复杂的事物。复杂并不可取,因为它会给我们的工作带来隐患。

马克斯从他久负盛名的技术博客CodeSimplicity中精选了一部分文章,对如何在软件行业工作以及取得成功给出了自己的想法和建议。相信这43篇文章能够让你学会如何在工作中避免复杂,拥抱简约,从而让你的职业生涯更加顺利和成功。

d159e50102e94492bb1acb8019593519.png

02

计算机系统解密

从理解计算机到编写高效代码    

c646e8072cdd35582a994188301858e0.png

作者:[美]乔纳森·E.斯坦哈特(Jonathan E. Steinhart )

译者:张开元、张淼

计算机程序硬件软件从底层实现到高层展现原理讲解

对底层知识的多个主题进行了公平的覆盖

推荐语:计算机编程不是抽象的,程序是在机器上运行的。了解计算机如何工作以及程序如何在计算机上运行是成为一名更好的程序员的必要条件。在本书中,资深工程师Jonathan E. Steinhart深入探讨了计算机背后的基础概念,比如计算机硬件,软件在硬件上的行为,如何编写高效的程序,计算机安全基础知识,以及在编写代码时需要考虑的现实问题。本书对底层知识的多个主题进行了公平的覆盖——介绍有助于提高整个系统质量的许多领域的知识(包括计算机硬件、组合逻辑、时序逻辑、计算机体系结构、计算机组成原理、操作系统、系统程序设计等)。

fdd81d061a779c337ad792914a3dd870.png

03

 深入理解计算机系统(原书第3版)

7098e5d5094fc0b587512699228041bd.png

作者:[美] 兰德尔 E.布莱恩特(Randal E. Bryant)

大卫 R. 奥哈拉伦(David R. O'Hallaron)

译者:龚奕利 贺莲

将所有计算机系统相关知识融会贯通,助你成为凤毛麟角的高级程序员的必备神书。如果你研究和领会了这本书里的概念,你将开始成为极少数的“牛人”!

推荐语:本书是一本将计算机软件和硬件理论结合讲述的经典教程,内容覆盖计算机导论、体系结构和处理器设计等多门课程。卡内基-梅隆大学、北京大学、上海交大等国内外众多知名高校选用指定教材。本书的最大优点是为程序员描述计算机系统的实现细节,通过描述程序是如何映射到系统上,以及程序是如何执行的,使读者更好地理解程序的行为,以及造成效率低下的原因。从程序员的角度来学习计算机系统是如何工作的会非常有趣。最理想的学习方法是在真正的系统上解决具体的问题,或是编写和运行程序。这个主题观念贯穿本书始终。

3d4205b64c6a1757eb527026523f75c2.gif

d79ec1a9072f89c835f873585881f085.png

扫码关注【华章计算机】视频号

每天来听华章哥讲书

2e72d88596644ea98fa043505891247a.gif

更多精彩回顾

书讯 | 1月书讯(下)| 2022年的第一本书

书讯 | 1月书讯(上)| 2022年的第一本书

资讯 | 重磅!达摩院发布2022十大科技趋势

书单 | 6本书,读懂2022年最火的边缘计算

干货 | Flink1.14.2发布,除了log4j漏洞你还需要关注什么?

收藏 | Docker冲顶技术热词,微服务应用热度不减,中国云原生开发者真实现状如何?

上新 | 【新书速递】金融领域可解释机器学习模型与实践

赠书 | 【第88期】这10本硬核技术书,带你读懂5G、物联网和边缘计算,玩转元宇宙

87c9a0553b29a1c7e29069025ee84342.gif

相关文章:

  • Electron开发者该如何提升自己的技能水平
  • 终于有人把ROS机器人操作系统讲明白了
  • 一文看懂——序列数据的生成:GAN的方法
  • “三行代码,确实需要耗上一整天!”
  • GraalVM下一代JVM到底是什么?
  • 【第89期】推荐几本电商必读书
  • 一文带你了解LoongArch自主指令系统
  • 2021年数据中台行业十大关键词
  • 测试工程师的未来发展方向在哪里?
  • 一个案例讲明白!如何更安全地实现数据备份和恢复
  • 省政协委员、南京大学人工智能学院院长周志华: 科研学习探索最重要的是“兴趣”和“勤奋”...
  • 为什么现在还有985高校给大一上C语言课?
  • 如何用数字化构建企业的“韧性”?
  • 前端应用和产品逻辑的核心:交互流
  • 2月书讯 (上)| 新年到,新书到!
  • 08.Android之View事件问题
  • 10个确保微服务与容器安全的最佳实践
  • Making An Indicator With Pure CSS
  • SQLServer之索引简介
  • Traffic-Sign Detection and Classification in the Wild 论文笔记
  • 从零开始学习部署
  • 大主子表关联的性能优化方法
  • 道格拉斯-普克 抽稀算法 附javascript实现
  • 给自己的博客网站加上酷炫的初音未来音乐游戏?
  • 日剧·日综资源集合(建议收藏)
  • 数据仓库的几种建模方法
  • 算法之不定期更新(一)(2018-04-12)
  • 网络应用优化——时延与带宽
  • 因为阿里,他们成了“杭漂”
  • 应用生命周期终极 DevOps 工具包
  • 你对linux中grep命令知道多少?
  • gunicorn工作原理
  • Nginx实现动静分离
  • 资深实践篇 | 基于Kubernetes 1.61的Kubernetes Scheduler 调度详解 ...
  • $var=htmlencode(“‘);alert(‘2“); 的个人理解
  • (PWM呼吸灯)合泰开发板HT66F2390-----点灯大师
  • (ZT)一个美国文科博士的YardLife
  • (附源码)计算机毕业设计SSM在线影视购票系统
  • (五)c52学习之旅-静态数码管
  • (转) ns2/nam与nam实现相关的文件
  • (转)jdk与jre的区别
  • **登录+JWT+异常处理+拦截器+ThreadLocal-开发思想与代码实现**
  • ./configure,make,make install的作用(转)
  • .bat批处理(四):路径相关%cd%和%~dp0的区别
  • .dat文件写入byte类型数组_用Python从Abaqus导出txt、dat数据
  • .NET CORE 2.0发布后没有 VIEWS视图页面文件
  • .NET Core、DNX、DNU、DNVM、MVC6学习资料
  • .NET MVC、 WebAPI、 WebService【ws】、NVVM、WCF、Remoting
  • .NET 发展历程
  • .Net 知识杂记
  • .Net高阶异常处理第二篇~~ dump进阶之MiniDumpWriter
  • .NET微信公众号开发-2.0创建自定义菜单
  • .sdf和.msp文件读取
  • /etc/shadow字段详解
  • @Not - Empty-Null-Blank