Yingkui

Yingkui

Solving Problems. Making Progress.

09 Jul 2015

一切从简

Skinny Everything

最重要的应该是readable,而不是cool或者为了简化而简化,你的目的是减少脑力消耗(思考量)

一切从简的好处:

工程角度: 减少程序的复杂度,减轻工作量(这是从数学角度,也就是计算量的缩减)

商业角度: 节约时间,降低成本(这是从神经科学角度,也就是工作量的减轻,大脑计算量的缩减)

数学角度:整体复合方式(模块化),抽象的方式。

工作量角度:工具化类比,图像化形象

一切从简的四个方法:

1. 避免反复解决那些反复出现的问题

1, Don’t repeat yourself.

把出现多次的重复代码整理起来。

例子:

# ==================================================================#
# ========== An example of making codes dry and skinny =============#
# ==================================================================#


# =============================================================== #
# ================== 1, Django's controller ===================== #
# =============================================================== #

def show
  product = Product.find params[:id]
  render 'products/show.html', variables: { product: product }
end

# ================================================================ #
# ================== 2, set default directory ==================== #
# ================================================================ #

# set default folder to 'products/', set default file extension to html
def show
  product = Product.find params[:id]
  render :show, variables: { product: product }
end

# ==================================================================== #
# ================== 3, remove variables argument ==================== #
# ==================================================================== #

# use instance variable instead of passing variables argument
def show
  @product = Product.find params[:id]
  render :show
end

# ======================================================= #
# ================== 4, use callback ==================== #
# ======================================================= #

# use before_action callback to do this repeated task.
before_action :set_product

def show
  # product = Product.find params[:id]
  render :show
end

def index
  # product = Product.find params[:id]
  render :index
end

def edit
  # product = Product.find params[:id]
  render :edit
end

private

def set_product
  @product = Product.find params[:id]
end

# ============================================================ #
# ================== 5, use function name ==================== #
# ============================================================ #

# use function name (show) to call render method
def show
end

# ============================================================ #

# THE END: write nothing
# Note: This is the rails way to deal with normal controller actions.

极端的假设:如果一段代码,需要你反复写一千次,你会怎么减轻自己重复敲代码(甚至是重复思考和解决)的痛苦。

更多例子:

1,Array counting

# ============================================ #
# ================== DRY ===================== #
# ============================================ #

# how many orders you have that has a total amount larger than $100
def expensive_orders_count
  self.orders.count { |order| order.total > 10000 }
end

# how many people you have referred have made their first orders.
def success_refers_count
  self.refers.count { |refer| refer.orders.count > 0 }
end

# array helper
class Array
  def count block
    counter = 0
    self.each { |a| counter ++ if block a  }
    counter
  end
end

# ============================================ #
# ================== WET ===================== #
# ============================================ #

# how many orders you have that has a total amount larger than $100
def expensive_orders_count
  counter = 0
  self.orders.each { |order| counter += 1 if order.total > 10000 }
  counter
end

# how many people you have referred have made their first orders.
def success_refers_count
  counter = 0
  self.refers.count { |refer|  counter += 1 if refer.orders.count > 0 }
  counter
end

2,jQuery selector

# dry codes
$form = $('#add-item-form')

$form[0].reset()
$form.find(".success").removeClass("success")
$form.find(".success-message").remove()
$form.submit()

# wet codes
$('#add-item-form')[0].reset()
$('#add-item-form').find(".success").removeClass("success")
$('#add-item-form').find(".success-message").remove()
$('#add-item-form').submit()

2, Convention over configuration.

把大多数人在大多数情况下所采取的设置,定为默认设置。

例如,自动生成数据库里table的名字:

# ===================================================== #
# ================== Configuration ==================== #
# ===================================================== #

# models/user.rb
User.DATABASE_TABLE_NAME     = "users"

# models/post.rb
Post.DATABASE_TABLE_NAME     = "posts"

