PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) |
---|---|---|
Planning | 計劃 | 120 |
Estimate | · 估計這個任務須要多少時間 | 5 |
Development | 開發 | 10 |
Analysis | · 需求分析 (包括學習新技術) | 300 |
Design Spec | · 生成設計文檔 | 30 |
Design Review | · 設計複審 (和同事審覈設計文檔) | 5 |
Coding Standard | · 代碼規範 (爲目前的開發制定合適的規範) | 10 |
Design | · 具體設計 | 30 |
Coding | · 具體編碼 | 1440 |
Code Review | · 代碼複審 | 60 |
Test | · 測試(自我測試,修改代碼,提交修改) | 180 |
Reporting | 報告 | 10 |
Test Report | · 測試報告 | 10 |
Size Measurement | · 計算工做量 | 10 |
Postmortem & Process Improvement Plan | · 過後總結, 並提出過程改進計劃 | 60 |
Total | 合計 | 2280 |
Information hiding is part of the foundation of both structured design and object-oriented design. In structured design, the notion of 「black boxes」 comes from
information hiding. In object-oriented design, it gives rise to the concepts of encapsulation and modularity, and it is associated with the concept of abstraction.
引自代碼大全,見:http://www.cnblogs.com/magiccode1023/archive/2012/10/23/2736257.htmlhtml
不用多說,最基本的抽象原則,保證程序的健壯性和靈活性。
上述連接中也提到了,信息隱藏要隱藏的信息包括兩方面:git
1.隱藏可能的複雜性。
2.隱藏外部改變會有危險的信息。github
隱藏複雜性中隱藏了分而治之的思想,而第2點則是爲了程序的安全性。
代碼中的不少地方都隱藏了複雜性:算法
for (int k = 1; k <= LEN; ++k) { if (Sudoku::count >= number) return; if (checkGeneratePos(i, j, k)) { //check if it is ok to set k on (i,j) board[i][j] = k + '0'; traceBackWriteFile(i, j + 1, number, outFile); //if can,recur to next place } }
上述代碼是回溯函數中的一部分,checkGeneratePos(i,j,k)
就是一個典型的複雜性隱藏,它隱去了方法實現的內部細節,從而讓咱們能專一於實現主要的算法。編程
咱們在coding中避免類成員和一些輔助函數暴露,因此將它們設置爲private
:數組
private: char board[LEN + 1][LEN + 1]; void init(); inline void traceBackN(int i, int j, int n, int result[][LEN*LEN]); inline bool traceBackSolve(int i, int j); inline int getBlock(int i); void traceBackWriteFile(int i, int j, int number, fstream &outFile); void traceBackCountSolution(int i, int j, int *solutionNumber, int bound); void digHoles(int count, int mode, int lower, int upper, int result[][LEN*LEN]); static long int count;
上述方法或者類成員一旦在外部被調用就會以不正常的方式修改類,因此須要避免。
咱們設計方法的一個很重要的原則就是:這個方法具備單一的功能。有些時候總有一些控制方法會整合不少功能,這個時候咱們須要分解這些功能爲一個個小的功能,並分別實現它們。安全
Interface Design簡單來講就是事先約定好模塊的接口,此次實現的兩個generate
接口以及solve
接口都是這個的體現,由於對接口有嚴格的規定,在step4咱們互換Core
模塊的時候纔沒有太多的麻煩,很容易就完成了代碼。框架
鬆耦合在下面連接中有較好的解釋:
stackoverflow-Loose Coupling
其中心思想就是儘可能減小模塊之間的「依賴」,最理想的狀態大概是更換程序中任何一個方法或是函數都只須要在方法或函數內部進行更改,這也是抽象的一種體現,我的認爲實現這個原則的最好方法就是在各個模塊之間的輸入輸出之間增長抽象層,這就至關於「鬆弛」了各個模塊之間的耦合。ide
咱們實現的solve
方法很好地說明了這一點:函數
bool Sudoku::solve(int puzzle[], int solution[]) throw(IllegalLengthException) { bool ret; convertToTwoDimension(puzzle); ret = traceBackSolve(1, 1); if (!check()) { return false; } convertToOneDimension(solution); return ret; }
這個方法中convertToOneDimension
會把當前數獨的解複製到solution
中,咱們並非直接將類成員中的數組傳出,咱們實際上就是在方法的輸出上增長了抽象層。這樣的方法使得若是當咱們將數獨類成員中的數組換成1維數組(原來是2維)的時候,咱們只須要在convertToOneDimension
內部進行修改。
咱們主要的算法邏輯都集中在Sudoku
這個類當中。
數獨求解的部分咱們使用回溯的思想進行解決。回溯方法traceBackSolve()
對第i
行第j
列元素及其後方(先向右,到最右則折返換行)空格進行求解,每次求解嘗試從1到9,檢測1到9每一個數字是否適合在此格子中填入(行、列、宮不重複),並在嘗試中遞歸調用traceBackSolve()
方法,從而驗證每次嘗試的正確性。求解數獨的接口solve()
方法負責調用traceBackSolve()
方法進行求解,並作一二維數組的轉換。
在生成數獨接口generate(int number, int lower, int upper, bool unique, int result[][])
中,咱們採用先生成終盤,再從終盤中挖空的形式進行數獨生成。首先調用generateCompleteN()
這個已經實現的生成終盤方法,獲得number
個終盤,再使用digHoles()
方法進行挖空。挖空策略一共有兩種,一種爲從頭數獨第一個數開始,一種爲隨機選擇。隨機挖空因爲速度較快,但容易出現挖出來的盤有多解的狀況,咱們只在unique爲假的狀況下使用它。unique爲真時,採用順序挖空的策略,以從左到右,從上到下的順序進行挖空,每次挖空以後,將原始數字用1到9中其餘數字進行替換,並調用solve()
對數獨進行求解,若能解出,則證實此空不能挖,不然可挖,繼續向後挖空。
第二個生成數獨接口二generate(int number, int mode, int result[][LEN*LEN])
中,咱們利用了第一個generate()
方法,根據mode
獲得相應的up
和down
傳入generate()
,即可獲得結果。
UML圖以下:
相互之間沒有依存關係。
下圖展現了生成1000000個完整數獨的性能分析
因爲此次繼承了我上次的代碼,因此代碼自己已經被優化過。
5.272秒,幾乎全部的時間都花費在回溯遞歸上,速度已經能夠接受。
一個可能的優化是在判斷重複的時候使用位操做。
下圖展現瞭解1000個數獨時候的性能分析:
首先注意到checksolve
花費較長時間,這個函數原來使用了3×9的時間來判斷,注意到這個方法的下界是1×9,遂更改了實現方式:
int row, col; row = getBlock(i); col = getBlock(j); for (int a = 1; a <= LEN; ++a) { if ((board[i][a] == k + '0') || (board[a][j] == k + '0') || (board[row + ((a - 1) / 3)][col + ((a - 1) % 3)] == k + '0')) return false; }
不過,這是常數級別的優化,因此效果不好,改進以後再次性能分析發現效果微弱。
一個可能的改進是使用bitmap來優化。
直接在-u
模式下測試,因爲當r的參數的值變大的時候生成10000個解的時間幾乎不可接受,因此選擇較低的數值,下圖是指令-n 10000 -r 25~55
的效能分析:
24秒
熱路徑主要集中於solve
函數,判斷緣由仍是因爲遞歸時形成的指數級增加的函數調用,在不更改現有結構的狀況下已經很難改進。
改進效能花費了30分鐘。
契約式編程咱們已經在OO課上實踐過,其中心思想爲:在完成代碼模塊的時候,首先制定「契約」,它描述這個模塊的一些性質,包括調用時候知足的前置條件(precondition)、方法完成時候知足的後置條件(postcondition)、方法拋出的異常(exception)以及方法知足的不變式(invariant),最後根據「契約」來完成代碼。一個比較典型的契約式編程的例子就是Assert語句了。
這種編程方式首先假定全部輸入都知足前置條件(precondition),而與其相反的防護式編程則假定輸入會有全部可能的狀況,包括正確和錯誤的。
很明顯,契約式編程很是適合於在程序開發時使用,同時也有不少工具簡化了這種編程方式,此次給出的參考連接中,Code Contracts for .NET就是一個這樣的工具,這個工具能夠自動檢測模塊中的契約來測試它們是否知足條件,從而實現Runtime checking,static checking等功能。不過,在程序發佈的時候通常須要取消契約檢查,由於它對性能也有必定影響。
契約式編程很是有助於錯誤的精肯定位,雖然絕大多數流行的程序語言在程序運行出錯時都會在必定程度上給出提示,但咱們更但願在早期發現程序的錯誤,而不是等到錯誤一層層傳遞到頂層才發現他們。
總結來講,契約式編程在設計層面保證了程序的正確性,但當咱們將程序發佈,咱們就必須作好準備應付各類可能的錯誤,而不是等待用戶去知足契約了。
此次實現的Core計算模塊的接口也是一種契約,好比generate(int number,int lower,int upper,bool unique,int result[][])
方法中,調用者須要知足參數的一些條件,而這個方法也須要知足在方法完成以後在result
數組中存儲number
個規定的數獨遊戲的後置條件(postcondition),咱們在測試的時候,也是根據這些條件進行測試的。
測試思路:給出一個題目,和答案對比。
ret = sudoku.solve(puzzle, temp); Assert::AreEqual(ret, true); for (int i = 0; i < 81; ++i) { Assert::AreEqual(temp[i], solution[i]); }
測試思路:對-r
指令,首先在生成以後用solve
函數測試是否可解,而後計算遊戲中的空的個數,判斷是否知足要求;對-u
指令,在-r
的基礎之上用回溯法求出解的個數,若是個數大於1,則出錯,測試-m
的時候也是相似的方式。
下面是測試-n 10 -r lower~upper -u
的部分代碼:
sudoku.generate(10, lower, upper, true, result); for (int i = 0; i < number; ++i) { Assert::AreEqual(sudoku.solve(result[i], solution), true); int solutionNumber = sudoku.countSolutionNumber(result[i], 2); Assert::AreEqual(solutionNumber, 1); int count = 0; for (int j = 0; j < 81; ++j) { if (result[i][j] == 0) count++; } Assert::AreEqual(count <= upper && count >= lower, true); }
測試思路:設置一個bool
型變量exceptionThrown
(初始值爲false
)以及異常的條件,只要catch
到異常,就將exceptionThrown
設置爲true
,而後進行斷言。
下面是測試SudokuCountException
的代碼:
bool exceptionThrown = false; try { // Test first SudokuCountException sudoku.generate(-1, 1, result); } catch (SudokuCountException& e) { exceptionThrown = true; e.what(); } Assert::IsTrue(exceptionThrown);
這裏generate
方法生成的數獨個數不能是負數,因此會拋出異常。
測試思路:用strcpy_s
初始化argv
,設置argc
,而後進行調用相關方法進行分析和斷言。
下面是測試指令-n 1000 -m 2
的代碼:
InputHandler* input; strcpy_s(argv[3], length, "-n"); strcpy_s(argv[4], length, "1000"); strcpy_s(argv[1], length, "-m"); strcpy_s(argv[2], length, "2"); argc = 5; input = new InputHandler(argc, argv); input->analyze(); Assert::AreEqual(input->getMode(), 'n'); Assert::AreEqual(input->getNumber(), 1000); Assert::AreEqual(input->getHardness(), 2); delete input;
這裏打亂了參數的順序,其餘參數的組合也是用相似的方法來測試的。
咱們的program中,參數錯誤的狀況下會直接報錯而後退出,同時輸入分析在完成以後通常不會改變,因此咱們直接在控制檯中進行了測試,主要看是否有相應的輸出,錯誤種類參看下圖:
Error Code | 異常說明 | 錯誤提示 |
---|---|---|
1 | 參數數量不正確 | bad number of parameters. |
2 | 參數模式錯誤 | bad instruction.expect -c or -s or -n |
3 | -c指令的數字範圍錯誤 | bad number of instruction -c |
4 | -s指令找不到文件 | bad file name |
5 | -s指令的puzzle.txt中的數獨格式錯誤 | bad file format |
6 | -s指令的puzzle.txt中的數獨不可解 | bad file can not solve the sudoku |
9 | -r指令後的數字範圍有錯誤 | the range of -r must in [20,55] |
10 | -m指令後的模式有錯誤 | the range of -m must be 1,2 or 3 |
11 | 11 -m指令與-u或-r指令同時出現 | -u or -r can not be used with -m |
12 | c指令的參數範圍錯誤 | the number of -c must in [1,1000000] |
13 | -n指令的參數範圍錯誤 | the number of -n must in [1,10000] |
14 | -n指令的參數類型錯誤 | the parameter of -n must be a integer |
18 | -n不能單獨使用 | parameter -n cann't be used without other parameters |
其中code不連續是由於有的code替換成了exception。
一些測試情景能夠參考下圖:
總的覆蓋率約爲94%
沒有測到的代碼主要是Output相關的代碼,已經在7.5節進行了說明。
下圖展現了咱們對於異常的設計:
Error Code | 異常類 | 異常說明 | 錯誤提示 |
---|---|---|---|
8 | SudokuCountRangeException | generate(int number,int lower,int upper,bool unique,int result[][])中number範圍錯誤 | number in generate(int number,int lower,int upper,bool unique,int result[][]) must in[1,10000] |
16 | LowerUpperException | generate(int number,int lower,int upper,bool unique,int result[][])中lower和upper的值錯誤 | the lower and upper in generate(int number,int lower,int upper,bool unique,int result[][]) must satisfy:lower<upper,lower > 20,upper < 55 |
17 | ModeRangeException | generate(int number, int mode, int result[][])函數中mode的值錯誤 | the number of mode must in [1,3] |
下面分別給出異常對應的測試樣例,測試方法已經在以前說明。
int result[1][81]; bool exceptionThrown = false; try { // Test first SudokuCountException sudoku.generate(0, 1, result); } catch (SudokuCountException& e) { exceptionThrown = true; e.what(); } Assert::IsTrue(exceptionThrown); exceptionThrown = false; try { sudoku.generate(100000, 20, 50, true, result); } catch (SudokuCountException& e) { exceptionThrown = true; e.what(); } Assert::IsTrue(exceptionThrown);
上例中兩次調用generate
函數,生成數量分別爲0和100000,都會拋出異常。
//test LowerUpperException,case 1 exceptionThrown = false; try { sudoku.generate(1, 1, 50, true, result); } catch (LowerUpperException& e) { exceptionThrown = true; e.what(); } Assert::IsTrue(exceptionThrown); //test LowerUpperException,case 2 exceptionThrown = false; try { sudoku.generate(1, 20, 56, true, result); } catch (LowerUpperException& e) { exceptionThrown = true; e.what(); } Assert::IsTrue(exceptionThrown); //test LowerUpperException,case 3 exceptionThrown = false; try { sudoku.generate(1, 50, 1, true, result); } catch (LowerUpperException& e) { exceptionThrown = true; e.what(); } Assert::IsTrue(exceptionThrown);
上例中測試了upper
和lower
拋出異常的3種狀況,分別是lower
超出範圍,upper
超出範圍和lower
、upper
不知足lower<upper的狀況
//test ModeRangeException exceptionThrown = false; try { sudoku.generate(1, -1, result); } catch (ModeRangeException& e) { exceptionThrown = true; e.what(); } Assert::IsTrue(exceptionThrown);
上例中generate
調用的模式出錯,只能是一、二、3,因此拋出異常。
QPushButton#blueButton { color: white; } QPushButton#blueButton:enabled { background: rgb(0, 165, 235); color: white; } QPushButton#blueButton:!enabled { background: gray; color: rgb(200, 200, 200); } QPushButton#blueButton:enabled:hover { background: rgb(0, 180, 255); } QPushButton#blueButton:enabled:pressed { background: rgb(0, 140, 215); }
QPushButton#puzzleButton { border-width: 1px; border-style: solid; border-radius: 0; } QPushButton#puzzleButtonTLCorner { border-radius: 0; border-top-left-radius: 4px; border-width: 1px; border-style: solid; } QPushButton#puzzleButtonTRCorner { border-radius: 0; border-top-right-radius: 4px; border-width: 1px; border-style: solid; } QPushButton#puzzleButtonBLCorner { border-radius: 0; border-bottom-left-radius: 4px; border-width: 1px; border-style: solid; } QPushButton#puzzleButtonBRCorner { border-radius: 0; border-bottom-right-radius: 4px; border-width: 1px; border-style: solid; } QPushButton#puzzleButtonRE { border-radius: 0; border-width: 1px; border-right-width: 3px; border-style: solid; } QPushButton#puzzleButtonBE { border-radius: 0; border-width: 1px; border-bottom-width: 3px; border-style: solid; } QPushButton#puzzleButtonBRE { border-radius: 0; border-width: 1px; border-right-width:3px; border-bottom-width: 3px; border-style: solid; }
小結:界面風格不是咱們在設計UI時最先考慮的部分,原本打算風格只進行簡單修改,只用setStyleSheet()方法來設計界面風格。不事後來發現自帶的界面實在太醜,因而決定借鑑已有的風格,針對項目要求進行調整,最終效果還算不錯。
歡迎、幫助與選擇難度界面統一使用QVBoxLayout對控件進行對齊
效果見下圖
爲保證比例的美觀,遊戲窗體被強制固定,沒法進行縮小與放大。
小結: 設計佈局過程有些小曲折,一開始因爲沒有經驗,不知道該如何用代碼該出想要的佈局效果,也想過不使用代碼修改佈局,直接在界面上拖拽。但考慮到代碼的靈活性,仍是決定使用代碼,放棄了拖拽設計(下次有機會作UI,但願嘗試下拖拽設計和代碼設計結合的形式)。好在有博客和Qt官方文檔的支持,仍是成功學會了Qt的佈局設計,作出了當前這個效果。
主要在開始新遊戲的時候使用,首先用generate
中生成數獨遊戲,而後再轉換成QString
顯示在界面的button
上,部分代碼以下:
int result[10][LEN*LEN]; sudoku->generate(10, degOfDifficulty, result); QString temp; QString vac(""); for (int i = 0; i < LEN; ++i) { for (int j = 0; j < LEN; ++j) { if (result[target][i*LEN + j] == 0) { tableClickable[i][j] = true; puzzleButtons[i][j]->setText(vac); puzzleButtons[i][j]->setEnabled(true); puzzleButtons[i][j]->setCheckable(true); // Able to be checked } else { tableClickable[i][j] = false; puzzleButtons[i][j]->setText(temp.setNum(result[target][i*LEN + j])); puzzleButtons[i][j]->setEnabled(false); // Unable to be editted } } }
對於已經有數字的位置,則設置按鈕不可用,一個樣例的盤面以下:
主要用在提示功能上,首先判斷是否可解,若是可解則在相應的位置上給出提示,不可解則給出相應的提示,部分代碼以下:
if (sudoku->solve(board, solution)) { puzzleButtons[currentX][currentY]->setText(QString::number(solution[currentX*LEN + currentY])); puzzleButtons[currentX][currentY]->setChecked(false); // Set button unchecked checkGame(); } else { QMessageBox::information(this, tr("Bad Sudoku"), tr("Can not give a hint.The current Sudoku\ is not valid\nPlease check the row,rolumn or 3x3 block to correct it.")); }
此次咱們的結對過程比較順利,雙方都能作到互相理解支持,咱們的大部分工做在國慶期間完成,過程按照《構建之法》上講到的,1小時切換一次。個人partner有些缺少積極性,因此雖然有點很差意思不過我會去督促他,這樣就保證了效率,另外一方面我在UI設計上經驗不足,個人partner解決了這個問題。我認爲咱們基本實現了取長補短。
同時我也體會到了在高強度編程的時候,高頻次地更換駕駛員和領航員的職責是頗有必要的,這樣會緩解疲勞和壓力,從而提升了代碼的質量。
不過,在結對的過程當中,我也由於編程過程被人監督而有些不自在,感受沒有徹底發揮本身的水平。
整體而言,我認爲咱們發揮告終對編程的優點,但要進一步提升效率和質量,也許我和partner之間須要更多的磨合。
下面是隊友的感覺:
咱們結對的過程整體來講算是不錯的,成功完成了基本功能要求與附加的Step四、Step5。咱們的大部分工做在國慶期間完成,那段時間嚴格遵照結對編程規範,一人敲代
碼,另外一人在一旁幫助審覈代碼與提供思路,每一小時進行工做交換,每次交換都把代碼push到Github上,記錄這一步工做的結果。咱們用了三天時間實現了邏輯部分的> 完善與測試,並搭建起了UI的三個頁面框架,整體效率還算不錯。期間也遇到過找不着源頭的bug,費了咱們很多時間,不過好在是兩我的協力查資料、想辦法,最終仍是> 解決了問題。國慶事後因爲兩人的時間不太能湊得上,咱們便將工做分工,一人主攻功能,一人主攻界面,一步步推動項目並達到預期目標。
如下爲咱們二人結對編程時的照片。
在我看來,若是想要發揮結對編程的所有做用,就須要本人和partner之間加深瞭解和合做、互相不介意暴露問題、而且深入領會領航員和駕駛員的職責所在,取長補短,這樣纔能有好的結果。
咱們互評了優缺點,結果以下:
15061119
優勢:
1.極高的編碼效率。
2.專一於解決每一個問題。
3.充滿責任心與工做熱情。
缺點:
1.編碼風格不太統一。
15061104
優勢:
1.能理解支持partner。
2.能力較強。
3.解決了我一直苦惱的設計問題。
缺點:
1.某種程度上,欠缺一些積極性。
如下是個人自我評價:
在博客中指明合做小組兩位同窗的學號,分析兩組不一樣的模塊合併以後出現的問題,爲什麼會出現這樣的問題,
以及是如何根據反饋改進本身模塊的。
15061111
15061129
問題描述
咱們組的dll在64位下生成,而合做小組的是在32位下生成的,這樣致使模塊不可調用。
解決方案
從新生成了64位的dll,問題解決。
SODUCORE_API void generate_m(int number, int mode, int **result); SODUCORE_API void generate_r(int number, int lower, int upper, bool unique, int **result); SODUCORE_API bool solve_s(int *puzzle, int *solution);
而咱們本身的接口爲:
void generate(int number, int lower, int upper, bool unique, int result[][LEN*LEN]); void generate(int number, int mode, int result[][LEN*LEN]); bool solve(int puzzle[], int solution[]);
這就致使改變計算模塊以後須要更名字。
問題描述
注意到在13.2.2的雙方的接口中,咱們組定義result位二維數組,而合做小組定義爲二維指針,這就致使參數錯誤。
解決方案
將result轉換位二維指針便可。
咱們在step4的基礎上進行了增量開發,主要實現了:幫助、錯誤提示、快速存檔讀檔以及繼續遊戲的功能。
咱們在主界面加入了幫助按鈕,進入以後會顯示數獨的規則以及一個完整的數獨:
用戶點擊return按鈕能夠返回到主界面。
錯誤提示就是當用戶填入的數字不知足數獨的約束條件的時候,對不知足的數字對標紅,這樣用戶能夠很容易發現本身的錯誤,參看下圖:
上圖中填入的1和同列以及同行的1衝突,因此顯示爲紅色,更改以後顏色回覆正常:
用戶進入遊戲以後若是想要保存當前的盤面,則只須要點擊菜單欄的QuickSave進行存檔,以後若是想回到存檔時候的狀態,則只須要點擊QuicjLoad。
點擊QuickSave存檔:
繼續遊戲:
而後點擊QuicjLoad相應的存檔點:
恢復:
當用戶上次未完成遊戲直接退出,再一次進入遊戲能夠點擊Continue來恢復界面:
點擊以後恢復:
PSP2.1 | Personal Software Process Stages | 實際耗時(分鐘) |
---|---|---|
Planning | 計劃 | 180 |
Estimate | · 估計這個任務須要多少時間 | 5 |
Development | 開發 | 10 |
Analysis | · 需求分析 (包括學習新技術) | 300 |
Design Spec | · 生成設計文檔 | 20 |
Design Review | · 設計複審 (和同事審覈設計文檔) | 5 |
Coding Standard | · 代碼規範 (爲目前的開發制定合適的規範) | 10 |
Design | · 具體設計 | 50 |
Coding | · 具體編碼 | 2700 |
Code Review | · 代碼複審 | 180 |
Test | · 測試(自我測試,修改代碼,提交修改) | 240 |
Reporting | 報告 | 10 |
Test Report | · 測試報告 | 10 |
Size Measurement | · 計算工做量 | 10 |
Postmortem & Process Improvement Plan | · 過後總結, 並提出過程改進計劃 | 60 |
Total | 合計 | 3790 |
此次做業中,我收穫了不少,我學會了如何用Qt進行GUI設計、以及如何將程序導出成DLL進行復用,同時我也實踐告終對編程。 經過此次的結對編程我對合做的優點和劣勢有了更深的體會,若是兩人的之間有足夠的支持,並能積極改進自身的缺點,就能很好地進行合做,合做的重點就在於對事不對人、取長補短和理解包容。