日常学习

Ruby 面向对象语言描述

March 12, 2016

面向对象设计实践指南Ruby语言描述

问题比答案更重要, 下面的章节中会关注与问题的提问,然后才是问题的解决方案

面向对象设计

  1. 设计的目的(要解决的问题)
    1. 应对变化
    2. 变化困难的原因(建立了太多的依赖关系,对周围的环境期望太多)
    3. 设计良好的大型程序必须由设计良好的小程序演变而来, 不存在设计错误小程序演变为设计良好的大型程序(当然大量的重构和重新设计也不无可能,然而存在的可能性太小)
    4. 实用的设计不回去预测未来将要发生什么
    5. 设计的目的时为了将来进行设计(应对变化), 首要目的: 降低变化所来成本
    6. 工程师的目的:权衡 现在设计、 现在不设计+将来改动成本,之间的权重比较
  2. 设计的工具
    1. solid原则(Single Responsibility、Open-Closed Principle、Liskov Substitution、Interface Segregation、 Dependency Inversion、 DRY)
    2. 设计模式
  3. 设计实践
    1. 设计失败的原因:没有足够的经验和设计反思(不懂设计、懂设计但是讲过多的设计套用)
    2. 设计的时机: 反复的应用,不进行大规模的预先设计,设计应该发生在项目的过程中,不断的迭代的一个过程
    3. 设计评判:1. 设计需要代价 2. 设计的盈亏点依赖于工程师(时间表、能力)

      实践是检验真理的唯一标准, 实践会弄脏你的双手、是充满选择的。

      面向对象设计问题是----------需求的变化。
      面向对象设计本质是----------依赖关系的管理
      警示:小的坏的程序不太可能形成好的大的程序(当然可以通过重构来实现, 但是不应该期待谁能重构它)
      

设计单一职责

  1. 如何确定一个类是否具有单一职责

    将类的每个方法都改述为一个问题(例如:齿轮请问你的直径多大) 2. 尝试使用一句话描述方法做的事情,这件事情的描述应该很简单。而不是需要使用 “和” 这样的字眼。

  2. 解决问题的方法
    1. 依赖行为而不是数据(变量) // ruby中对这样的支持非常好, att_accessor 等元编程
    2. 隐藏数据结构
      # 代码示例
      class Example
         attr_reader :data
         # data 的数据结构应该为[[one, two], [three, four]]
         def initialize(data)
             @data = data
         end
      
      
         def sum
             data.inject(0) do |sum, item|
                 sum += item[0] * item[1]
             end
         end
      end
       # 重构之后的代码 (这里使用了额外的类,来隔离具体的数据结构)
      class Example
         attr_read :data
         def initialize(data)
             @data = data.map do |item|
                 Infer.new(item[0], item[1])
             end
         end
         def sum
             @data.inject(0) &:product
         end
      end
      
      class Infer
         attr_reader :one, :two
         def initialize(one, two)
             @one, @two = one, two
         end
      
         def product
             one * two
         end
      end
      
  3. 将外在的依赖关系尽量的隔离开来

    将经常重复的代码封装,将对外在的依赖尽量隔离在一个地方(建立单独的方法,统一隔离对外在的依赖关系,思想类似与 依赖方法而不是数据)

管理依赖关系

