about 5 years ago

昨天在 Happy Designer 5 上,有聊起了做 Responsive Web Design (特別是 mobile 版)的一些狀況。在這裡我提供一些自己的經驗和技巧:

  1. Mobile First:如果你要做 Desktop / Mobile 雙版本的網站。先設計 Mobile 版的,而且要從最小的尺寸開始做。

  2. 盡量讓 Mobile 的版本:大部分功能保持唯讀狀態。因為在 Mobile 上要將「輸入」這件事做得好,是很困難的,而且不少的寫入會牽涉到動線及頁面的跳轉,會大幅降低 user experience。雖然某些功能和元素幹掉很可惜,但要懂得「取捨」。

  3. 不要過於迷信 Media Query: media query 只能解決 CSS 的問題。但是 mobile client 上需要解決的問題不只是這些:

  • 內圖的尺寸與傳輸速度:圖可能是用 Desktop version 的圖硬縮的。
  • 3rd party social plugin:3rd party social plugin 多半沒有考慮 mobile 版本的問題。所以在 mobile 版面上,幾乎都會有尺寸問題。而這些 social plugin 的 js 因為都是由放置在外國的原站提供,在 mobile 版上 loading 更加的緩慢。

所以我會採取「偵測 agent 」與 media query 混合使用的招數進行開發。如果這個 agent 確定是手機,則直接移除掉 social 功能(直接從 server side 幹掉 DOM,而不是用 CSS 隱藏),並且吐尺寸較小的圖片。

  1. 我個人還是認為 Responsive 的用途比較適合用在 Tablet 的 Portrait / Landscape 的用途上。畢竟 Responsive Web Design 的想法,是讓 Desktop / Mobile 擠在同一個版面上,用同一份 code。再 conditional override 或者 remove。在需要「持續維護」的產品上,容易搞死 Developer…

P.S. 如果你對 Responsive Web Design 好奇,可以用手機打開 T 客邦 以及 Digiphoto ,這個站都是用此技巧設計的…

===

廣告:二月份 xdite 家桌遊團 又開放了,限八人,請儘速報名。

 
about 5 years ago

I had a ruby project named "bootstrap_helper", which has 43 watchers on Github.

螢幕快照 2012-02-09 下午4.09.51

I thought having 43 watchers is sort of popular. But there is a weired thing: when I search keyword : "bootstrap helper", my gem is not in first page result.

螢幕快照 2012-02-09 下午3.59.56

Then I found I made a hugh mistake: Ruby naming convention told Ruby developer to name everything with underscore. So every developer names their projects with underscore.

But it's a giant SEO mistake: Search Engine treats snake_keywords as CamelKeywords. If your project's name contains meaningful keywords, it won't be matched. Because "snake_keywords" means "SnakeKeyword" not "snake keywords".

It's really bad for SEO.

 
about 5 years ago

Twitter 的 Bootstrap 是一套很好用的 CSS Framework。可以讓開發者加上工具類 CSS 如 .btn,就設計出一個按鈕。

但有時候這東西也像 inline style 一樣討厭,比如說我要設計一排「按鈕] link,就非得每行都加個 class = "btn"

<div class="job-options">
  <span class="option-title"> 工作類別: </span>
  <%= link_to("網站設計師", jobs_path+"?m_type=1", :class=>"btn ") %>
  <%= link_to("美術設計師", jobs_path+"?m_type=2", :class=>"btn ") %>
  <%= link_to("手機APP開發", jobs_path+"?m_type=3", :class=>"btn ") %>
  <%= link_to("市場行銷", jobs_path+"?m_type=4", :class=>"btn ") %>
  <%= link_to("社群管理", jobs_path+"?m_type=5", :class=>"btn ") %>
  <%= link_to("其他", jobs_path+"?m_type=0", :class=>"btn ") %>
</div>

我在開發時並不是使用 Twitter 自己提供的 LESS 版本,而是採用人家拆好的 SCSS 版本 anjlab/bootstrap-rails

如何翻修這樣的 code 呢?我使用了 SCSS 的 @extend。

.job-options{
   padding-bottom: 10px;
   a{
      @extend .btn;
   }
}
<div class="job-options">
  <span class="option-title"> 工作類別: </span>
  <%= link_to("網站設計師", jobs_path+"?m_type=1") %>
  <%= link_to("美術設計師", jobs_path+"?m_type=2") %>
  <%= link_to("手機APP開發", jobs_path+"?m_type=3") %>
  <%= link_to("市場行銷", jobs_path+"?m_type=4") %>
  <%= link_to("社群管理", jobs_path+"?m_type=5") %>
  <%= link_to("其他", jobs_path+"?m_type=0") %>
</div>

這樣就可以把 .btn 從 HTML 裡面拿掉了。

其他

其實表格也可以比較辦理...

#feeds-list{
   @extend .zebra-striped;
}

====

不過不能 span2 這種定位的 class 不能 @extend。@extend bootstrap 這招只能用在與「位置」無關的 style 上。

 
about 5 years ago

在 Rails 開發中,有時候我們會遇到要在表單中設計 select 的選項。select 吃的 collection 是個 Array 。通常我們會往往第一直覺的想法是將之塞到 model 的 CONSTANT 裡,再寫一個 class method 包裝起來,再寫自己的 collection Helper 叫出來。

<%
def my_collection_select(title, target_id, default_val, objs)
  html = '<div class="clearfix"><label for="normalSelect">'+title+'</label><div class="input">'
  html += '<select name="'+target_id+'" id="normalSelect">'
  objs.each do |obj|
    selected = (default_val.to_s == obj[:downcase].to_s) ? 'selected="selected"' : ''
    html += '<option '+selected+'value="'+obj[:downcase].to_s+'">'+obj[:titleize]+'</option>'
  end 
  html += '</select></div></div>'
  return raw(html)