# models/product.rb
Product.DATABASE_TABLE_NAME = "products"

# ================================================== #
# ================== Convention ==================== #
# ================================================== #

# models/base.rb
BaseModel.DATABASE_TABLE_NAME= self.class.name.pluralize.lower

3, 抽象出一个处理通用逻辑的基类 (特殊的DRY)

例如,一个RESTful API的基类

class API::BaseController

  def create
    create_resource(params)
    render :show, status: :created
  end

  def read
    @resource = resource_class.find params[:id]
    render :show
  end

  def update
    @resource = resource_class.find params[:id]
    @resource.update params
    render :show
  end

  def destroy
    @resource.destroy
    render :no_content
  end

end

2. 把问题归类成相互独立的几部分,各司其职

1, 函数要短,最好五行以内,尽量不超过20行

把程序分解成函数主要有三个目的:

1,反复使用相同的逻辑,也就是第一节讨论的话题

2,提高逻辑的抽象等级(raise the level of abstraction),以便更好的理解

如果一个函数里有多个等级的逻辑混杂在一起,就会使得程序更加难以理解(受到污染,变得不纯净)

3,把复杂的问题切割成更好处理的小块,也就是第三节将要讨论的话题

这样做的好处是,便于程序的理解、测试、debug以及保养

在一个函数里,应该:

1,只表述有别于其他任何函数的逻辑

2,只表述同一层面的逻辑

3,只做一件事,并把它做好

举例说明,一个要把大象装冰箱的函数

# ================================================== #
# ================== Fat Codes ===================== #
# ================================================== #
def put_elephant_into_fridge
  # open
  @user.hand.hold @fridge.door
  @user.hand.pull "10cm"
  @user.hand.clear
  # put
  @user.hand.hold @elephant
  @user.hand.up "30cm"
  @user.body.move "forward", by: "30cm"
  @user.hand.down "30cm"
  @user.hand.clear
  # close
  @user.hand.hold @fridge.door
  @user.hand.push "10cm"
  @user.hand.clear
end

# ======================================================= #
# ================== Polluted Codes ===================== #
# ======================================================= #

def put_elephant_into_fridge
  # open
  @user.hand.hold @fridge.door
  @user.hand.pull "10cm"
  @user.hand.clear
  # put
  @user.put @elephant, into: @fridge
  # close
  @user.hand.hold @fridge.door
  @user.hand.push "10cm"
  @user.hand.clear
end

# ===================================================== #
# ================== Skinny Codes ===================== #
# ===================================================== #
# this is super easy to understand,
# it only has three lines, each line means a clear step
def put_elephant_into_fridge
  @user.open_door @fridge.door
  @user.put @elephant, into: @fridge
  @user.close_door @fridge.door
end

# ===================================================== #
# ================== Better Codes ===================== #
# ===================================================== #

# open/close fridge is a common task.
# this piece of logic is shared by multiple functions.
# so it should have its own function.
# by this way, we reduced 11 lines to one line.

def put_elephant_into_fridge
  operate_fridge { |user, fridge| user.put @elephant, into: fridge }
end

def grab_coke_from_fridge
  operate_fridge { |user, fridge| user.grab @coke, from: fridge }
end

def operate_fridge &block
  @user.open @fridge.door
  block.call @user, @fridge
  @user.close @fridge.door
end

2, 一个类要短,并且只做一类事

类所要追从的是Single Responsibility Principle, 也就是说:

“There should never be more than one reason for a class to change.”

更直接的,一个类只做一类事,多了不干。木匠做家具,瓦匠砌墙。

举例说明, 比如在一个卖地皮的网站上,要标注面积和价格信息

/*
  # ================================================== #
  # ================== Fat Codes ===================== #
  # ================================================== #
*/
square_sides_length = 20
price_in_cents = 3000

console.log("Area: " + square_sides_length ** 2)
console.log("Price: " + "$" + (price_in_cents / 100).toFixed(2))


