2012年3月21日星期三

A way to expose signleton object and its constructor in node.js

In Node.js world, we usually encapsulate a service into a module, which means the module need to export the façade of the service. In most case the service could be a singleton, all apps use the same service.
But in some rare cases, people might would like to create several instances of the service ,which means the module also need to also export the service constructor.

A very natrual idea is to export the default service, and expose the constructor as a method of the default instance. So we could consume the service in this way:

var defaultService = require('service');
var anotherService = service.newService();

So we need to write the module in this way:

function Service() { }

module.exports = new Service();
moudle.exports.newService = Service;

But for some reason, node.js doesn't allow module to expose object by assigning the a object to module.exports. 
To export a whole object, it is required to copy all the members of the object to moudle.exports, which drives out all kinds of tricky code.
And things can become much worse when there are backward reference from the object property to itself.

So to solve this problem gracefully, we need to change our mind.
Since it is proved that it is tricky to export a object, can we try to expose the constructor instead? 
Then answer is yes. And Node.js does allow we to assign a function to the moudle.exports to exports the function. 
So we got this code.

function Service() { }
module.exports = Service;

So we can use create service instance in this way:

var Service = require('service');
var aService = new Service();

As you see, since the one we exported is constructor so we need to create a instance manually before we can use it. Another problem is that we lost the shared instance between module users, and it is a common requirement to share the same servcie intance between users.

How to solve this problem? Since as we know, function is also kind of object in javascript, so we can kind of add a memeber to the constructor called default, which holds the shared instance of the service.
This solution works but not in a graceful way! A crazy but fansy idea is that can we transform the constructor itsself into kind of singleton instance??!! Which means you can do this:

var defaultService = require('service');
defaultService.foo();
var anotherService = service();
anotherService,foo();

The code style looks familiar? Yes, jQuery, and many other well-designed js libraries are designed to work in this way. 
So our idea is kind of feasible but how?
Great thank to Javascript's prototype system (or maybe SELF's prototype system is more accurate.), we can simply make a service instance to be the constructor's prototype.

function Service() { }
module.exports = Service;
Service.__proto__ = nw Serivce;

Sounds crazy, but works, and gracefully! That's the bueaty of Javascript. 

Best regards, 

TimNew
-----------
If not now then when?
If not me then who?

Release your passion
To realize your potential

Sent with Sparrow

Posted via email from 米良的实验室

2012年3月18日星期日

Enhanced typeof() operator in JavaScript

Javascript is weakly typed, and its type system always behaves different than your expectation.

Javascript provide typeof operator to test the type of a variable. it works fine generally. e.g.
typeof(1) === 'number'
typeof('hello') === 'string'
typeof({}) === 'object'
typeof(function(){}) === 'function'

But it is not enough, it behaves stupid when you dealling with objects created by constructors. e.g.
if you expected typeof(new Date('2012-12-12')) === 'date', then you must be disappointed, since actually typeof(new Date('2012-12-12')) === 'object'.
Yes, when you apply typeof() opeartor on any objects, it just yield the general type "object" rather than the more meaningful type "date".

How can we make the typeof() operator works in the way as we expected?
As we know when we create a object, the special property of the object constructor will be set to the function that create the object. which means:
(new Date('2012-1-1')).constructor === [Function Date]
So ideally we can retrieve the name of the function as the type of the variable. And to be compatible with javascript's native operator, we need to convert the name to lower case. So we got this expression
(new Date('2012-1-1')).constructor.name.toLowerCase() === 'date'
And luckily, we can also apply this to other primitive types, e.g:
(123).constructor.name.toLowerCase() === 'number'
('hello').constructor.name.toLowerCase() === 'string'
(function(){}).constructor.name.toLowerCase() === 'function'
or even
({}).constructor.name.toLowerCase() === 'object'
So in general, we use this expression as new implementation of the typeof() operator! EXCEPT One case!

If someone declare the object constructor in this way, our new typeof() implementation will work inproperly!

var SomeClass = (function() {
return function() {
this.someProperty='some value';
}
})();

or even define the constructor like this

var SomeClass = function() {
this.someProperty = 'some value';
}

And we will find that 
(new SomeClass).constructor.name.toLowerCase() === ''
the reason behind this is because the real constructor of the SomeClass is actually an anonymous function, whose name is not set.