end

%>

<%= my_collection_select("職務類別", "job[job_category]", @job.job_category, Job.categories) %>
class Job < ActiveRecord::Base
#職務類別


CAT_OTHER                   = 0   #其他

CAT_WEB_DEVLOPER            = 1   #網站設計師

CAT_DESIGNER                = 2   #美術設計師

CAT_PHONE_APP_DEVLOPER      = 3   #手機APP開發

CAT_MARKETING_SALES         = 4   #市場規劃 & 業務

CAT_WEB_SOCIAL_MANAGER      = 5   #社群管理


def self.categories
  [
      {:downcase=>CAT_OTHER, :titleize=>'其他職缺'},
      {:downcase=>CAT_WEB_DEVLOPER, :titleize=>'網站設計師'},
      {:downcase=>CAT_DESIGNER, :titleize=>'美術設計師'},
      {:downcase=>CAT_PHONE_APP_DEVLOPER, :titleize=>'手機APP開發'},
      {:downcase=>CAT_MARKETING_SALES, :titleize=>'行銷&業務'},
      {:downcase=>CAT_WEB_SOCIAL_MANAGER, :titleize=>'社群管理'}
  ]
end

def category_str
   Job.categories.each { |item| 
     return item[:titleize] if item[:downcase] == self.job_category 
   }
end

會這樣設計的原因是:通常程式設計師會想要對一個值 assign 一個數字,又想要用一個英文字包裝它,以方便取用。

這樣設計的手法很常見,但其實這樣的設計一聞下來就有「壞味道」。我自己也是思考了這個問題好幾年,換了非常多寫法,直到最近終於想出一個比較好的方式去設計 select。

翻修

我設計出一個比較漂亮的方式去改寫這樣的 code。當中用到了 settings_logicsimple_form 這兩個 gem。

Simple Form

<%= f.input :job_category, :label => "職務類別" do %>
    <%= f.select :job_category, Job.categories %>
<% end %>

SettingsLogic

app/models/job_data.rb
class JobData < Settingslogic
  source "#{Rails.root}/config/job_data.yml"
  namespace Rails.env
end

把數值塞到 settings

config/job_data.yml
defaults: &defaults
   job_categories:
      other : 0
      web_developer: 1
      designer: 2
      app_developer: 3
      marketing_sales: 4
      web_social_manager: 5
development:
  <<: *defaults

test:
  <<: *defaults

production:
  <<: *defaults

Rails 內建的 I18n

config/locals/job.zh-TW.yml
"zh-TW":
   job_categories:
      other : "其他職缺"
      web_developer: "網站設計師"
      designer: "美術設計師"
      app_developer: "手機APP開發"
      marketing_sales: "行銷&業務"
      web_social_manager: "社群管理"

Model : Job

JobData.job_categories 拉出來會是這樣的內容:

{"other"=>0, "web_developer"=>1, "designer"=>2, "app_developer"=>3, "marketing_sales"=>4, "web_social_manager"=>5}

但 select 要吃的是: [["其他職缺", 0],["網站設計師",1]] 這樣的 Array。所以我們再用 map 去對 I18n 求值包裝。

app/modesl/job.rb
class Job < ActiveRecord::Base

  def self.categories
    JobData.job_categories.map{ |k,v| [I18n.t("job_categories.#{k}"),v] }
  end

end

Helper

最後是如何把 category_str 從 model 搬出來。

這樣很明顯是錯的
  • 這應該是 view 要做的事。
  • 應該善用 Ruby 的特性,而不是跑 each 比較拿數值。
def category_str
   Job.categories.each { |item| 
     return item[:titleize] if item[:downcase] == self.job_category 
   }
end
利用 Ruby 的 Hash 的 key,從翻譯檔裡面拿出正確的中文。
app/helpers/job_helper.rb
  def render_job_category(job)
    key = JobData.job_categories.key(job.job_category)
    I18n.t("job_categories.#{key}")
  end

其他

如果以後想拿數值:

可以這樣下:

  • JobData.job_categories[:designer] => 2
  • I18n.t("job_categories.designer") => "美術設計師"
 
about 5 years ago

前幾天 Code School 的 CSS Cross Country 課程釋出了。這一集是我相當期待的一集,整理和澄清了非常多「非常基礎但如果觀念不好」就會搞得一塌糊塗的 CSS 知識。

看完把筆記整理在這裡,不過相較於原網站,我寫的算是非常簡陋的版本。

我相當強烈推薦大家購買回去和練習,相信會對自己的 CSS 掌握能力有高度的提升。

Ch1

Style 生效的位置

  • inline style
  • <head> </head> 裡面
  • external link: 如 link rel="stylesheet" href="style.css"

Selectos

  • Element selector
  • Class selector
  • ID selector
  • Compound selector : h1#header

Advanced Selector

ref: Taming Advanced CSS Selectors

  • #sidebar h2— 0, 1, 0, 1
  • h2.title — 0, 0, 1, 1
  • h2 + p — 0, 0, 0, 2
  • #sidebar p:first-line — 0, 1, 0, 2

cascade order

優先權照

  • external <link>
  • <head>
  • inline style
  • !important
  • 相同的 selector 重複的屬性,後寫的會覆蓋前面的。沒有重複的則會合併。

Float

定義
  • 把元素從傳統的 document flow 中移除,然後浮動掛在指定的邊界上。
  • 其他在 parent 元素的內容會繞著這個 float 的元素排列。
種類
  • float: left
  • float: right
  • float: none