/*
  # ===================================================== #
  # ================== Skinny Codes ===================== #
  # ===================================================== #
*/
square_sides_length = 20
price_in_cents = 3000

console.log("Area: "  +  Math.Square.calculate_area(square_sides_length));
console.log("Price: " +  Currency.cents_to_string(price_in_cents));

第一个方案的职责是输出数值,但它又承担了计算正方形面积和生成货币字符的任务.

第一个方案虽然比较快,但是污染了类的职责,其主要弊端在于:

不便于理解程序,在这个层面上,程序员只需要知道这是在计算面积,而无需理解计算面积的方式.

3, 宏观的模块化,每一大块负责一个具体的任务

不在一个文件里处理两个模块的问题,彻底把责任分开。例如,在html里:


<!--
  # ================================================== #
  # ================== Fat Codes ===================== #
  # ================================================== #
-->
<!-- it has too much responsibilities, 3 rather than 1 -->
<div class="post" style="background-color: red" onclick="sayHello()"></div>

<!--
  # ===================================================== #
  # ================== Skinny Codes ===================== #
  # ===================================================== #
-->
<!-- post.html  -->
<div class="post"></div>

<!-- post.css  -->
.post {
  background-color: red
}

<!-- post.js  -->
$(".post").click(function(){sayHello()});

两个更加具体的例子:

1,Model-View-Controller

# ============================================== #
# ================== Model ===================== #
# ============================================== #

Tweet:
  id:, user:, text:
  url: -> "/#{user.name}/tweets/#{id}"

# ============================================= #
# ================== View ===================== #
# ============================================= #

.tweet
  .left.thumb
    = @user.thumb.url
  .right
    .name
      = @user.name
    .text
      = link @tweet.text, to: @tweet.url

# =================================================== #
# ================== Controller ===================== #
# =================================================== #

TweetsController:
  show: ->
    @tweet = Tweet.find params.id
    @user  = @tweet.user

2,HTML-CSS-JavaScript

# ========================================================= #
# ================== HTML / Structure ===================== #
# ========================================================= #

.tweet
  .left.thumb
    = image user.thumb.url, height: "50px"
  .right
    .name
      = user.name
    .text
      = link tweet.text, to: tweet.url
  .message


# ===================================================== #
# ================== CSS / Styles ===================== #
# ===================================================== #

# generic
.left, .right
  display: inline-block

# tweet
.tweet < .card

  .message
    color: green
    padding: 8px
    resize: (event) ->

      # based on properties
      if self.text.count > 10
        self.padding = "15px"

      # based on events
      if event == "click"
        self.padding = "20px"


# ============================================================ #
# ================== Javascript / Events ===================== #
# ============================================================ #

# locate layers
layers = $ ".tweet"

# how to response to events
layers.click -> message.text "You clicked the tweet!"
layers.on "fancy event", (event) ->
  name = self.find ".name"
  name.text "I am fancy #{event}!"

4, 任何包含逻辑的文件都应该短,任何文件夹(根目录下)里的文件也应该少

任何手工写的文件,或是要人读的文件都应该非常简短,最好限制在200行以内

这样其他人阅读的时候可以减少压力感(avoid overwhelming feeling),提高理解程序的效率。

Less is more,写的短,往往意味着写的精简,不罗嗦,不重复。

除了单一文件外,文件夹根目录下的文件数量也要少(不多于10个?).

只把最核心的文件放在根目录下,其他的文件分类整理到文件夹里,这样便于其他人阅读和查找。

3. 把问题细化成较为简单的小问题,逐个突破

这是最简单的一个原则.

1, 把特别长的文件,切割成多个短小的文件。例如,

一个网页的主页有三个部分,把三个部分的逻辑分别放到不同的文件:

views/
  ├── home/
  |   ├── hero.html
  |   ├── how-it-works.html
  |   └── press.html
  └── home.html

2, 把包含特别多文件的文件夹,整理到少数几个文件夹里,例如