To solve this problem, we need to declare the name of the constructor:
var SomeClass = (function() {
return function SomeClass() {
this.someProperty='some value';
}
})();

or even define the constructor like this

var SomeClass = function SomeClass() {
this.someProperty = 'some value';
}

Best regards,

TimNew
-----------
If not now then when?
If not me then who?

Release your passion
To realize your potential

Sent with Sparrow

Posted via email from 米良的实验室

2012年3月15日星期四

Distribute files to multiple servers via scp

The most common task when operating the servers is to distribute a file to multiple servers.

So I wrote a piece of shell script to solve this problem:

  1. #!/bin/bash  
  2. echo "mscp <source file> <target dir>"  
  3. SourceFile=$1  
  4. TargetDir=$2  
  5. echo "Copy $SourceFile to $TargetDir as $RemoteUser"  
  6. echo "Enter the servers:"  
  7. if [ -f $SourceFile ]  
  8. then  
  9.   printf "File found, preparing to transfer\n"  
  10.   while read server  
  11.   do  
  12.   scp -p $SourceFile ${server}:$TargetDir  
  13.   done  
  14. else  
  15.   printf "File \"$SourceFile\" not found\n"  
  16.   exit 1  
  17. fi  
  18. exit 0  

 

call the script "mscp <source file> <target dir>", then the script will ask you the list of target servers. So you can type them one by one. If the remote user is different than you current user, you can also explicitly identify it by typeing user@server

 

Beside the previous scenario, there is a more common sceanrio, that you have got a server list stored in afile already. Then instead of type the servers line by line, you can pipe the file content to the script.

e.g: cat server_list.txt > mscp src_files dest_path

Best regards,

TimNew
-----------
If not now then when?
If not me then who?

Release your passion
To realize your potential

Sent with Sparrow

Posted via email from 米良的实验室

2012年2月4日星期六

HTML codes to put special characters on your Web page

尝试了一下用 LinqPad 把各种诡异的字母转成 Html 编码~结果发现不是左右字符都能转过去~
.net 内置的工具并不能完美的处理所有的Html编码~

字符来源

Query
"A,a,À,à,Á,á,Â,â,Ã,ã,Ä,ä,Å,å,Ā,ā,Ă,ă,Ą,ą,Ǟ,ǟ,Ǻ,ǻ,Æ,æ,Ǽ,ǽ,B,b,Ḃ,ḃ,C,c,Ć,ć,Ç,ç,Č,č,Ĉ,ĉ,Ċ,ċ,D,d,Ḑ,ḑ,Ď,ď,Ḋ,ḋ,Đ,đ,Ð,ð,DZ,dz,DŽ,dž,E,e,È,è,É,é,Ě,ě,Ê,ê,Ë,ë,Ē,ē,Ĕ,ĕ,Ę,ę,Ė,ė,Ʒ,ʒ,Ǯ,ǯ,F,f,Ḟ,ḟ,ƒ,ff,fi,fl,ffi,ffl,ſt,G,g,Ǵ,ǵ,Ģ,ģ,Ǧ,ǧ,Ĝ,ĝ,Ğ,ğ,Ġ,ġ,Ǥ,ǥ,H,h,Ĥ,ĥ,Ħ,ħ,I,i,Ì,ì,Í,í,Î,î,Ĩ,ĩ,Ï,ï,Ī,ī,Ĭ,ĭ,Į,į,İ,ı,IJ,ij,J,j,Ĵ,ĵ,K,k,Ḱ,ḱ,Ķ,ķ,Ǩ,ǩ,ĸ,L,l,Ĺ,ĺ,Ļ,ļ,Ľ,ľ,Ŀ,ŀ,Ł,ł,LJ,lj,M,m,Ṁ,ṁ,N,n,Ń,ń,Ņ,ņ,Ň,ň,Ñ,ñ,ʼn,Ŋ,ŋ,NJ,nj,O,o,Ò,ò,Ó,ó,Ô,ô,Õ,õ,Ö,ö,Ō,ō,Ŏ,ŏ,Ø,ø,Ő,ő,Ǿ,ǿ,Œ,œ,P,p,Ṗ,ṗ,Q,q,R,r,Ŕ,ŕ,Ŗ,ŗ,Ř,ř,ɼ,S,s,Ś,ś,Ş,ş,Š,š,Ŝ,ŝ,Ṡ,ṡ,ſ,ß,T,t,Ţ,ţ,Ť,ť,Ṫ,ṫ,Ŧ,ŧ,Þ,þ,U,u,Ù,ù,Ú,ú,Û,û,Ũ,ũ,Ü,ü,Ů,ů,Ū,ū,Ŭ,ŭ,Ų,ų,Ű,ű,V,v,W,w,Ẁ,ẁ,Ẃ,ẃ,Ŵ,ŵ,Ẅ,ẅ,X,x,Y,y,Ỳ,ỳ,Ý,ý,Ŷ,ŷ,Ÿ,ÿ,Z,z,Ź,ź,Ž,ž,Ż,ż"
.Split(',')
.ToDictionary(k=>k,HttpUtility.HtmlEncode)