Stacking order
  • 浮動的元素從 parent 的左/右邊界開始排,不夠放的話會找另下一個可以停泊的邊界繼續排
  • 如果浮動的元素高度不同,例如左邊的特別長,旁邊的元素的比他短,則下一個停泊的不會是左邊界,而是會停在左邊元素的右邊。(下一個可以停泊的邊界原則)
floating left & right

同時使用 float:left 與 float:right,寬度夠的話,會分列兩邊。如果寬度不夠,以先 claim 的會是第一排(靠右),後 claim 的會被扔到下一排(靠左)去。

<div>
    <div class="content1"> </div>
    <div class="content2"> </div>
</div>
.content1 {
    float:right;
}
.content2 {
    float:left;
}

Ch2

clearfix

為什麼要使用 cleafix
  • 狀況一:float 元素可能比旁邊的非 float 元素高。(因為 float 沒有高度)所以下一段會黏上來。
  • 狀況二:所有的 children 都是 float。(因為 float 沒有高度)所以 container 看起來會太短。
常用 clearfix 技巧
clear with a subsequent elemet ( 在下面的元素放 clear: both )
  • 但這一招移動 div 順序就會爆炸
  • 背景和邊界不會被展延

失敗!

manual clearing (手動在下面塞一個空的 div,對它下 clear: both)
  • 需要塞一個空的 element
  • 看起來很礙眼
正解技巧 ( self-cleared )
 
.group:before, .group:after { 
   content: "";
   display: table; 
}
.group:after {
   clear: both; 
}
.group { 
   zoom: 1; /* IE6&7 */ 
}
.group 要下在 parent node
<div class="group">
   <div class="float-left"> </div>
   <p> blah </p>
</div>

Inheritance & Specificity

  • 巢狀元素會自動繼承 parent 的 style
  • 可以 override parent 的 style
  • id 比 class 的優先權高
優先權算法

0,0,0,0

  • 第一個數字:inline style
  • 第二個數字:of ID selectors
  • 第三個數字:of class selectors
  • 第四個數字:of element seletors
