美文网首页
【设计模式】设计原则-SOLID、DRY、KISS、YAGNI、

【设计模式】设计原则-SOLID、DRY、KISS、YAGNI、

作者: ByteStefan | 来源:发表于2021-02-06 20:27 被阅读0次
    修改记录 修改时间 备注
    新建 2021.02.09 整理自极客时间-王争的设计模式之美(推荐购买学习)

    1. SOLID原则

    1.1 SRP(Single Responsibility Principle) 单一职责

    1.1.1 定义:一个类或模块只负责完成一个功能。

    理解:不要设计大而全的类,要设计粒度小、高性能单一的类。该原则的目的是为了实现代码高内聚、低耦合、提高代码复用性、可读性以及可维护性。

    1.1.2 以下场景可能会出现类没有指责单一:

    • 类中的代码行数、函数、属性是否过多。可以考虑对该类进行拆分;
    • 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想;
    • 私有方法过多,可以考虑将私有方法独立到新类中,设置为 public 方法,提高代码复用性;
    • 当发现类名比较难命名或类名笼统、冗长时,说明该类职责定义不够清晰;
    • 类中大量方法集中操作某几个属性时,可以考虑将这几个属性和方法拆分出去;

    补充:在保证单一职责时,要避免过分拆分,否则会降低内聚性,影响代码可维护性。

    1.1.3 举例:

    /**
    * 如果下面的用户信息类仅在一个场景中使用,则没有什么问题;
    * 如果后面用户的地址信息在其他模块中使用时,就可以将地址信息进行拆分。
    * 以及各个属性的操作方法都要进行聚合到一个类中,提高代码的维护性。
    */
    data class UserData(val userId:Long, 
                        val userName:String, 
                        val email:String,
                        val telephone:String,
                        val provinceOfAddress:String,
                        val cityOfAddress:String,
                        val regionOfAddress:String,
                        //.....其他属性
                       )
    

    1.2 OCP(Open Closed Principle) 开闭原则

    1.2.1 定义:(模块、类、方法)对拓展开放,对修改关闭。

    理解:对于新功能尽量通过拓展已有代码而非修改的方式完成。

    补充:在开发中不需要识别、预留所有拓展点,切勿过度设计。最合理的做法是,保证短期内、可确定的部分进行拓展设计。做常用的代码扩展性的方法:多态、依赖注入、基于接口开发,以及部分设计模式(装饰、策略、模板、责任链、状态等)

    1.2.2 举例

    /**
    * 基于接口开发。对于外部调用者,内部逻辑是无感知的,方便后面进行逻辑拓展,例如国内更新逻辑后面可能会支持跳转指定应用商店、H5链接等。
    */
    interface IUpgradeService{
      fun checkUpgrade(ctx:Activity)
    }
    
    abstract class BaseUpgradeService : IUpgradeService{
      override fun checkUpgrade(ctx:Activity){
        //网络请求
        //....
        //执行需要更新
        startUpgrade()
      }
      
      fun startUpgrade()
    }
    
    class CnUpgradeService : BaseUpgradeService{
      override fun startUpgrade(){
        //国内执行更新逻辑。例如应用内下载安装等
      }
    }
    
    class I18nUpgradeService : BaseUpgradeService{
      override fun startUpgrade(){
        //海外执行更新逻辑。例如跳转google play
      }
    }
    
    //实际执行Activity
    class MainActivity : AppCompactActivity{
      override fun onCreate(savedInstanceState : Bundle){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //执行更新逻辑
        ServiceLoader.instance.load(IUpgradeService::class.java).checkUpgrade(this)
      }
    }
    

    1.3 LSP(Liskov Substitution Principle) 里氏替换

    1.3.1 定义:子类对象能够替换程序中父类对象出现的任何地方,并保证原来程序的逻辑行为不变及正确性不被破坏。

    理解:在代码中可以用子类来替换父类,和多态类似,区别在于“里氏替换原则”是子类不能违背父类的协议,如父类要实现的功能、入参、出参、异常情况等。

    1.3.2 举例

    /**
    * 下面代码违反里氏替换原则。因为父类并没有对参数进行校验和抛异常,子类违背了父类的协议(入参判断、异常情况)。
    */
    class UpgradeService{
      fun checkUpgrade(ctx: Activity, appId:Int, channelId:Int){
        //... 检查逻辑
      }
    }
    
    class CnUpgradeService : UpgradeService{
      override fun checkUpgrade(ctx: Activity, appId:Int, channelId:Int){
        if(appId == 0 || channelId == 0){
          throw Exception(...);
        }
        //...国内检测逻辑
      }
    }
    

    1.4 ISO(Interface Segregation Principle) 接口隔离

    1.4.1 定义:客户端(接口调用者)不应该被强迫依赖它不需要的接口。

    理解:与“单一职责”类似,区别在于“单一职责”针对的是模块、类、接口的设计,“接口隔离”一方面更侧重于接口的设计,另一方面思考的角度不同。

    补充:这里的“接口”可以理解为:①一组API接口集合;②单个API接口或函数;③OOP中的接口概念

    1.4.2 举例

    • 一组API接口集合
    /**
    * 下面代码违背了接口隔离原则。
    * 因为后期新增的删除接口,对外所有服务都可以调用,非常容易导致误操作场景。
    * 在没有做鉴权时,建议将删除接口单独做一个接口服务,供特殊场景使用。
    */
    interface UserService{
      fun register(userName:String, password:String):Boolean
      fun login(userName:String, password:String):Boolean
      
      //后期新增了删除用户信息的接口
      fun deleteUserById(userId:Long):Boolean
    }
    
    • 单个API接口或函数
    enum class ComputeType{
        ADD,
        SUBTRACT, 
        MULTIPLY , 
        DIVIDE
    }
    
    /**
    * 假设下面代码每一种计算方式都比较复杂,则违背了接口隔离原则。
    * 如果逻辑复杂的情况下,建议将每种情况作为一个单独的接口或函数进行处理。
    * 例如:
    * fun dataAdd(){}
    * fun dataSubtract(){}
    */
    fun dataCompute(firstNum:Int, secondNum:Int, computeType:ComputeType): Int{
      retrun when(computeType){
        ComputeType.ADD -> //....
        //....
      }
    }
    
    • OOP中的接口概念
    /**
    * 尽量避免设计大而全的接口,大而全会导致强迫调用者依赖不必要的接口
    * 例如下面接口,如果调用者只是想配置监控和更新,还必须空实现配置日志数据。推荐根据功能进行拆分。
    */
    interface IConfig{
      //更新配置信息
      fun update()
      //配置日志输出
      fun outputLog():String
      //配置监控
      fun monitorConfig()
    }
    

    1.5 DIP(Dependency Inversion Principle) 依赖倒置/依赖反转

    1.5.1 定义:高层模块不依赖低层模块,它们共同依赖同一个抽象,抽象不要依赖具体实现细节,具体实现细节依赖抽象。

    理解:该原则用于指导框架层面的设计,调用者与被调用者没有直接依赖关系,而是通过一个抽象(规范)来建立关系,同时抽象(规范)不依赖具体的调用者和被调用者的实现细节,而调用者和被调用者需要依赖抽象(规范)。例如,暴露请求参数,由调用者来实现具体的请求,并将结果再返回。

    1.5.2 控制反转(IOC)、依赖反转(DIP)、依赖注入(DI)的区别与联系

    • 控制反转:提供一个可拓展的代码骨架,用来组装对象、管理整个执行流程。不是一种具体的实现技巧,而是一种设计思想,一般用于指导框架层面的设计,具体的方式有很多,例如依赖注入、模板模式等。
    abstract class TestCase{
      fun run(){
        if(doTest()){
          println("Test success")
        }else{
          println("Test failed")
        }
      }
      
      abstract fun doTest():Boolean
    }
    
    class UserServiceTest: TestCase{
      override doTest():Boolean{
        //....控制逻辑
      }
    }
    
    fun main(){
      UserServiceTest().run()
    }
    
    • 依赖注入:不通过 new()方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好后,通过构造函数、函数参数等方式传递(或注入)给类使用。
    //Notification类使用通过构造函数传入的类对象messageSender调用发送逻辑
    class Notification(val messageSender: MessageSender){
      fun sendMessage(cellphone: String, message: String){
        messageSender.send(cellphone, message)
      }
    }
    
    interface MessageSender{
      fun send(cellphone: String, message: String)
    }
    
    class SmsSender: MessgeSender{
      override fun send(cellphone: String, message: String){
        //...短信通知逻辑
      }
    }
    
    class EmailSender: MessageSender{
      override fun send(cellphone: String, message: String){
        //...邮件通知逻辑
      }
    }
    
    fun main(){
      val messageSender = SmsSender()
      val notification = Notification(messageSender)
      notification.sendMessage("xxxxx","xxxxx")
    }
    
    • 依赖反转:高层模块(调用者)不要依赖底层模块(被调用者代码)。高层模块和底层模块赢通过抽象来互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。
    //抽象层
    interface ISendTypeConfig{
        fun httpRequest(params: String)
        fun socketRequest(params: String)
    }
    //底层模块逻辑
    class SendTypeManager(private val config: ISendTypeConfig){
        fun sendMessage(sendByHttp:Boolean, params: String){
            if (sendByHttp){
                config.httpRequest(params)
                return
            }
            //使用socket进行消息发送
        }
    }
    
    //高层模块逻辑
    class SendTypeConfig: ISendTypeConfig{
        override fun httpRequest(params: String) {
            //使用http请求
        }
    
        override fun socketRequest(params: String) {
            //使用socket请求
        }
    
    }
    
    fun main(){
        //这段代码属于[底层模块]逻辑。高层模块只需关注消息发送方式的具体实现,然后调用底层模块的发送消息即可,不会关注底层模块的具体实现。
        SendTypeManager(SendTypeConfig()).sendMessage(true, "这是一条http发送的消息")
    }
    

    2. DRY(Don't Repeat Yourself)原则

    理解:不要开发重复代码,可以复用或提取公共代码,同时也要注意遵守“单一职责”和“接口隔离”原则。

    提升代码复用性的方法:

    • 减少代码耦合
    • 满足单一职责原则
    • 模块化
    • 业务与非业务逻辑分离
    • 通用代码下沉
    • 继承、抽象、多态、封装
    • 应用模板等设计模式

    3. KISS(Keep It Simple And Stupid)原则

    理解:尽量保证代码简洁,使用通用技术(同事都懂的技术)、不重复造轮子、不过度优化。

    举例:对于某个数值的提取或者匹配判断,使用正则表达式可以使代码行数更少,看似更简单,但其实并不是所有同事都熟悉正则表达式,而且在编写正则规则时易出现bug,所以可以采用通用技术来实现。

    4. YAGNI(You Aint't Gonna Need It)原则

    理解:不去设计与开发当前功能用不到的代码,但并不意味着不考虑拓展性,可以预留好拓展点,后面需要时再开发。

    举例:目前项目只对国内市场,未来将会面向国内海外同时使用。所以在开发中不需要提前编写海外部分代码,但是在国内海外有差异的逻辑上要预留好拓展点,方便后面对海外逻辑进行补充。

    5. LOD(Law of Demeter)原则/迪米特法则

    理解:不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。

    举例:

    /**
    * NetworkTransporter 类负责底层网络通信,根据请求获取数据。
    *
    * 该类的入参类型为 HtmlRequest 对象,作为底层类,应保证通用性,而不是仅服务于下载HTML。所以违反了迪米特法则,依赖了不该有直接依赖的 HtmlRequest 类。
    */
    public class NetworkTransporter {
        // 省略属性和其他方法...
        public Byte[] send(HtmlRequest htmlRequest) {
          //...
        }
    }
    
    public class HtmlDownloader {
      private NetworkTransporter transporter;//通过构造函数或IOC注入
      
      public Html downloadHtml(String url) {
        Byte[] rawHtml = transporter.send(new HtmlRequest(url));
        return new Html(rawHtml);
      }
    }
    
    /**
    * Document 表示网页文档,后续的网页内容抽取、分词、索引都是以此为处理对象。
    *
    * 该类总有如下3个问题:
    * 1. 构造函数中的 downloader.downloadHtml() 逻辑复杂,耗时长,不应该放到构造函数中,会影响代码的可测试性。
    * 2. HtmlDownloader 对象在构造函数中通过 new 来创建,违反了基于接口而非实现编程的设计思想,也会影响到代码的可测试性。
    * 3. 从业务含义上来讲,Document 网页文档没必要依赖 HtmlDownloader 类,违背了迪米特法则。
    */
    public class Document {
      private Html html;
      private String url;
      
      public Document(String url) {
        this.url = url;
        HtmlDownloader downloader = new HtmlDownloader();
        this.html = downloader.downloadHtml(url);
      }
      //...
    }
    

    相关文章

      网友评论

          本文标题:【设计模式】设计原则-SOLID、DRY、KISS、YAGNI、

          本文链接:https://www.haomeiwen.com/subject/ufgotltx.html