Result
5Dictionary<String,String> (304 items)
Key Value

A

A

a

a

À

&#192;

à

&#224;

Á

&#193;

á

&#225;

Â

&#194;

â

&#226;

Ã

&#195;

ã

&#227;

Ä

&#196;

ä

&#228;

Å

&#197;

å

&#229;

Ā

Ā

ā

ā

Ă

Ă

ă

ă

Ą

Ą

ą

ą

Ǟ

Ǟ

ǟ

ǟ

Ǻ

Ǻ

ǻ

ǻ

Æ

&#198;

æ

&#230;

Ǽ

Ǽ

ǽ

ǽ

B

B

b

b

C

C

c

c

Ć

Ć

ć

ć

Ç

&#199;

ç

&#231;

Č

Č

č

č

Ĉ

Ĉ

ĉ

ĉ

Ċ

Ċ

ċ

ċ

D

D

d

d

Ď

Ď

ď

ď

Đ

Đ

đ

đ

Ð

&#208;

ð

&#240;

DZ

DZ

dz

dz

DŽ

DŽ

dž

dž

E

E

e

e

È

&#200;

è

&#232;

É

&#201;

é

&#233;

Ě

Ě

ě

ě

Ê

&#202;

ê

&#234;

Ë

&#203;

ë

&#235;

Ē

Ē

ē

ē

Ĕ

Ĕ

ĕ

ĕ

Ę

Ę

ę

ę

Ė

Ė

ė

ė

Ʒ

Ʒ

ʒ

ʒ

Ǯ

Ǯ

ǯ

ǯ

F

F

f

f

ƒ

ƒ

G

G

g

g

Ǵ

Ǵ

ǵ

ǵ

Ģ

Ģ

ģ

ģ

Ǧ

Ǧ

ǧ

ǧ

Ĝ

Ĝ

ĝ

ĝ

Ğ

Ğ

ğ

ğ

Ġ

Ġ

ġ

ġ

Ǥ

Ǥ

ǥ

ǥ

H

H

h

h

Ĥ

Ĥ

ĥ

ĥ

Ħ

Ħ

ħ

ħ

I

I

i

i

Ì

&#204;

ì

&#236;

Í

&#205;

í

&#237;

Î

&#206;

î

&#238;

Ĩ

Ĩ

ĩ

ĩ

Ï

&#207;

ï

&#239;

Ī

Ī

ī

ī

Ĭ

Ĭ

ĭ

ĭ

Į

Į

į

į

İ

İ

ı

ı

IJ

IJ

ij

ij

J

J

j

j

Ĵ

Ĵ

ĵ

ĵ

K

K

k

k

Ķ

Ķ

ķ

ķ

Ǩ

Ǩ

ǩ

ǩ

ĸ

ĸ

L

L

l

l

Ĺ

Ĺ

ĺ

ĺ

Ļ

Ļ

ļ

ļ

Ľ

Ľ

ľ

ľ

Ŀ

Ŀ

ŀ

ŀ

Ł

Ł

ł

ł

LJ

LJ

lj

lj

M

M

m

m

N

N

n

n

Ń

Ń

ń

ń

Ņ

Ņ

ņ

ņ

Ň

Ň

ň

ň

Ñ

&#209;

ñ

&#241;

ʼn

ʼn

Ŋ

Ŋ

ŋ

ŋ

NJ

NJ

nj

nj

O

O

o

o

Ò

&#210;

ò

&#242;

Ó

&#211;

ó

&#243;

Ô

