精华内容
下载资源
问答
  • 在动态上给对象发的话
    万次阅读
    2018-10-14 19:56:31
    /**
     * 动态<td>填充当前页
     */
    function fillPage() {
    
        // 根据记录数确定要生成的行数
        for (var i = 0; i != dataArray.length; ++i) {
            // 创建一个行元素
            var row = document.createElement('tr');
    
            // 创建td单元格
            var idCell = document.createElement('td');
            var addressCell = document.createElement('td');
            var timeCell = document.createElement('td');
            var amountCell = document.createElement('td');
            var logCell = document.createElement('td');
            var yesterdayCell = document.createElement('td');
            var todayCell = document.createElement('td');
            var flagCell = document.createElement('td');
    
            // 给创建的td单元格填充数据
            idCell.innerHTML = dataArray[i].id;
            console.log("dataArray.id:" + dataArray[i].id);
            addressCell.innerHTML = dataArray[i].address;
            timeCell.innerHTML = dataArray[i].time;
            amountCell.innerHTML = dataArray[i].amount;
            logCell.innerHTML = dataArray[i].log;
            yesterdayCell.innerHTML = dataArray[i].yesterday;
            todayCell.innerHTML = dataArray[i].today;
            // flag填充设置
            if (dataArray[i].flag == false || dataArray[i].flag == "flase") {
                flagCell.innerHTML = "无需处理";
                flagCell.style.color = "gray";
                flagCell.style.border = "#eee 2px outset";
                flagCell.onclick = function() {
                    alert("该用户今天无转账或已处理");
                }
            } else {
                flagCell.innerHTML = "未处理";
                flagCell.style.color = "orange";
                flagCell.style.border = "#eee 2px outset";
                flagCell.onclick = function() {
                    var r = confirm("确认处理完该用户今天转账了?");
                    if (r == true) {
                        // 请求后台修改数据库转账状态
                        // 获取当前这行记录的id节点
                        changeState(this.parentNode.childNodes[0].innerHTML);
                        // 修改前端显示
                        this.innerHTML = "无需处理";
                        this.style.color = "gray";
                    }
                    
                }
            }   
    
            // 添加点击函数[address,time, log,today,yesterday]
            addressCell.onclick = function() {
                alert(this.innerHTML);
            }
            timeCell.onclick = function() {
                alert(this.innerHTML);
            }
            logCell.onclick = function() {
                alert(this.innerHTML);
            }
            yesterdayCell.onclick = function() {
                alert(this.innerHTML);
            }
            todayCell.onclick = function() {
                alert(this.innerHTML);
            }
    
            // 将新建的td加入新建的tr行
            row.appendChild(idCell);
            row.appendChild(addressCell);
            row.appendChild(timeCell);
            row.appendChild(amountCell);
            row.appendChild(logCell);
            row.appendChild(yesterdayCell);
            row.appendChild(todayCell);
            row.appendChild(flagCell);
    
            // 将这行加到tbody
            // 获取tbody
            var tbody = document.getElementById("tbody");
            tbody.appendChild(row);
        }
    }

    其他dataArray是来自于后台发的一个json中

    {
        "currentPage":1,
        "dataArray":[
             {"address":"","amount":0,"flag":false,"id":1,"log":"","time":"","today":"","yesterday":""},
             {"address":"","amount":0,"flag":false,"id":8888,"log":"","time":"","today":"","yesterday":""},
             {"address":"","amount":0,"flag":false,"id":8889,"log":"","time":"","today":"","yesterday":""},
             {"address":"","amount":0,"flag":false,"id":8890,"log":"","time":"","today":"","yesterday":""}
        ],
        "firstPage":1,
        "nextPage":1,
        "pageSize":5,
        "prePage":1,
        "totalCount":4,
        "totalPage":1
    }

     

    注重/意点:

    function fillPage() {
        ....
        ....
        ....
        // 给创建的td单元格填充数据
        idCell.innerHTML = dataArray[i].id;
    
        .....
        .....
        flagCell.onclick = function() {
            var r = confirm("确认处理完该用户今天转账了?");
            if (r == true) {
                // 请求后台修改数据库转账状态
                // 获取当前这行记录的id节点
                changeState(this.parentNode.childNodes[0].innerHTML);
                // 修改前端显示
                this.innerHTML = "无需处理";
                this.style.color = "gray";
            }
                    
        }
    }

    this.parentNode.childNodes[0].innerHTML就是idCell.innerHTML,

    也是上面的idCell.innerHTML = dataArray[i].id;这句,

    但是这里不能用 dataArray[i].id 会爆未定义的错

    index.js:xxx Uncaught TypeError: Cannot read property 'xx' of undefined

    就是函数里面创建的函数不能调用外面的变量,尽管是外面的全局变量。

     

    也不能用 idCell.innerHTML 因为我这有多条记录,for循环下每个新建的第一个td都是叫 idCell

    因为js是解析语言,所以一个for后,这样写没运行这个idCell也是指到了最后一条记录的那个。

     

    正确写法: 采用this, this表示调用它的对象、元素、节点Node。
    flagCell.onclick = function() {
          ...
         changeState(this.parentNode.childNodes[0].innerHTML);
         // 修改前端显示
         this.innerHTML = "无需处理";
         this.style.color = "gray";
    }

    这里的this表示flagCell这个<td>节点,因为是flagCell这个节点调用的内部。

    使用this就指定了某个节点,就算很多节点命名一样,也是可以分开的

    this.parentNode.childNodes[0] 获取flagCell的父亲节点(一个<tr>)的子节点集合(<tr>下创建的那些<td>如:flagCell, idCell)

    下标从 0 开始,第一个创建的是idCell所以就获取到了idCell节点。
                    

    // 添加点击函数[address,time, log,today,yesterday]
            addressCell.onclick = function() {
                alert(this.innerHTML);
            }
            timeCell.onclick = function() {
                alert(this.innerHTML);
            }
            logCell.onclick = function() {
                alert(this.innerHTML);
            }
            yesterdayCell.onclick = function() {
                alert(this.innerHTML);
            }
            todayCell.onclick = function() {
                alert(this.innerHTML);
            }

    这些也一样,不能改为addressCell.innerHTML,这样如果是for循环多个重名的话,会只对最后一个有效,故使用this

    更多相关内容
  • 当下的程序员应该都听过“面向对象编程”一词,也经常有人问能不能用一句解释下什么是“面向对象编程”,我们先来看看比较正式的说法。 把一组数据结构和处理它们的方法组成对象(object),把相同行为的...

    面向对象编程基础

    活在当下的程序员应该都听过“面向对象编程”一词,也经常有人问能不能用一句话解释下什么是“面向对象编程”,我们先来看看比较正式的说法。

    把一组数据结构和处理它们的方法组成对象(object),把相同行为的对象归纳为类(class),通过类的封装(encapsulation)隐藏内部细节,通过继承(inheritance)实现类的特化(specialization)和泛化(generalization),通过多态(polymorphism)实现基于对象类型的动态分派。

    这样一说是不是更不明白了。所以我们还是看看更通俗易懂的说法,下面这段内容来自于知乎

    这里写图片描述

    说明:以上的内容来自于网络,不代表作者本人的观点和看法,与作者本人立场无关,相关责任不由作者承担。(终于有机会享受一下把这段话反过来说的乐趣了,乐得牙都快碎了。)

    之前我们说过“程序是指令的集合”,我们在程序中书写的语句在执行时会变成一条或多条指令然后由CPU去执行。当然为了简化程序的设计,我们引入了函数的概念,把相对独立且经常重复使用的代码放置到函数中,在需要使用这些功能的时候只要调用函数即可;如果一个函数的功能过于复杂和臃肿,我们又可以进一步将函数继续切分为子函数来降低系统的复杂性。但是说了这么多,不知道大家是否发现,所谓编程就是程序员按照计算机的工作方式控制计算机完成各种任务。但是,计算机的工作方式与正常人类的思维模式是不同的,如果编程就必须得抛弃人类正常的思维方式去迎合计算机,编程的乐趣就少了很多,“每个人都应该学习编程”这样的豪言壮语就只能说说而已。当然,这些还不是最重要的,最重要的是当我们需要开发一个复杂的系统时,代码的复杂性会让开发和维护工作都变得举步维艰,所以在上世纪60年代末期,“软件危机”、“软件工程”等一系列的概念开始在行业中出现。

    当然,程序员圈子内的人都知道,现实中并没有解决上面所说的这些问题的“银弹”,真正让软件开发者看到希望的是上世纪70年代诞生的Smalltalk编程语言中引入的面向对象的编程思想(面向对象编程的雏形可以追溯到更早期的Simula语言)。按照这种编程理念,程序中的数据和操作数据的函数是一个逻辑上的整体,我们称之为“对象”,而我们解决问题的方式就是创建出需要的对象并向对象发出各种各样的消息,多个对象的协同工作最终可以让我们构造出复杂的系统来解决现实中的问题。

    说明:当然面向对象也不是解决软件开发中所有问题的最后的“银弹”,所以今天的高级程序设计语言几乎都提供了对多种编程范式的支持,Python也不例外。

    类和对象

    简单的说,类是对象的蓝图和模板,而对象是类的实例。这个解释虽然有点像用概念在解释概念,但是从这句话我们至少可以看出,类是抽象的概念,而对象是具体的东西。在面向对象编程的世界中,一切皆为对象,对象都有属性和行为,每个对象都是独一无二的,而且对象一定属于某个类(型)。当我们把一大堆拥有共同特征的对象的静态特征(属性)和动态特征(行为)都抽取出来后,就可以定义出一个叫做“类”的东西。

    这里写图片描述

    定义类

    在Python中可以使用class关键字定义类,然后在类中通过之前学习过的函数来定义方法,这样就可以将对象的动态特征描述出来,代码如下所示。

    class Student(object):
    
        # __init__是一个特殊方法用于在创建对象时进行初始化操作
        # 通过这个方法我们可以为学生对象绑定name和age两个属性
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def study(self, course_name):
            print('%s正在学习%s.' % (self.name, course_name))
    
        # PEP 8要求标识符的名字用全小写多个单词用下划线连接
        # 但是很多程序员和公司更倾向于使用驼峰命名法(驼峰标识)
        def watch_av(self):
            if self.age < 18:
                print('%s只能观看《熊出没》.' % self.name)
            else:
                print('%s正在观看岛国爱情动作片.' % self.name)

    说明:写在类中的函数,我们通常称之为(对象的)方法,这些方法就是对象可以接收的消息。

    创建和使用对象

    当我们定义好一个类之后,可以通过下面的方式来创建对象并给对象发消息。

    def main():
        # 创建学生对象并指定姓名和年龄
        stu1 = Student('骆昊', 38)
        # 给对象发study消息
        stu1.study('Python程序设计')
        # 给对象发watch_av消息
        stu1.watch_av()
        stu2 = Student('王大锤', 15)
        stu2.study('思想品德')
        stu2.watch_av()
    
    
    if __name__ == '__main__':
        main()
    

    访问可见性问题

    对于上面的代码,有C++、Java、C#等编程经验的程序员可能会问,我们给Student对象绑定的nameage属性到底具有怎样的访问权限(也称为可见性)。因为在很多面向对象编程语言中,我们通常会将对象的属性设置为私有的(private)或受保护的(protected),简单的说就是不允许外界访问,而对象的方法通常都是公开的(public),因为公开的方法就是对象能够接受的消息。在Python中,属性和方法的访问权限只有两种,也就是公开的和私有的,如果希望属性是私有的,在给属性命名时可以用两个下划线作为开头,下面的代码可以验证这一点。

    class Test:
    
        def __init__(self, foo):
            self.__foo = foo
    
        def __bar(self):
            print(self.__foo)
            print('__bar')
    
    
    def main():
        test = Test('hello')
        # AttributeError: 'Test' object has no attribute '__bar'
        test.__bar()
        # AttributeError: 'Test' object has no attribute '__foo'
        print(test.__foo)
    
    
    if __name__ == "__main__":
        main()
    

    但是,Python并没有从语法上严格保证私有属性或方法的私密性,它只是给私有的属性和方法换了一个名字来“妨碍”对它们的访问,事实上如果你知道更换名字的规则仍然可以访问到它们,下面的代码就可以验证这一点。之所以这样设定,可以用这样一句名言加以解释,就是“We are all consenting adults here”。因为绝大多数程序员都认为开放比封闭要好,而且程序员要自己为自己的行为负责。

    class Test:
    
        def __init__(self, foo):
            self.__foo = foo
    
        def __bar(self):
            print(self.__foo)
            print('__bar')
    
    
    def main():
        test = Test('hello')
        test._Test__bar()
        print(test._Test__foo)
    
    
    if __name__ == "__main__":
        main()
    

    在实际开发中,我们并不建议将属性设置为私有的,因为这会导致子类无法访问(后面会讲到)。所以大多数Python程序员会遵循一种命名惯例就是让属性名以单下划线开头来表示属性是受保护的,本类之外的代码在访问这样的属性时应该要保持慎重。这种做法并不是语法上的规则,单下划线开头的属性和方法外界仍然是可以访问的,所以更多的时候它是一种隐喻,关于这一点可以看看我的《Python - 那些年我们踩过的那些坑》文章中的讲解。

    面向对象的支柱

    面向对象有三大支柱:封装、继承和多态。后面两个概念在下一个章节中进行详细的说明,这里我们先说一下什么是封装。我自己对封装的理解是“隐藏一切可以隐藏的实现细节,只向外界暴露(提供)简单的编程接口”。我们在类中定义的方法其实就是把数据和对数据的操作封装起来了,在我们创建了对象之后,只需要给对象发送一个消息(调用方法)就可以执行方法中的代码,也就是说我们只需要知道方法的名字和传入的参数(方法的外部视图),而不需要知道方法内部的实现细节(方法的内部视图)。

    练习

    练习1:定义一个类描述数字时钟

    class Clock(object):
        """
        数字时钟
        """
    
        def __init__(self, hour=0, minute=0, second=0):
            """
            构造器
    
            :param hour: 时
            :param minute: 分
            :param second: 秒
            """
            self._hour = hour
            self._minute = minute
            self._second = second
    
        def run(self):
            """走字"""
            self._second += 1
            if self._second == 60:
                self._second = 0
                self._minute += 1
                if self._minute == 60:
                    self._minute = 0
                    self._hour += 1
                    if self._hour == 24:
                        self._hour = 0
    
        def __str__(self):
            """显示时间"""
            return '%02d:%02d:%02d' % \
                   (self._hour, self._minute, self._second)
    
    
    def main():
        clock = Clock(23, 59, 58)
        while True:
            print(clock)
            sleep(1)
            clock.run()
    
    
    if __name__ == '__main__':
        main()
    

    练习2:定义一个类描述平面上的点并提供移动点和计算到另一个点距离的方法。

    from math import sqrt
    
    
    class Point(object):
    
        def __init__(self, x=0, y=0):
            """
            构造器
    
            :param x: 横坐标
            :param y: 纵坐标
            """
            self.x = x
            self.y = y
    
        def move_to(self, x, y):
            """
            移动到指定位置
    
            :param x: 新的横坐标
            "param y: 新的纵坐标
            """
            self.x = x
            self.y = y
    
        def move_by(self, dx, dy):
            """
            移动指定的增量
    
            :param dx: 横坐标的增量
            "param dy: 纵坐标的增量
            """
            self.x += dx
            self.y += dy
    
        def distance_to(self, other):
            """
            计算与另一个点的距离
    
            :param other: 另一个点
            """
            dx = self.x - other.x
            dy = self.y - other.y
            return sqrt(dx ** 2 + dy ** 2)
    
        def __str__(self):
            return '(%s, %s)' % (str(self.x), str(self.y))
    
    
    def main():
        p1 = Point(3, 5)
        p2 = Point()
        print(p1)
        print(p2)
        p2.move_by(-1, 2)
        print(p2)
        print(p1.distance_to(p2))
    
    
    if __name__ == '__main__':
        main()
    

    说明:本章中的插图来自于Grady Booch等著作的《面向对象分析与设计》一书,该书是讲解面向对象编程的经典著作,有兴趣的读者可以购买和阅读这本书来了解更多的面向对象的相关知识。

    展开全文
  • 以非耍流氓的方式讨论C++中的面向对象编程

    千次阅读 多人点赞 2022-01-18 02:14:10
    老规矩,先解释一下标题,我承认有点...其实C++面向对象编程的问题,很多大牛都讨论过,但是因为人家是大牛,所以文字描述居多,很少出代码。人家毕竟是阅近天下**, 心中早已无码。我个人认为讨论编程的问题,如果.

    老规矩,先解释一下标题,我承认有点标题党,其实就是为了抓人眼球,希望通过这种标题吸引你点进来看看。我有自信这是一篇好文章。我曾经写过一篇很有名的博客:《为什么C语言不会过时》 很多的网络媒体都进行了转载。我相信这篇博客的质量应该比《为什么C语言不会过时》还好。因为它会颠覆你对C++面向对象编程的传统的认知。

    再详细解释一下什么是非耍流氓。其实C++面向对象编程的问题,很多大牛都讨论过,但是因为人家是大牛,所以文字描述居多,很少给出代码。人家毕竟是阅近天下**, 心中早已无码。我个人认为讨论编程的问题,如果你不给出代码就是在耍流氓。 而一些C++初学者,倒是愿意给出一大堆关于什么dog继承animal的类似的源代码,但是这样讨论面向对象编程有些肤浅,片面,甚至是把人带向了错误的方向。所以我这篇文章首先要基于源代码的方式讨论面向对象,而且介绍现代方式的面向对象编程,你会惊讶的发现,无论是从理念上,还是实现方式上,它都和传统意义的面向对象编程截然不同。

    我刚开始接触C++的面向对象的时候,首先接触的例子就是duck是animal, student是一个people等等那一套。这种例子铺天盖地,充斥于各种C++的各种初级书本中。我相信大部分人和我都有一样的记忆。当时感觉这个面向貌似没什么难的。但是当自己写程序的时候,大部分时间都根本没怎么使用到继承。这个是第一个层次。

    Class Animal{
       Virtual Speak() = 0;
    }
    Class Duck: Animal{
       Virtual Speak(){cout<<"quack"<<endl;
    }

    慢慢地开始接触设计模式,这个就有点难度了。这里没有什么鸭子,学生之类的东西了。基本上都是一些很抽象的东西。比如说,策略是一个基类,命令是一个基类等等。当时看的云山雾罩,不得所以。只是机械的记住几个名字,几个类关系图而已。

    再慢慢地,开始意识到一些本质的东西。继承是面向对象编程的核心实现方式,其思想本质是把变化的东西封装到子类中,而客户只针对一个基类的指针来调用:

    Class Base{
       Virtual Do{}= 0
    };
    Class Implement1: Base{
       Do(){do it in this way};
    }
    Class Implement2: Base{
       Do(){do it in thatway};
    }
    
    //Client code
    Base* Ibase;
    Ibase->do(); //

    如果你仔细研究设计模式,尤其是建造模式和行为模式,你会发现它们都符合这个基本的想法。例如工厂方法模式,我们把Do 函数换成Factorymethod方法就OK了,换句话说,我们把对象生成方法的变化下推到子类中,使得这种变化不会影响到客户的代码。再举了例子,策略模式,你把Do函数换成AlgorithmInterface()就可以了。我们把算法的变化封装到不同的子类中。当运行的时候调用不同的子类,客户端的代码也不受影响。顺便说一句,这种思想后面还有两个非常凡尔塞的原则。一个是里氏替换原则:如果S是T的子类型,则类型T的对象可以用类型S的对象替换(即类型T的对象可以用子类型S的任何对象替换(即向上转换))而不更改程序的任何期望属性(正确性,任务执行等)。其实就是我们能用Implement2替换掉IBase, 客户的程序不受影响。另外一个就是反向依赖原则:高层模块不应依赖低层模块,低层模块也不应依赖高层模块,两个都应该依赖抽象层。抽象不应该依赖细节,细节应该依赖抽象。 这个其实更简单, 换成人话就是一旦Do()这个玩意定下来,客户就去调用它,子类就去实现它。换句话说,客户不依赖于子类了,而是客户和子类都依赖于Do()这个玩意。这里很多人把Do()这个玩意也叫做接口,抽象,基类,契约等等。 看明白了吗?其实这两个原则其实说的都是一回事。

    如果上面我说的你都明白,那么我恭喜你。你对传统意义的面向对象编程已经基本了解了,而且23个设计模式对你来说就不那么难懂了。

    在传统的面向对象编程中,你会发现另外的两个问题,1) 什么应该是基类?2)如果基类需要变化怎么办? 而恰巧正是这两点,直接击中了传统面向对象编程的痛点。让我们一个一个说。 首先什么应该是基类?关于这个问题,C++面向对象里面有一个经典的案例,那就是:椭圆应不应该是圆的基类。我这里只是简单的介绍一下,详细的讨论可以参考后面的参考文献1。 长话短说,欧几里得认为圆形就是椭圆,但是C++里面你却不能这么用,这是因为椭圆有两个圆心,但是圆却只有一个圆心。 没有办法直接使用继承。请看源代码;如果使用了继承,你会发现圆这个类有三个圆心。而且setLongRadius在圆形里面也没什么实际的意义。这明显违背了里氏替换原则。

    Class Ellipse{
       setLongRadius(float);
       setShortRadius(float);
       pair<float, float> center1;
       pair<float, float> center2;
    }
    
    Class Circle{
       setRadius(float);
       pair<float, float> center;
    }

    问题出在哪里,到底是欧几里得错了? 还是Bjarne Stroustrup(C++之父)错了?大部分人的第一反应都是一定是我错了。然后针对这个问题,层出不穷的解决方案就出来了。但是无非就三种方法,让基类变弱,让子类变强。或者再建造一个更抽象的shape基类,然后让ellipse和circle都继承于它。但是无论是哪种方法,都相当的别扭和不自然。

    如果如何在开始的时候设计类继承体系已经很成问题了,那么未来的变化更是大麻烦。 这里有另外一个例子。比如我们现在设计了一个面向对象的继承体系如下图:

    Class Animal{
       walk();
       speak();
    }

    现在People和Dog继承于它。 貌似非常地完美,没有任何问题。但是两个月以后,我们需要新加入一个类鲨鱼。问题就来了。鱼能继承于Animal吗?不能,因为调用Shark.walk()的方法没有办法解释。但是鱼明明就是一种Animal吗?没关系,面向对象是不会错的,C++语言也是没错的,只是我分类的方法不对。我应该再加入一个哺乳动物类,鱼类,然后把swim这个行为放到鱼这个类中。看着不错,虽然改变类体系这种事情会对现有的系统带来巨大的冲击和影响(几乎所有过去的代码需要改写和重新编译。)但是你相信现在的分类是完美的。但是两个月以后,鲸鱼来了。没关系,我们可以使用双继承去解决这个问题。(一旦使用了双继承,基本上就是代表你的设计需要一个补丁了。)鲸鱼同时继承哺乳动物类和鱼类,也算暂时过关了。但是两个月以后,鸭子来了。这个时候,我估计你快疯了。这个能walk, speak 和swim的东西到底应该放到哪里呢?真没想到,你会被一个鸭子伤害到。 其实你不是个例。 Linux之父炮轰过C++,指出任何现在看起来伟大,光明正确的类设计,两年以后都会有各种问题,那个时候修复起来成本很大,因为整个的系统实现都是基于现有的类设计架构,非常难于修改。同时继承关系是一种很强的耦合关系,这种强耦合使得整个程序变成了一大坨,牵一发而动全身。通过我们上面这个类似玩具的例子,我希望你能对这句话有所体会。

    传统面向对象编程(我是指那种上来就去设计复杂的类的分类和继承体系)的问题在于把人类日常科学的分类方法照搬到编程领域,这是不对的。再详细点说,is-a关系并不能完美地适用于编程领域,is-substituable(可替代关系)更适合。在Python语言中,这种思想得到了贯彻和体现。Python语言中有著名的(duck type)鸭子类型。只要你叫起来象鸭子,走路像鸭子,那么你就可以替代鸭子。 至于你本身是什么,我不care。

    我们再论is-substituable(可替代关系),现代面向对象的本质到底是什么呢?是构造一个分类准确,漂亮并且复杂的分类体系吗?当然不是!编程的最终目的就是完成一个任务,这个任务需要多个对象配合,换句话说就是各个对象能够彼此发送消息(A对象给B对象发消息就是A对象调用B对象的某个方法。)现代面向对象编程的本质就是轻对象分类体系(少用继承),而重视可替代关系的表达和实现。有了这种可替代关系的加持,对象之间就能更好的彼此合作(对象间调用彼此方法),从而更好地完成一个任务。

    一旦我们把is-a关系换成is-substituable(可替换关系),我们就完成了面向对象编程部分的思想改造,但是不要忘记,C++是一门静态语言,不是动态语言。就语言技术本身,我们还不能像Python语言那样直接地支持动态数据类型(鸭子类型)。 但是感谢C++的generic编程。这给现代C++语言打开了一扇新的大门,使得我们不用使用继承也能支持泛型和多态(编译期间的)。不仅如此,现代的C++语言对泛型的支持更安全,对多态的支持更高效。可以这么说,C++11以后,尤其是C++/14/17/20的不断进步和演化,为我们的现代面向对象编程提供了强大的语言技术支持,使得我们完成了现代面向对象编程部分的工具改造


    思想和工具都改造完成,我们该上码了。这里我们对一个形状求面积。 我会首先给出基于传统面向对象编程的实现,然后再给出现代面向对象编程的实现。

    首先是设计一个形状的基类, 然后派生出三角形, 椭圆,圆形等等。

    Class Shape{
       virtual getArea() = 0;
    }
    Class Rectangle: public Shape{
       setLength(float);
       setHight(float);
       virtual getArea(){return length* hight;}
    }
    Class Triangle: publich Shape{
       ....
    }
    float getShapeArea(Shape* ptr_shape){
       return ptr_shape->getArea();
    }

    代码相当标准和传统,这里不过多解释。请注意,使用传统的面向对象编程,你就会遇到上面我们讨论的所有的面向对象的问题。圆形是不是椭圆?点是不是个形状等等。

    好吧,现在我们用现代面向对象编程的思路去解决这个问题。看下面的代码:

    Template<typename T>
    float getTArea(T t){
       return t.getArea();
    }

    请注意,并不是使用模板了就实现现代化了,而是这段代码背后的思想。现在我们已经不需要T一定是Shape类型了。相反地,只要在计算面积这个事情上,某个类型T有可替换性,那么它就可以上。 注意到没,我们再也不需要关注椭圆是不是圆形这个问题了。而且以后就算是出现了3D的对象,只要它在计算面积这个事情上有可替换性,那么我们的代码就不需要更改,你只需要不断的加入新类就行了。就这样。

    比起Shape基类,T也是类型安全的。因为在C++编译并且实例化这个函数模板的时候,编译器会检查实例化的T类型支不支持getArea()这个函数。在C++20中,我们更是引入了concept的概念。使得对类型的静态检查变得更加简单和方便。

    template <typename T>
    concept supportArea = requires(T v)
    {
        v.getArea()
    };
    Template<supportArea T> //supportArea is concept
    float getTArea(T t){
       return t.getArea();
    }

    不仅不再需要纠结圆形是不是椭圆这个问题了,我们还可以在可替换性上再前进一步。你想得到面积,我给你面积就得了,至于我怎么算的,你不用担心。并不是我有getArea()函数才有可替代性,而是我把面积返还给你我就有可替代性。假设有个圆形,我们想求面积,但是目前圆形中还没有实现getArea()方法,没关系,我们可以用一个矩形替换圆形来计算面积。这个理解起来很简单。我们可以把一个圆形转换成一个矩形,参考下图

    有了现代C++语言特性,实现起来也不难。参看源代码:

    Template<Typename T> 
    float getTArea(T t){
      if constexpr(Is_Circle<T>::value){
         auto getArea = [](auto& t){
               Rectangle r;
               r.setLengh(pi*t.Radius);
               r.setHigth(t.Radius);
               return r.getArea();
              };
          return getArea();      
      }
      else{
         return t.getArea();
      }  
    }

    这段代码用到了很多的C++14/17的特性,比如if constexpr,lambda和 type_trait等等。限于篇幅我不多解释了。这里简单介绍一下思想:首先构建一个Is_Circle<T> type_trait类。然后判断T是否是一个圆形,如果是,那么构建一个矩形,然后把矩形的长设置为pi*r,把高度设置为r。然后利用矩形的求面积公式计算圆形的面积。如果不是圆形,那就直接调用类型T自己的getArea。

    也就是说,在求面积这件事上,只要在语义层面上满足可替换性,那么一个圆甚至可以被一个矩形所替代,而且我们也彻底摆脱了有没有getArea()函数这种语法层次的的限制。怎么样,够酷吗?

    如果这个例子你读懂了,一次类似的例子还可以阅读参考文献2《Linux多线程服务端编程》。第一章里面有一个使用variadic template实现Observer设计模式的例子。书中的一个观点令我印象很深:设计复杂的分类和继承体系就是“叠床架屋”,不如直接基于对象编程(不使用继承,直接使用各种对象彼此协作),这样才能“拳拳到肉”。我深以为然。

    这里必须要再提一句,可替代性的度量不仅依赖于某个函数本身的语法和语义限制,它还依赖于调用这个函数的前后条件的限制。也就是pre-condition和post-condition也要同时满足客户的要求。这么说有点抽象,再举个例子,现在有两个鸭子类,调用一个鸭子类的speak方法,它返回“quack”,调用另外一个鸭子类的speak方法,它返回“姐,好久没来了!”。单从speak方法本身,语法和语义都貌似符合可替代性。但是很明显,客户对调用speak方法后的post-codition的期待是完全不一样的。所以在这种情况下,貌似完美的可替代性是不成立的。

    最后总结一下:

    1)现代的面向对象设计并不是基于Is-a关系设计复杂的类继承体系。而是基于可替代关系构建一个对象间能够高效彼此协作的系统。

    2) 可替代关系的理解是基于语义的,而不是基于语法的。例如有的时候语法上不可替代,但是语义上可替代就行。例如圆形即使没有getArea函数,它也可以被矩形替代(在计算面积的时候)。而有的时候,即使语法上完全可以替代,但是postcondition不满足,也不满足替代关系。例如两种不同的鸭子。

    3)利用现代C++的语言特性,尤其是基于generic programming语言特性实现上面介绍的基于可替代关系的设计意图。

    4)现代面向对象设计并不完全排斥对象,那种没有附加成本的基于数据抽象的对象还是推荐的。例如把一个点对象和数值对象抽象成一个圆形是推荐的。这种对象是更方便我们编程,而且没有任何附加成本。我们反对的是设计复杂的类分类和继承体系。

    参考文献:

    1. Inheritance — Proper Inheritance and Substitutability, C++ FAQ (isocpp.org)
    2. Linux多线程服务端编程 (豆瓣) (douban.com)

     

    展开全文
  • antd Table 组件动态合并单元格

    千次阅读 2020-08-28 16:00:35
    Table 组件动态数据合并单元格 我最开始的想法是 columns 对象的 render 中去完成这个功能。因为 render 中本来就是循环,暴露的参数又有限,所以很难去对比每个对象的相同项。后来经大牛指导可以先处理数据源。...

    前言

    使用 antd 开发 PC端应用的人肯定知道的 Table 组件。一个功能很完善,界面很优化的表格组件。通过查阅官方文档,你可以很轻松地使用这个组件。但是如果表格中涉及到合并单元格昵?

    文档中也有关于合并单元格的处理,但是只是静态数据的范例。而我们开发中往往都是获取的动态数据。那么Table 组件改如何动态合并单元格昵?

    Table 组件静态数据合并单元格

    这里简单地讲一下核心步骤,详细的步骤请直接移步官方文档。

    在这里插入图片描述
    以上是个简单的表格。现在我们将【分类】中相同的值合并,达到如下效果。

    在这里插入图片描述
    实现思路:

    1. 找到要合并列的第一列所在位置。记为 index。并且设置 rowSpan 为合并总列数。
      这里需要将 index 为 0 的 rowSpan 设置为 2。后面的相同项(也就是 index 为 1) 的 rowSpan 设置为 0。
    2. 同样地,需要将 index 为 2 的 rowSpan 设置为 1(没有相同项,就设置为1。也可以不用设置 rowSpan)。
    3. 最后,将 index 为 3 的 rowSpan 设置为 3,后面的相同项(也就是 index 为 4/5) 的 rowSpan 设置为 0。
    
    const columns = [
        {
            title: '分类',
            dataIndex: 'category',
            render: (value, row, index) => {
    	      const obj = {
    	        children: value,
    	        props: {},
    	      };
    	      if (index === 0) {
    	        obj.props.rowSpan = 2;
    	      }
    	      if (index === 1) {
    	        obj.props.rowSpan = 0;
    	      }
    	      //没有相同项时可以不用设置 colSpan 
    	      if (index === 2) {
    	        obj.props.rowSpan = 1;
    	      }
    	      if (index === 3) {
    	        obj.props.rowSpan = 3;
    	      }
    	      if (index === 4) {
    	        obj.props.rowSpan = 0;
    	      }
    	      if (index === 5) {
    	        obj.props.rowSpan = 0;
    	      }
    	      return obj;
          }   
        },
        {
            title: '名称',
            dataIndex: 'name',
        },
        {
            title: '评价',
            dataIndex: 'desc',
        },
    ];
    
    const data = [
      {
          "category":"水果",
          "name": "桃子",  
          "desc": "好吃"
        },{
          "category":"水果",
          "name": "梨子",  
          "desc": "真好吃"
        },{
          "category":"蔬菜",
          "name": "茄子",  
          "desc": "真TM好吃"
        },{
        "category":"家禽",
         "name": "牛肉",  
          "desc": "太好吃了"
        },{
        "category":"家禽",
         "name": "羊肉",  
          "desc": "好吃到停不下来"
        },{
        "category":"家禽",
         "name": "猪肉",  
          "desc": "吃不起,太贵"
        }
    ]
    

    实现静态数据单元格合并总结一下就是两点。

    1. 设置合并列的第一列 rowSpan 为合并数。
    2. 设置其他相同项 rowSpan 为 0。

    以上是处理静态数据,我们能够很快找到合并列的第一列以及合并数。但是如果是动态数据昵?我们不知道有几个【水果】,几个【蔬菜】和几个【肉类】。

    Table 组件动态数据合并单元格

    我最开始的想法是在 columns 对象的 render 中去完成这个功能。因为 render 中本来就是循环,暴露的参数又有限,所以很难去对比每个对象的相同项。后来经大牛指导可以先处理数据源。render 中直接使用带有逻辑的数据源就好了。

    于是茅塞顿开,涉及到要处理数据逻辑,那么这个问题就转换成了一道算法题。

    算法题:
    假如在真实的开发场景中接口返回如下数据格式。

    let data = [
      {
          "category":"水果",
          "name": "桃子",  
          "desc": "好吃"
        },{
          "category":"水果",
          "name": "梨子",  
          "desc": "真好吃"
        },{
          "category":"蔬菜",
          "name": "茄子",  
          "desc": "真TM好吃"
        },{
        "category":"家禽",
         "name": "牛肉",  
          "desc": "太好吃了"
        },{
        "category":"家禽",
         "name": "羊肉",  
          "desc": "好吃到停不下来"
        },{
        "category":"家禽",
         "name": "猪肉",  
          "desc": "吃不起,太贵"
        }
    ]
    

    界面要求:将【分类】中相同值进行合并单元格。
    经过上面对静态数据的逻辑处理,我们知道需要将每个相同项的第一项的 rowSpan 设置为合并数。并且其他的相同项设置为 0。得到后的数据格式如下:

    data = [
      {
          "category":"水果",
          "name": "桃子",  
          "desc": "好吃",
          "rowSpan":2 
        },{
          "category":"水果",
          "name": "梨子",  
          "desc": "真好吃",
          "rowSpan":0 
        },{
          "category":"蔬菜",
          "name": "茄子",  
          "desc": "真TM好吃",
           "rowSpan":1
        },{
        "category":"家禽",
         "name": "牛肉",  
          "desc": "太好吃了",
           "rowSpan":3
        },{
        "category":"家禽",
         "name": "羊肉",  
          "desc": "真不错", 
          "rowSpan":0
        },{
        "category":"家禽",
         "name": "猪肉",  
          "desc": "吃不起,太贵", 
          "rowSpan":0
        }
    ]
    

    算法题解思路:

    1. 定义 count 为重复项的第一项索引。
    2. 定义 indexCount 为下一项的索引。
    3. 取出数组第一个对象 item,先将 rowSpan 初始化为1。然后将 item 的【分类】值与下一项进行对比,发现一个相同项,rowSpan 就加1,并且下一项的 rowSpan 设置为 0。每对比一次,indexCount 累加。当没有遇到相同项时将 indexCount 赋值给 count。获取下一个相同项首项。
    4. 按照1/2/3的步骤,数组循环完之后就可以正确的赋值 rowSpan。

    代码:

    import dataJson from './data/Table.json';
    let data = dataJson.data;
    let field = 'category';
    const  changeData = (data,field)=>{
        let count = 0;//重复项的第一项
        let indexCount = 1;//下一项
        while (indexCount<data.length){
            var item = data.slice(count,count+1)[0];//获取没有比较的第一个对象
            if(!item.rowSpan){
                item.rowSpan = 1;//初始化为1
            }
            if(item[field] === data[indexCount][field]){//第一个对象与后面的对象相比,有相同项就累加,并且后面相同项设置为0
                item.rowSpan++;
                data[indexCount].rowSpan = 0;
            }else {
                count = indexCount;
            }
            indexCount++;
        }
    }
    changeData(data,field);//处理数据
    

    总结

    总结就一句话:数据处理逻辑思维,MVC 和 MVP 思想

    最开始做这个功能的时候,我一直沉浸在渲染页面 column 对象的 render 函数里去处理逻辑。但是 render 里面本来就是一个循环,参数有限,所以思维变得非常拘泥。

    经大神点播可以先把数据源处理好再直接渲染页面,我才发应过来这种业务逻辑最好的处理方式就是直接处理数据状态,用数据状态代替业务逻辑。这不就是所谓的 MVC 和 MVP 思想吗?所以程序设计思维真的很重要。

    展开全文
  • 面向对象六大原则

    万次阅读 多人点赞 2015-11-30 00:10:44
    本文出自《Android源码设计模式解析与实战》中的第一章。 1、优化代码的第一步——单一职责原则单一职责原则的英文名称是...就像秦小波老师《设计模式之禅》中说的:“这是一个备受争议却又及其重要的原则。只要你
  • 面向对象程序设计的基本概念

    千次阅读 2020-06-30 16:56:24
    原文链接:面向对象设计—类和对象** 1. 面向对象程序设计的基本概念 Java是一种面向对象的...如果说传统的过程式编程语言是以过程为中心、以算法为驱动的,面向对象的编程语言则是以对象为中心、以消息为驱动。用
  • 心里Python

    千次阅读 2020-11-21 04:04:36
    49 −#说句心里name=input("")heartword=input("")str1=name+',我想对你说,'+heartwordprint(str1)...0381相关推荐2019-09-28 21:13 −Python python是一种跨平台的计算机程序设计语言,是一种面向对象动态类型...
  • 文章主要介绍了静态CDN、动态CDN、全站加速、GAAP、AIA、CLB跨地域部署的原理与方法。
  • JVM-剖析对象内存分配流程

    千次阅读 2020-06-27 00:19:49
    众所周知, JAVA中的对象都是进行分配,当对象没有被引用的时候,需要GC。 如果对象数量较多的时候, GC 压力较大,也间接影响了应用的性能 。 为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象...
  • (十)vue实例对象介绍

    千次阅读 2018-11-12 13:27:47
    前三个基本已经讲了,我们直接来看实例对象的计算属性 (4)Computed 计算属性 1)什么是计算属性 1.计算属性即computed, 和前面我们讲的methods方法基本一样。他们的写法都是函数形式。 2.不同的是,methods...
  • ASPX一句木马详细分析

    千次阅读 2018-12-28 09:29:15
    首先回顾一下以前ASP一句的经典木马吧...VBS中execute就是动态运行指定的代码而JSCRIPT中也同样有eval函数可以实现,也就是说ASP一句木马也有个版本是采用JSCRIPT的 eval的!网上也有例子我就不多说了!..... 然...
  • 概述 本系列文章重写了java、.net、php三个版本的一句木马,可以解析并执行客户端传递...利用动态二进制加密实现新型一句木马之Java篇 利用动态二进制加密实现新型一句木马之.net篇 利用动态二进制加密实现新...
  • 当黑洞、AI、机器人、宇宙大爆炸等概念被科幻片玩滥了以后,量子成功上位成为了目前的科幻新秀,新推出的科幻片不掺杂点「量子概念」都不好意思电影预告。 量子就像是烹饪时放的万能咖喱酱,只要往电影里一加,...
  • js面向对象

    千次阅读 多人点赞 2020-02-23 18:18:26
    什么是面向对象
  • 来源丨Hackhttps://mp....今天就来大家分享一下,什么叫大数据抓出轨。据史料证明,马爸爸年轻时曾被绿过 18 次,所以才有了今天像淘宝和支付宝这样的抓出轨利器。(woluanshuode)虽然是我胡诌,但我也是有证...
  • 所用教材:谭火彬 编著 笔者纯手打整理,喜欢的请点个赞! UML相关的书就不能精简一点嘛?配套答案公众号“书圈”内下载 共50’/2h=选择题10’【(前4题1'+后3题2')来自1~5章(重点3、4、5章)课后选择题】 ...
  • 超硬核!数据结构学霸笔记,考试面试吹牛就靠它

    万次阅读 多人点赞 2021-03-26 11:11:21
    上次操作系统笔记,很快浏览上万,这次数据结构比上次硬核的多哦,同样的会超硬核代码,关注吧。
  • 面向对象:类的概念和定义!

    万次阅读 多人点赞 2018-05-28 10:16:37
    面向对象的概念: 对象: Object,含有“物体”的概念,一切皆物体(对象)。对象由静态的属性和动态的行为组成。 属性:行为:存储、保温 类: 一组具有相同属性和行为的对象的抽象。杯子: ...
  • 深入理解Class对象 ...认识Class对象之前,先来了解一个概念,RTTI(Run-Time Type Identification)运行时类型识别,对于这个词一直是 C++ 中的概念,至于Java中出现RRTI的说法则是源于《Thin...
  • 访问控制第一层级:根据当前登录用户动态加载菜单。
  • django中的queryset对象和object对象详解

    千次阅读 2019-10-01 17:11:08
    也就是说,你创建一个QuerySet对象的时候,Django并不会立即向数据库发出查询命令,只有你需要用到这个QuerySet的时候才回去数据库查询。 2. Objects是django实现的mvc框架中的数据层(...
  • 对象怎样有新生代转到年老代

    千次阅读 2015-11-22 23:18:39
    自动内存管理即:给对象分配内存以及分配给对象的内存 现在探讨内存分配对象的那点事 首先:对象内存分配都分配到那个地方? 堆分配,但也可能经过jIT编译后被拆散为标量类型并间接地分配 1:对象优先...
  • 牛逼!Java 从入门到精通,超全汇总版

    万次阅读 多人点赞 2021-05-06 19:40:33
    但是我认为,如果市面这些资料、书籍你都啃的差不多,你能所有的 Java 程序员中跻身前 0.1% 的,你就可以达到"精通" 这个阶段了,因为没人比你强了,你当然是精通了。 所以,我还是选择罗列一些知识点推荐...
  • 有人私信我说看了篇文章,但是不知道怎么创建pdf模板,而且有一些代码不明白。更了那么久博客总算有人私信问我问题/(ㄒoㄒ)/~~,正好今天又做这个功能所以更一篇文章详细讲述从使用Adobe Acrobat DC创建模板,到...
  • Java常见设计模式总结

    万次阅读 多人点赞 2021-09-18 17:18:54
    项目中合理的运用设计模式可以完美的解决很多问题,每种模式现实中都有相应的原理来与之对应,每种模式描述了一个我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。...
  • 秒懂Java代理与动态代理模式

    万次阅读 多人点赞 2018-06-30 17:08:23
    版权申明】非商业目的可自由转载 博文地址: 出自:shusheng007 概述 什么是代理模式?解决什么问题(即为什么需要)?...什么是动态代理模式?... 定义:为其他对象提供一种代理以控制对这个对象的访问 ...
  • 系统分析与设计方法---面向对象的分析与设计

    万次阅读 多人点赞 2018-09-14 20:26:32
    面向对象的分析与设计  面向对象方法是一种非常实用的软件开发方法,它一出现就受到软件技术人员的青睐,现已成为...面向对象方法中,分析和设计的界面并不明显,它们采用相同的符号表示,能够方便地从分析阶段...
  • Bean创建的时候可以该Bean打标,如果递归调用回来发现正在创建中的,即说明了循环依赖了。 Spring如解决Bean循环依赖问题? Spring中循环依赖场景有: 构造器的循环依赖 属性的循环依赖 singletonObjects:第...
  • 作为一个软件开发者,你一定会对网络应用如何工作有一个完整的... 首先嘛,你得浏览器里输入要网址:2. 浏览器查找域名的IP地址导航的第一步是通过访问的域名找出其IP地址。DNS查找过程如下:* 浏览器缓存 – 浏览器会
  • Java的堆是一个运行时数据区,类的实例对象从中分配空间.Java虚拟机的堆中存储着正在运行的应用程序所建立的所有对象,这些对象通过new,newarray,anewarray和multainewarray等指令建立,但是它们不需要程序代码来显式地...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 98,117
精华内容 39,246
关键字:

在动态上给对象发的话