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

修改记录 修改时间 备注
新建 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);
  }
  //...
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容

  • SOLID原则是指: Simple Responsibility Principle,SRP单一职责原则 Open...
    读行笔记阅读 490评论 0 2
  • SOLID 原则,并非单纯的 1 个原则,而是由 5 个设计原则组成的,它们分别是:单一职责原则、开闭原则、里式替...
    兮兮码字的地方阅读 724评论 0 4
  • 单一职责原则 单一职责原则(Single Responsibility Principle),缩写为 SRP。这个...
    _1633_阅读 342评论 0 0
  • SOLID 分别为: 单一职责原则 开闭原则 里氏替换原则 接口隔离原则 依赖倒置原则 单一职责原则 1. 如何理...
    西山薄凉阅读 250评论 0 0
  • 单一职责原则Single Responsibility Principle 一个类只负责完成一个职责或者功能。 不...
    karee000阅读 247评论 0 0