&#212;

ô

&#244;

Õ

&#213;

õ

&#245;

Ö

&#214;

ö

&#246;

Ō

Ō

ō

ō

Ŏ

Ŏ

ŏ

ŏ

Ø

&#216;

ø

&#248;

Ő

Ő

ő

ő

Ǿ

Ǿ

ǿ

ǿ

Œ

Œ

œ

œ

P

P

p

p

Q

Q

q

q

R

R

r

r

Ŕ

Ŕ

ŕ

ŕ

Ŗ

Ŗ

ŗ

ŗ

Ř

Ř

ř

ř

ɼ

ɼ

S

S

s

s

Ś

Ś

ś

ś

Ş

Ş

ş

ş

Š

Š

š

š

Ŝ

Ŝ

ŝ

ŝ

ſ

ſ

ß

&#223;

T

T

t

t

Ţ

Ţ

ţ

ţ

Ť

Ť

ť

ť

Ŧ

Ŧ

ŧ

ŧ

Þ

&#222;

þ

&#254;

U

U

u

u

Ù

&#217;

ù

&#249;

Ú

&#218;

ú

&#250;

Û

&#219;

û

&#251;

Ũ

Ũ

ũ

ũ

Ü

&#220;

ü

&#252;

Ů

Ů

ů

ů

Ū

Ū

ū

ū

Ŭ

Ŭ

ŭ

ŭ

Ų

Ų

ų

ų

Ű

Ű

ű

ű

V

V

v

v

W

W

w

w

Ŵ

Ŵ

ŵ

ŵ

X

X

x

x

Y

Y

y

y

Ý

&#221;

ý

&#253;

Ŷ

Ŷ

ŷ

ŷ

Ÿ

Ÿ

ÿ

&#255;

Z

Z

z

z

Ź

Ź

ź

ź

Ž

Ž

ž

ž

Ż

Ż

ż

ż

Best regards,

TimNew
------------
If not now then when?
if not me then who?

Release your passion
To Realize your potential

I am a pessimist, I feel I'm living in a world without light.
But I am also a prayer, I believe I’m going towards a world full of sunshine!

Posted via email from 米良的实验室

2011年11月21日星期一

Chrome Extension Isolation

I met a wield problem today, while I'm developing a chrome extension for our QAs.
I try to inject a script to click a group of specific links on the page. The script runs quite well when I paste it in firebug or chrome script console.
But once it is loaded from extension, it behaves in a strange way.
I spent several hours to figure out the problem. Finally I found the reason seems caused by jQuery function click event not fired correctly.
Then I try to debug the script line by line. And I found the code is indeed being executed. To confirm my assumption, I added an alert there, and it does be fired.
So I decide to publish the anchor object to a global variable(I mean a member defined on window).
Then I found a more wield phenomenon: when I debugger breaks in my script, the variable is being set correctly. But when I try to print the variable in console, the variable become undefined again!!!
I was confused by the result, then I guess the call might be muted by chrome for security reason. Then there should be some permission configuration somewhere.
But after I read every pages related to chrome extension permission configuration,  I still haven't found the answer I wish to found.
Suddenly I saw there are several words said  the extension is running in a isolated environment.
This inspired me that the chrome extension is working in a pattern that different than my understanding.
The content script doesn't manipulates the DOM directly but via a proxy and marshal.
Then it must be the reason why the even isn't fired, the event call couldn't be marshaled.
And the only solution is to inject the script to the page as a part of the DOM. Then the script can interact with the DOM directly.
Then I modified the manifest JSON, make it load a new script as content script, and I use that script insert a script tag to tag to load my original functionality script.
So as a conclusion: chrome extension is running in a isolated sandbox rather than running the DOM. We should remember this all the time, even though chrome makes the content script can access the Some directly. Since there are some limitations in such kinda simulation, such as function call and variable is not marshaled.

Sent from TimNew's Desire HD

Posted via email from 米良的实验室

2011年11月13日星期日

Simple Design

最近TW HR改變了Code Reivew的流程。現在所有應聘者的提交的代碼都會被上傳到一個SVN Server上去,然後大家 Review 完,通過 myTW 的投票機制打分。
其實這份代碼,麼一個 TWer 應該都有寫過,於是不知道從誰開始的,最近突然流行起了把自己當年面試時提交的代碼也上傳到SVN,然後讓大家幫忙 Review。