p { color: #fff; }  # 0,0,0,1
.intro { color: #98c7d4; } # 0,0,1,0
#header { color: #444245; } # 0,1,0,0
<h1 style="color: #000;">Mogul</h1> # 1,0,0,0
p { color: #fff !important; }

越大的可以把祖先蓋掉。

延伸閱讀:[Don’t use ID selectors in CSS]http://screwlewse.com/2010/07/dont-use-id-selectors-in-css/

Ch3

box model

由內往外是

  • content area
  • padding area
  • border area
  • margin area

寬度計算

box width = content width + padding width + border width

overflow

  • visible: the default value, which allows content to extend beyond container boundaries
  • auto: adds a scrollbar as needed when content overflows
  • hidden: hides content that extends beyond the container
  • scroll: adds a scrollbar at all times, even if unneeded

positioning

有四種

  • static
  • relative
  • absolute
  • fixed

定義

  • Elements have a position value of static by default
  • Using a value other than static causes an object to become a positioned element
  • Positioned elements may use the top, left, bottom, and right properties for placement
Relative positioning

Renders in the normal flow, then shifted via positioning properties

Absolute positioning

Takes an element out of the normal flow for manual positioning

定位技巧

parent element 下 relative,內部需要定位的元素下 absolute,就不會超過邊界。

Fixed positioning

Affixes an element to a specific place in the window, where it will stay regardless of scrolling。(在螢幕上永遠保持不動,釘住)

z-index

  • No z-index or equal z-index = overlap determined by placement in DOM (後放的疊在先放的上面)
  • Higher values appear above lower values( z-index 比較大的在比較上面)
  • Elements must be positioned for z-index to take effect. Use relative if you're not interested in moving the object (必須先被「定位」,才能使用 z-index)

延伸閱讀: Understanding CSS z-index

Ch4

整理 CSS code

,
p .content h3{
   color: red;
}
padding/margin 上右下左
.content {
   margin: 15px 10px 0 20px; /* top right bottom left */ }
}

其他綜合
   font: 16px/18px bold italic sans-serif; /* size/line-height weight style family */
   background: #000 url(image.jpg) no-repeat center top; /* color image repeat x-pos y-pos */
   list-style: disc inside none; /* style position image */
   margin or padding: 0 10px 0 10px / 0 10px 0 / 0 10px; /* top right bottom left / top right&left bottom / top&bottom right&left */
   border: 3px solid #ccc; /* width style color */

Display types

block
  • Stretch the full width of their container ( container 有多寬就延伸到多寬)
  • Behave as though there is a line break before and after the element (有斷行效果)
  • Full box model can be manipulatedDisplay Types

Block elements: Tags that are block-level by default: <div>, <p>, <ul>, <ol>, <li> and <h1> through <h6>.

inline
  • Typically found within block-level elements (通常可以在 block 元素裡找到)
  • Only take up the space of the content inside (只有內容的寬度)
  • Do not generate a line break before and after the content (沒有斷行效果)

Tags that are inline by default include <span>, <a>, <em>, <img>

inline-block
  • Same flow as an inline element but behave as a block element (可以玩 box model)

水平置中技巧

Centering a block-level element
  • Define a width, and the element width must be less than that of the parent container
  • margin: 0 auto;

Centering inline and inline-block elements

  • text-align: center

Ch5

margin 重疊問題

多個 margin 疊在一起,會有 margin 重疊問題( margin 會合併計算,取最大的。假設第一個 div margin-bottom: 40px,第二個 div margin-top: 20px。則他們的距離會是 40px ,而不是 60px)。

解決重疊技巧:

Collapsing margins will not occur when one or more block element has:

  • Padding or border
  • Relative or absolute positioning
  • A float left or right

延伸閱讀: Collapsing margins

rest & normalize

Eric Meyer's Reset CSS
Normalize.css

Ch6

Content Image 與 Layout image

  • Content should be marked up as inline images
  • Layout elements should be defined as background images

Image Cropping

<li class="crop">
  <img src="snowboard.jpg" alt="Snowboard" />
</li>

crop {
    height: 300px; 
    width: 400px; 
    overflow: hidden;
}
.crop img {
    height: 300px; 
    width: auto;
}

其實沒有多少好的方法,建議取代方案:

  • Resize images to a square < height and width of all of your images
  • Resize them server-side
  • Provide image-uploading instructions in your CMS

延伸閱讀:Experiments with wide images

Ch7

圖片取代文字技巧

使用 text-indent: -9999px;

.logo {
    background: url(logo.png);
    display: block;
    height: 100px;
    width: 200px;
    text-indent: -9999px;
}

css spirite 技巧

為什麼要使用 CSS spirite

Issues with swapping background images:

  • Adds an extra HTTP request
  • Image is not preloaded (網路不夠快時,hover 感覺畫面會閃一下)

Advantages to the sprite approach:

  • Reduces number of HTTP image requests
  • Removes loading flash / need for preload
使用方法

Multiple images & states:

 .twitter, .github {
     background: url(social.png);
     display: block;
     height: 100px;
     width: 100px;
     text-indent: -9999px;
}
.github {
    background-position: -100px 0;
}
.twitter:hover, .twitter:focus {
   background-position: 0 -100px;
}
.github:hover, .github:focus {
   background-position: -100px -100px;
}

延伸閱讀: Spritecow

Ch8

psuedo class

Allow you to conditionally select an element based on state or position

  • last-child
  • nth-child (an+b)

延伸閱讀:

psuedo element

<article>
   <p>Coffee? Hah! Our cocoa is far better.</p>
   <p>Visit from 4-5 for cocoa happy hour!</p>
</article>
article p:last-child:after {
   content: '\2744';
}

利用 before, after 技巧

取代掉無用 element
<blockquote>
Coffee? Hah! Our cocoa is far better.
<span></span>
</blockquote>

原本是對 blockquote span 下 styling。

改成對 blockquote:before 下 styling。

html 可以砍成

<blockquote>
Coffee? Hah! Our cocoa is far better.
</blockquote>
利用 before, after 實作縮排

延伸閱讀:A Whole Bunch of Amazing Stuff Pseudo Elements Can Do

 
about 5 years ago

在設計需要顏色循環的表格時你會怎麼作?

table

Rails

初入門者

使用兩種不同 HTML class : even 與 odd,上不同的顏色
<% count = 0 %>
<table>
<% @items.each do |item| %>
  <% if count % 2 == 0 %>
    <% css_class = "even "%>
  <% else %>
    <% css_class = "odd" %>
  <% end %>
  <tr class="<%= css_class %>">
    <td>item</td>
  </tr>
  <% count += 1%>
<% end %>
</table>

<% end %>

懂一點 Ruby

Ruby 裡面有 each_with_index,不需要在外部宣告 count
<table>
<% @items.each_with_index do |item, index| %>
  <% if index % 2 == 0 %>
    <% css_class = "even "%>
  <% else %>
    <% css_class = "odd" %>
  <% end %>
  <tr class="<%= css_class %>">
    <td>item</td>
  </tr>
<% end %>
</table>

熟 Rails Helper

Rails 有 cycle 這個 helper,可以做出循環效果
<table>
<% @items.each do |item| %>
  <tr class="<%= cycle("odd", "even") %>">
    <td>item</td>
  </tr>
<% end %>
</table>

熟 CSS

使用 pseudo class 中的 nth-child
<table>
<% @items.each do |item| %>
  <tr>
    <td>item</td>
  </tr>
<% end %>
</table>

用法::nth-child(an+b)

tr:nth-child(2n+1) {
  color: #ccc;
}
tr:nth-child(2n) {
  color: #000;
}

小結

快改用 nth-child,不要繼續在 class 裡面寫 even, odd, one, two, three 了 XD

 
about 5 years ago

Rails 3.2 四天前 Release 了。這次主要的改進幾乎都在效能部分。

最大的改版應該屬於 Route recognition 這部分。原本這部分是由 rack-mount 擔綱,Aaron Patterson (a.k.a. @tenderlove) 將之抽換成他自己寫的 Gem : jounery。速度快了非常多倍。

但相關的原理並沒有 jounery 的 About 頁面並沒有被詳加敘述,

SYNOPSIS: Too complex right now. :(

不過根據有限的線索,我還是從 @tenderlove 的 slideshare 上挖出來了。

jounery 的原理是用 FSM ( Fininte State Machine ) 實做的。有興趣的可以從投影片裡面繼續挖。

其他豆知識:

ActiveRecord 背後的 SQL 生成引擎 Arel 背後原理是用 Relational Algerbra 生成的,可以生成非常複雜的 SQL Query 但又兼顧到效能問題。

 
about 5 years ago

今天看到 Techorange 的 創投包心菜-vc-專欄:創投最害怕的那些人-1 引用了我之前寫的一篇關於創業資金與創業策略的文章: Startup 需不需要一開始注意資金的問題?

剛好晚上香港推友要來台灣一遊,問我家裡有沒有 農家樂 (Agricola) 這套遊戲。突然掉進時光機...

若是 2008 的我,當時應該寫不出這種關於精實創業的文章。我也可能會像其他人一樣天真,「覺得」作任何網站一定要募很多錢,作一個完美的產品,堅持「純真的想法」到最後,就算遭到嚴重挫敗,也會堅信是自己運氣不好。若有機會重來,絕對再次重蹈覆轍。

後來在事業想法和決策性格上有了大轉變,全因為我玩到了一套遊戲 農家樂 (Agricola)。當時我在玩了十場之後,寫下了遊戲感想。

這篇文章被埋在灰塵裡,我覺得很可惜,於是決定再貼一遍。這個遊戲教會我的最重要一件事,就是「只想放完美的大絕,不懂見機行事,絕對會被天譴到死

從農家樂 ( Agricola ) 看 Startup / Website 發展現況與策略

最近迷上打一款 BoardGame:農家樂「Agricola」,玩這個遊戲其實非常有助於我對一些想法的印證,因此花點時間整了下來。

這個遊戲其實並不複雜,也算蠻快結束的。不過新手講解規則可能要很久就是…

大概講解一下遊戲場景:

每個玩家經營一對夫妻,起始發給一片農莊,兩間木屋,兩份食物。隨著季節的演變,玩家可以輪流派人出去執行工作。工作大概會有:犁田、種稻、養牲口、生小孩、蓋房 / 翻修、蓋建築物(主要發展卡、次要發展卡)、學習技能(職業卡)、擷取資源(拿木、磚、蘆葦)…這幾類。

執行動作是大家輪流的,如果有人在你之前先搶先犁田了,這一回合其他人就不能犁田。遊戲分為 14 個月 ( 六季 ),每季結束可以繁殖牲畜、收成、但也要支付勞動所得。值得注意的是,通常如果你季節結束,要是每付不出一份糧食(夫妻二人共要支付四份,以此類推),就會得到一張乞討卡( -3 分),通常拿到乞討卡的人幾乎必輸無疑就是了。

計分標準偏向希望你不能偏廢,少一個達成條件扣一分。但是若達成一個條件,就幾乎加一分以上。最後比計分高低。

( 建議各位有興趣的話,買一套回家玩,大致上就能理解我下面說的是什麼。不過這一套不便宜就是了,要 NT 2620 )

而我非常喜歡這個遊戲的原因,並非最近興起的正夯種田樂。而在於這個遊戲非常考驗玩家隨機的應變能力,以及「不貪心」的忍耐功夫。而這些重點,練起來讓我受益良多…

有這麼誇張?

該怎麼說起呢,我在第一次學習打農家樂時,其實牌拿的不差,但分數卻低的相當難看的…直到回家上了桌遊版,參考了 chenglap 大大的 Agricola 心得,我才體悟到在這款遊戲中犯了哪些基本人性錯誤。

而回去找朋友打,更從他們的每個犯錯的 move 中學到了更多道理。印證到一些網站發展策略,跟幾年來見聞的心得所見不遠,甚至看到更深的面向,更覺得本款遊戲惠我良多。

下面是整理的一些重點:

( 以下並沒有意思對陪我打牌的朋友有不敬之意,純粹只是拿來舉例佐證)

1. 別人上一場獲勝的策略,並非就能成為這一場你的致勝主軸,甚至抄襲會成為害死自己的主因。

農家樂其實是非常注重隨機應變的,通常一個玩家的布局方法會隨著他執行動作的優先順位、場上資源以及 職業 / 次要發展卡而有所變化。有可能這一場我拿的局就適合耕田種菜,但貪心硬要蓋牧場養滿羊反倒害死我自己。

曾經打過一場對手,因為上一局見我生了四個人拿了不少分數(四人 12 分),開局就猛蓋屋生人,結果差一點因為每季糧食湊不到,每到收穫季節就抓襟見肘的局,雖然比其他玩家多一個人,但卻沒多少明顯優勢。

2. 手上拿了資源,就要適時利用。集了滿山滿谷資源,等到終於想建設時,卻毫無用武之地,也是一種浪費。

有朋友拿磚拿上癮,手上握了十幾顆磚。進行到遊戲後面,想說集了很多磚,來蓋點烤爐好了。卻發現因為大家覺得缺磚,早早就搶先蓋好烤爐。結果雖然拿了滿手磚,但完全沒有東西可蓋。為了集磚的 move 可以說完全都被浪費。

3. 搶先卡位以及 perfect combo 未必是正確的道路,甚至是失敗的主因。

農家樂的特色之一就是,職業卡與次要發展卡相當誘人(每個玩家各發給七張)。有一些卡片組合起來威力無窮,但另一些卡片雖誘人(效果強大)但其廢無比(比如說需要集滿 3 個職業以上才可以施展,但那時候已經接近遊戲尾聲,作用並不大)。

玩家可能費盡心機打出 perfect combo,卻忽略到其實賺的資源以及花費的資源遠不成比例。(因為粗估 28 個 move,可能玩家就花了 7 個以上的 move 在施展卡片,4 個以上的 move 在集施展卡片需要的資源。幾乎佔了一半以上。)

4. 基本建設的紮實,穩定的 income,才能健康的成長。

遊戲的 keypoint 就在於糧食匱乏與否(就跟公司現金流 / 獲利程度 )。養人就是要 做事 + 燒錢,然後用人有效的去賺錢。沒那麼多事可做卻亂燒錢,多養人亂開產品線以為沒差,其實搞得企業上吐下瀉。

因此開場注重的是想辦法建立自己的食物引擎(金流 / 產品健康的運作帶來營收),不虞匱乏才可以隨心所欲有效的施展自己的 move。而非一昧猛攻多人多 move 或者是建造無敵牧場(但其實發現自己到最後什麼動物都抓不到,甚至是養了超多動物物卻買不到烤爐結果做不了食物)。

5. 別買華而不實的東西,別鑽研很炫卻加分有限的技術。加分有限,應該投資在「有效」(分數夠多)的行動上。

蓋石屋(要先蓋磚屋才能升級成石屋,此中資源花費多多)其實投資報酬率非常低。木屋夠用就好。

6. 不會馬上回饋但肯定有正面效益的事,在別人不重視時,可以悄悄地進行。有時候甚至是致勝的關鍵。

因為在農家樂規則中,每多一塊空地上面沒建設,就會扣一分。因此像犁田這種普通粗活,前期沒人搶,但後期大家搶著犁(但很可惜,一個月只能有一個人犁)。而其實越早犁田 / 種田 其實是蠻不錯的選擇,可以有效衝高 小麥 與 蔬菜 的數量(但並非絕對)。

7. 回合數有限,效益最大化。

續上,第五季時有一張不錯的行動:「犁田並播種」。這種就是壓一次 move,卻可以同時做兩個 move 的典型範例。但這時候手上有 麥子 / 蔬菜的人,才多半能顯示押這個 move 的威力。同時,後期其實收穫期非常緊密,因此也只有糧食足夠的人可以安心押這個 move。其他人可能因為糧食緊張在頭疼。

其實做網站的時候,我們是可能犯下一些基本的迷思,更甚是通往地獄卻樂在其中。遊戲中的一些貪婪失恆的場景也反應在網路界現實中

比如:

  1. 覺得發展 XX 是未來卡位之道,結果蓄積了一堆能力,最後卻完全用不上。
  2. YY 正夯,成本低廉,便覺得浪費一個 move 也無所謂。
  3. 為了比敵手更搶先,大幅 hire RD 開發新功能 / 炫功能,卻忽略了自己體質或者是現金流並不健康,結果就是 A 做不好,B 也做不好,RD 累到炸。
  4. 看到敵手 F 領先,便花大錢狂投人力抄襲對方,到最後一刻才發現自己體質完全不適合這樣玩,最後搞得自己噴掉。
  5. ZZ 適合大規模環境使用,深信自己用得上,預先砸了一堆 RD 研究。結果完全達不到那個量,浪費成本研發。
  6. 開心的用策略打出連環 combo 殺招,上了一連串活動宣傳、廣告、上功能。結果其實在營收上入遠不敷出。
  7. 金主的耐心與底線以及 founder 的熱情,通常也只有六個季 ( 18 個月)。隨意在任何一個月亂 move ,看似輕忽,其實是一種嚴重浪費…

特別在熬夜打完兩場農家樂後,用僅存一點的清醒意識記之。

======

感想

兩年多前寫的感觸,放到目前還是一點沒變…

其實關於「創業」主題的桌遊,我還寫過一篇 相當歡樂的桌上遊戲「Burn Rate」。比較偏向人力資源使用的。

Burnrate 這篇文章遠遠比 Agricola 那篇文章有名,我也不懂為什麼。大概是因為所謂的 Lean Startup 這個概念最近才比較紅吧。

廣告一下:大年初五我在我家開桌遊團,限八人。歡迎報名。打桌遊和大食團。

 
about 5 years ago

本系列其他文章:

===

Helper AntiPatterns

Helper (輔助方法)的存在目的是用來輔助整理 View 中內嵌的複雜 Ruby 程式碼。設計得當的 Helper 可以加速專案的開發,以及增進程式的可讀性。然而設計不好的 Helper 卻可能造成嚴重的反效果。

以下列舉常見的幾種糟糕的 Helper 設計模式:

1. 矯往過正:用 Helper 作 partial 該做的事

有些開發者以為 partial 效率是低下的,刻意不使用 partial,而使用 Helper 完成所有的動作。就將需要重複使用的 HTML 通通寫成了 Ruby code,串接成 HTML:

def show_index_block(block_name, post, is_show_game)
  
  block_title = content_tag(:h3, block_name)
  section_header = content_tag(:div, block_title, :class => "section-header")
  
  game_name = is_show_game ? "【 #{post.games.first.name} 】" : ""
  title = content_tag(:h4, link_to("#{game_name} #{post.title}", post_path(post)))
  image = content_tag(:div, render_post_image(post), :class => "thumbnail")
  content = content_tag(:p, truncate( post.content, :length => 100))
  section_content = content_tag(:div, "#{title}#{image}#{content}", :class => "section-content")
  
  section_footer = content_tag(:div, link_to("閱讀全文", post_path(post)), :class => "section-footer")
  
  return content_tag(:div, "#{section_header}#{section_content}#{section_footer}" , :class => "article-teaser")
end

Helper 的作用只是協助整理 HTML 中的邏輯程式碼,若有大片 HTML 需要重複使用,應該需要利用 partial 機制進行 HTML 的重複利用。這樣的寫法,非但效率低下(可以用 HTML 產生,卻使用 Ruby 呼叫 Tag Helper,且製造大量 Ruby Object),而且更降低程式的可讀性,其他維護者將難以對這樣的 DOM 進行後續的維護翻修。

2. 容易混淆:在 Helper 裡面穿插 HTML tag

這也是另外一個矯枉過正的例子,不過剛好相反,因為覺得使用 Ruby code 產生 HTML tag 可能浪費效能,而直接插入 HTML 在 Helper 裡面與 Ruby Code 混雜。結果造成維護上的困難。因為 Ruby 中的字串是使用雙引號",而 HTML 也是使用雙引號",,所以就必須特別加入 \" 跳脫,否則就可能造成 syntax error。

錯誤
def post_tags_tag(post, opts = {})
  # ....

   raw tags.collect { |tag|  "<a href=\"#{posts_path(:tag => tag)}\" class=\"tag\">#{tag}</a>" }.join(", ")
end

大量的 " 混雜在程式碼裡面,嚴重造成程式的可閱讀性,而且發生 syntax error 時難以 debug。

錯誤
def post_tags_tag(post, opts = {})
  # ....

  raw tags.collect { |tag| "<a href='#{posts_path(:tag => tag)}' class='tag'>#{tag}</a>" }.join(", ")
end

即便換成 ' 單引號,狀況並沒有好上多少。

正確
def post_tags_tag(post, opts = {})
# ...

  raw tags.collect { |tag| link_to(tag,posts_path(:tag => tag)) }.join(", ")
end

正確的作法應該是妥善使用 Rails 內建的 Helper,使 Helper 裡面維持著都是 Ruby code 的狀態,並且具有高可讀性。

3. 強耦合:把 CSS 應該做的事綁在 Ruby Helper 上。

錯誤
def red_alert(message)
  return content_tag(:span,message, :style => "font-color: red;")
end

def green_notice(message)
  return content_tag(:span,message, :style => "font-color: green;")
end

開發者不熟悉 unobtrusive 的設計手法,直接就把 design 就綁上了 Ruby Helper。造成將來有例外時,難以變更設計或擴充。

正確
def stickies(message, message_type)
  content_tag(:span,message, :class => message_type.to_sym)
end

<span class="alert"> Please Login!! </span>

樣式應該由 CSS 決定,使用 HTML class 控制,而非強行綁在 Helper 上。

4. 重複發明輪子

Rails 已內建許多實用 Helper,開發者卻以較糟的方式重造輪子。在此舉幾個比較經典的案例:

如何設計 table 的雙色效果?

<% count = 0 >
<table>
<% @items.each do |item| %>
  <% if count % 2 == 0 %>
    <% css_class = "even "%>
  <% else %>
    <% css_class = "odd" %>
  <% end %>
  <tr class="<%= css_class %>">
    <td>item</td>
  </tr>
  <% count += 1%>
<% end %>
</table>

一般的想法會是使用兩種不同 HTML class : event 與 odd,上不同的顏色。

<table>
<% @items.each_with_index do |item, count| %>
  <% if count % 2 == 0 %>
    <% css_class = "even "%>
  <% else %>
    <% css_class = "odd" %>
  <% end %>
  <tr class="<%= css_class %>">
    <td>item</td>
  </tr>
  <% count += 1%>
<% end %>
</table>

這是一般初心者會犯的錯誤。實際上 Ruby 中有 each_with_index,不需要另外需要宣告一個 count。

<table>
<% @items.each do |item| %>
  <tr class="<%= cycle("odd", "even") %>">
    <td>item</td>
  </tr>
<% end %>
</table>

但 Rails 其實內建了 cycle 這個 Helper。實際上只要這樣寫就好了...

常用你可能不知道的 Helper

限於篇幅,直接介紹幾個因為使用機率高,所以很容易被重造輪子的 Helper。開發者會寫出的相關 AntiPattern 部分就跳過了。

5. Ask, Not Tell

這也是在 View 中會常出現的問題,直接違反了 Law of Demeter 原則,而造成了效能問題。十之八九某個 View 緩慢無比,最後抓出來背後幾乎都是這樣的原因。

不少開發者會設計出這樣的 helper:

def post_tags_tag(post, opts = {})
  tags = post.tags
  tags.collect { |tag| link_to(tag,posts_path(:tag => tag)) }.join(", ")
end
<% @posts.each do |post| %>
  <%= post_tags_tag(post) %>
<% end %>

這種寫法會造成在 View 中,執行迴圈時,造成不必要的大量 query (n+1),以及在 View 中製造不確定數量的大量物件。View 不僅效率低落也無法被 optimized。

def post_tags_tag(tags, opts = {})
  tags.collect { |tag| link_to(tag,posts_path(:tag => tag)) }.join(", ")
end
<% @posts.each do |post| %>
  <%= post_tags_tag(post.tags) %>
<% end %>
  def index
    @posts = Post.recent.includes(:tags)
  end

正確的方法是使用 Tell, dont ask 原則,主動告知會使用的物件,而非讓 Helper 去猜。並配合 ActiveRecord 的 includes 減少不必要的 query( includes 可以製造 join query ,一次把需要的 posts 和 tags 撈出來)。

且在 controller query 有 object cache 效果,在 view 中則無。

小結

Helper 是 Rails Developer 時常在接觸的工具。但可惜的是,多數開發者卻無法將此利器使得稱手,反而造成了更多問題。在我所曾經參與的幾十個 Rails 專案中,很多設計和效能問題幾乎都是因為寫的不好的 View / Helper 中的 slow query 或伴隨產生的大量 object 所造成的 memory bloat 導致的。但參與專案的開發者並沒有那麼多的經驗,能夠抓出確切的病因,卻都將矛頭直接是 Rails 的效能問題,或者是沒打上 Cache 的關係。這樣的說法只是把問題掩蓋起來治標,而非治本。

下次若有遇到 performance issue,請先往 View 中瞧看看是不是裡面出現了問題。也許你很快就可以找到解答。

===

接下來兩章我將會介紹:

自用 Helper 的設計整理原則、如何將常用 Helper 抽取出來可以複用。

本篇文章將會收錄在 Essential Rails Pattern,目前已有部分章節已可預覽,歡迎預購支持我的寫作,謝謝!

 
about 5 years ago

What is "callbacks"?

Rails 的 ActiveRecord 提供了相當方便的 callbacks,能讓開發者在寫 Controller 時,能夠寫出更加 DRY 的程式碼:

  • before_crearte
  • before_save
  • after_create
  • after_save

在從前,在 Controller 裡面想要再 object 儲存之後 do_something,直觀的思路會是這樣:

class PostController
  def create
    @post = Post.new(params[:post])
    @post.save
    @post.do_something
    redirect_to posts_path
  end
end

當時的最佳模式:通常是建議開發者改用 callbacks 或者是 Observer 模式實作。避免 controller 的髒亂。

  • callbacks : after_create
class PostController < ApplicationController
  def create
    @post = Post.new(params[:post])
    @post.save
    redirect_to posts_path
  end
end

class Post < ActiveRecord::Base
  after_create :do_something

  protected
  
  def do_something
  end
end

或者是使用 Observer

  • Observer
    class PostController < ApplicationController
    def create
    @post = Post.new(params[:post])
    @post.save
    redirect_to posts_path
    end
    end
    
    
    

    class PostObserver < ActiveRecord::Observer

    def after_create(post)
    post.do_something
    end

    end

    class Post < ActiveRecord::Base

    protected

    def do_something
    end
    end


    使用 callbacks 所產生的問題

    callbacks 雖然很方便,但也產生一些其他的問題。若這個 do_something 是很輕量的 db update,那這個問題還好。但如果是很 heavy 的 hit_3rd_party_api 呢?

    在幾個情形下,開發者會遇到不小的麻煩。

    • Model 測試:每次在測試時都會被這個 3rd_party_api 整到,因為外部通訊很慢。
    • do_something_api 是很 heavy 的操作:每次寫測試還是會被很慢的 db query 整到。
    • do_something_api 是很輕微的 update:但是綁定 after_save 操作,在要掃描資料庫,做大規模的某欄位修改時,會不小心觸發到不希望引發的 callbacks,造成不必要的效能問題。

    當然,開發者還是可以用其他招數去閃開:

    比如說若綁定 after_save 。

    可以在 do_somehting 內加入對 dirty object 的偵測,避免被觸發:

    def do_somthing
      # 資料存在,且變動的欄位包括 content
    
      if presisited? && changed.include?("content")
         the_real_thing  
      end
    end
    

    但這一招並不算理想,原因有幾:

    1. 每次儲存還是需要被掃描一次,可能有效能問題。
    2. 寫測試時還是會呼叫到可能不需要引發的 do_somehting。
    3. if xxx && yyy 這個 condiction chain 可能會無限延伸下去。

    Facade Pattern

    那麼要怎樣才能解決這個問題呢?其實我們應該用 Facade Pattern 解決這個問題。

    設計模式裡面有一招 Facade Pattern,這一招其實是沒有被寫進 Design Pattern in Ruby 中的。Russ Olson 有寫了一篇文章解釋沒有收錄的原因:因為在 Ruby 中,這一招太簡單太直觀,所以不想收錄 XDDD。但他還是在網站上提供當時寫的草稿,供人參考。

    What is Facade Pattern?

    Facade Pattern 的目的是「將複雜的介面簡化,將複雜與瑣碎的步驟封裝起來,對外開放簡單的介面,讓客戶端能夠藉由呼叫簡單的介面而完成原本複雜的程式演算。」(來源

    延伸閱讀: (原創) 我的Design Pattern之旅[5]:Facade Pattern (OO) (Design Pattern) (C/C++)

    實際舉例:

    在上述的例子中,其實 do_something 有可能只會在 PostController 用到,而非所有的 model 操作都「需要」用到。所以我們 不應該將 do_somehting 丟進 callbacks(等於全域觸發),再一一寫 case 去閃避執行

    與其寫在 callbacks 裡。我們更應該寫的是一個 Service Class 將這一系列複雜昂貴的行為包裝起來,以簡單的介面執行。

    class PostController < ApplicationController
      def create
        CreatePostService(params[:post])
        redirect_to posts_path
      end
    end
    
    class CreatePostService
      def self.create(params)
        post = Post.new(params[:post])
        post.save
        post.do_something_a
        post.do_something_b
        post.do_something_c
      end
    end
    
    

    而在寫測試,只需要對 PostCreateService 這個商業邏輯 class 寫測試即可。而 PostController 和 Post Model 就不會被殃及到。

    小結

    不少開發者討厭測試的原因,不只是「因為」寫測試很麻煩的原因,「跑一輪測試超級久」也是讓大家很不爽的主因之一。

    其實不是這些測試框架寫的爛造成「寫測試很麻煩」、「執行測試超級久」。而是另有其他因素。

    許多資深開發者逐漸意識到,真正的主因是在於目前 Rails 的 model 的設計,耦合度太高了。只要沾到 db 就慢,偏偏 db 是世界的中心。只是測某些邏輯,搞到不小心觸發其他不需要測的東西。

    ActiveRecord 的問題在於,讓開發者太誤以為 ORM = model。其實開發者真正要寫的測試應該是對商業邏輯的測試,不是對 db 進行測試。

    所以才會出現了用 Facade Pattern 取代 callbacks 的手法。

    其他

    MVC 其實有其不足的部份。坦白說,Rails 也不是真正的 MVC,而是 Model2

    目前 MVC 其實是不足的,演化下來,開發者會發現 User class 裡面會開始出現這些東西:

    • current_user.buy_book(book)
    • current_user.add_creadit_point(point)

    這屬於 User 裡面應該放的 method 嗎?well,你也可以說適合,也可以說不適合。

    適合的原因是:其實你也不知道應該放哪裡,這好像是 User 執行的事,跟他有關,那就放這裡好了!不然也不知道要擺哪裡。

    不適合的原因是:這是一個「商業購買行為」。不是所有人都會購物啊。這應該是一個商業購買邏輯。但是....也不知道要放在哪啊。

    一直到最近,James Copelin 提出了:DCI 去補充了現有的 MVC 的不足,才算勉強解決了目前浮現的這些問題。

    DCI ,與本篇談到的 Facade Pattern 算是頗類似的手法。

    有關於 DCI ( Data, Context, Interaction ) 的文章,我會在之後發表。我同時也推薦各位去看這方面的主題。這個方向應該會是 Rails 專案設計上未來演化的方向之一。