彻底搞懂面向对象7大设计原则(附真实场景案例)

说明:本文有点长,但是只要看完肯定会有收获,建议收藏慢慢看。

为什么要学习设计模式和设计原则

如果给你一堆(30 件)衣服,让你放在一个柜子里,你会怎么放?以下是 A 和 B 的做法。
A:直接全部丢在柜子里。“好快啊,我的活儿干完了”。并且对 B 说:“你真慢”。
B:分门别类,将春夏秋冬的衣服分别整理,然后将不同颜色的衣服分别叠在一起。

这时候 A 的老公说:“老婆,你可以在 30 秒之内把我夏天经常穿的那件白裤子拿出来吗?”。A 傻眼儿了:“臣妾做不到啊。”

设计模式和设计原则是一种思想。其目的是面对经常变更的需求,怎样规整、构建代码,使其更容易拓展和维护。它的重点并不是能够减少多少代码量,反而对于有些场景还有可能增加代码量。当然如果设计合理,业务复杂,大部分情况下是可以减少很多代码量的。所以减少代码并不是重点,重点是如何让你的代码写得更有拓展性和维护性。

比如以上案例中如果这些衣服是直接丢掉,以后再也不用了。以上案例中的 B 也不需要分门别类去整理了。但是很显然,这些衣服是要重复使用的。就行我们的业务需求一样,会经常变更。

设计模式和设计原则就是引导你怎样写出高内聚、低耦合,更高的复用性,维护性,灵活性,拓展性的代码,并且让你的代码结构更清晰,更方便看出业务之间的逻辑关系。不管是 Java 还是 Javascript 都是面向对象编程。在构建和设计程序的时候,本着高内低耦的宗旨应当遵循以下7大设计原则。

说明一下:其实在程序界,函数、方法、类都是一个概念,都是用来实现功能的,只是在不同的语言里面叫法不一样而已。

本文大纲内容

  1. 介绍这7种设计原则的概念。
  2. 用一些通俗易懂的说明来帮助理解概念。
  3. 通过一个案例来深入对这 7 种设计原则的理解。

开闭原则

概念:开闭原则是当需求发生改变时,对拓展开放,对修改关闭。

单一职责原则

概念:单一职责很好理解。尽量保证每个功能的颗粒度最小,那么它能实现复用的可能性就更大。如果一个功能模块现在或者将来(应对需求变更)需要由多个功能组成。就应该将每个小功能作为单独的函数,再进行组合。

理解:比如很多公司都会将行政和人事这两个职责分开。行政专门负责办公物资采购,办公环境清洁等方面的工作。人事专门负责招聘、培训、绩效等与人力资源相关的工作。

依赖倒置原则

概念:依赖倒置原则是通过面向接口编程来降低耦合度。抽象不应该依赖具体实现。这段话对于 Java 或者任何面向接口编程的开发者很好理解。但是对于写 JS 的人来说,可能不那么好理解。其实它的目的就是从抽象层面去保证系统(功能模块)的稳定性。而具体的实现则通过另一个类(JS 里面就是一个单独的函数)去完成。那什么叫从抽象层面保证功能模块的稳定性昵?以下案例会有说明。

理解:一个公司的经营状况不会依赖于一个底层员工。但是底层员工的薪水、福利待遇却依赖于公司的经营状况。

合成复用原则

概念:合成复用原则指的是可以通过组合的方法实现复用的情况下,应当优先考虑组合,其次才是继承。也可以理解为是有些功能可以组合到一起,就不用分不同情况写很多独立的方法。以下案例会有说明。

接口隔离原则

概念:接口隔离原则其实跟单一职责原则很像,区别在于它们的颗粒度。单一职责是一个功能一个方法。而接口是一个包含相关功能的多个方法的集合。不同的功能要独立成不同的方法,那么一系列相关功能的集合与另一系列相关功能的集合也需要相互独立。

理解:假设你的午餐要吃苹果,梨,青菜,土豆。你可以定义一个接口为水果,包含苹果,梨子。一个接口叫蔬菜,包含青菜,土豆。这样你就为你的午餐设立了两个相互隔离独立的接口。而不是一个庞大的午餐接口。如果定义一个庞大的接口,假设你明天午餐没有水果了,这个接口就没法用了。以下也会有实际场景的案例说明。

里氏替换原则

概念:里氏替换原则其实是一种更高层面的抽象。其目的也是为了使父类更稳定,子类尽量去拓展父类的功能而不是重写。避免发生错误。以下案例会有说明。

理解:麻雀和企鹅都属于鸟类,但是企鹅不能飞。如果你要设计一个计算飞行时间的鸟类,企鹅是不能够直接继承去修改飞行时间的。所以你可以设计一个更抽象的动物类。让鸟类和企鹅继承这个动物类,麻雀继承鸟类。

迪米特法则

