[ADSS18]
Vard Antinyan, Jesper Derehag, Anna Sandberg, and Miroslaw Staron. Mythical Unit Test Coverage. IEEE Software. 35:73-79, 2018.
[And10]
Jackie Andrade. What does doodling do? Applied Cognitive Psychology. 24(1):100-106, 2010, January.
[Arm07]
Joe Armstrong. Programming Erlang: Software for a Concurrent World. The Pragmatic Bookshelf, Raleigh, NC, 2007.
[BR89]
Albert J. Bernstein and Sydney Craft Rozen. Dinosaur Brains: Dealing with All Those Impossible People at Work. John Wiley & Sons, New York, NY, 1989.
[Bro96]
Frederick P. Brooks, Jr. The Mythical Man-Month: Essays on Software Engineering. Addison-Wesley, Reading, MA, Anniversary, 1996.
[CN91]
Brad J. Cox and Andrew J. Novobilski. Object-Oriented Programming: An Evolutionary Approach. Addison-Wesley, Reading, MA, Second, 1991.
[Con68]
Melvin E. Conway. How do Committees Invent? Datamation. 14(5):28-31, 1968, April.
[de 98]
Gavin de Becker. The Gift of Fear: And Other Survival Signals That Protect Us from Violence. Dell Publishing, New York City, 1998.
[DL13]
Tom DeMacro and Tim Lister. Peopleware: Productive Projects and Teams. Addison-Wesley, Boston, MA, Third, 2013.
[Fow00]
Martin Fowler. UML Distilled: A Brief Guide to the Standard Object Modeling Language. Addison-Wesley, Boston, MA, Second, 2000.
[Fow04]
Martin Fowler. UML Distilled: A Brief Guide to the Standard Object Modeling Language. Addison-Wesley, Boston, MA, Third, 2004.
[Fow19]
Martin Fowler. Refactoring: Improving the Design of Existing Code. Addison-Wesley, Boston, MA, Second, 2019.
[GHJV95]
Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, Reading, MA, 1995.
[Hol92]
Michael Holt. Math Puzzles & Games. Dorset House, New York, NY, 1992.
[Hun08]
Andy Hunt. Pragmatic Thinking and Learning: Refactor Your Wetware. The Pragmatic Bookshelf, Raleigh, NC, 2008.
[Joi94]
T.E. Joiner. Contagious depression: Existence, specificity to depressed symptoms, and the role of reassurance seeking. Journal of Personality and Social Psychology. 67(2):287--296, 1994, August.
[Knu11]
Donald E. Knuth. The Art of Computer Programming, Volume 4A: Combinatorial Algorithms, Part 1. Addison-Wesley, Boston, MA, 2011.
[Knu98]
Donald E. Knuth. The Art of Computer Programming, Volume 1: Fundamental Algorithms. Addison-Wesley, Reading, MA, Third, 1998.
[Knu98a]
Donald E. Knuth. The Art of Computer Programming, Volume 2: Seminumerical Algorithms. Addison-Wesley, Reading, MA, Third, 1998.
[Knu98b]
Donald E. Knuth. The Art of Computer Programming, Volume 3: Sorting and Searching. Addison-Wesley, Reading, MA, Second, 1998.
[KP99]
Brian W. Kernighan and Rob Pike. The Practice of Programming. Addison-Wesley, Reading, MA, 1999.
[Mey97]
Bertrand Meyer. Object-Oriented Software Construction. Prentice Hall, Upper Saddle River, NJ, Second, 1997.
[Mul18]
Jerry Z. Muller. The Tyranny of Metrics. Princeton University Press, Princeton NJ, 2018.
[SF13]
Robert Sedgewick and Phillipe Flajolet. An Introduction to the Analysis of Algorithms. Addison-Wesley, Boston, MA, Second, 2013.
[Str35]
James Ridley Stroop. Studies of Interference in Serial Verbal Reactions. Journal of Experimental Psychology. 18:643--662, 1935.
[SW11]
Robert Sedgewick and Kevin Wayne. Algorithms. Addison-Wesley, Boston, MA, Fourth, 2011.
[Tal10]
Nassim Nicholas Taleb. The Black Swan: Second Edition: The Impact of the Highly Improbable. Random House, New York, NY, Second, 2010.
[WH82]
James Q. Wilson and George Helling. The police and neighborhood safety. The Atlantic Monthly. 249[3]:29--38, 1982, March.
[YC79]
Edward Yourdon and Larry L. Constantine. Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design. Prentice Hall, Englewood Cliffs, NJ, 1979.
[You95]
Edward Yourdon. When good-enough software is best. IEEE Software. 1995, May. Copyright © 2020 Pearson Education, Inc.
ผมเลือกที่จะมีคำถามที่หาคำตอบไม่ได้ ดีกว่ามีคำตอบที่ห้ามสงสัย
Richard Feynman
Appendix 2
แนวทางคำตอบสำหรับแบบฝึกหัด
Answer 1 (จาก แบบฝึกหัดที่ 1)
ในมุมมองของเรา Class Split2 นั้นมีความเป็น orthogonal มากกว่า เพราะมันโฟกัสอยู่กับหน้าที่ของตัวเองคือการ splitting lines และไม่สนใจรายละเอียดว่าบรรทัดเหล่านั้นจะมาจากที่ไหน วิธีนี้ไม่เพียงแต่จะทำให้ Code พัฒนาง่ายขึ้นเท่านั้น แต่มันยังยืดหยุ่นมากขึ้นด้วย โดยที่ Split2 สามารถแบ่งบรรทัดที่อ่านมาจาก File, บรรทัดที่ถูก Generate มาจาก Routine อื่น หรือบรรทัดที่ถูกส่งผ่านมาทาง Environment ก็ได้
Answer 2 (จาก แบบฝึกหัดที่ 2)
เริ่มด้วยข้อความสั้นๆ ว่า: คุณสามารถเขียน Code ที่ดีและมีความเป็น orthogonal ได้เกือบทุกภาษา แต่ในเวลาเดียวกัน ทุกภาษาก็มีสิ่งที่ยั่วยวนคุณอยู่เหมือนกัน นั่นก็คือ Feature ที่อาจจะนำไปสู่การเพิ่มขึ้นของ coupling และลดความเป็น orthogonality ลง
ในภาษา OO Feature อย่างเช่น multiple inheritance, exceptions, operator overloading และการ overriding Parent-method (ผ่าน subclassing) ให้โอกาสอย่างมากในการเพิ่ม coupling ในทางที่มองไม่ค่อยออก นอกจากนี้ยังมี coupling อีกรูปแบบหนึ่งเพราะ Class นั้นทำการเชื่อมโยง Code เข้ากับ Data ซึ่งปกติแล้วมันเป็นเรื่องที่ดี (เมื่อ coupling ดี เราจะเรียกมันว่า cohesion) แต่ถ้าคุณไม่ทำให้ Class ของคุณมีความเจาะจงมากพอ มันอาจจะนำไปสู่ Interface ที่ดูแย่มากได้
ในภาษา Functional คุณจะถูกส่งเสริมให้เขียน Function เล็กๆ ที่ decoupled จากกันจำนวนมาก แล้วนำมารวมกันในวิธีที่แตกต่างกันเพื่อแก้ปัญหาของคุณ ในทางทฤษฎีนั้นฟังดูดี และในทางปฏิบัติส่วนใหญ่ก็เป็นเช่นนั้นจริงๆ แต่ยังมี coupling บางรูปแบบที่สามารถเกิดขึ้นที่นี่ได้ด้วย โดยปกติ Function เหล่านี้จะทำหน้าที่ transform ข้อมูล ซึ่งหมายความว่าผลลัพธ์ของ Function หนึ่งอาจกลายเป็น Input ให้กับอีก Function หนึ่งได้ ถ้าคุณไม่ระวัง การเปลี่ยน Data Format ที่ Function หนึ่งสร้างขึ้นอาจนำไปสู่ความผิดพลาดที่จุดถัดไปในสาย transform ได้ ภาษาที่มี Type System ที่ดีจะช่วยลดปัญหานี้ได้
Answer 3 (จาก แบบฝึกหัดที่ 3)
ใช้เทคโนโลยีพื้นๆ มาช่วยนี่แหละ! วาดรูปการ์ตูนด้วย Marker บน Whiteboard เช่น รูปคน, โทรศัพท์ และบ้าน ไม่ต้องวาดสวยมาก แค่ลายเส้นพื้นฐานก็พอ แล้วใช้ Post-it แปะเพื่ออธิบายเนื้อหาของหน้าเป้าหมายลงบน clickable areas เมื่อการประชุมดำเนินไป คุณก็สามารถปรับปรุงรูปวาดและตำแหน่งของ Post-it ได้
Answer 4 (จาก แบบฝึกหัดที่ 4)
เพราะว่าเราต้องการทำให้ภาษานี้สามารถขยายต่อได้ เราจึงจะทำให้ Parser เป็นแบบ Table Driven โดยในแต่ละ Entry ของตารางจะประกอบด้วย Command Letter, Flag ที่บอกว่าต้องมี Argument หรือไม่ และชื่อของ Routine ที่จะเรียกใช้เพื่อจัดการกับ Command นั้นๆ
| | typedef struct { |
|---|---|
| | char cmd; / the command letter / |
| | int hasArg; / does it take an argument / |
| | void (func)(int, int); / routine to call */ |
| | } Command; |
| | |
| | static Command cmds[] = { |
| | { 'P', ARG, doSelectPen }, |
| | { 'U', NO_ARG, doPenUp }, |
| | { 'D', NO_ARG, doPenDown }, |
| | { 'N', ARG, doPenDir }, |
| | { 'E', ARG, doPenDir }, |
| | { 'S', ARG, doPenDir }, |
| | { 'W', ARG, doPenDir } |
| | }; |
โปรแกรมหลักนั้นค่อนข้างง่าย: อ่านทีละบรรทัด, ค้นหา Command, รับ Argument ถ้าจำเป็น, แล้วจึงเรียกใช้ Handler Function
| | while (fgets(buff, sizeof(buff), stdin)) { |
|---|---|
| | |
| | Command cmd = findCommand(buff); |
| | |
| | if (cmd) { |
| | int arg = 0; |
| | |
| | if (cmd->hasArg && !getArg(buff+1, &arg)) { |
| | fprintf(stderr, "'%c' needs an argument ", buff); | | | continue; | | | } | | | | | | cmd->func(buff, arg); | | | } | | | } |
Function ที่ทำหน้าที่ค้นหา Command จะใช้วิธีแบบ Linear Search ในตาราง และคืนค่าเป็น Entry ที่ตรงกันหรือคืนค่าเป็น NULL
| | Command *findCommand(int cmd) { |
|---|---|
| | int i; |
| | |
| | for (i = 0; i < ARRAY_SIZE(cmds); i++) { |
| | if (cmds[i].cmd == cmd) |
| | return cmds + i; |
| | } |
| |
| | fprintf(stderr, "Unknown command '%c' ", cmd); | | | return 0; | | | } |
สุดท้าย การอ่าน Numeric Argument นั้นทำได้ค่อนข้างง่ายโดยใช้ sscanf
| | int getArg(const char buff, int result) { |
|---|---|
| | return sscanf(buff, "%d", result) == 1; |
| | } |
Answer 5 (จาก แบบฝึกหัดที่ 5)
ที่จริงแล้ว คุณได้แก้ปัญหานี้ไปแล้วในแบบฝึกหัดก่อนหน้านี้ โดยที่ Code ที่คุณเขียน Interpreter สำหรับภาษาภายนอก (external language) นั้น จะประกอบไปด้วย Interpreter ภายใน (internal interpreter) ด้วย ในกรณีของ Code ตัวอย่างของเรา นี่คือ Function doXxx ต่างๆ
Answer 6 (จาก แบบฝึกหัดที่ 6)
การใช้ BNF ข้อกำหนดเรื่องเวลาอาจจะเป็นดังนี้
| time | ::= | hour ampm | hour : minute ampm | hour : minute | | --- | --- | --- | | | | | | ampm | ::= | am | pm | | | | | | hour | ::= | digit | digit digit | | | | | | minute | ::= | digit digit | | | | | | digit | ::= | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
การให้คำนิยามของ hour และ minute ที่ดีกว่านี้ ควรจะคำนึงถึงว่าชั่วโมงสามารถเป็นได้ตั้งแต่ 00 ถึง 23 และนาทีตั้งแต่ 00 ถึง 59:
| hour | ::= | h-tens digit | digit | | --- | --- | --- | | minute | ::= | m-tens digit | | h-tens | ::= | 0 | 1 | | m-tens | ::= | 0 | 1 | 2 | 3 | 4 | 5 | | digit | ::= | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
Answer 7 (จาก แบบฝึกหัดที่ 7)
นี่คือ Parser ที่เขียนโดยใช้ Library JavaScript ที่ชื่อ Pegjs:
lang/peg_parser/time_parser.pegjs
| | time |
|---|---|
| | = h:hour offset:ampm { return h + offset } |
| | / h:hour ":" m:minute offset:ampm { return h + m + offset } |
| | / h:hour ":" m:minute { return h + m } |
| | |
| | ampm |
| | = "am" { return 0 } |
| | / "pm" { return 12*60 } |
| | |
| | hour |
| | = h:two_hour_digits { return h*60 } |
| | / h:digit { return h*60 } |
| | |
| | minute |
| | = d1:[0-5] d2:[0-9] { return parseInt(d1+d2, 10); } |
| | |
| | digit |
| | = digit:[0-9] { return parseInt(digit, 10); } |
| | |
| | two_hour_digits |
| | = d1:[01] d2:[0-9 ] { return parseInt(d1+d2, 10); } |
| | / d1:[2] d2:[0-3] { return parseInt(d1+d2, 10); } |
การทดสอบแสดงให้เห็นถึงการใช้งาน:
lang/peg_parser/test_time_parser.js
| | let test = require('tape'); |
|---|---|
| | let time_parser = require('./time_parser.js'); |
| |
| | // time ::= hour ampm | |
| | // hour : minute ampm | |
| | // hour : minute |
| | // |
| | // ampm ::= am | pm | | | // | | | // hour ::= digit | digit digit | | | // | | | // minute ::= digit digit | | | // | | | // digit ::= 0 |1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | | | | | | | | | const h = (val) => val*60; | | | const m = (val) => val; | | | const am = (val) => val; | | | const pm = (val) => val + h(12); | | | | | | let tests = { | | | | | | "1am": h(1), | | | "1pm": pm(h(1)), | | | | | | "2:30": h(2) + m(30), | | | "14:30": pm(h(2)) + m(30), | | | "2:30pm": pm(h(2)) + m(30), | | | | | | } | | | | | | test('time parsing', function (t) { | | | for (const string in tests) { | | | let result = time_parser.parse(string) | | | t.equal(result, tests[string], string); | | | } | | | t.end() | | | }); |
Answer 8 (จาก แบบฝึกหัดที่ 8)
นี่คือแนวทางคำตอบที่เป็นไปได้ในภาษา Ruby:
| | TIME_RE = %r{ |
|---|---|
| | (?<digit>[0-9]){0} |
| | (?<h_ten>[0-1]){0} |
| | (?<m_ten>[0-6]){0} |
| | (?<ampm> am | pm){0} |
| | (?<hour> (\g<h_ten> \g<digit>) | \g<digit>){0} |
| | (?<minute> \g<m_ten> \g<digit>){0} |
| | |
| | \A( |
| | ( \g<hour> \g<ampm> ) |
| | | ( \g<hour> : \g<minute> \g<ampm> ) |
| | | ( \g<hour> : \g<minute> ) |
| | )\Z |
| | |
| | }x |
| | |
| | def parse_time(string) |
| | result = TIME_RE.match(string) |
| | if result |
| | result[:hour].to_i * 60 + |
| | (result[:minute] || "0").to_i + | | | (result[:ampm] == "pm" ? 12*60 : 0) | | | end | | | end |
(โค้ดนี้ใช้เทคนิคการกำหนด named patterns ที่ส่วนต้นของ regular expression แล้วจึงอ้างอิงถึงพวกมันในฐานะ subpatterns ในการจับคู่จริง)
Answer 9 (จาก แบบฝึกหัดที่ 9)
คำตอบของเรานั้นต้องตั้งอยู่บนข้อสันนิษฐานหลายประการ:
- อุปกรณ์จัดเก็บข้อมูลมีข้อมูลที่เราจำเป็นต้องถ่ายโอนอยู่ภายใน
- เราทราบความเร็วที่คนเดิน
- เราทราบระยะห่างระหว่างเครื่องจักร
- เราไม่ได้นำเวลาที่ใช้ในการถ่ายโอนข้อมูลเข้าและออกจากอุปกรณ์จัดเก็บข้อมูลมาคำนวณด้วย
- ภาระงาน (overhead) ในการจัดเก็บข้อมูลนั้นมีค่าใกล้เคียงกับภาระงานในการส่งข้อมูลผ่านสายสื่อสาร
Answer 10 (จาก แบบฝึกหัดที่ 10)
ภายใต้ข้อควรระวังในคำตอบก่อนหน้านี้: เทปขนาด 1TB ประกอบด้วยข้อมูล 8 × 2^40 หรือ 2^43 บิต ดังนั้นสายสื่อสารความเร็ว 1Gbps จะต้องส่งข้อมูลเป็นเวลาประมาณ 9,000 วินาที หรือประมาณ 2 ชั่วโมงครึ่ง เพื่อถ่ายโอนข้อมูลในปริมาณที่เท่ากัน หากคนเดินด้วยความเร็วคงที่ 3½ mph เครื่องจักรทั้งสองเครื่องของเราจะต้องอยู่ห่างกันเกือบ 9 ไมล์ เพื่อให้สายสื่อสารทำงานได้ดีกว่าพนักงานส่งของ มิฉะนั้น คนจะเป็นฝ่ายชนะ
Answer 14 (จาก แบบฝึกหัดที่ 14)
เราจะแสดง Function Signature ในภาษา Java พร้อมกับ pre- และ postconditions ในคอมเมนต์
เริ่มจาก invariant ของ Class:
| | /** |
|---|---|
| | * @invariant getSpeed() > 0 |
| | * implies isFull() // Don't run empty |
| | * |
| | * @invariant getSpeed() >= 0 && |
| | * getSpeed() < 10 // Range check |
| | */ |
ต่อมาคือ pre- และ postconditions:
| | /** |
|---|---|
| | * @pre Math.abs(getSpeed() - x) <= 1 // Only change by one |
| | * @pre x >= 0 && x < 10 // Range check |
| | * @post getSpeed() == x // Honor requested speed |
| | */ |
| | public void setSpeed(final int x) |
| | |
| | /** |
| | * @pre !isFull() // Don't fill it twice |
| | * @post isFull() // Ensure it was done |
| | */ |
| | void fill() |
| | |
| | /** |
| | * @pre isFull() // Don't empty it twice |
| | * @post !isFull() // Ensure it was done |
| | */ |
| | void empty()
Answer 15 (จาก แบบฝึกหัดที่ 15)
มีทั้งหมด 21 พจน์ในอนุกรมนี้ ถ้าคุณตอบว่า 20 แสดงว่าคุณเพิ่งจะพบกับ fencepost error (ความผิดพลาดที่เกิดจากการไม่รู้ว่าต้องนับที่เสารั้วหรือช่องว่างระหว่างเสา)
Answer 16 (จาก แบบฝึกหัดที่ 16)
-
ในเดือนกันยายน ค.ศ. 1752 มีเพียง 19 วัน ซึ่งเป็นผลจากการปรับเปลี่ยนปฏิทินเพื่อให้สอดคล้องกันตามการปฏิรูปปฏิทินแบบเกรกอเรียน (Gregorian Reformation)
-
Directory อาจจะถูกลบออกไปโดย Process อื่น, คุณอาจจะไม่มีสิทธิ์ในการอ่านมัน, Drive อาจจะไม่ถูก Mount, ...; คุณน่าจะพอเห็นภาพแล้ว
-
เราแอบไม่ได้ระบุ Type ของ a และ b ไว้ ซึ่ง Operator overloading อาจจะทำให้ +, =, หรือ != มีพฤติกรรมที่เราคาดไม่ถึง นอกจากนี้ a และ b อาจเป็น alias ของ Variable ตัวเดียวกัน ดังนั้นการกำหนดค่าครั้งที่สองจะไปเขียนทับค่าที่เก็บไว้ในการกำหนดค่าครั้งแรก และถ้าโปรแกรมทำงานแบบ Concurrent และถูกเขียนมาไม่ดี ค่าของ a อาจจะถูก Update ไปแล้วในขณะที่กำลังทำการบวก
-
ในเรขาคณิตแบบไม่ใช่ยูคลิด (non-Euclidean geometry) ผลรวมของมุมภายในรูปสามเหลี่ยมจะไม่เท่ากับ 180° ลองนึกถึงรูปสามเหลี่ยมที่วาดลงบนพื้นผิวของทรงกลมสิ
-
นาทีอธิกสุรทิน (Leap minutes) อาจมีได้ถึง 61 หรือ 62 วินาที
-
ในบางภาษา การเกิด Numeric overflow อาจทำให้ผลลัพธ์ของ a+1 กลายเป็นค่าลบได้
Answer 17 (จาก แบบฝึกหัดที่ 17)
ในการเขียนด้วย C และ C++ ส่วนใหญ่ ไม่มีทางที่เราจะตรวจสอบได้เลยว่า Pointer ชี้ไปที่หน่วยความจำที่ยังใช้งานได้อยู่จริงหรือไม่ ความผิดพลาดที่พบบ่อยคือการคืนหน่วยความจำ (deallocate) ไปแล้ว แต่ยังมีการอ้างอิงถึงหน่วยความจำนั้นอีกในภายหลัง ซึ่งในตอนนั้น หน่วยความจำดังกล่าวอาจจะถูกจัดสรรไปใช้ในวัตถุประสงค์อื่นแล้วก็ได้ การตั้งค่า Pointer ให้เป็น NULL จึงเป็นสิ่งที่โปรแกรมเมอร์หวังว่าจะช่วยป้องกันการอ้างอิงที่ผิดพลาดเหล่านี้ เพราะในกรณีส่วนใหญ่ การทำ dereferencing ของ NULL pointer จะทำให้เกิด runtime error ขึ้น
Answer 18 (จาก แบบฝึกหัดที่ 18)
การตั้งค่า Reference ให้เป็น NULL จะช่วยลดจำนวน Pointer ที่ชี้ไปยัง Object นั้นลงหนึ่งตำแหน่ง และเมื่อตัวเลขนี้กลายเป็นศูนย์ Object นั้นจะอยู่ในสถานะที่พร้อมสำหรับการทำ Garbage collection การตั้งค่า Reference ให้เป็น NULL มีความสำคัญมากสำหรับโปรแกรมที่ทำงานต่อเนื่องเป็นเวลานาน ซึ่งโปรแกรมเมอร์ต้องมั่นใจว่าการใช้งานหน่วยความจำจะไม่เพิ่มขึ้นเรื่อยๆ เมื่อเวลาผ่านไป
Answer 19 (จาก แบบฝึกหัดที่ 19)
การ Implementation แบบง่ายๆ อาจจะเป็นดังนี้:
| | class FSM |
|---|---|
| | def initialize(transitions, initial_state) |
| | @transitions = transitions |
| | @state = initial_state |
| | end |
| | def accept(event) |
| | @state, action = TRANSITIONS[@state][event] || TRANSITIONS[@state][:default] | | | end | | | end |
(ดาวน์โหลดไฟล์นี้เพื่อดู Code ที่ได้รับการปรับปรุงซึ่งใช้ Class FSM ใหม่นี้)
Answer 20 (จาก แบบฝึกหัดที่ 20)
-
...เหตุการณ์ network interface down สามครั้งภายในห้านาที
กรณีนี้สามารถ Implement โดยใช้ State Machine ได้ แต่มันจะซับซ้อนกว่าที่เห็นในตอนแรก: หากคุณได้รับเหตุการณ์ที่นาทีที่ 1, 4, 7 และ 8 คุณควรจะส่งสัญญาณเตือนในเหตุการณ์ที่สี่ ซึ่งหมายความว่า State Machine จะต้องสามารถจัดการเรื่องการ Reset ตัวเองได้
ด้วยเหตุนี้ Event Streams จึงดูเป็นเทคโนโลยีที่เหมาะสมกว่า มี Reactive Function ที่ชื่อว่า
bufferซึ่งมี Parameter เป็น size และ offset ที่จะช่วยให้คุณดึงกลุ่มของเหตุการณ์สามเหตุการณ์ที่เกิดขึ้นต่อเนื่องกันออกมาได้ จากนั้นคุณสามารถดู Timestamp ของเหตุการณ์แรกและเหตุการณ์สุดท้ายในกลุ่มเพื่อตัดสินใจว่าควรจะส่งสัญญาณเตือนหรือไม่ -
...หลังพระอาทิตย์ตกดิน และมีการตรวจพบความเคลื่อนไหวที่ชั้นล่างของบันได ตามด้วยการตรวจพบความเคลื่อนไหวที่ชั้นบนของบันได...
กรณีนี้อาจจะ Implement โดยใช้ Pubsub ร่วมกับ State Machine คุณสามารถใช้ Pubsub เพื่อกระจายเหตุการณ์ไปยัง State Machine จำนวนเท่าใดก็ได้ แล้วให้ State Machine เหล่านั้นเป็นตัวกำหนดว่าควรทำอย่างไรต่อ
-
...แจ้งเตือนไปยังระบบรายงานต่างๆ เมื่อคำสั่งซื้อเสร็จสมบูรณ์
กรณีนี้ควรจัดการโดยใช้ Pubsub มากที่สุด คุณอาจจะอยากใช้ Streams ก็ได้ แต่นั่นหมายความว่าระบบที่รับการแจ้งเตือนจะต้องเป็นแบบ Stream-based ด้วย
-
...บริการหลังบ้าน (backend services) สามแห่งและรอการตอบกลับ
กรณีนี้คล้ายกับตัวอย่างของเราที่ใช้ Streams ในการดึงข้อมูลผู้ใช้
Answer 21 (จาก แบบฝึกหัดที่ 21)
-
การคิดค่าจัดส่งและภาษีการขายลงในคำสั่งซื้อ:
basic order → finalized order ใน Code แบบเดิม เป็นไปได้ว่าคุณจะมี Function หนึ่งที่ใช้คำนวณค่าจัดส่ง และอีก Function หนึ่งที่ใช้คำนวณภาษี แต่ในกรณีนี้เรากำลังพูดถึงการทำ transformation เราจึงทำการ transform คำสั่งซื้อที่มีเพียงรายการสินค้าให้กลายเป็นสิ่งใหม่ นั่นก็คือคำสั่งซื้อที่พร้อมสำหรับการจัดส่ง
-
แอปพลิเคชันของคุณโหลดข้อมูลการตั้งค่า (configuration) จากไฟล์ที่ระบุชื่อ:
file name → configuration structure -
มีคนเข้าสู่ระบบในเว็บแอปพลิเคชัน:
user credentials → session
Answer 22 (จาก แบบฝึกหัดที่ 22)
Transformation ในระดับสูง (high-level transformation):
| | field contents as string |
|---|---|
| | → [validate & convert] |
| | → {:ok, value} | {:error, reason} |
สามารถแบ่งย่อยออกเป็น:
| | field contents as string |
|---|---|
| | → [convert string to integer] |
| | → [check value >= 18] |
| | → [check value <= 150] |
| | → {:ok, value} | {:error, reason} |
นี่คือภายใต้ข้อสันนิษฐานว่าคุณมี pipeline สำหรับจัดการข้อผิดพลาด (error-handling pipeline)
Answer 23 (จาก แบบฝึกหัดที่ 23)
ขอตอบคำถามส่วนที่สองก่อน: เราชอบ Code ส่วนแรกมากกว่า
ใน Code ส่วนที่สอง แต่ละขั้นตอนจะคืนค่าเป็น Object ที่มีการ Implement Function ถัดไปที่เราเรียกใช้อยู่: เช่น Object ที่ถูกคืนค่ามาจาก content_of จะต้องมี find_matching_lines และเป็นเช่นนี้ไปเรื่อยๆ
นั่นหมายความว่า Object ที่คืนค่ามาจาก content_of นั้นถูกผูกติด (coupled) เข้ากับ Code ของเรา ลองจินตนาการว่าความต้องการเปลี่ยนไป และเราต้องละเว้นบรรทัดที่ขึ้นต้นด้วยเครื่องหมาย # ในรูปแบบของ transformation นั้นทำได้ง่ายมาก:
| | const content = File.read(file_name); |
|---|---|
| | const no_comments = remove_comments(content) |
| | const lines = find_matching_lines(no_comments, pattern) |
| | const result = truncate_lines(lines) |
เรายังสามารถสลับลำดับของ remove_comments และ find_matching_lines ได้ด้วยซ้ำ และมันก็ยังทำงานได้อยู่
แต่ในรูปแบบของการเชื่อมต่อ Method (chained style) สิ่งนี้จะทำได้ยากกว่า เราควรเอา Method remove_comments ไปไว้ที่ไหนดี: ใน Object ที่คืนค่ามาจาก content_of หรือใน Object ที่คืนค่ามาจาก find_matching_lines ดี? และจะมี Code ส่วนอื่นๆ พังอีกไหมถ้าเราเปลี่ยน Object นั้น? Coupling ในลักษณะนี้เป็นสาเหตุที่ทำให้บางครั้งการทำ method chaining ถูกเรียกว่า "train wreck" (ซากรถไฟชนกัน)
Answer 24 (จาก แบบฝึกหัดที่ 24)
การประมวลผลภาพ (Image processing)
สำหรับการจัดสรรภาระงาน (scheduling workload) แบบง่ายๆ ระหว่างกระบวนการที่ทำงานคู่ขนานกัน คิวงานที่ใช้ร่วมกัน (shared work queue) ก็น่าจะเพียงพอแล้ว แต่คุณอาจจะต้องการพิจารณาใช้ระบบกระดานดำ (blackboard system) หากมีเรื่องของ Feedback เข้ามาเกี่ยวข้อง นั่นคือหากผลลัพธ์จากส่วนที่ประมวลผลไปแล้วมีผลกระทบต่อส่วนอื่นๆ เช่น ในแอปพลิเคชันเกี่ยวกับคอมพิวเตอร์วิทัศน์ (machine vision) หรือการทำ 3D image-warp transform ที่ซับซ้อน
การจัดการตารางเวลาแบบกลุ่ม (Group calendaring)
นี่อาจจะเป็นสิ่งที่เหมาะสมมาก คุณสามารถโพสต์ตารางการประชุมและความว่างลงบน Blackboard คุณมี Entity ที่ทำงานแยกจากกันอย่างเป็นอิสระ มี Feedback จากการตัดสินใจที่เป็นเรื่องสำคัญ และผู้เข้าร่วมอาจจะมาและไปได้ตลอดเวลา
คุณอาจพิจารณาแบ่งส่วน (partitioning) ของระบบ Blackboard นี้ตามผู้ที่เข้ามาค้นหาข้อมูล: พนักงานระดับเริ่มต้นอาจสนใจเพียงแค่สำนักงานใกล้เคียง ฝ่ายบุคคลอาจต้องการเพียงแค่สำนักงานที่พูดภาษาอังกฤษได้ทั่วโลก และ CEO อาจต้องการข้อมูลทั้งหมดทุกส่วน
นอกจากนี้ยังมีความยืดหยุ่นในเรื่องของ Data Format: เรามีอิสระที่จะละเว้นรูปแบบหรือภาษาที่เราไม่เข้าใจ เราจำเป็นต้องเข้าใจรูปแบบที่แตกต่างกันเฉพาะสำหรับสำนักงานที่มีการประชุมร่วมกันเท่านั้น และเราไม่จำเป็นต้องเปิดเผยรูปแบบที่เป็นไปได้ทั้งหมดให้กับผู้เข้าร่วมทุกคน นี่เป็นการลด Coupling ลงเฉพาะจุดที่จำเป็น และไม่สร้างข้อจำกัดให้กับเราโดยไม่จำเป็น
เครื่องมือเฝ้าระวังเครือข่าย (Network monitoring tool)
กรณีนี้คล้ายกับ โปรแกรมขอสินเชื่อ/กู้ยืมเงิน มาก คุณมีรายงานปัญหาที่ส่งมาจากผู้ใช้และข้อมูลสถิติที่รายงานเข้ามาโดยอัตโนมัติ ซึ่งทั้งหมดจะถูกโพสต์ลงบน Blackboard โดยที่มนุษย์หรือ Agent ของซอฟต์แวร์สามารถวิเคราะห์ Blackboard เพื่อวินิจฉัยความล้มเหลวของเครือข่าย: ความผิดพลาดสองครั้งบนสายสื่อสารอาจเป็นแค่ผลจากรังสีคอสมิก แต่ถ้าเป็น 20,000 ครั้ง คุณกำลังเจอปัญหาที่ฮาร์ดแวร์แล้วล่ะ เช่นเดียวกับที่นักสืบช่วยกันคลี่คลายคดีฆาตกรรม คุณสามารถมี Entity หลายๆ ตัวช่วยกันวิเคราะห์และเสนอไอเดียเพื่อแก้ปัญหาเครือข่ายได้
Answer 25 (จาก แบบฝึกหัดที่ 25)
โดยปกติแล้ว ข้อสันนิษฐานสำหรับรายการของคู่ key-value คือ key จะต้องไม่ซ้ำกัน และโดยทั่วไป Library ของ Hash จะบังคับใช้กฎนี้ไม่ว่าจะเป็นด้วยตัวพฤติกรรมของ Hash เอง หรือด้วยข้อความแจ้งเตือนข้อผิดพลาดที่ระบุชัดเจนหากมี key ที่ซ้ำกัน อย่างไรก็ตาม Array มักจะไม่มีข้อจำกัดเหล่านี้ และจะยอมเก็บ key ที่ซ้ำกันได้อย่างไม่มีปัญหา นอกเสียจากว่าคุณจะเขียน Code เพื่อป้องกันไว้โดยเฉพาะ ดังนั้นในกรณีนี้ key แรกที่พบว่าตรงกับ DepositAccount จึงเป็นฝ่ายชนะ และรายการที่เหลือที่ตรงกันจะถูกละเว้นไป ซึ่งลำดับของข้อมูลที่เข้ามานั้นไม่มีอะไรการันตีได้ ทำให้บางครั้งมันก็ทำงานได้ถูกต้อง แต่บางครั้งก็ไม่
แล้วความแตกต่างระหว่างเครื่องที่ใช้ในการพัฒนาและเครื่องที่ใช้จริง (production) ล่ะ? มันก็แค่เรื่องบังเอิญเท่านั้นเอง
Answer 26 (จาก แบบฝึกหัดที่ 26)
ความจริงที่ว่าฟิลด์ที่เป็นตัวเลขล้วนสามารถทำงานได้ในสหรัฐอเมริกา แคนาดา และแคริบเบียนนั้นเป็นเพียงเรื่องบังเอิญ ตามข้อกำหนดของ ITU รูปแบบการโทรระหว่างประเทศจะเริ่มต้นด้วยเครื่องหมาย + ตัวอักษร * ยังถูกใช้ในบางพื้นที่ด้วย และที่พบบ่อยกว่านั้นคือเลขศูนย์นำหน้าสามารถเป็นส่วนหนึ่งของหมายเลขได้ อย่าเก็บหมายเลขโทรศัพท์ไว้ในฟิลด์ที่เป็นตัวเลข (numeric field) เป็นอันขาด
Answer 27 (จาก แบบฝึกหัดที่ 27)
ขึ้นอยู่กับว่าคุณอยู่ที่ไหน ในสหรัฐอเมริกา การวัดปริมาตรจะอ้างอิงกับแกลลอน (gallon) ซึ่งคือปริมาตรของรูปทรงกระบอกที่สูง 6 นิ้วและมีเส้นผ่านศูนย์กลาง 7 นิ้ว โดยปัดเศษให้เป็นลูกบาศก์นิ้วที่ใกล้เคียงที่สุด
ในแคนาดา "หนึ่งถ้วย" ในสูตรอาหารอาจหมายถึงข้อใดข้อหนึ่งต่อไปนี้:
- 1/5 ของ imperial quart หรือ 227 มล.
- 1/4 ของ US quart หรือ 236 มล.
- 16 ช้อนโต๊ะแบบเมตริก หรือ 240 มล.
- 1/4 ลิตร หรือ 250 มล.
เว้นแต่ว่าคุณกำลังพูดถึงหม้อหุงข้าว ซึ่งในกรณีนี้ "หนึ่งถ้วย" คือ 180 มล. สิ่งนี้มีที่มาจาก koku ซึ่งเป็นปริมาณข้าวสารโดยประมาณที่ใช้เลี้ยงคนหนึ่งคนเป็นเวลาหนึ่งปี โดยเห็นได้ชัดว่าอยู่ที่ประมาณ 180 ลิตร ถ้วยตวงหม้อหุงข้าวคือ 1 gō ซึ่งเท่ากับ 1/1000 ของ koku ดังนั้นมันจึงเป็นปริมาณข้าวที่คนคนหนึ่งจะรับประทานในมื้อเดียวโดยประมาณ [85]
Answer 28 (จาก แบบฝึกหัดที่ 28)
เห็นได้ชัดว่าเราไม่สามารถให้คำตอบที่สมบูรณ์ตายตัวสำหรับแบบฝึกหัดนี้ได้ อย่างไรก็ตาม เราสามารถให้แนวทางเล็กน้อยแก่คุณได้
หากคุณพบว่าผลลัพธ์ของคุณไม่ได้เป็นไปตามเส้นโค้งที่เรียบสม่ำเสมอ คุณอาจต้องการตรวจสอบดูว่ามีกิจกรรมอื่นๆ กำลังใช้งานพลังของโปรเซสเซอร์ของคุณอยู่หรือไม่ คุณอาจจะไม่ได้ตัวเลขที่ดีนักหากมีกระบวนการเบื้องหลัง (background processes) คอยดึงรอบการทำงาน (cycles) ไปจากโปรแกรมของคุณเป็นระยะๆ นอกจากนี้คุณอาจต้องการตรวจสอบหน่วยความจำด้วย: หากแอปพลิเคชันเริ่มใช้พื้นที่ swap ประสิทธิภาพการทำงานจะลดลงอย่างรวดเร็ว
นี่คือกราฟแสดงผลลัพธ์จากการรันโค้ดบนเครื่องหนึ่งของเรา:
กราฟถูกวาดขึ้นเพื่อคำนวณเวลาที่ใช้ในการเรียงลำดับเวกเตอร์ของจำนวนเต็มโดยใช้อัลกอริทึมต่างๆ แกนนอนแสดงขนาดของอินพุต (หารด้วย 1000 และหารด้วย 100,000 สำหรับ quicksort) โดยมีค่าตั้งแต่ 0 ถึง 110 เพิ่มขึ้นทีละ 10 และแกนตั้งแสดงเวลาเป็นมิลลิวินาที โดยมีค่าตั้งแต่ 0 ถึง 16,000 เพิ่มขึ้นทีละ 4,000 เส้นโค้งสำหรับ bubble เริ่มต้นจากจุดกำเนิดและแสดงการเพิ่มขึ้นอย่างรวดเร็วและสิ้นสุดที่ (110, 14000) เส้นโค้งสำหรับ selection และ insertion เริ่มต้นจากจุดกำเนิดและแสดงการเพิ่มขึ้นอย่างช้าๆ และสิ้นสุดที่ (110, 3500) และ (110, 3600) ตามลำดับ เส้นของ quicksort เริ่มต้นจากจุดกำเนิดและแสดงการเพิ่มขึ้นเพียงเล็กน้อย และสุดท้ายสิ้นสุดที่ (110, 1000)
Answer 29 (จาก แบบฝึกหัดที่ 29)
มีสองวิธีที่จะไปถึงจุดนั้น วิธีแรกคือการมองปัญหาในมุมกลับกัน หากอาเรย์ (array) มีเพียงองค์ประกอบเดียว เราจะไม่วนซ้ำในลูป การวนซ้ำในแต่ละครั้งที่เพิ่มขึ้นจะทำให้ขนาดของอาเรย์ที่เราสามารถค้นหาได้เพิ่มขึ้นเป็นสองเท่า ดังนั้นสูตรทั่วไปสำหรับขนาดของอาเรย์คือ
โดยที่
คือจำนวนรอบการวนซ้ำ หากคุณใส่ลอการิทึมฐาน 2 ทั้งสองข้าง คุณจะได้
ซึ่งตามนิยามของลอการิทึมจะกลายเป็น
Answer 30 (จาก แบบฝึกหัดที่ 30)
นี่อาจจะเป็นการย้อนรำลึกถึงวิชาคณิตศาสตร์สมัยมัธยมมากเกินไปสักหน่อย แต่สูตรในการแปลงลอการิทึมฐาน
ไปเป็นฐาน
คือ:
เนื่องจาก
เป็นค่าคงที่ ดังนั้นเราจึงสามารถละเว้นมันได้เมื่ออยู่ในผลลัพธ์แบบ Big-O
Answer 31 (จาก แบบฝึกหัดที่ 31)
คุณสมบัติหนึ่งที่เราสามารถทดสอบได้คือ คำสั่งซื้อจะสำเร็จหากคลังสินค้ามีสินค้าเพียงพอในมือ เราสามารถสร้างคำสั่งซื้อสำหรับสินค้าในปริมาณที่สุ่มขึ้นมา และตรวจสอบว่ามีการคืนค่าทูเพิล (tuple) "OK" กลับมาหรือไม่หากคลังสินค้ามีสต็อกสินค้าอยู่
Answer 32 (จาก แบบฝึกหัดที่ 32)
นี่เป็นการใช้ประโยชน์ที่ดีของการทดสอบแบบอิงคุณสมบัติ (property-based testing) การทดสอบระดับหน่วย (unit tests) สามารถโฟกัสไปที่กรณีเฉพาะตัวที่คุณได้คำนวณผลลัพธ์ไว้แล้วด้วยวิธีอื่น และการทดสอบแบบอิงคุณสมบัติสามารถโฟกัสไปที่สิ่งต่างๆ เช่น:
- มีลัง (crates) สองใบใดๆ วางซ้อนทับกันหรือไม่?
- มีส่วนใดส่วนหนึ่งของลังใบใดเกินความกว้างหรือความยาวของรถบรรทุกหรือไม่?
- ความหนาแน่นของการจัดวาง (พื้นที่ที่ลังใช้หารด้วยพื้นที่ของกระบะรถบรรทุก) น้อยกว่าหรือเท่ากับ 1 หรือไม่?
- หากเป็นส่วนหนึ่งของข้อกำหนด ความหนาแน่นของการจัดวางนั้นเกินความหนาแน่นขั้นต่ำที่ยอมรับได้หรือไม่?
Answer 33 (จาก แบบฝึกหัดที่ 33)
-
ข้อความนี้ฟังดูเหมือนเป็นความต้องการที่แท้จริง (real requirement): อาจมีข้อจำกัดที่เกิดจากสภาพแวดล้อมที่แอปพลิเคชันนั้นทำงานอยู่
-
โดยลำพังตัวมันเองแล้ว ข้อความนี้ไม่ใช่ความต้องการที่แท้จริง แต่เพื่อจะหาว่าสิ่งที่ต้องการจริงๆ คืออะไร คุณต้องถามคำถามวิเศษว่า "ทำไม?"
อาจเป็นไปได้ว่านี่คือมาตรฐานขององค์กร ซึ่งในกรณีนี้ความต้องการที่แท้จริงควรจะเป็นอะไรประมาณว่า "องค์ประกอบ UI ทั้งหมดต้องเป็นไปตามมาตรฐาน User Interface ของ MegaCorp เวอร์ชัน 12.76"
อาจเป็นไปได้ว่านี่คือสีที่ทีมออกแบบบังเอิญชอบ ในกรณีนั้น คุณควรคำนึงถึงวิธีที่ทีมออกแบบชอบเปลี่ยนใจด้วย และควรระบุความต้องการว่า "สีพื้นหลังของหน้าต่างแบบ modal ทั้งหมดต้องสามารถกำหนดค่าได้ โดยสีเริ่มต้นที่ส่งมอบจะเป็นสีเทา" และจะดียิ่งขึ้นไปอีกหากระบุให้กว้างขึ้นว่า "องค์ประกอบทางภาพทั้งหมดของแอปพลิเคชัน (สี, ฟอนต์ และภาษา) ต้องสามารถกำหนดค่าได้"
หรือมันอาจหมายความง่ายๆ ว่าผู้ใช้จำเป็นต้องสามารถแยกแยะความแตกต่างระหว่างหน้าต่างแบบ modal และ nonmodal ได้ หากเป็นเช่นนั้น ก็จำเป็นต้องมีการพูดคุยกันเพิ่มเติม
-
ข้อความนี้ไม่ใช่ความต้องการ (requirement) แต่มันคือสถาปัตยกรรม (architecture) เมื่อต้องเผชิญกับสิ่งนี้ คุณต้องขุดให้ลึกลงไปเพื่อค้นหาว่าผู้ใช้กำลังคิดอะไรอยู่ นี่เป็นปัญหาเรื่องการปรับขนาด (scaling) หรือไม่? หรือเป็นเรื่องประสิทธิภาพ (performance)? ต้นทุน? ความปลอดภัย? คำตอบเหล่านี้จะเป็นข้อมูลประกอบการออกแบบของคุณ
-
ความต้องการเบื้องหลังน่าจะเป็นสิ่งที่ใกล้เคียงกับ “ระบบจะป้องกันไม่ให้ผู้ใช้กรอกข้อมูลที่ไม่ถูกต้องลงในฟิลด์ และจะแจ้งเตือนผู้ใช้เมื่อมีการกรอกข้อมูลเหล่านั้นลงไป”
-
ข้อความนี้อาจจะเป็นความต้องการที่ตายตัว (hard requirement) ซึ่งมีพื้นฐานมาจากข้อจำกัดของฮาร์ดแวร์บางอย่าง
และนี่คือแนวทางแก้ไขปัญหาจุดสี่จุด (four-dots problem):
ฟุตโน้ต
ขอขอบคุณเกร็ดความรู้นี้จากคุณ Avi Bryant (@avibryant)
Copyright © 2020 Pearson Education, Inc.