把放字体的文件夹整理起来:

# ================================================== #
# ================== Fat Codes ===================== #
# ================================================== #
fonts/
  ├── futurastd-condensed-webfont.woff
  ├── futurastd-condensedbold-webfont.woff
  ├── futurastd-condensedboldobl-webfont.woff
  ├── proximanova-bold-webfont.woff
  ├── proximanova-light-webfont.woff
  ├── proximanova-semibold-webfont.woff
  ├── proximanova-regular-webfont.woff
  ├── raleway-bold.woff
  └── raleway-regular.woff

# ===================================================== #
# ================== Skinny Codes ===================== #
# ===================================================== #
fonts/
  ├── futurastd-condensed/
  |   ├── futurastd-condensed-webfont.woff
  |   ├── futurastd-condensedbold-webfont.woff
  |   └── futurastd-condensedboldobl-webfont.woff
  ├── proximanova/
  |   ├── proximanova-bold-webfont.woff
  |   ├── proximanova-light-webfont.woff
  |   ├── proximanova-semibold-webfont.woff
  |   └── proximanova-regular-webfont.woff
  └── raleway/
      ├── raleway-bold.woff
      └── raleway-regular.woff

在写这个文章的过程中,为了更好的放置不同的文件夹,我设计了一个针对GUI的框架(framework):

Scenarios based framework for GUI softwares

Scenarios based framework for GUI softwares

这个框架方案的理念就是:把写程序当成是按照剧本(设计图稿)拍电影

Scenario指的是场景,Scene指的是场景里的一个画面

为什么会出现scenario?是因为数据的相似性,也就是信息出现的频率,还是计算缩减的一部分。

计算缩减包括了完整信息的缩减,也包括了模糊信息的缩减(所谓模糊就是指的,信息的外延,变形,但是变得不是很大)

接下来举例说明。

这个是程序主目录的列表,(借鉴Rails的设计)

twitter/
  ├── app/
  ├── bin/
  ├── config/
  ├── lib/
  ├── log/
  ├── test/
  └── vendor/

其中,app是和这个软件直接相关的逻辑

app文件夹里:

app/
  ├── models/
  ├── controllers/
  ├── helpers/
  └── views/

按照MVVMC的设计理念分为四个部分,其中helpers就是View Model(把view里重复的逻辑放到helpers里)

model文件夹里:

models/
  ├── generic/
  ├── vendor/
  |   ├── stripe.rb
  |   ├── shopify.rb
  |   └── instagram.rb
  ├── user/
  ├── product/
  |   ├── properties.rb
  |   ├── relationships.rb
  |   └── callbacks.rb
  ├── user.rb
  └── product.rb

值得注意的是:1,把一个大的class分解成小的部分,2,把相近的model归纳到一个文件夹里

接下来进入主题

对于与views相关的:view,controller和helper,它们都是按照scenarios的不同来建立文件夹。

其中generic就是多个scenarios共用的逻辑。

app/
  ├── models/
  ├── controllers/
  |   ├── generic/
  |   ├── auth/
  |   ├── checkout/
  |   └── site/
  ├── helpers/
  |   ├── generic/
  |   ├── auth/
  |   ├── checkout/
  |   └── site/
  └── views/
      ├── generic/
      |   ├── styles/
      |   ├── events/
      |   ├── layouts/
      |   └── scenes/
      ├── auth/
      ├── checkout/
      └── site/
          ├── styles/
          ├── events/
          ├── layouts/
          └── scenes/
              ├── home/
              |   ├── hero.html
              |   ├── how-it-works.html
              |   └── press.html
              ├── home.html
              ├── about-us.html
              └── contact.html

这样整理的优点在于:

1,Consistency is king。这样的结构相对比较清晰,便于理解和查找逻辑

2,同一场景里的多个画面,许多设计元素以及需要处理的动作是非常接近的(甚至是一致的)