概念:迪米特法则是为了降低 A 类与 B 类之间的耦合关系,使其更加独立。从而设计一个与 A 类和 B 类都有关联的 C 类来处理 A 类与 B 类之间的逻辑关系。

理解:迪米特法则是代理和中介的一种体现,通过 nginx 来转发服务就是一种迪米特法则。在现实生活中,明星与粉丝/媒体公司之间往往有一个经纪人角色,明星只需要演好戏,唱好歌就行。与签约媒体公司和与粉丝之间的微博互动等都是由经纪人去衔接。再比如领导与员工之间的助理角色。有些新员工对于行政上的疑问可以直接找助理,而不是事事都找领导。领导给员工分配任务也可以由助理来组织和传达。

案例

用一个案例来说明什么叫【开闭原则】【单一职责原则】【依赖倒置原则】【合成复用原则】【接口隔离原则】【里氏替换原则】【迪米特法则】。

场景1
产品经理说:“小王啊,你能帮我实现一个这样的功能吗?”。如下图实现一个改变任务状态功能(点击任务出现下划线表示完成, 再次点击去掉下划线表示未完成)。
在这里插入图片描述
你可以这样写代码

    function init() {
        $("#content").on('click','li',function(){
             if($(this).hasClass("done")){
                 $(this).removeClass("done");
                 $(this).find("input").prop("checked",false)
             }else {
                 $(this).addClass("done");
                 $(this).find("input").prop("checked",'checked')
             }
        })
    }
    init();

从实现功能的角度,这样写没问题。

场景2
过了 2 天,产品经理又找你了,说道: “小王,能不能在完成任务的时候给用户弹个框嘉奖两句。”

你当然也可以这样写

    function init() {
        $("#content").on('click','li',function(){
             if($(this).hasClass("done")){
                 $(this).removeClass("done");
                 $(this).find("input").prop("checked",false);
                 alert("加油哦,你的【"+$(this).find("span").html()+"】任务没有完成");
             }else {
                 $(this).addClass("done");
                 $(this).find("input").prop("checked",'checked');
                alert("真棒,你已经完成了【"+$(this).find("span").html()+"】这个任务");
             }
        })
    }
    init();

场景3
过了一周,产品经理又找上你,“小王,小王,你能不能…”;

如果是跟某条记录相关的功能,你当然也可以继续在 li 的监听事件上去累加代码。但是随着功能的增加,监听事件里面会有非常多的代码。如果这时候想要修改或者删除某个功能,你就得在臃肿的代码里找到相关功能再去修改。时间一久,你根本就不记得那个功能写在那个位置。