於是我也就跟風了一把~把我的代碼也搞上去了~
然後也看了看別人的代碼。

很有意思的是發現肖鵬的代碼有兩份,一份是當年面試提交的,另外一份應該是近期寫的。兩種完全不同的風格~
還有就是光磊的代碼,Java版,3個類搞定~

對比肖鵬的新版本和光磊的代碼~
我覺得我面試的那份代碼簡直就是 Over Design 和 Tricky Code 的絕佳範例!
一個小小的面試題裡,我居然實現了一個輕量級的依賴注入框架,一個輕量級的 RX 推送框架,以及一個 Message Bus 系統~
然後外部依賴也多到嚇人,xUnit,Moq, MVVM.Light, System.Windows.Interaction, Expression.Drawing, 最要命的是還有一個對 Code Contract的運行時編譯器 ccrewriter 的依賴,木有這玩意兒,不要想把代碼編譯過去!唯一值得慶幸的是,這些依賴大部分,能添加的都被我通過 Nuget 在編譯時進行自動下載了~

在看了光磊的極簡風格的代碼後,我就在想,當年的代碼風格確實是走上了一條不歸路。於是造就了這份連傳說中號稱無所不能的新哥也說看不懂的代碼~
現在看來,確實,這樣的代碼,不僅讓讀的人痛苦,也讓寫代碼的我無比痛苦~
最無語的,我現在已經不是很明白代碼的一些細節地方為什麼是那麼寫的了~而這時間,僅僅過去了幾個月而已~~

於是,我效仿,肖鵬同學,按照我現在的理解,寫了一個第二版出來。感謝 Scott Robinson 的那次169字符的打賭,在學習了無數超精簡的代碼版本後,我的第二版代碼也極大的精簡了。最讓我開心的是我徹底的在代碼裡去掉了傳說中的Switch Case~在Enumeration不支持定義方法和進行重載的 C#裡,我也寫出了近似於 Java 那樣精簡的代碼~ 當然,也利用了一些 Java 所沒有的 C# 特性~

最有意思的事情是,上次寫這個代碼,花了我整整三天,每天都還搞到 11 點半以後~(最後還是沒有足夠的時間去設計GUI的配置界面,於是及其 Tricky 通過在 WPF App 中重用了 Console 版的配置輸入解析模塊,於是造就那個從命令輸入參數,在GUI控制流程和渲染動畫的詭異應用~)
而這次,我僅僅花了4個小時,就搞定了所有的代碼~時間大概只花了原來的1/10~而且明顯代碼可讀性和可維護性都有了極大的提高~由於設計很簡單,所以可擴展性和靈活性在一定的修改前提下,也是不錯的~
相比之前那種通過大而全的框架去保證靈活性和可擴展性的方法來看,Simple Design 有著其非常獨特的優勢~

這是我第一次通過實踐,真正的見證了Simple Design 的強大~也讓我對這Simple Design的Simple有了更深一層的理解~Simple Design其實並不Simple~

Best regards,

TimNew
------------
If not now then when?
if not me then who?

Release your passion
To Realize your potential

I am a pessimist, I feel I'm living in a world without light.
But I am also a prayer, I believe I’m going towards a world full of sunshine!


Posted via email from 米良的实验室

2011年10月13日星期四

Chrome Remote Desktop is amazing

Google Chrome Remote Desktop Extension is really amazing!!!!!
I try to install the extension on both Chrome on my iMac and the one on my laptop with Win 7.
Then I try to connect the iMac from Win 7 via Chrome Remote Desktop!
The performance is really amazing!!!! It is less well than Windows RDP but much better than the famous but sucks VNC!!!!!
I totally have no idea how Google implemented this! But it do works super well in my environment!

Chrome_remote_desktop_control_

You know, before Chrome Remote Desktop, there is only one real cross platform remote desktop solution: VNC, and the performance of which is unacceptable poor, and we have to install a lot of ugly software to make it happen.
 But now, we can achieve it by simply install chrome and the chrome extension....
It is amazing!

I found the desktop fade in animation works smoothly via remote desktop! And I can even play movie from Mac!!!!!
Chrome_remote_desktop_play_vid
While playing video, the peak of the network traffic might be reach 800kBps to 1MBps... but normally it should be 300 kBps.....