应该说,正因为这些设计元素和需要处理的动作非常接近,我们才归纳这些画面为一个场景

3,用户操作时,同一场景的多个画面是连续的,有利于将程序切割成相互独立、关联性较小的模块

4. 突出重点,消除杂音

这个原则的目的是,降低阅读程序的压力,用简短清晰的符号来表达逻辑。

也就是,Reduce learning time, Remove distractions, Avoid stress.

晕头假设:假设你现在比较迷糊,却要在短时间理解一个程序,如何才能让这时的你更好的理解程序

1,同一等级的逻辑占用同样行数的代码,最好都是一行,例如:

# clear way
checkout: ->
  if order.charge.
    send_order_email.
    success_alert "You are awesome!"
  else
    error_alert "Error!"

# distracting way
checkout: ->
  if order.charge.
    send_order_email.
    AlertViewController.presentAlert
      self,
      title: "Success"
      message: "You are awesome!"
      actions: ["close"]
  else
    AlertViewController.presentAlert
      self,
      title: "Error"
      message: "Error!"
      actions: ["close"]

第二个策略的问题在于,喧宾夺主,alert并不是逻辑主要解决的问题,却要抢夺大量的注意力。

2,在不引起歧义的前提下,尽量减少字符数(对常用函数,使用4-7个字符的单一单词最佳)

例如:

# clean
UserMail.welcome(current_user).deliver

# verbose
MSEmailController.SendEmailMessage(currentUser.emailAddress,
                                   withTemplateNamed: welcome,
                                   withLocalVariables: { user:  currentUser })

# ugly
mail = MSMail.new

mail.addTo(currentUser.emailAddress)
mail.setSubject('Welcome to Our Site')

template = MSTemplate.new(locateTemplate('includes/mail/user-welcome.php'))
template.setVar('userName', $currentUser.name)
bodyStr = template.render

mail.setHTMLBody(bodyStr)

mail.send

最后一个方案的问题还在于:没有把责任分担给其他的类。

在这个场景下,唯一要表达的信息就是”给用户发一封欢迎邮件“,具体怎么发的(尤其是发邮件的逻辑),那是其他类需要解决的问题。

3,选择或设计一个减少键盘敲击的编程语言

例如:python, ruby,scala, coffeescript

结束语

结束语

写程序的时候,无需从一开始就严格的按照简洁的方法写,而是先想着实现,然后再整理 (Make it work, then make it better)。也就是,

第一步:Get it to work - 第二步:Make it skinny

第一步的快感是实现目标的成就感,目的是做出雏形。第二步的快感是清晰简洁的美感,目的是成品上市。

2018年更新:

1,笨拙实现。通过穷举的方式,以最为笨拙的方式进行实现,也就是在最底层的方式,混乱的,每一个concern纠缠的形式实现

而且我推荐初学者如此去做(当然也要掌握好分寸,既不能让初学者丧失了解决问题的乐趣,又不能让人失去了解决底层问题的能力)

2,分块整理。把各个concern通过文件或者文件夹的方式割裂开来,让每一个文件尽可能的小一些,所知道的信息少一些,以便于区分职能,各个突破。

3,减少重复。DYI的过程,把重复出现的信息用一个函数也好,把大家约定俗成的用一个函数实现

但是问题是:不要过分的依赖DYI,因为不仅仅是计算量的减少,还要兼顾工作量的减少,也是就说,要考虑到人的因素。

如果DYI不能显著的增加计算量,却使得工作量,无论是新人(自己长时间不看,也算新人)的学习,或者是文档的数量,都变得更加长和抽象,那人理解起来的时间增加,那么在工程上,就增加了工作累,毕竟人是程序员,而不是机器编程,故而,要必要时缩减。

4,避免 magic number。定义一个变量,而不是定义一个奇怪的突兀的数字,这个的好处在于,人的因素,大家思考问题的时间减少了,让人一目了然,清晰明了,也是节约了工作量