开闭原则】:上面案例中产品经理后来增加了弹框功能,所以在最开始设计功能的时候就应该把修改任务状态功能封装到一个方法里面。那么增加了弹框需求之后就只需要再拓展一个弹框方法。

   function init() {
        $("#content").on('click','li',function(){
             changeStatus($(this));
             Toast($(this)) 
       };
   }
   init();
  function changeStatus(it) {
        if (it.hasClass("done")) {
             it.removeClass("done");
             it.find("input").prop("checked", false);
        } else {
             it.addClass("done");
             it.find("input").prop("checked", 'checked');
        }
    }
  function Toast(it) {
        if (it.hasClass("done")) {
             $("#trigger").removeClass("hide");
             $("#alert").html("加油哦,你的【" + it.find("span").html() + "】任务没有完成");
        } else {
             $("#trigger").removeClass("hide");
             $("#alert").html("真棒,你已经完成了【" + it.find("span").html() + "】这个任务");
        }
    }

单一职责原则】:每个功能都应该单独封装在一个独立的函数里面,方便组合复用。比如上面案例中应该将
已完成任务封装在一个独立的函数 alreadyDone
未完成任务封装在一个独立的函数 notDone
已完成任务弹框封装在一个独立的函数 alreadyDoneAlert
未完成任务弹框封装在一个独立的函数 notDoneAlert

    function alreadyDone(it) {
        it.addClass("done");
        it.find("input").prop("checked",'checked');
    }
    function notDone(it) {
	    it.removeClass("done");
	    it.find("input").prop("checked",false);
    }
    function alreadyDoneAlert(it) {
         $("#trigger").removeClass("hide");
         $("#alert").html("真棒,你已经完成了【" + it.find("span").html() + "】这个任务");
    }
    function notDoneAlert(it) {
	     $("#trigger").removeClass("hide");
	     $("#alert").html("加油哦,你的【"+it.find("span").html()+"】任务没有完成");      
    }

合成复用原则】:有些功能可以合成到一起,就不用分不同情况写太多颗粒函数。比如上面案例中对于已完成和未完成弹框。假设又多了一种“待定”的任务状态,就需要再写一个待定弹框。其实这几个弹框状态的不同之处只是一句话而已,我们完全可以通过传参的方式写成一个函数,组合到其他功能中,而不用写多个。

   function Toast(it) {
        if (it.hasClass("done")) {
            alertText("加油哦,你的【"+it.find("span").html()+"】任务没有完成");
        } else {
            alertText("真棒,你已经完成了【" + it.find("span").html() + "】这个任务");
        }
    }
    function alertText(text) {
      $("#trigger").removeClass("hide");
      $("#alert").html(text);
  }

依赖倒置原则】:尽量从抽象层面去保证功能模块的稳定性。比如上面案例中用抽象层面理解它的需求就是一句话:绑定li事件实现功能。所以我们可以这样写。

   function init() {
        $("#content").on('click','li',function(){
            doSomething($(this))
        })
    }
    function doSomething(it) {
        changeStatus(it);
        Toast(it);
    }
    init();

这样写的话不管 li 事件还要实现其他任何具体的功能,这个抽象层都是稳定的,不需要修改。

接口隔离原则】:为了更好地实现复用,解耦依赖性。一种系列的功能集合(接口)与另一种系列的功能集合(接口)应当隔离开来。

比如以上案例中,在 doSomething 内部做了两件事。第一:修改任务状态,第二:出现弹框。假设现在有另外一个列表,也要实现类似的功能,只是不再需要弹框了。这时候你就需要把弹框相关的代码都找到,然后一点点地删掉。另外大家也知道,在 WEB 应用中,弹框的功能使用非常普遍。把弹框跟其他的功能写在一起就没法实现复用了。

所以这时候最好的办法就是将任务状态单独作为一个接口(其实也就是单独写成一个方法)。再把弹框单独作为一个接口。我们以上案例中已经实现了接口隔离。

过了几个月如果产品经理再找上你,跟你说 “喂,小王,我看这个弹框挺不爽的,去掉吧”。这时候你就可以很方便地直接把 Toast 方法注释掉就行,而不用去分析逻辑,一点点找代码,思考需要删掉哪些代码。

里氏替换】:在以上案例中,我们把 alertText 方法当成了公共方法调用。但是如果HTML 代码如下。区别是添加了一个"温馨提示"。显然我们直接 $("#alert").html(text) 是不合理的,会把 <div>温馨提示</div> 替换掉。所以我们需要更改 alertText 方法。

<div id="trigger" class="hide">
    <div class="container">
        <div id="alert">
            <div>温馨提示:</div>
            <div class="text"></div>
        </div>
    </div>
</div>

更改后的 alertText 方法如下。 setText 方法属于具体实现类,根据不同的业务场景去实现就好了。

function alertText(text) {
    $("#trigger").removeClass("hide");
    setText(text);
 }
function setText(text) {
    $("#alert .text").html(text);
 }

迪米特法则】:由于以上案例比较简单,很难模拟出迪米特法则。就像如果一个部门只有一个领导和员工,肯定就不需要助理角色了。不过从抽象的角度来说。我们也可以把 doSomething 看成是一种迪米特法则。用 doSomething 隔离开了 li 事件监听这个抽象功能和 changeStatus(it)Toast(it) 这些具体功能,使双方更加独立。

完整代码:
以下代码只是为了说明这 7 大设计原则而设定的,不一定是最佳实践。实际项目中可根据这 7 大设计原则随意搭配,灵活运用。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="initial-scale=1">
    <title>React App</title>
</head>
<body>
<style>
    html {
        font-size: 20px;
    }

    body {
        margin: 30px;
    }

    ul, li {
        margin: 0;
        padding: 0;
    }

    .fw-bold {
        font-weight: bold;
    }

    .done {
        text-decoration: line-through;
    }

    li {
        list-style: none;
        margin-top: 10px;
        margin-left: 10px;
    }

    .hide {
        display: none;
    }

    .container {
        position: fixed;
        left: 0;
        bottom: 0;
        top: 0;
        right: 0;
        background: rgba(0, 0, 0, 0.7);
        width: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .container #alert {
        width: 80%;
        height: 40%;
        background: #fff;
        padding: 20px;
        border-radius: 5%;
    }
</style>
<div class="fw-bold">任务列表</div>
<ul id="content">
    <li><input type="checkbox"><span>开会</span></li>
    <li><input type="checkbox"><span>测试</span></li>
    <li><input type="checkbox"><span>发版</span></li>
</ul>
<div id="trigger" class="hide">
    <div class="container">
        <div id="alert">
        </div>
    </div>
</div>
<script src="https://code.jquery.com/jquery-3.5.0.js"></script>
<script>
    function init() {
        $("#content").on('click', 'li', function (e) {
            e.stopPropagation();
            doSomething($(this))
        })
        $("body").on('click',function () {
            $("#trigger").addClass("hide");
        })
    }
    function doSomething(it) {
        changeStatus(it);
        Toast(it);
    }
    function changeStatus(it) {
        if (it.hasClass("done")) {
            notDone(it);
        } else {
            alreadyDone(it);
        }
    }
    function Toast(it) {
        if (it.hasClass("done")) {
            alertText("真棒,你已经完成了【" + it.find("span").html() + "】这个任务");
        } else {
            alertText("加油哦,你的【" + it.find("span").html() + "】任务没有完成");
        }
    }
    function alreadyDone(it) {
        it.addClass("done");
        it.find("input").prop("checked", 'checked');
    }
    function notDone(it) {
        it.removeClass("done");
        it.find("input").prop("checked", false);
    }
    function alertText(text) {
        $("#trigger").removeClass("hide");
        setText(text)
    }
    function setText(text) {
        $("#alert").html(text);
    }
    init();
</script>
</body>
</html>
  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 设计模式是一种通过提供可复用的解决方案来解决面向对象软件开发中常见问题的方法。这些问题可能包括对象之间的通信、对象的创建和销毁、以及如何组织代码等。 设计模式能够提供一种标准化的方法来解决这些问题,使得开发人员可以更加高效地构建软件系统。设计模式提供了一种用于描述和交流解决方案的共同语言,使得不同开发人员之间可以更好地合作。 设计模式的另一个重要特点是其可复用性。一旦开发人员学会了某个设计模式,他们可以在不同的项目中重复使用该模式,从而节省开发时间和资源。这种可复用性使得设计模式成为面向对象软件开发的基础之一。 《设计模式可复用面向对象软件的基础》这本书将深入介绍设计模式的原理、分类和具体实现。通过学习这本书,读者可以掌握常见设计模式的应用方法,并了解如何根据具体问题选择最合适的设计模式。该书还包含了大量的示例代码和实际案例,帮助读者更好地理解和应用设计模式。 总结来说,设计模式是一种解决面向对象软件开发中常见问题的方法,具有可复用性。《设计模式可复用面向对象软件的基础》这本书通过系统地介绍设计模式的原理、分类和实现,在提高开发效率的同时,帮助读者建立起良好的面向对象软件开发思维方式。 ### 回答2: 设计模式是一种解决软件设计问题的经典方法,它提供了一些通用的解决方案和思想,可用于构建复用的面向对象软件。设计模式的目标是提高软件的可维护性、可扩展性和灵活性。 设计模式包括三种类型:创建型、结构型和行为型。创建型设计模式关注如何实例化对象,包括简单工厂、工厂方法、抽象工厂、建造者和原型。结构型设计模式关注对象之间的组合,包括适配器、装饰器、代理、组合、外观、享元和桥接。行为型设计模式关注对象之间的通信和职责分配,包括观察者、模板方法、策略、状态、责任链、命令、备忘录、迭代器和访问者。 设计模式可提供可复用的解决方案,不仅可以提高软件的开发效率,还能确保软件的可靠性和可维护性。通过使用设计模式,开发人员可以更加清晰地理解软件系统的结构和功能,使得软件系统更易于理解和维护。同时,设计模式还能促进团队之间的协作和交流,提高团队的开发效率。 《设计模式可复用面向对象软件的基础》这本书提供了系统和详细的介绍和讲解了各种设计模式的原理、实现方法以及应用场景。通过阅读这本书,读者可以深入理解设计模式的核心概念,学习如何在实际项目中应用设计模式,提高软件的质量和可维护性。这本书对于想要深入学习和应用设计模式的软件开发人员来说是一本非常有价值的参考资料。 ### 回答3: 设计模式指的是用于解决软件设计中常见问题的经验性解决方案。它们是软件开发人员在解决类似问题时所提炼出来的最佳实践。设计模式的目标是提高软件的可复用性、可扩展性和可维护性。 设计模式是通过将常见的设计问题和对应的解决方案进行抽象和总结而得到的。它们是由经验丰富的软件开发人员们共同提炼和归纳出来的,是他们在实际项目中不断探索和总结出来的经验。 设计模式是可复用的,因为它们提供了一种标准化的解决方案,可以在不同的项目中重复使用。这样可以减少开发人员的工作量,提高开发效率。 同时,设计模式也能够提升软件的可扩展性,使得软件在面对变化时更加容易进行修改和扩展。通过使用设计模式,软件的各个组件之间的耦合性得到了降低,使得系统更加灵活和易于维护。 最后,设计模式使得软件更易于维护。因为设计模式遵循了一系列约定和规范,开发人员能够更快地理解和修改代码,从而降低了维护成本。 设计模式可复用面向对象软件的基础,是因为它们提供了一套可复用的解决方案,能够解决软件设计中常见的问题。通过学习和应用设计模式,可以提高软件的质量和开发效率,同时也可以提升开发人员的设计能力和职业素养。在软件开发领域中,设计模式是不可或缺的一部分。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值