消息的三中类型, 1. 自身实现 2. 继承 3. 依赖(指代发送消息)

  1. 问题: 什么是依赖关系(个人认为3. 4 没有太多用处,虽然ruby中可以广泛的应用hash传递参数,但是依然没有避免参数的依赖)
    1. 另一个类的名字。 代表自身期待另一个类的存在
    2. 消息的名字。
    3. 消息所需要的参数。
    4. 参数的顺序
  2. 解决依赖关系的方法
    • 注入依赖关系(依赖注入)
        class Gear
            attr_reader :chainring, :cog, :rim, :tire
            def initialize(chainring, cog, rim, trie)
                @chaining = chainring
                @cog = cog
                @rim = rim
                @tire = tire
            end
      
            def gear_inches
             ratio * Wheel.new(rim, tire).diameter
            end
        end
        Gear.new(10, 10, 10, 10).gear_inches
      
        # 重构之后代码
         class Gear
             attr_reader :chainring, :cog, :wheel
             def initialize(chainring, cog, wheel)
                 @chainring = chanring
                 @cog = cog
                 @wheel = wheel
             end
             def get_inches
                 ratio * wheel.diameter
             end
         end
         Gear.new(10, 10, Wheel.new(12, 10)).gear_inches
      
    • 隔离脆弱的外部信息
       def gear_inches
           ratio * wheel.diameter
       end
      
       def gear_inches
           // ......
           wheel.diameter
           //......
       end
       # 现在对wheel.diameter的引用嵌入在一个复杂的应用过程中, 这样做会变得更加脆弱
       def gear_inches
           //.....
           diameter
           //.....
       end
      
       def diameter
           wheel.diameter
       end
       #移除依赖关系,并将其封装在自己的某个方法中
      

      当一个类包含了对某个可能发生变化的消息的嵌入引用时,这样的技术变得非常有用。另一种方法为:将依赖关系反转(后面)

  3. 为什么需要管理依赖关系:
    1. 依赖关系是可以被改变的(通过函数的参数改变)
    2. 依赖关系的方向的选择会对未来的变化产生影响
  4. 选择依赖方向:
    1. 有些类比其他类更容易发生变化
    2. 具体类比抽象类更容易发生变化(例如ruby中方法参数建立在抽象上,java建立在具体类上)
    3. 更改拥有多的依赖关系的类会造成广泛的影响

      依赖于那些变化情况比你所做的更改还要少的事情

  5. 总结:

    对依赖关系的管理时创建面向未来的应用程序的核心。

    1. 依赖注入 可以建立松耦合的依赖关系
    2. 依赖于抽象 可以降低面对变化的可能行(建立在更高层次的抽象上)
    3. 管理依赖的关键时管理依赖的方向(依赖于更不容易变化的对象)

创建灵活的接口(类里面的接口)

  1. 什么是接口
    1. 暴露了其主要职责
    2. 期望被其他对象调用
    3. 不容易改变
    4. 对其他依赖它的对象来说是安全的
    5. 在测试里面被详尽描述的

      找出并定义公共接口是一种艺术,它呈现出一种设计挑战,因为这里没有现成的规则可以使用。并且很难从错误中学习

  2. 更好的找出接口

    关注消息,而非领域对象。绘制时序图来明确消息的传递,并提问做什么,而不是如何做!
    使用时序图,描述对象之间的消息,来确定类之间需要暴露的接口,从而创建更小的接口,更最小上下文的依赖关系。松耦合的设计。
    总结:
    应用程序有消息来定义而成, 而非类。而设计时的提问方式为 “我需要什么,谁来做” 而不是 “告诉接收者如何做”,
    使用时序图来暴露消息。

使用鸭子类型技术降低成本

  1. 使用ruby的鸭子类型设计,可以创建出灵活、更结构化的设计,也可能创建出混乱不堪的设计
  2. 具体化与抽象化之间的取舍,是面向对象设计的基础内容
  3. 识别鸭子类型,kind_of? is_a? responds_to?
  4. 记录鸭子类型(做好测试)

通过继承获得行为

  1. 继承的核心为:自动的消息委托
  2. 建立继承的时机以及方法: 在需要第三种同种类型的时候(在拥有更多的抽象信息的时候), 应当尽力推迟继承的抽象。
  3. 继承抽象的方法: 从下往上, 先将方法下降至具体类型,抽象出更高层次的继承体系,不断的重构,将方法提升(将方法下降到具体层次的代价更高)
  4. 设计的决策(或者任何事情的): 1. 错误成本, 2: 实现成本。 抽象继承的方法采用 从下向上的原因在于从上往下的成本过高。
  5. 使用模板方法有助于解耦合父子类之间的关系,但是也仅仅限于父子两个层次之间的关系。(这样的解决子类的初始化问题,在于,Ruby的设计问题,不应该使用这样的方法给语言打补丁)