But since it is only the beta version, so there are some limitations in Chrome Remote Desktop:
1. The Hotkey doesn't work well, which means you cannot Press Cmd+Space to pop up quick-silver or spot-light...
2. Mouse Wheel doesn't work well, which means you cannot scroll the page with your mouse wheel, or magic mouse.
3. Sound doesn't bring to remote side, if you wanna play movie with Chrome Remote Desktop, then you might be have to read the subtitle rather than hear the speech.
4. CPU consuming is high, I guess Chrome Remote Desktop spend a lot of CPU power on compressing the data to be transferred, so the CPU consuming is higher than other Remote Desktop solution......

And special Precondition required for Chinese Netizens:
If you are try to use Chrome Remote Desktop in China, this miracle land, you might need some other special technology tool to help you get rid of the famous GFW. To my experience, sometimes Remote Desktop OAuth might be blocked by GFW. 

Best regards,

TimNew
------------
If not now then when?
if not me then who?

Release your passion
To Realize your potential

I am a pessimist, I feel I'm living in a world without light.
But I am also a prayer, I believe I’m going towards a world full of sunshine!


Posted via email from 米良的实验室

2011年9月19日星期一

Incredipede:一个神奇的游戏

http://incredipede.com/

这真的是一个神奇的游戏,代码估计是用 C# +  Farseer 物理引擎写的!
这创意绝了~
游戏是要求玩家创造一个生物,然后生物重复某种动作,不断前进。当它卡住的时候,玩家可以改造这个生物的形态,然后让它继续前进~

Best regards,

TimNew
------------
If not now then when?
if not me then who?

Release your passion
To Realize your potential

I am a pessimist, I feel I'm living in a world without light.
But I am also a prayer, I believe I’m going towards a world full of sunshine!


Posted via email from 米良的实验室

2011年9月16日星期五

TDD vs Natural Selection : Part II

As discussed in previous part, both TDD and natural selection can produce suitable designs. But there are costs.
The design from mayual selection is just perfect, but the cost is 99% unsuitable species die out. It is the cost of life.
Same to TDD, except TDD isn't so cruel. If you try to write a piece of code in TDD and pure factoring way, which means all the designs are in order to eliminate smell. And you might find that to eliminate the smell sometime is not so easy that you can have it done in minutes. You might need several tries to find out the most proper approach, since you might find that you just introduced a new, and maybe more serious smell while you eliminating a smell. Sometimes you might find that the upcoming new smells just drive into a dead road, and you just want revert the changes and retry from a fresh start. In worst case, you might find you can hardly find the right way, and you just got lost in the code.
In some simple project, you might find the situation is just acceptable, but in some complex project, you can hardly do that or you might find at the  end of the day you and your pair produced nothing with great effort.

So in my opinion, TDD doesn't mean no design at all. When you practicing TDD, you must focus on the detailed code. At this time if you can easily got lost without the guide from a clear, more general, high level vision. It just works like the architecture design or general solution to specific type of problem.

Such design can save you tons of time wasted on times of retries.

Sent from TimNew's Desire HD

Posted via email from 米良的实验室

2011年9月14日星期三

FluentAssertion is not compatible with xUnit.Extensions

I met a weird problem that I found the Resharper Test Runner hangs when I introduced theory test case in my unit test. 
After some spikes, I found the problem seems caused by the incompatibility between FluentAssertion(http://fluentassertions.codeplex.com/) and xUnit.Extension (http://xunit.codeplex.com/).
It is wired, and there seems to be no quick fix.
So I replace the Fluent Assertion with Should and Should.Fluent(http://should.codeplex.com/), which is a port of ShouldIt(http://code.google.com/p/shouldit/). 
After that, everything goes well except the syntax between Fluent Assertion and Should Fluent are not compatible with each other, although they're really similar.
But Should.Fluent doesn't support something.Should.Be(), it requires something.Should.Be.Equals(), which is really annoying to me.

According to the Fluent's introduction, Fluent is a direct fork of xUnit. And I'm not sure what's the impact caused by this.

Best regards,

TimNew
------------
If not now then when?
if not me then who?

Release your passion
To Realize your potential

I am a pessimist, I feel I'm living in a world without light.
But I am also a prayer, I believe I’m going towards a world full of sunshine!


Posted via email from 米良的实验室