Chuyên mục lưu trữ: Công nghệ

Clean Architecture – Phụ Lục A. Khảo Cổ Kiến Trúc (Phần Kết)

Lễ tân điện tử

Năm 1983, công ty của chúng tôi nằm ở vị trí giao giữa hệ thống máy tính, hệ thống viễn thông và hệ thống thoại. Giám đốc điều hành của chúng tôi cho rằng đây có thể là một vị trí béo bở để phát triển các sản phẩm mới. Để đạt được mục tiêu này, anh ấy đã giao cho một nhóm ba người (trong đó có tôi) lên ý tưởng, thiết kế và triển khai một sản phẩm mới cho công ty.

Chúng tôi không mất nhiều thời gian để tìm ra Nhân Viên Lễ Tân Điện Tử (Electronic Receptionist – ER).

Ý tưởng này rất đơn giản. Khi bạn gọi cho một công ty, ER sẽ trả lời và hỏi bạn muốn nói chuyện với ai. Bạn sẽ sử dụng âm-chạm (touch-tone) để đánh vần tên của người đó và ER sau đó sẽ kết nối bạn. Người dùng ER có thể quay số và bằng cách sử dụng các lệnh âm-chạm đơn giản, cho nó biết số điện thoại mà người mong muốn có thể liên lạc, ở bất kỳ đâu trên thế giới. Trên thực tế, hệ thống này có thể liệt kê ra vài số thay thế.

Khi bạn gọi cho ER và quay số RMART (mã của tôi), ER sẽ gọi đến số đầu tiên trong danh sách của tôi. Nếu tôi không trả lời và xác minh bản thân, nó sẽ gọi đến số tiếp theo và số tiếp theo nữa. Nếu tôi vẫn không liên lạc được, ER sẽ ghi lại một tin nhắn từ người gọi.

Sau đó, theo định kỳ, ER sẽ cố gắng tìm tôi để gửi tin nhắn đó hay bất kỳ tin nhắn nào khác mà ai đó để lại cho tôi.

Đây là hệ thống thư thoại đầu tiên từ trước đến nay và chúng tôi[1] đã nắm giữ bằng sáng chế cho hệ thống này.

Chúng tôi đã xây dựng tất cả phần cứng cho hệ thống này – bo mạch máy tính, bo mạch bộ nhớ, bo mạch thoại/viễn thông, mọi thứ. Bo mạch chính của máy tính này là Deep Thought, bộ xử lý Intel 80286 mà tôi đã đề cập trước đó.

Mỗi bảng mạch thoại hỗ trợ một đường dây điện thoại. Chúng bao gồm một giao diện điện thoại, một bộ mã hóa/giải mã giọng nói, bộ nhớ và một máy vi tính Intel 80186.

Phần mềm cho bo mạch máy tính chính được viết bằng C. Hệ điều hành là MP/M-86, một hệ điều hành chạy bằng dòng lệnh, hỗ trợ đa tiến trình. MP/M là UNIX của người nghèo.

Phần mềm cho bảng mạch giọng nói được viết bằng trình biên dịch hợp ngữ và không có hệ điều hành. Giao tiếp giữa Deep Thought và bảng mạch giọng nói xảy ra thông qua bộ nhớ được chia sẻ.

Kiến trúc của hệ thống này ngày nay sẽ được gọi là hướng dịch vụ (service oriented). Mỗi đường dây điện thoại được giám sát bởi một listener process (tiến trình lắng nghe) chạy bên dưới MP/M. Khi một cuộc gọi đến, một process xử lý khởi tạo được bắt đầu và cuộc gọi được chuyển đến nó. Khi cuộc gọi được tiếp tục từ trạng thái này sang trạng thái khác, quá trình xử lý thích hợp sẽ được bắt đầu và giành quyền kiểm soát.

Các gói tin được chuyển giữa các dịch vụ này thông qua các file trên đĩa cứng. Dịch vụ hiện đang chạy sẽ xác định dịch vụ tiếp theo sẽ là gì; nó sẽ ghi thông tin trạng thái cần thiết vào một file trên ổ đĩa; sẽ đưa ra dòng lệnh để bắt đầu dịch vụ đó; và sau đó sẽ thoát.

Đây là lần đầu tiên tôi xây dựng một hệ thống như thế này. Quả thực, đây là lần đầu tiên tôi là kiến ​​trúc sư chính của toàn bộ sản phẩm. Mọi thứ liên quan đến phần mềm đều là của tôi – và nó hoạt động hoàn hảo.

Tôi sẽ không nói rằng kiến ​​trúc của hệ thống này là “tinh gọn” theo nghĩa của cuốn sách này; nó không phải là một kiến ​​trúc “plugin”. Tuy nhiên, nó chắc chắn cho thấy dấu hiệu của ranh giới thực sự. Các dịch vụ có thể triển khai độc lập và nằm trong phạm vi trách nhiệm của riêng chúng. Có các quy trình cấp cao và quy trình cấp thấp, và nhiều phần phụ thuộc chạy đúng hướng.

ER băng hà

Thật không may, việc tiếp thị sản phẩm này không diễn ra tốt đẹp. Teradyne là một công ty bán thiết bị kiểm tra. Chúng tôi không hiểu làm thế nào để thâm nhập vào thị trường thiết bị văn phòng.

Sau những nỗ lực lặp đi lặp lại trong suốt hai năm, CEO của chúng tôi đã bỏ cuộc và không may là đã từ bỏ đơn đăng ký bằng sáng chế. Bằng sáng chế đã được dành cho một công ty nộp đơn muộn hơn chúng tôi ba tháng; do đó, chúng tôi đã từ bỏ toàn bộ thị trường thư thoại và chuyển tiếp cuộc gọi điện tử.

Oạch!

Mặt khác, bạn không thể đổ lỗi cho tôi vì những cỗ máy phiền phức hiện đang ảnh hưởng đến sinh hoạt của chúng ta.

Hệ thống phân phối Thợ

ER đã không thành công với tư cách là một sản phẩm, nhưng chúng tôi vẫn có tất cả phần cứng và phần mềm này, chúng tôi có thể sử dụng nó để cải tiến các dòng sản phẩm hiện có của mình. Hơn nữa, thành công trên thị trường của chúng tôi với VRS đã thuyết phục chúng tôi rằng chúng tôi nên cung cấp một hệ thống phản hồi bằng giọng nói để tương tác với các thợ điện thoại mà không phụ thuộc vào hệ thống kiểm tra của chúng tôi.

Do đó, CDS ra đời, Craft Dispatch System (Hệ thống Điều phối Thợ). CDS về cơ bản là ER, nhưng đặc biệt tập trung vào lĩnh vực rất hẹp là quản lý việc triển khai các thợ sửa chữa điện thoại trong lĩnh vực này.

Khi một sự cố được phát hiện trong một đường dây điện thoại, một phiếu xử lý sự cố đã được tạo trong trung tâm dịch vụ. Phiếu sự cố được lưu giữ trong một hệ thống tự động. Khi một thợ sửa chữa tại hiện trường hoàn thành công việc, anh ta sẽ gọi cho trung tâm bảo hành để có nhiệm vụ tiếp theo. Người điều hành trung tâm dịch vụ sẽ đưa ra phiếu sự cố tiếp theo và đọc nó cho thợ sửa chữa.

Chúng tôi sẽ tự động hóa quy trình đó. Mục tiêu của chúng tôi là để thợ sửa chữa tại hiện trường gọi vào CDS và yêu cầu nhiệm vụ tiếp theo. CDS sẽ tham khảo ý kiến ​​của hệ thống kiểm tra sự cố và đọc kết quả. CDS sẽ theo dõi thợ sửa chữa nào được chỉ định cho phiếu sự cố nào và sẽ thông báo cho hệ thống phiếu sự cố về tình trạng của việc sửa chữa.

Có một số tính năng khá thú vị của hệ thống này liên quan đến việc tương tác với hệ thống kiểm tra sự cố, hệ thống quản lý nhà máy và bất kỳ hệ thống kiểm tra tự động nào.

Trải nghiệm với kiến ​​trúc hướng dịch vụ của ER khiến tôi muốn thử mạnh mẽ hơn ý tưởng tương tự. Máy trạng thái (state machine) cho một phiếu sự cố liên quan nhiều hơn máy trạng thái để xử lý một cuộc gọi với ER. Tôi chuẩn bị tạo ra cái mà ngày nay được gọi là kiến ​​trúc micro-service.

Mọi chuyển đổi trạng thái của bất kỳ cuộc gọi nào, dù nhỏ đến mấy, cũng đều khiến hệ thống khởi động một dịch vụ mới. Thật vậy, máy trạng thái đã được ngoại hóa thành một tệp văn bản mà hệ thống đọc được. Mỗi sự kiện đi vào hệ thống từ một đường dây điện thoại sẽ biến thành một quá trình chuyển đổi trong máy trạng thái hữu hạn đó. Tiến trình hiện có sẽ bắt đầu một tiến trình mới do máy trạng thái ra lệnh để xử lý sự kiện đó; sau đó tiến trình hiện tại sẽ thoát hoặc chờ trên một hàng đợi.

Máy trạng thái bên ngoài này cho phép chúng tôi thay đổi luồng của ứng dụng mà không cần thay đổi bất kỳ mã nào (Nguyên Lý Mở-Đóng). Chúng tôi có thể dễ dàng thêm một dịch vụ mới, độc lập với bất kỳ dịch vụ nào khác và đưa nó vào quy trình bằng cách sửa đổi tệp văn bản chứa máy trạng thái. Chúng tôi thậm chí có thể làm điều này trong khi hệ thống đang chạy. Nói cách khác, chúng tôi đã có sự hoán đổi nóng và một BPEL (Business Process Excecution Language – Ngôn Ngữ Thực Thi Quy Trình Nghiệp Vụ) hiệu quả.

Cách tiếp cận ER cũ của việc sử dụng file ổ cứng để giao tiếp giữa các dịch vụ quá chậm đối với việc chuyển đổi các dịch vụ nhanh hơn nhiều này, vì vậy chúng tôi đã phát minh ra một cơ chế bộ nhớ chia sẻ mà chúng tôi gọi là 3DBB[2]. Cơ chế này cho phép dữ liệu được truy xuất bởi tên gọi: các tên gọi mà chúng tôi sử dụng được gắn với mỗi thực thể máy trạng thái.

Cơ chế 3DBB này rất tốt để lưu trữ các chuỗi ký tự và hằng số, nhưng không thể được sử dụng để lưu trữ các cấu trúc dữ liệu phức tạp. Lý do cho việc này là vấn đề kỹ thuật nhưng khá dễ hiểu. Mỗi tiến trình trong MP/M nằm trong phân vùng bộ nhớ của riêng nó. Con trỏ tới dữ liệu trong một phân vùng bộ nhớ không có ý nghĩa trong phân vùng bộ nhớ khác. Do đó, dữ liệu trong 3DBB không thể chứa con trỏ. Các chuỗi ký tự vẫn ổn, nhưng cây, danh sách được liên kết hoặc bất kỳ cấu trúc dữ liệu nào có con trỏ sẽ không hoạt động được.

Các ticket sự cố trong hệ thống ticket sự cố này đến từ nhiều nguồn khác nhau. Một số được tự động hóa và một số là thủ công. Các dữ liệu thủ công được tạo ra bởi những người điều hành đang nói chuyện với khách hàng về những rắc rối của họ. Khi khách hàng mô tả các vấn đề của họ, những người vận hành sẽ nhập các khiếu nại và hiện tượng họ gặp phải vào một dòng văn bản có cấu trúc. Nó trông giống như thế này:

/ pno 8475551212 / noise / drop-call

Bạn hiểu rồi chứ. Ký tự / bắt đầu một chủ đề mới. Theo sau dấu gạch chéo là một mã, và theo sau mã đó là các tham số. Có hàng nghìn mã và một ticket sự cố riêng lẻ có thể có hàng chục mã trong phần mô tả. Tệ hơn, vì chúng được nhập theo cách thủ công, nên chúng thường bị sai chính tả hoặc định dạng không đúng. Chúng được dùng để con người biên dịch, không phải cho máy móc xử lý.

Vấn đề của chúng tôi là giải mã các chuỗi bán tự do này, diễn giải và sửa bất kỳ lỗi nào, sau đó chuyển chúng thành đầu ra giọng nói để chúng tôi có thể đọc chúng cho thợ sửa chữa, trên cột, nghe bằng thiết bị cầm tay. Điều này đòi hỏi một kỹ thuật phân tích cú pháp và biểu diễn dữ liệu rất linh hoạt. Việc biểu diễn dữ liệu đó phải được chuyển qua 3DBB, thứ mà chỉ có thể xử lý các chuỗi ký tự.

Và vì vậy, trên một chuyến bay, bay giữa các chuyến thăm khách hàng, tôi đã phát minh ra một lược đồ mà tôi gọi là FLD: Field Labled Data (Dữ liệu được gắn nhãn trường). Ngày nay chúng ta gọi đây là XML hoặc JSON. Định dạng thì khác nhau, nhưng ý tưởng thì giống nhau. FLD là cây nhị phân liên kết tên với dữ liệu trong một hệ thống phân cấp đệ quy. Các FLD có thể được truy vấn bằng một API đơn giản và có thể được dịch sang một định dạng chuỗi ký tự thuận tiện, lý tưởng cho 3DBB.

Vì vậy, các micro-service giao tiếp thông qua bộ nhớ chia sẻ tương tự như các socket sử dụng một XML – vào năm 1985.

Không có gì mới dưới ánh Mặt trời.

Clear Communications

Năm 1988, một nhóm nhân viên của Teradyne rời công ty để thành lập một công ty khởi nghiệp tên là Clear Communications. Tôi tham gia cùng họ một vài tháng sau đó. Nhiệm vụ của chúng tôi là xây dựng phần mềm cho một hệ thống giám sát chất lượng thông tin liên lạc của các tuyến T1 – các tuyến kỹ thuật số truyền thông tin liên lạc đường dài trên khắp đất nước. Viễn cảnh là một màn hình khổng lồ với bản đồ của Hoa Kỳ được đan chéo bởi các đường T1 nhấp nháy màu đỏ nếu chúng đang bị xuống cấp.

Hãy nhớ rằng, giao diện người dùng đồ họa vẫn còn là điều mới mẻ vào năm 1988. Apple Macintosh khi đó mới được 5 năm tuổi. Windows thì chỉ là một trò đùa hồi đó. Nhưng Sun Microsystems đang xây dựng Sparcstations có GUI X-Windows đáng tin cậy. Vì vậy, chúng tôi đã đi theo Sun – và do đó với ngôn ngữ C và UNIX.

Đây là một công ty khởi nghiệp. Chúng tôi đã làm việc tới 70 đến 80 giờ mỗi tuần. Chúng tôi đã có tầm nhìn. Chúng tôi đã có động lực. Chúng tôi đã có ý chí. Chúng tôi đã có năng lượng. Chúng tôi đã có nghiệp vụ. Chúng tôi đã có vốn chủ sở hữu. Chúng tôi đã có ước mơ trở thành triệu phú. Chúng tôi đã như một đống sh!t.

Code C tuôn ra từ mọi lỗ thông trên cơ thể của chúng tôi. Chúng tôi ném phịch nó ở đây, và quẳng nó ở kia. Chúng tôi đã xây dựng những lâu đài khổng lồ trên không. Chúng tôi đã có các tiến trình, và các hàng đợi gói tin, và các kiến trúc vĩ đại, tột bực. Chúng tôi đã viết toàn bộ stack truyền thông tin ISO bảy lớp từ đầu – cho tới lớp liên kết dữ liệu (data link layer).

Chúng tôi đã viết GUI code. GOOEY CODE! OMG! Chúng tôi đã viết GOOOOOEY code.

Riêng tôi đã viết một hàm C tên là gi() dài 3000 dòng; tên của nó có nghĩa là Graphic Interpreter (trình dịch đồ họa). Nó là một kiệt tác của gooey. Đó không phải là code gooey duy nhất tôi viết tại Clear Communications,  nhưng đó là thứ tai tiếng nhất của tôi.

Kiến trúc ư? Bạn đùa à? Đó là một startup. Chúng tôi không có thời gian cho kiến trúc. Chỉ có code thôi, khốn thật! Code vì chính cuộc sống của chúng tôi!

Vì vậy chúng tôi code. Và chúng tôi code. Và chúng tôi code. Nhưng, sau ba năm, cái chúng tôi thất bại đó là việc bán hàng. Ồ, chúng tôi có được một lượt cài đặt hoặc là hai gì đó. Nhưng thị trường không hứng thú lắm với tầm nhìn vĩ đại của chúng tôi, và những nhà tài chính đầu tư mạo hiểm cho chúng tôi đã chán ngấy.

Tôi ghét cuộc sống của bản thân vào thời điểm đó. Tôi đã nhìn thấy tất cả nỗ lực và giấc mơ của mình bị vỡ vụn. Tôi có những xung đột trong công việc, xung đột tại gia đình bởi vì công việc, và xung đột với chính bản thân.

Và sau đó tôi nhận được một cuộc điện thoại đã thay đổi mọi thứ.

Cài đặt

Hai năm trước khi có cuộc gọi đó, hai điều quan trọng đã xảy ra.

Đầu tiên, tôi đã thiết lập một kết nối uucp tới một công ty gần đó có kết nối uucp với một cơ sở khác được kết nối tới Internet. Tất nhiên, những kết nối này là quay số. Máy trạm chính của chúng tôi (máy trên bàn làm việc của tôi) đã sử dụng modem 1200-bps để gọi máy chủ uucp của chúng tôi hai lần mỗi ngày. Điều này đã cho phép chúng tôi có email và Netnews (một mạng xã hội ban đầu, nơi mọi người thảo luận về các vấn đề thú vị).

Thứ hai, Sun đã phát hành một trình biên dịch C++. Tôi đã quan tâm đến C ++ và OO từ năm 1983, nhưng rất khó để tạo ra các trình biên dịch. Vì vậy, khi cơ hội đến, tôi đã thay đổi ngôn ngữ ngay lập tức. Tôi bỏ lại các hàm C 3000 dòng phía sau và bắt đầu viết mã C ++ tại Clear. Và tôi đã học được …

Tôi đã đọc nhiều cuốn sách. Dĩ nhiên, tôi đã đọc cuốn The C++ Programming LanguageThe Annotated C++ Reference Manual (The ARM) của Bjarne Stroustrup. Tôi đã đọc cuốn sách đáng yêu về thiết kế hướng trách nhiệm của Rebecca Wirfs Brock: Desining Object Oriented Software. Tôi đã đọc OOA OOD OOP của Peter Coad. Tôi đã đọc Smalltalk-80 của Adele Goldberg. Tôi đã đọc Advanced C++ Programming Styles and Idioms của James O. Coplien. Nhưng có lẽ quan trọng nhất trong tất cả, tôi đã đọc cuốn Object Oriented Design with Applications của Grady Booch.

Đúng là một cái tên! Grady Booch. Làm sao ai đó có thể quên được một cái tên như thế. Hơn thế nữa, ông ấy còn là Trưởng Khoa Nghiên Cứu (Chief Scientist) tại một công ty có tên là Rational! Làm cách nào tôi muốn trở thành một Trưởng Khoa Nghiên Cứu! Và vì vậy tôi đã đọc cuốn sách của ông ấy. Và tôi đã học, và tôi đã học, và tôi đã học được …

Khi tôi học, tôi cũng bắt đầu tranh luận trên Netnews, như cái cách mà mọi người hiện nay hay tranh luận trên Facebook. Các cuộc tranh luận của tôi là về C ++ và OO. Trong hai năm, tôi đã giải tỏa những thất vọng đang có tại nơi làm việc bằng cách tranh luận với hàng trăm người trên Usenet về các tính năng ngôn ngữ tốt nhất và các nguyên tắc thiết kế tốt nhất. Sau một thời gian, tôi thậm chí đã bắt đầu hiểu được một số điều nhất định.

Từ một trong những cuộc tranh luận đó, nền tảng của các nguyên tắc SOLID đã được đặt ra.

Và tất cả những tranh luận đó, và thậm chí có lẽ là một phần nào đó, đã khiến tôi chú ý …

Uncle Bob

Một trong những kỹ sư tại Clear là một người trẻ tuổi tên là Billy Vogel. Billy đặt biệt danh cho mọi người. Anh ấy gọi tôi là Uncle Bob (chú Bob). Tôi nghi ngờ, mặc dù tên tôi là Bob, nhưng anh ta đang ám chỉ trực tiếp đến J. R. “Bob” Dobbs (xem https://en.wikipedia.org/wiki/File:Bobdobbs.png).

Lúc đầu, tôi đã phải chịu đựng nó. Nhưng nhiều tháng trôi qua, những câu nói không ngớt của anh ta về “Chú Bob, … Chú Bob,” trong bối cảnh áp lực và thất vọng của công ty khởi nghiệp, nên tôi đã bắt đầu không để tâm tới nó nữa.

Và rồi, một ngày, chuông điện thoại reo.

Cuộc điện thoại

Đó là một nhà tuyển dụng. Anh ấy đã biết tên tôi như một người biết C ++ và thiết kế hướng đối tượng. Tôi không chắc làm thế nào mà anh ấy biết, nhưng tôi nghi là nó có liên quan đến sự hiện diện trên Netnews của tôi.

Anh ấy nói rằng anh ấy đã có một cơ hội ở Thung lũng Silicon, tại một công ty tên là Rational. Họ đang tìm kiếm sự trợ giúp để xây dựng một công cụ CASE[3].

Máu từ mặt tôi chảy ra. Tôi đã biết đó là cái gì. Tôi không biết làm thế nào tôi biết, nhưng tôi biết. Đây là công ty của Grady Booch. Tôi đã thấy trước cơ hội được hợp lực với Grady Booch!

Rose

Tôi gia nhập Rational, với tư cách là một lập trình viên theo hợp đồng, vào năm 1990. Tôi làm việc với sản phẩm ROSE. Đây là một công cụ cho phép các lập trình viên vẽ sơ đồ Booch – những sơ đồ mà Grady đã viết trong cuốn sách Object-Oriented Analysis and Design with Applications (Phân Tích Và Thiết Kế Hướng Đối Tượng Với Các Ứng Dụng) (Hình A.9 là một ví dụ).

Hình A.9 Biểu đồ Booch

Ký hiệu Booch rất mạnh mẽ. Nó định sẵn các ký hiệu như UML.

ROSE có một kiến trúc – một kiến trúc thực sự. Nó được xây dựng trong các lớp thực và sự phụ thuộc giữa các lớp được kiểm soát đúng cách. Kiến trúc làm cho nó có thể phát hành được, có thể phát triển được và có thể triển khai được độc lập.

Ồ, nhưng nó không hoàn hảo. Có rất nhiều điều mà chúng tôi vẫn chưa hiểu về các nguyên tắc kiến trúc. Ví dụ, chúng tôi đã không tạo một cấu trúc plugin thực sự.

Chúng tôi cũng rơi vào một trong những xu hướng đáng tiếc nhất trong thời đó – chúng tôi sử dụng cái gọi là cơ sở dữ liệu hướng đối tượng.

Nhưng nhìn chung, trải nghiệm là một trải nghiệm tuyệt vời. Tôi đã dành một năm rưỡi đáng yêu để làm việc với nhóm Rational trên ROSE. Đây là một trong những trải nghiệm kích thích trí tuệ nhất trong cuộc đời làm nghề của tôi.

Cuộc tranh luận tiếp tục

Dĩ nhiên, tôi đã không dừng tranh luận trên Netnews. Trên thực tế, tôi đã tăng đáng kể sự hiện diện mạng của mình. Tôi bắt đầu viết bài cho C++ Report. Và, với sự trợ giúp của Grady, tôi bắt đầu làm cuốn sách đầu tiên của mình: Designing Object-Oriented C++ Applications Using the Booch Method (Thiết Kế Ứng Dụng C ++ Hướng Đối Tượng Bằng Phương Pháp Booch).

Một điều khiến tôi bận tâm. Điều này thật buồn cười, nhưng đó là sự thật. Không ai gọi tôi là “Uncle Bob” cả. Tôi cảm thấy rằng tôi nhớ cách gọi đó. Vì vậy, tôi đã mắc sai lầm khi đặt “Uncle Bob” trong email và chữ ký của Netnews. Và cái tên đó đã dính với tôi. Cuối cùng tôi nhận ra rằng đó là một thương hiệu khá tốt.

…Bởi bất kỳ cái tên nào khác

ROSE là một ứng dụng C++ khổng lồ. Nó bao gồm các lớp, với một quy tắc phụ thuộc được thực thi nghiêm ngặt. Quy tắc đó không phải là quy tắc mà tôi đã mô tả trong cuốn sách này. Chúng tôi không hướng sự phụ thuộc của mình vào các chính sách cấp cao. Thay vào đó, chúng tôi đã hướng sự phụ thuộc của mình theo hướng kiểm soát luồng truyền thống hơn. GUI hướng vào biểu diễn, hướng vào các quy tắc thao tác, hướng vào cơ sở dữ liệu. Cuối cùng, nó đã thất bại trong việc hướng các phụ thuộc của chúng tôi vào chính sách mà điều này góp phần dẫn tới sự ra đi cuối cùng của sản phẩm.

Kiến trúc của ROSE tương tự như kiến ​​trúc của một trình biên dịch tốt. Ký hiệu đồ họa đã được “phân tích cú pháp” thành một biểu diễn nội bộ; biểu diễn đó sau đó được thao tác bởi các quy tắc và được lưu trữ trong cơ sở dữ liệu hướng đối tượng.

Cơ sở dữ liệu hướng đối tượng là một ý tưởng tương đối mới, và thế giới OO đều rất ngạc nhiên với những tác động của nó. Mọi lập trình viên hướng đối tượng đều muốn có một cơ sở dữ liệu hướng đối tượng trong hệ thống của mình. Ý tưởng này tương đối đơn giản và mang tính lý tưởng sâu sắc. Cơ sở dữ liệu lưu trữ các đối tượng, không phải bảng. Cơ sở dữ liệu được cho là trông giống như RAM. Khi bạn truy cập một đối tượng, nó chỉ xuất hiện trong bộ nhớ. Nếu đối tượng đó trỏ đến một đối tượng khác, đối tượng kia sẽ xuất hiện trong bộ nhớ ngay khi bạn truy cập vào nó. Nó giống như một phép thuật.

Cơ sở dữ liệu đó có lẽ là sai lầm thực tế lớn nhất của chúng tôi. Chúng tôi muốn điều kỳ diệu, nhưng những gì chúng tôi nhận được là một framework bên thứ ba cồng kềnh, chậm chạp, dễ xâm nhập, đắt đỏ khiến cho cuộc sống của chúng tôi trở thành địa ngục bằng cách cản trở tiến độ của chúng tôi ở mọi cấp bậc.

Cơ sở dữ liệu đó không phải là sai lầm duy nhất mà chúng tôi mắc phải. Trên thực tế, sai lầm lớn nhất là kiến ​​trúc quá mức (over-architecture). Có nhiều lớp hơn những gì tôi đã mô tả ở đây, và mỗi lớp có một mác truyền thông riêng của nó. Điều này làm giảm đáng kể năng suất của đội phát triển.

Thật vậy, sau nhiều năm làm việc, đấu tranh cực khổ và hai lần phát hành tẻ nhạt, toàn bộ công cụ đã bị loại bỏ và thay thế bằng một ứng dụng nhỏ dễ thương được viết bởi một nhóm nhỏ ở Wisconsin.

Và vì vậy tôi học được rằng những kiến ​​trúc vĩ đại đôi khi dẫn đến những thất bại lớn. Kiến trúc phải đủ linh hoạt để thích ứng với quy mô của vấn đề. Khi tất cả những gì bạn thực sự cần là một công cụ desktop nhỏ xinh, thì kiến trúc dành cho enterprise là một công thức dẫn đến thất bại.

Bài kiểm tra đăng ký kiến trúc sư

Vào đầu những năm 1990, tôi đã trở thành một nhà tư vấn thực thụ. Tôi đã đi khắp thế giới để dạy mọi người về thứ OO mới mẻ này. Công việc tư vấn của tôi tập trung mạnh vào thiết kế và kiến ​​trúc của các hệ thống hướng đối tượng.

Một trong những khách hàng tư vấn đầu tiên của tôi là Dịch vụ Khảo thí Giáo dục (Educational Testing Service – ETS). Theo hợp đồng với Hội Đồng Đăng Ký Kiến Trúc Sư Quốc gia (National Council of Architects Registry Board – NCARB) để tiến hành các kỳ thi đăng ký cho các ứng viên kiến ​​trúc sư mới.

Bất kỳ ai muốn trở thành một kiến ​​trúc sư bảo đảm (kiểu kiến trúc sư thiết kế các tòa nhà) ở Hoa Kỳ hoặc Canada đều phải vượt qua kỳ thi đăng ký này. Kỳ thi này liên quan đến việc ứng viên giải quyết một số vấn đề kiến ​​trúc liên quan đến thiết kế tòa nhà. Ứng viên có thể được đưa ra một tập hợp các yêu cầu đối với thư viện công cộng, nhà hàng hoặc nhà thờ, sau đó được yêu cầu vẽ các sơ đồ kiến ​​trúc thích hợp.

Các kết quả sẽ được thu thập và lưu lại cho đến khi một nhóm kiến ​​trúc sư cao cấp có thể được tập hợp lại với nhau với tư cách là bồi thẩm viên, để chấm điểm các bài nộp. Những cuộc họp này là những sự kiện lớn, tốn kém và là nguồn gốc của nhiều sự không rõ ràng và chậm trễ.

NCARB muốn tự động hóa quy trình bằng cách để các ứng viên làm bài thi bằng máy tính, sau đó nhờ máy tính khác đánh giá và cho điểm. NCARB yêu cầu ETS phát triển phần mềm đó và ETS đã thuê tôi tập hợp một nhóm các nhà phát triển để sản xuất sản phẩm.

ETS đã chia nhỏ vấn đề thành 18 mô hình thử nghiệm riêng lẻ. Mỗi ứng dụng sẽ yêu cầu một ứng dụng GUI giống như CAD mà ứng viên sẽ sử dụng để thể hiện giải pháp của mình. Một ứng dụng chấm điểm riêng biệt sẽ tiếp nhận các giải pháp và tạo ra điểm số.

Đối tác của tôi, Jim Newkirk, và tôi đã nhận ra rằng 36 ứng dụng này có rất nhiều điểm giống nhau. 18 ứng dụng GUI đều sử dụng các hành vi và cơ chế tương tự. 18 ứng dụng cho điểm đều sử dụng các kỹ thuật toán học giống nhau. Với những yếu tố được chia sẻ này, Jim và tôi đã quyết tâm phát triển một framework có thể tái sử dụng cho tất cả 36 ứng dụng. Thật vậy, chúng tôi đã bán ý tưởng này cho ETS bằng cách nói rằng chúng tôi sẽ dành nhiều thời gian làm việc trên ứng dụng đầu tiên, nhưng sau đó phần còn lại sẽ chỉ xuất hiện vài tuần một lần.

Tại thời điểm này, bạn có lẽ đã tái mặt hoặc đập đầu vào cuốn sách này. Những người trong số các bạn đủ lớn để có thể nhớ lời hứa “tái sử dụng” của OO. Khi đó, tất cả chúng tôi đều bị thuyết phục rằng nếu bạn chỉ cần viết mã C++ hướng đối tượng sạch đẹp thì bạn sẽ tự nhiên tạo ra rất nhiều mã có thể tái sử dụng.

Vì vậy, chúng tôi chuẩn bị viết ứng dụng đầu tiên – ứng dụng phức tạp nhất trong gói đó. Nó được gọi là Vignette Grande.

Hai chúng tôi đã làm việc toàn thời gian trên Vignette Grande với mục tiêu tạo ra một framework có thể tái sử dụng. Chúng tôi đã mất một năm. Vào cuối năm đó, chúng tôi có 45.000 dòng mã framework và 6.000 dòng mã ứng dụng. Chúng tôi đã giao sản phẩm này cho ETS và họ đã ký hợp đồng với chúng tôi để viết tiếp 17 ứng dụng còn lại một cách vội vã.

Vì vậy, Jim và tôi đã tuyển dụng một nhóm gồm ba lập trình viên khác và chúng tôi bắt đầu làm việc trên một vài ứng dụng tiếp theo.

Nhưng có gì đó không ổn. Chúng tôi nhận thấy rằng framework có thể tái sử dụng mà chúng tôi đã tạo ra không thể thực sự tái sử dụng được. Nó không phù hợp với các ứng dụng mới đang được viết. Có những trở ngại nhỏ khiến cho nó không thể hoạt động.

Điều này gây nản lòng vô cùng, nhưng chúng tôi tin rằng chúng tôi biết phải làm gì với nó. Chúng tôi đã đến ETS và nói với họ rằng sẽ có sự chậm trễ – rằng framework 45.000 dòng cần được viết lại, hoặc ít nhất là điều chỉnh lại. Chúng tôi đã nói với họ rằng sẽ mất một thời gian nữa để hoàn thành việc đó.

Tôi không cần phải nói bạn cũng biết rằng ETS không hề vui với tin tức này.

Vì vậy, chúng tôi đã bắt đầu lại. Chúng tôi đặt framework cũ sang một bên và bắt đầu viết bốn ứng dụng mới cùng một lúc. Chúng tôi sẽ mượn các ý tưởng và mã từ framework cũ nhưng làm lại chúng để chúng phù hợp với cả bốn ứng dụng mà không cần sửa đổi. Nỗ lực này mất thêm một năm. Nó tạo ra một framework 45.000 dòng khác, cộng với bốn ứng dụng theo thứ tự từ 3000 đến 6000 dòng mỗi ứng dụng.

Không cần phải nói, mối quan hệ giữa các ứng dụng GUI và framework tuân theo Quy Tắc Phụ Thuộc. Các ứng dụng là plugin cho framework. Tất cả chính sách GUI cấp cao đều nằm trong framework. Mã ứng dụng chỉ là lớp kết dính.

Mối quan hệ giữa các ứng dụng tính điểm và framework phức tạp hơn một chút. Chính sách chấm điểm ở cấp độ cao nằm trong ứng dụng. Framework chấm điểm được gắn vào ứng dụng chấm điểm.

Tất nhiên, cả hai ứng dụng này đều là ứng dụng C++ được liên kết tĩnh, vì vậy khái niệm plugin không xuất hiện trong tâm trí chúng tôi. Tuy nhiên, cách mà các phụ thuộc chạy đều nhất quán với Quy Tắc Phụ Thuộc.

Sau khi gửi bốn ứng dụng đó, chúng tôi bắt đầu với bốn ứng dụng tiếp theo. Và lần này họ bắt đầu phản hồi sau vài tuần một lần, giống như những gì chúng tôi đã dự đoán. Sự chậm trễ đã khiến chúng tôi mất gần một năm trong lịch trình của mình, vì vậy chúng tôi đã thuê một lập trình viên khác để đẩy nhanh quá trình.

Chúng tôi đã đáp ứng được thời hạn và các cam kết của chúng tôi. Khách hàng của chúng tôi đã rất vui. Chúng tôi đã rất hạnh phúc. Cuộc sống thật tuyệt vời.

Nhưng chúng tôi đã học được một bài học hay: Bạn không thể tạo một framework có thể tái sử dụng cho đến khi bạn lần đầu tiên tạo ra một framework có thể tái sử dụng. Các framework có thể tái sử dụng đó sẽ cần bạn phải xây dựng chúng cùng với một vài ứng dụng tái sử dụng nó.

Kết luận

Như tôi đã nói ở phần đầu, phần phụ lục này mang hơi hướng tự truyện. Tôi đã đạt được đỉnh cao của các dự án mà tôi cảm thấy mình có tác động đến kiến trúc. Và, tất nhiên, tôi đã đề cập đến một vài tình tiết không liên quan chính xác đến nội dung kỹ thuật của cuốn sách này, nhưng dù sao nó cũng rất quan trọng.

Tất nhiên, đây chỉ là một phần lịch sử. Có rất nhiều dự án khác mà tôi đã thực hiện trong nhiều thập kỷ. Tôi cũng cố ý dừng dòng lịch sử này tại thời điểm đầu những năm 1990 – bởi vì tôi có một cuốn sách khác để viết về các sự kiện của cuối những năm 1990.

Tôi hy vọng rằng bạn thích chuyến đi nhỏ này trong ký ức của tôi; và rằng bạn có thể học một số điều trong suốt hành trình này.


[1] Công ty của chúng tôi đã giữ bằng sáng chế. Hợp đồng lao động của chúng tôi nói rõ rằng bất cứ thứ gì chúng tôi phát minh ra đều thuộc về công ty của chúng tôi. Sếp của tôi nói với tôi rằng: “Anh đã bán nó cho chúng tôi với giá một đô la và chúng tôi không trả cho anh số tiền đó.”

[2] Bảng đen ba chiều. Nếu bạn sinh vào những năm 1950, bạn có thể nhận được thông tin tham khảo này: Drizzle, Drazzle, Druzzle, Drone.

[3] Computer Aided Software Engineering – Kỹ thuật Phần mềm Hỗ trợ Máy tính.

Clean Architecture – Phụ Lục A. Khảo Cổ Kiến Trúc (Phần 3)

BOSS

Nền tảng 8085 của chúng tôi không có hệ điều hành. Kinh nghiệm của tôi với hệ thống MPS của M365 và các cơ chế ngắt của IBM System 7 đã thuyết phục tôi rằng chúng tôi cần một bộ chuyển đổi tác vụ đơn giản cho 8085. Vì vậy, tôi đã hình thành nên BOSS: Basic Operating System and Scheduler[1].

Phần lớn BOSS được viết bằng C. Nó cung cấp khả năng tạo các nhiệm vụ đồng thời. Những nhiệm vụ đó không phải là nhiệm vụ ưu tiên — việc chuyển đổi nhiệm vụ không diễn ra dựa trên các ngắt. Thay vào đó, giống như với hệ thống MPS trên M365, các tác vụ được chuyển đổi dựa trên một cơ chế polling đơn giản. Việc polling diễn ra bất cứ khi nào một nhiệm vụ bị chặn bởi một sự kiện.

Lệnh gọi BOSS để khóa một nhiệm vụ trông như thế này:

block(eventCheckFunction);

Lệnh gọi này đã tạm ngừng tác vụ hiện tại, đặt eventCheckFunction vào danh sách polling và liên kết nó với tác vụ mới bị chặn. Sau đó, nó đợi trong vòng lặp polling, gọi từng hàm trong danh sách polling cho đến khi một trong số chúng trả về true. Tác vụ được liên kết với hàm đó sau đó sẽ được phép chạy.

Nói cách khác, như tôi đã nói trước đây, nó là một công cụ chuyển đổi tác vụ đơn giản.

Phần mềm này đã trở thành cơ sở cho rất nhiều dự án trong những năm sau đó. Một trong những dự án đầu tiên là pCCU.

pCCU

Cuối những năm 1970 và đầu những năm 1980 là thời kỳ hỗn loạn của các công ty điện thoại. Một trong những nguồn gốc của sự hỗn loạn đó là cuộc cách mạng kỹ thuật số.

Trong thế kỷ trước, kết nối giữa văn phòng chuyển mạch trung tâm và điện thoại của khách hàng là một cặp dây đồng. Những sợi dây này được bó lại thành những sợi cáp trải dài trong một mạng lưới khổng lồ khắp vùng nông thôn. Lúc thì chúng được treo trên cột, lúc thì chúng được chôn dưới đất.

Đồng là một kim loại quý, và công ty điện thoại đã có hàng tấn (nghĩa đen của hàng tấn) đồng phủ khắp đất nước. Vốn đầu tư này là rất khổng lồ. Phần lớn số vốn đó có thể được thu hồi bằng cách truyền tải cuộc trò chuyện điện thoại qua các kết nối kỹ thuật số. Một đôi dây đồng có thể thực hiện hàng trăm cuộc hội thoại ở dạng kỹ thuật số.

Để đáp lại, các công ty điện thoại đã bắt tay vào quá trình thay thế thiết bị chuyển mạch trung tâm cũ của họ bằng các thiết bị chuyển mạch kỹ thuật số hiện đại.

Sản phẩm 4-Tel của chúng tôi đã thử nghiệm dây đồng, không phải kết nối kỹ thuật số. Vẫn còn khá nhiều dây đồng trong môi trường kỹ thuật số, nhưng chúng ngắn hơn nhiều so với trước đây và chúng được địa phương hóa gần điện thoại của khách hàng. Tín hiệu sẽ được truyền kỹ thuật số từ văn phòng trung tâm đến điểm phân phối cục bộ, nơi mà nó sẽ được chuyển đổi trở lại thành tín hiệu analog và phân phối tới khách hàng qua dây đồng tiêu chuẩn. Điều này có nghĩa là thiết bị đo lường của chúng tôi cần phải được đặt ở nơi bắt đầu các dây đồng, nhưng thiết bị quay số của chúng tôi cần phải ở văn phòng trung tâm. Vấn đề là tất cả các COLT của chúng tôi đều thể hiện cả tính năng quay số và đo lường trong cùng một thiết bị. (Chúng tôi có thể đã tiết kiệm cho mình một gia tài nếu chúng tôi nhận ra ranh giới kiến ​​trúc rõ ràng đó một vài năm trước đó!)

Do đó, chúng tôi đã hình thành một kiến ​​trúc sản phẩm mới: CCU / CMU (bộ điều khiển COLT và bộ đo lường COLT). CCU sẽ được đặt tại văn phòng chuyển mạch trung tâm và sẽ xử lý việc quay số của các đường dây điện thoại sẽ được kiểm tra. CMU sẽ được đặt tại các điểm phân phối địa phương và sẽ đo các dây đồng dẫn đến điện thoại của khách hàng.

Vấn đề là đối với mỗi CCU, có nhiều CMU. Thông tin về CMU nào sẽ được sử dụng cho mỗi số điện thoại do chính bộ chuyển mạch kỹ thuật số nắm giữ. Do đó CCU đã phải truy vấn bộ chuyển mạch kỹ thuật số để xác định CMU nào cần giao tiếp và điều khiển.

Chúng tôi đã hứa với các công ty điện thoại rằng chúng tôi sẽ làm kiến trúc mới này hoạt động kịp thời cho quá trình chuyển đổi của họ. Chúng tôi biết là chúng phải mất vài tháng, nếu không muốn nói là nhiều năm, vì vậy chúng tôi không cảm thấy vội vã. Chúng tôi cũng biết rằng sẽ mất vài năm nhân lực để phát triển phần cứng và phần mềm CCU / CMU mới này.

Bẫy lịch trình

Theo thời gian, chúng tôi nhận thấy rằng luôn có những vấn đề cấp bách buộc chúng tôi phải trì hoãn việc phát triển kiến ​​trúc CCU/CMU. Chúng tôi cảm thấy an toàn về quyết định này bởi vì các công ty điện thoại khác cũng liên tục trì hoãn việc triển khai các bộ chuyển mạch kỹ thuật số. Khi xem lịch trình của họ, chúng tôi cảm thấy tự tin rằng mình còn nhiều thời gian, vì vậy chúng tôi đã liên tục trì hoãn sự phát triển của mình.

Rồi đến một ngày sếp của tôi gọi tôi vào văn phòng của ông ấy và nói: “Một trong những khách hàng của chúng ta đang triển khai bộ chuyển mạch kỹ thuật số vào tháng tới. Đến lúc đó chúng ta phải có CCU/CMU hoạt động.

Tôi đã rất kinh hoàng! Làm thế nào chúng ta có thể đạt được số năm phát triển của con người trong một tháng? Nhưng sếp của tôi đã có một kế hoạch …

Trên thực tế, chúng tôi không cần một kiến ​​trúc CCU/CMU đầy đủ. Công ty điện thoại đang triển khai bộ chuyển mạch kỹ thuật số này rất nhỏ. Họ chỉ có một văn phòng trung tâm và chỉ có hai điểm phân phối tại địa phương. Quan trọng hơn, các điểm phân phối “địa phương” không phải đúng là địa phương. Họ thực ra có các thiết bị chuyển mạch analog cũ bình thường chuyển mạch cho hàng trăm khách hàng. Tốt hơn nữa, những bộ chuyển mạch đó thuộc loại có thể quay số bằng COLT bình thường. Thậm chí tốt hơn nữa, số điện thoại của khách hàng chứa tất cả thông tin cần thiết để quyết định sử dụng điểm phân phối cục bộ nào. Nếu số điện thoại có 5, 6 hoặc 7 ở một vị trí nhất định, nó sẽ chuyển đến điểm phân phối 1; nếu không, nó đã đi đến điểm phân phối 2.

Vì vậy, như sếp của tôi đã giải thích với tôi, chúng tôi thực sự không cần CCU/CMU. Những gì chúng tôi cần là một máy tính đơn giản tại văn phòng trung tâm được kết nối bằng đường dây modem với hai COLT tiêu chuẩn tại các điểm phân phối. SAC sẽ giao tiếp với máy tính của chúng tôi tại văn phòng trung tâm và máy tính đó sẽ giải mã số điện thoại và sau đó chuyển tiếp các lệnh quay số và đo lường tới COLT tại điểm phân phối thích hợp.

Do đó, pCCU đã được sinh ra.

Đây là sản phẩm đầu tiên được viết bằng C và sử dụng BOSS đã được triển khai cho khách hàng. Tôi mất khoảng một tuần để phát triển. Câu chuyện này không có ý nghĩa kiến ​​trúc sâu sắc, nhưng nó tạo nên một lời mở đầu đẹp cho dự án tiếp theo.

DLU/DRU

Vào đầu những năm 1980, một trong những khách hàng của chúng tôi là một công ty điện thoại ở Texas. Họ bao phủ một khu vực địa lý rộng lớn. Trên thực tế, các khu vực đó rộng lớn đến mức mỗi một khu vực dịch vụ cần có nhiều văn phòng khác nhau để điều phối thợ. Những văn phòng đó có những người thợ kiểm tra, những người cần thiết bị đầu cuối vào SAC của chúng tôi.

Bạn có thể nghĩ rằng đây là một vấn đề đơn giản để giải quyết – nhưng hãy nhớ rằng câu chuyện này diễn ra vào đầu những năm 1980. Thiết bị đầu cuối từ xa không phổ biến lắm. Để làm cho vấn đề tồi tệ hơn, phần cứng của SAC cho rằng tất cả các thiết bị đầu cuối đều là cục bộ. Các thiết bị đầu cuối của chúng tôi thực sự nằm trên một bus nối tiếp, tốc độ cao, được phát triển riêng.

Chúng tôi có khả năng truy cập thiết bị đầu cuối từ xa, nhưng nó dựa trên modem và vào đầu những năm 1980, modem thường bị giới hạn ở tốc độ 300 bit mỗi giây. Khách hàng của chúng tôi không hài lòng với tốc độ chậm chạp đó.

Thời đó đã bắt đầu có modem tốc độ cao, nhưng chúng rất đắt và chúng cần phải chạy trên các kết nối cố định “có điều kiện”. Chất lượng kết nối quay số chắc chắn là không đủ tốt.

Khách hàng của chúng tôi yêu cầu một giải pháp. Câu trả lời của chúng tôi là DLU/DRU.

DLU/DRU là viết tắt của “Bộ Hiển Thị Cục Bộ” (Display Local Unit) và “Bộ Hiển Thị Từ Xa” (Display Remote Unit). DLU là một bảng mạch máy tính cắm vào khung máy tính SAC và mô phỏng là một bảng quản lý thiết bị đầu cuối. Tuy nhiên, thay vì điều khiển bus nối tiếp cho các thiết bị đầu cuối cục bộ, nó đã lấy luồng ký tự và ghép nó qua một kết nối modem có điều kiện 9600-bps.

DRU là một hộp được đặt ở vị trí của khách hàng ở xa. Nó kết nối với đầu kia của kết nối 9600-bps và có phần cứng để điều khiển các thiết bị đầu cuối trên bus nối tiếp độc quyền của chúng tôi. Nó phân kênh các ký tự nhận được từ liên kết 9600-bps và gửi chúng đến các thiết bị đầu cuối cục bộ thích hợp.

Thật kỳ lạ, phải không? Chúng tôi đã phải thiết kế một giải pháp mà ngày nay đã phổ biến đến mức chúng tôi thậm chí không bao giờ nghĩ đến nó. Nhưng hồi đó …

Chúng tôi thậm chí đã phải phát minh ra giao thức truyền thông của riêng mình bởi vì, trong những ngày đó, các giao thức truyền thông tiêu chuẩn không phải là dạng phần mềm chia sẻ mã nguồn mở. Quả thực, đây là thời điểm rất lâu trước khi chúng ta có bất kỳ loại kết nối Internet nào.

Kiến trúc

Kiến trúc của hệ thống này rất đơn giản, nhưng có một số điểm kỳ quặc thú vị mà tôi muốn nhấn mạnh. Đầu tiên, cả hai thiết bị này đều sử dụng công nghệ 8085 của chúng tôi và cả hai đều được viết bằng C và sử dụng BOSS. Nhưng đó là những điểm tương đồng duy nhất.

Dự án đó có hai người. Tôi là trưởng dự án, và Mike Carew là cộng sự thân thiết của tôi. Tôi đảm nhận việc thiết kế và viết mã của DLU; Mike đã thực hiện DRU.

Kiến trúc của DLU dựa trên mô hình luồng dữ liệu. Mỗi tác vụ thực hiện một công việc nhỏ và tập trung, sau đó chuyển đầu ra của nó cho tác vụ tiếp theo trong hàng, sử dụng một hàng đợi. Hãy nghĩ về mô hình pipe và bộ lọc trong UNIX. Kiến trúc này rất rắc rối. Một nhiệm vụ có thể cung cấp một hàng đợi mà nhiều tác vụ khác sẽ phục vụ. Các tác vụ khác sẽ cung cấp một hàng đợi mà chỉ một tác vụ sẽ phục vụ.

Hãy nghĩ về một dây chuyền lắp ráp. Mỗi vị trí trên dây chuyền lắp ráp đều có một công việc duy nhất, đơn giản, tập trung cao độ để thực hiện. Sau đó, sản phẩm di chuyển đến vị trí tiếp theo trong hàng. Đôi khi dây chuyền lắp ráp chia thành nhiều dây chuyền. Đôi khi những dòng này hợp nhất lại thành một dòng. Đó là DLU.

DRU của Mike đã sử dụng một sơ đồ hoàn toàn khác. Anh ấy đã tạo một nhiệm vụ cho mỗi thiết bị đầu cuối và chỉ cần thực hiện toàn bộ công việc cho thiết bị đầu cuối đó trong nhiệm vụ đó. Không có hàng đợi. Không có luồng dữ liệu. Chỉ là nhiều nhiệm vụ lớn giống hệt nhau, mỗi tác vụ quản lý thiết bị đầu cuối của riêng mình.

Điều này ngược lại với một dây chuyền lắp ráp. Trong trường hợp này, cũng tương tự như có nhiều chuyên gia xây dựng, mỗi người trong số họ lại xây dựng toàn bộ sản phẩm của mình.

Vào thời điểm đó, tôi nghĩ rằng kiến ​​trúc của tôi là vượt trội. Với Mike thì tất nhiên, nghĩ rằng anh ấy tốt hơn. Chúng tôi đã có nhiều cuộc thảo luận thú vị về điều này. Cuối cùng, tất nhiên, cả hai đều hoạt động khá tốt. Và tôi nhận ra rằng các kiến ​​trúc phần mềm có thể rất khác biệt, nhưng vẫn có hiệu quả như nhau.

VRS

Khi những năm 1980 tiến triển, các công nghệ ngày càng mới hơn đã xuất hiện. Một trong những công nghệ đó là máy tính điều khiển bằng giọng nói.

Một trong những tính năng của hệ thống 4-Tel là khả năng xác định vị trí lỗi trong cáp của người thợ. Thủ tục thực hiện như sau:

• Người kiểm tra, tại văn phòng trung tâm, sẽ sử dụng hệ thống của chúng tôi để xác định khoảng cách gần đúng, tính bằng feet, tới lỗi. Điều này sẽ chính xác trong khoảng 20% ​. Người kiểm tra sẽ cử một thợ sửa chữa cáp đến một điểm truy cập thích hợp gần vị trí đó.

• Khi đến nơi, thợ sửa chữa cáp sẽ gọi cho người kiểm tra và yêu cầu bắt đầu quá trình xác định vị trí lỗi. Người kiểm tra sẽ gọi tính năng định vị lỗi của hệ thống 4-Tel. Hệ thống sẽ bắt đầu đo các đặc tính điện của đường dây bị lỗi đó và sẽ in thông báo trên màn hình yêu cầu thực hiện một số thao tác nhất định, chẳng hạn như mở cáp hoặc đoản mạch cáp.

• Người kiểm tra sẽ cho thợ sửa chữa biết hệ thống muốn vận hành như thế nào nào và người thợ sửa chữa sẽ cho người kiểm tra biết sau khi hành động hoàn tất. Sau đó, người kiểm tra sẽ thông báo cho hệ thống biết rằng hành động đã hoàn thành và hệ thống sẽ tiếp tục kiểm tra.

• Sau hai hoặc ba lần tương tác như vậy, hệ thống sẽ tính toán khoảng cách mới đến lỗi. Sau đó, người thợ cáp sẽ lái xe đến vị trí đó và bắt đầu lại quy trình.

Hãy tưởng tượng điều đó sẽ tốt hơn bao nhiêu nếu những người thợ sửa chữa cáp, đang ở trên cột hoặc đứng ở bệ, có thể tự vận hành hệ thống. Và đó chính xác là những gì công nghệ giọng nói mới cho phép chúng tôi làm. Các thợ sửa cáp có thể gọi trực tiếp vào hệ thống của chúng tôi, điều khiển hệ thống bằng cảm biến âm thanh và lắng nghe kết quả được đọc lại cho họ bằng một giọng nói dễ chịu.

Cái tên

Công ty tôi đã tổ chức một cuộc thi nhỏ để chọn một cái tên cho hệ thống mới. Một trong những cái tên sáng tạo nhất được gợi ý là SAM CARP. Nó có nghĩa là “Still Another Manifestation of Capitalist Avarice Repressing the Proletariat” (Vẫn Còn Một Biểu Hiện Khác Của Sự Bạo Ngược Của Tư Bản Đàn Áp Giai Cấp Vô Sản). Không cần phải nói nhiều, cái tên đó đã không được chọn.

Một cái tên khác là Teradyne Interactive Test System (Hệ thống Kiểm tra Tương tác Teradyne). Cái đó cũng không được chọn.

Vẫn còn một cái tên khác là Service Area Test Access Network (Mạng Truy Cập Kiểm Tra Khu Vực Dịch vụ). Nó cũng không được chọn.

Cuối cùng, cái tên thắng cuộc là VRS: Voice Response System (Hệ Thống Phản Hồi Bằng Giọng Nói).

Kiến trúc

Tôi không làm việc với hệ thống này, nhưng tôi đã nghe về những gì đã xảy ra. Câu chuyện tôi sắp kể với bạn là một câu chuyện truyền miệng, nhưng phần lớn, tôi tin nó là chính xác.

Đó là những ngày đầu tiên của máy vi tính, hệ điều hành UNIX, ngôn ngữ C và cơ sở dữ liệu SQL. Chúng tôi đã quyết tâm sử dụng tất cả những công nghệ đó.

Từ rất nhiều nhà cung cấp cơ sở dữ liệu, cuối cùng chúng tôi đã chọn UNIFY. UNIFY là một hệ thống cơ sở dữ liệu hoạt động với UNIX, hệ thống này hoàn hảo đối với chúng tôi.

UNIFY hỗ trợ một công nghệ mới được gọi là Embedded SQL. Công nghệ này cho phép chúng tôi nhúng các lệnh SQL, dưới dạng chuỗi, ngay vào mã C của chúng tôi. Và vì vậy, chúng tôi đã làm điều đó – ở khắp mọi nơi.

Ý tôi là, nó thật tuyệt khi bạn có thể đưa ngay SQL vào mã của mình, ở bất cứ đâu bạn muốn. Và chúng tôi muốn ở đâu? Mọi nơi! Và do đó, các lệnh SQL đã bị bôi ra khắp phần thân code đó.

Tất nhiên, trong những ngày đó, SQL không phải là một tiêu chuẩn vững chắc. Có rất nhiều cú pháp SQL đặc biệt dành riêng cho từng nhà cung cấp. Vì vậy, các lệnh gọi SQL đặc biệt và các lệnh gọi API UNIFY đặc biệt cũng bị bôi ra trong toàn bộ mã.

Những thứ này hoạt động tuyệt vời! Hệ thống đã thành công. Các thợ sửa chữa đã sử dụng nó, và các công ty điện thoại yêu thích nó. Cuộc sống toàn là những nụ cười hạnh phúc.

Sau đó sản phẩm UNIFY chúng tôi đang sử dụng đã bị hủy bỏ.

Ố. Ồ.

Vì vậy, chúng tôi quyết định chuyển sang SyBase. Hay đó là Ingress gì đó? Tôi không nhớ rõ. Chỉ cần nói rằng, chúng tôi đã phải tìm kiểm qua tất cả code C đó, tìm tất cả các lệnh gọi API đặc biệt và SQL được nhúng và thay thế chúng bằng các chức năng tương ứng của nhà cung cấp mới.

Sau ba tháng nỗ lực, chúng tôi đã bỏ cuộc. Chúng tôi không thể làm cho nó hoạt động. Chúng tôi đã bị gắn kết chặt với UNIFY đến nỗi không còn hy vọng gì về việc tái cấu trúc lại chỗ code này với bất cứ chi phí khả thi nào.

Vì vậy, chúng tôi đã thuê một bên thứ ba để duy trì UNIFY cho chúng tôi, dựa trên hợp đồng bảo trì. Và, tất nhiên, phí bảo trì tăng lên từ năm này qua năm khác.

Kết luận VRS

Đây là một trong những cách mà tôi đã học được rằng cơ sở dữ liệu là chi tiết nên được tách biệt khỏi mục đích nghiệp vụ tổng thể của hệ thống. Đây cũng là một trong những lý do mà tôi không thích gắn kết chặt chẽ với các hệ thống phần mềm của bên thứ ba.


[1] Phần mềm này sau đó được đổi tên thành Bob’s Only Successful Software – Phần mềm thành công duy nhất của Bob.

Clean Architecture – Phụ Lục A. Khảo Cổ Kiến Trúc (Phần 2)

4-TEL

Vào tháng 10 năm 1976, sau khi bị OMC sa thải, tôi trở lại một bộ phận khác của Teradyne – một bộ phận mà tôi sẽ gắn bó trong 12 năm. Sản phẩm tôi làm có tên là 4-TEL. Mục đích của nó là để kiểm tra mọi đường dây điện thoại trong một khu vực dịch vụ điện thoại vào hàng đêm và đưa ra một báo cáo về tất cả các đường dây cần sửa chữa. Nó cũng cho phép nhân viên kiểm tra điện thoại kiểm tra chi tiết các đường dây điện thoại cụ thể.

Hệ thống này bắt đầu ra đời với kiểu kiến ​​trúc tương tự như hệ thống cắt bằng tia Laser. Nó là một ứng dụng nguyên khối được viết bằng hợp ngữ mà không có bất kỳ ranh giới nào đáng kể. Nhưng vào thời điểm tôi gia nhập công ty, điều đó sắp sửa thay đổi.

Hệ thống được sử dụng bởi những người kiểm tra đặt tại một trung tâm dịch vụ (Service Center – SC). Một trung tâm dịch vụ bao gồm nhiều văn phòng trung tâm (Central Office – CO), mỗi văn phòng có thể xử lý tới 10.000 đường dây điện thoại. Phần cứng đo lường và quay số phải được đặt bên trong CO. Vì vậy, đó là nơi đặt các máy tính M365. Chúng tôi gọi những chiếc máy tính đó là máy kiểm tra đường truyền văn phòng trung tâm (Central Office Line Tester – COLT). Một chiếc M365 khác đã được đặt tại SC; nó được gọi là máy tính vùng dịch vụ (Service Area Computer – SAC). SAC có một số modem mà nó có thể sử dụng để quay số COLT và giao tiếp với tốc độ 300-baud (30-cps).

Lúc đầu, các máy tính COLT làm mọi thứ, bao gồm tất cả các giao tiếp bảng điều khiển, menu và báo cáo. SAC chỉ là một bộ ghép kênh đơn giản lấy đầu ra từ các COLT và đưa nó lên màn hình.

Vấn đề với cách thiết lập này là tốc độ 30-cps thực sự chậm. Những người kiểm tra không thích xem các ký tự xuất hiện rề rề trên màn hình, đặc biệt là vì họ chỉ quan tâm đến một vài bit dữ liệu quan trọng. Ngoài ra, trong những ngày đó, bộ nhớ nhân trong M365 rất đắt tiền và chương trình này thì khá lớn.

Giải pháp được đưa ra là tách phần phần mềm quay số và đo đường khỏi phần phân tích kết quả và in báo cáo. Cái sau sẽ được chuyển vào SAC, và cái trước sẽ ở lại COLTs. Điều này sẽ cho phép COLT trở thành một chiếc máy nhỏ hơn, với ít bộ nhớ hơn nhiều và sẽ tăng tốc đáng kể phản hồi tại thiết bị đầu cuối, vì các báo cáo sẽ được tạo trong SAC.

Kết quả này là một thành công xuất sắc. Cập nhật màn hình rất nhanh (sau khi COLT thích hợp đã được quay số) và bộ nhớ của các COLT đã giảm đi rất nhiều.

Ranh giới rất sạch và được tách biệt rõ ràng. Các gói dữ liệu rất ngắn đã được trao đổi giữa SAC và COLT. Các gói này là một dạng DSL rất đơn giản, đại diện cho các lệnh như “DIAL XXXX” hoặc “MEASURE.”

M365 được nạp từ các cuộn băng từ. Những ổ băng từ đó rất đắt tiền và hoạt động không đáng tin cậy lắm – đặc biệt là trong môi trường công nghiệp của văn phòng trung tâm điện thoại. Ngoài ra, M365 là một chiếc máy đắt tiền so với phần còn lại của các thiết bị điện tử trong COLT. Vì vậy, chúng tôi bắt tay vào một dự án thay thế M365 bằng một máy vi tính dựa trên bộ vi xử lý 8085.

Máy tính mới này bao gồm một bảng mạch để lắp vi xử lý 8085, một bảng mạch RAM chứa 32K RAM và ba bảng mạch ROM chứa 12K bộ nhớ chỉ đọc. Tất cả các bo mạch này đều nằm gọn trong cùng một bộ vỏ với phần cứng đo lường, do đó loại bỏ được lớp vỏ ngoài cồng kềnh đã chứa M365.

Các bảng ROM chứa 12 chip Intel 2708 EPROM (Erasable Programmable Read-Only Memory – Bộ nhớ chỉ đọc lập trình có thể xóa)[1]. Hình A.8 cho một ví dụ về con chip như vậy. Chúng tôi đã nạp những con chip đó bằng phần mềm bằng cách chèn chúng vào các thiết bị đặc biệt gọi là đầu ghi PROM được điều khiển bởi môi trường phát triển của chúng tôi. Những con chip này có thể được xóa bằng cách phơi chúng dưới tia cực tím cường độ cao[2].

Bạn tôi CK và tôi đã dịch chương trình hợp ngữ M365 cho COLT sang hợp ngữ của 8085. Bản dịch này được thực hiện bằng tay và chúng tôi mất khoảng 6 tháng. Kết quả cuối cùng là khoảng 30 nghìn dòng code 8085.

Môi trường phát triển của chúng tôi có 64K RAM và không có ROM, vì vậy chúng tôi có thể nhanh chóng tải xuống các file nhị phân đã biên dịch của mình vào RAM và kiểm tra chúng.

Sau khi chương trình hoạt động, chúng tôi chuyển sang sử dụng EPROM. Chúng tôi đã ghi 30 con chip và lắp chúng vào đúng các khe cắm trong ba bảng ROM. Mỗi con chip đều được dán nhãn để chúng tôi có thể biết con chip nào đã đi vào khe nào.

Chương trình 30K là một chương trình nhị phân duy nhất, dài 30K dòng. Để ghi các chip, chúng tôi chỉ cần chia ảnh nhị phân đó thành 30 phần 1K khác nhau và ghi từng phần vào chip được dán nhãn thích hợp.

Hình A.8 Chip EPROM

Điều này hoạt động rất tốt, và chúng tôi bắt đầu sản xuất hàng loạt phần cứng và triển khai hệ thống trên hiện trường.

Nhưng phần mềm thì mềm[3]. Các chức năng thì cần được thêm vào. Các bug thì cần được sửa chữa. Và khi chương trình cài đặt lớn dần, việc cập nhật phần mềm bằng cách ghi 30 con chip mỗi lần cài đặt, ngoài ra còn phải có người xuống tận nơi để thay thế tất cả 30 con chip này tại tất cả các khu vực đã trở thành một cơn ác mộng.

Điều này dẫn tới có thể xảy ra tất cả các loại vấn đề. Đôi khi các chip sẽ bị dán nhãn sai hoặc nhãn bị rơi ra. Đôi khi kỹ sư dịch vụ hiện trường đã nhầm lẫn thay sai chip. Đôi khi kỹ sư dịch vụ hiện trường vô tình làm gãy chân của một trong các chip mới. Kết quả là các kỹ sư hiện trường đã phải mang theo tất cả 30 con chip bổ sung bên mình.

Tại sao chúng tôi phải thay đổi tất cả 30 con chip? Mỗi khi chúng tôi thêm hoặc xóa code khỏi file thực thi 30K của mình, nó sẽ thay đổi địa chỉ mà mỗi lệnh được nạp. Nó cũng thay đổi địa chỉ của các chương trình con và hàm mà chúng tôi đã gọi. Vì vậy, mọi con chip đều bị ảnh hưởng, bất kể sự thay đổi nhỏ đến mức nào.

Một ngày nọ, sếp của tôi đến gặp tôi và yêu cầu tôi giải quyết vấn đề đó. Ông ấy nói rằng chúng tôi cần một cách để thay đổi đối với firmware mà không cần thay thế tất cả 30 con chip mỗi lần. Chúng tôi cùng nhau suy nghĩ về vấn đề này một lúc, và sau đó khởi hành dự án “Vectorization”. Tôi đã mất ba tháng vì nó.

Ý tưởng này rất đơn giản. Chúng tôi chia chương trình 30K thành 32 file nguồn có thể biên dịch độc lập, mỗi file nhỏ hơn 1K. Ở đầu mỗi file nguồn, chúng tôi cho trình biên dịch biết địa chỉ nào để nạp chương trình kết quả (ví dụ: ORG C400 cho con chip sẽ được đưa vào vị trí C4).

Ngoài ra, ở đầu mỗi file nguồn, chúng tôi đã tạo một cấu trúc dữ liệu đơn giản, có kích thước cố định chứa tất cả địa chỉ của tất cả các chương trình con trên chip đó. Cấu trúc dữ liệu này dài 40 byte, vì vậy nó có thể chứa không quá 20 địa chỉ. Điều này có nghĩa là không con chip nào có thể có nhiều hơn 20 chương trình con.

Tiếp theo, chúng tôi tạo một vùng đặc biệt trong RAM được gọi là vectơ. Nó chứa 32 bảng 40 byte – vừa đủ RAM để chứa các con trỏ ở đầu mỗi chip.

Cuối cùng, chúng tôi đã thay đổi tất cả các lệnh gọi đến các chương trình con trên tất cả các chip thành một lệnh gọi gián tiếp thông qua vectơ RAM thích hợp.

Khi bộ vi xử lý của chúng tôi khởi động, nó sẽ quét từng chip và nạp bảng vectơ ở đầu mỗi con chip vào các vectơ RAM. Sau đó, nó sẽ nhảy tới chương trình chính.

Phương án này hoạt động rất tốt. Giờ đây, khi chúng tôi sửa lỗi hoặc thêm một tính năng, chúng tôi có thể chỉ cần biên dịch lại một hoặc hai chip và chỉ gửi những chip đó cho các kỹ sư dịch vụ hiện trường.

Chúng tôi đã làm cho các chip có thể triển khai độc lập. Chúng tôi đã phát minh ra điều phối đa hình. Chúng tôi đã phát minh ra các đối tượng.

Theo nghĩa đen, đây là một kiến ​​trúc plugin. Chúng tôi đã cắm những con chip đó vào. Cuối cùng, chúng tôi đã thiết kế nó để một tính năng có thể được cài đặt vào sản phẩm của chúng tôi bằng cách cắm con chip có tính năng đó vào một trong những ổ cắm chip đang mở. Quá trình điều khiển danh mục sẽ tự động xuất hiện, và việc gắn nó vào ứng dụng chính sẽ tự động xảy ra.

Tất nhiên, vào thời điểm đó, chúng tôi không biết gì về các nguyên tắc hướng đối tượng và chúng tôi cũng không biết gì về việc tách giao diện người dùng khỏi các quy tắc nghiệp vụ. Nhưng những thứ sơ khai đó đã có và chúng hoạt động rất mạnh mẽ.

Một lợi ích phụ không ngờ của phương pháp này là chúng tôi có thể vá firmware thông qua kết nối quay số. Nếu chúng tôi phát hiện thấy lỗi trong firmware, thì chúng tôi có thể quay số thiết bị của mình và sử dụng chương trình giám sát trên bo mạch để thay đổi vectơ RAM cho chương trình con bị lỗi để trỏ đến một bit RAM trống. Sau đó, chúng tôi sẽ nhập chương trình con đã sửa chữa vào vùng RAM đó, bằng cách nhập nó dưới dạng mã máy số hex.

Đây là một lợi ích tuyệt vời cho hoạt động dịch vụ hiện trường của chúng tôi và cho khách hàng của chúng tôi. Nếu họ gặp sự cố, họ không cần chúng tôi gửi chip mới và lên lịch cho cuộc gọi dịch vụ hiện trường khẩn cấp. Hệ thống đó có thể được vá và một con chip mới có thể được cài đặt vào lần bảo trì định kỳ tiếp theo.

Máy tính vùng dịch vụ

Máy tính vùng dịch vụ 4-TEL (Service Area Computer – SAC) dựa trên máy tính mini M365. Hệ thống này giao tiếp với tất cả các COLT ngoài hiện trường, thông qua modem chuyên dụng hoặc quay số. Nó sẽ ra lệnh cho các COLT đó đo đạc đường dây điện thoại, và nhận lại các kết quả thô, và sau đó sẽ thực hiện một phân tích phức tạp về các kết quả đó để xác định và định vị bất kỳ lỗi nào.

Xác định điều phối

Một trong những nền tảng kinh tế cho hệ thống này là dựa vào việc phân bổ các thợ sửa chữa chính xác. Theo quy tắc của hiệp hội, đội sửa chữa được tách ra thành ba loại: văn phòng trung tâm (CO), cáp và drop (thả). Thợ CO khắc phục các sự cố bên trong văn phòng trung tâm. Thợ cáp khắc phục sự cố cáp kết nối từ CO tới khách hàng. Thợ drop khắc phục các sự cố bên trong cơ sở của khách hàng và các đường kết nối cáp bên ngoài với cơ sở đó (“drop”).

Khi một khách hàng phàn nàn về một vấn đề, thì hệ thống của chúng tôi có thể chẩn đoán vấn đề và xác định loại thợ nào cần được cử đi sửa. Điều này tiết kiệm cho các công ty điện thoại rất nhiều tiền bởi vì việc điều phối không đúng nghĩa là khách hàng bị chậm trễ mà thợ thì lãng phí mất chuyến đi.

Phần code để xác định việc điều phối được thiết kế và viết bởi một ai đó rất thông minh, nhưng khả năng giao tiếp lại hết sức tồi tệ. Quá trình viết phần code này đã được mô tả là “Ba tuần nhìn chằm chằm lên trần nhà và hai ngày code tuôn ra từ mọi lỗ thông của cơ thể anh ta – sau đó anh ta nghỉ việc.”

Không ai hiểu nổi phần code này. Mỗi lần chúng tôi cố gắng thêm một chức năng hoặc để sửa một lỗi, thì chúng tôi lại làm hỏng nó theo cách nào đó. Và bởi vì phần code này là một trong những lợi ích kinh tế chính của hệ thống của chúng tôi, nên mọi lỗi mới đều khiến công ty vô cùng khốn đốn.

Cuối cùng, quản lý của chúng tôi chỉ đơn giản là yêu cầu chúng tôi khóa phần code đó lại và không bao giờ sửa đổi nó. Phần code đó chính thức trở nên cho đóng cứng.

Trải nghiệm này khiến tôi ấn tượng về giá trị của code tốt, tinh gọn.

Kiến trúc

Hệ thống này được viết vào năm 1976 bằng trình biên dịch hợp ngữ M365. Đó là một chương trình đơn, nguyên khối với khoảng 60.000 dòng code. Hệ điều hành này là một bộ chuyển đổi tác vụ tự phát triển dựa trên cơ chế polling. Chúng tôi gọi nó là MPS cho hệ thống đa xử lý. Máy tính M365 không có stack tích hợp, vì vậy các biến của một nhiệm vụ cụ thể được lưu giữ trong một vùng đặc biệt của bộ nhớ và được hoán đổi mỗi khi chuyển đổi ngữ cảnh. Các biến chia sẻ thì được quản lý bằng khóa và semaphore. Các vấn đề về reentrancy và race condition là những vấn đề thường gặp khi đó.

Hệ thống này không có sự tách biệt logic điều khiển thiết bị hoặc logic UI khỏi các quy tắc nghiệp vụ của hệ thống. Ví dụ: mã điều khiển modem có thể được tìm thấy trong phần lớn các quy tắc nghiệp vụ và mãUI. Không hề có nỗ lực nào để tập hợp nó vào một module hoặc một interface trừu tượng. Các modem được điều khiển, ở cấp độ bit, bằng mã nằm rải rác khắp nơi trong hệ thống.

Với UI thiết bị đầu cuối thì cũng giống như vậy. Các thông báo và mã điều khiển định dạng không được tách biệt. Chúng bao phủ rất xa và rộng khắp trong toàn bộ code base 60K dòng này.

Các module modem chúng tôi đang sử dụng được thiết kế để gắn trên bo mạch PC. Chúng tôi đã mua các bộ modem đó từ một bên thứ ba và tích hợp chúng với mạch điện khác vào một bảng phù hợp với bảng mạch chủ tùy chỉnh của chúng tôi. Những bộ modem này rất đắt tiền. Vì vậy, sau một vài năm, chúng tôi quyết định thiết kế modem của riêng mình. Chúng tôi, trong nhóm phần mềm, đã yêu cầu nhà thiết kế phần cứng sử dụng các định dạng bit tương tự để điều khiển modem mới. Chúng tôi giải thích rằng mã điều khiển modem đã bị lan tràn khắp nơi và hệ thống của chúng tôi sẽ phải xử lý cả hai loại modem trong tương lai. Vì vậy, chúng tôi đã cầu xin và tán tỉnh, “Xin hãy làm cho modem mới giống như modem cũ theo quan điểm kiểm soát phần mềm.”

Nhưng khi chúng tôi nhận modem mới, cấu trúc điều khiển lại hoàn toàn khác. Nó không chỉ khác một chút. Nó hoàn toàn, và hoàn toàn khác.

Cảm ơn, kỹ sư phần cứng.

Chúng ta phải làm gì đây? Chúng tôi không chỉ đơn giản là thay thế tất cả các modem cũ bằng các modem mới. Thay vào đó, chúng tôi đã trộn modem cũ và mới trong hệ thống của mình. Phần mềm yêu cầu phải xử lý cả hai loại modem cùng một lúc. Chúng tôi đã tiêu tùng khi bị mã điều khiển modem với các cờ và các trường hợp đặc biệt vây quanh mọi nơi phải không? Phải có tới hàng trăm nơi như vậy!

Cuối cùng, chúng tôi đã chọn một giải pháp thậm chí còn tồi tệ hơn.

Một chương trình con cụ thể đã ghi dữ liệu vào bus giao tiếp nối tiếp được sử dụng để điều khiển tất cả các thiết bị của chúng tôi, bao gồm cả modem của chúng tôi. Chúng tôi đã sửa đổi chương trình con đó để nhận dạng các mẫu bit dành riêng cho modem cũ và chuyển chúng thành các mẫu bit mà modem mới cần.

Điều này không đơn giản. Các lệnh tới modem bao gồm các chuỗi ghi vào các địa chỉ IO khác nhau trên bus nối tiếp. Kiểu hack của chúng tôi phải biên dịch các lệnh này theo trình tự và dịch chúng thành một trình tự khác bằng cách sử dụng các địa chỉ IO, định thời gian và vị trí bit khác nhau.

Chúng tôi đã làm cho nó hoạt động, nhưng đó là vụ hack tồi tệ nhất có thể tưởng tượng được. Chính vì sự thất bại này mà tôi đã học được giá trị của việc tách biệt phần cứng khỏi các quy tắc nghiệp vụ và của các interface trừu tượng.

Cuộc thiết kế lại vĩ đại trên trời

Vào thời điểm những năm 1980, ý tưởng sản xuất máy tính mini của riêng bạn và kiến ​​trúc máy tính của riêng bạn bắt đầu không còn hợp thời. Có rất nhiều máy vi tính trên thị trường, và việc sử dụng chúng thì rẻ hơn và tiêu chuẩn hơn so với việc tiếp tục dựa vào các kiến ​​trúc máy tính tự phát triển từ cuối những năm 1960. Điều đó, cộng với kiến ​​trúc khủng khiếp của phần mềm SAC, đã khiến ban quản lý kỹ thuật của chúng tôi bắt đầu tái cấu trúc toàn bộ hệ thống SAC.

Hệ thống mới sẽ được viết bằng C sử dụng UNIX O/S trên đĩa cứng, chạy trên máy vi tính Intel 8086. Các chuyên gia phần cứng của chúng tôi đã bắt đầu làm việc trên phần cứng của chiếc máy tính mới và một nhóm các lập trình viên được chọn lọc, “Tiger Team,” đã được giao nhiệm vụ viết lại.

Tôi sẽ không làm bạn buồn chán với các chi tiết về thất bại ban đầu. Chỉ cần nói rằng Tiger Team lần đầu tiên đã thất bại hoàn toàn sau khi đốt cháy hai hoặc ba năm nhân lực cho một dự án phần mềm không bao giờ mang lại điều gì.

Một hoặc hai năm sau, có thể là năm 1982, quá trình này lại được bắt đầu lại. Mục tiêu là thiết kế lại toàn bộ và hoàn chỉnh SAC bằng C và UNIX trên phần cứng 80286 được thiết kế mới, ấn tượng, mạnh mẽ của chúng tôi. Chúng tôi gọi chiếc máy tính đó là “Deep Thought”.

Phải mất nhiều năm, sau đó nhiều năm, và thậm chí nhiều năm nữa. Tôi không biết khi nào SAC dựa trên UNIX cuối cùng mới được triển khai; tôi nhớ rằng tôi đã rời công ty vào thời điểm đó (1988). Quả thực là tôi không chắc nó đã từng được triển khai hay chưa.

Tại sao lại chậm trễ như vậy? Nói ngắn gọn, đội ngũ thiết kế lại sẽ rất khó bắt kịp với đội ngũ lập trình viên đông đảo đang tích cực bảo trì hệ thống cũ. Đây chỉ là một ví dụ về những khó khăn mà họ gặp phải.

Châu Âu

Cùng thời điểm SAC được thiết kế lại bằng ngôn ngữ C, công ty cũng bắt đầu mở rộng bán hàng sang châu Âu. Họ không thể đợi phần mềm được thiết kế lại hoàn thiện, nên dĩ nhiên, họ đã triển khai các hệ thống M365 cũ vào châu Âu.

Vấn đề là hệ thống điện thoại ở châu Âu rất khác với hệ thống điện thoại ở Hoa Kỳ. Việc tổ chức thủ công và các bộ máy hành chính cũng khác nhau. Vì vậy, một trong những lập trình viên giỏi nhất của chúng tôi đã được cử sang Anh Quốc để lãnh đạo một nhóm các lập trình viên của Anh Quốc nhằm sửa đổi phần mềm SAC để giải quyết tất cả các vấn đề châu Âu này.

Tất nhiên, không có nỗ lực nghiêm túc nào được thực hiện để tích hợp những thay đổi này vào phần mềm của Hoa Kỳ. Điều này đã xảy ra rất lâu trước khi hệ thống mạng internet giúp cho việc truyền các code base lớn qua đại dương trở nên khả thi. Các lập trình viên Anh Quốc này chỉ cần dựa trên code của Hoa Kỳ và sửa đổi nó nếu cần.

Điều này tất nhiên gây ra khó khăn. Các lỗi được tìm thấy ở cả hai bên bờ Đại Tây Dương cần được sửa chữa ở phía bên kia. Nhưng các module đã thay đổi đáng kể, vì vậy rất khó xác định liệu bản sửa lỗi được thực hiện tại Hoa Kỳ có hoạt động ở Anh Quốc hay không.

Sau một vài năm không đâu vào đâu, một đường dây tốc độ cao kết nối các văn phòng giữa Hoa Kỳ và Anh Quốc được lắp đặt, một nỗ lực nghiêm túc đã được thực hiện để tích hợp hai code base này lại với nhau, làm cho sự khác biệt trở thành vấn đề về cấu hình. Nỗ lực này đã thất bại trong lần thử đầu tiên, lần thứ hai và lần thứ ba. Hai code base này, mặc dù rất giống nhau, vẫn còn quá nhiều điểm khác biệt để có thể tái tích hợp – đặc biệt là trong môi trường thị trường thay đổi nhanh chóng vào thời điểm đó.

Trong khi đó, “Tiger Team” vẫn đang cố gắng viết lại mọi thứ trong C và UNIX, nhận ra rằng họ cũng phải xử lý với cả sự khác nhau giữa Châu Âu / Hoa Kỳ này. Và tất nhiên, tiến độ của họ không thể nào mà đẩy nhanh lên được.

Kết luận SAC

Tôi có thể kể cho bạn nghe nhiều câu chuyện khác về hệ thống này, nhưng tôi quá nản để tiếp tục. Chỉ cần nói rằng nhiều bài học khó khăn trong cuộc đời làm phần mềm của tôi đã học được khi tôi đã chìm trong đống mã hợp ngữ khủng khiếp của SAC.

Ngôn ngữ C

Phần cứng máy tính 8085 mà chúng tôi sử dụng trong dự án 4-Tel Micro đã mang lại cho chúng tôi một nền tảng máy tính chi phí tương đối thấp có thể sử dụng cho nhiều dự án khác nhau được nhúng vào môi trường công nghiệp. Chúng tôi có thể nạp nó với 32K RAM và 32K ROM khác, và chúng tôi có một cỗ máy cực kỳ linh hoạt và mạnh mẽ để điều khiển các thiết bị ngoại vi. Những gì chúng tôi không có là một ngôn ngữ linh hoạt và thuận tiện để lập trình cỗ máy này. Trình biên dịch hợp ngữ 8085 chỉ đơn giản là không hề thú vị khi viết mã.

Trên hết, trình biên dịch hợp ngữ mà chúng tôi đang sử dụng được viết bởi các lập trình viên của chính chúng tôi. Nó chạy trên máy tính M365 của chúng tôi, sử dụng hệ điều hành băng từ như được mô tả trong phần “Cắt Laser”.

Như định mệnh đã xảy ra, kỹ sư phần cứng hàng đầu của chúng tôi đã thuyết phục CEO của chúng tôi rằng chúng tôi cần một chiếc máy tính thực sự. Anh ấy thực sự không biết mình sẽ làm gì với nó, nhưng anh ấy có rất nhiều ảnh hưởng chính trị. Vì vậy, chúng tôi đã được mua một chiếc PDP-11/60.

Tôi, một lập trình viên tầm thường vào thời điểm đó, đã rất ngạc nhiên. Tôi biết chính xác những gì tôi muốn làm với chiếc máy tính đó. Tôi đã xác định rằng đây sẽ là máy của tôi.

Khi sách hướng dẫn đến tay, nhiều tháng trước khi giao máy, tôi đã mang chúng về nhà và ngấu nghiến nó. Vào thời điểm máy tính được giao, tôi đã biết cách vận hành cả phần cứng và phần mềm ở mức độ quen thuộc – ít nhất là quen thuộc như việc học ở nhà có thể làm được.

Tôi đã giúp viết đơn đặt hàng. Đặc biệt, tôi đã chỉ định dung lượng lưu trữ đĩa mà máy tính mới sẽ có. Tôi quyết định chúng tôi nên mua hai ổ đĩa có thể tháo rời, mỗi chiếc 25 megabyte[4].

Năm mươi megabyte! Số này dường như là vô hạn rồi! Tôi nhớ khi đi bộ ngang qua các hành lang của văn phòng, vào đêm khuya, miệng lẩm nhẩm như mụ Phù Thủy Xấu Xa của Miền Tây (Wicked Witch of the West): “Năm mươi megabyte! Hahahahahahahahahah! ”

Tôi đã yêu cầu người quản lý cơ sở xây dựng một căn phòng nhỏ để chứa sáu thiết bị đầu cuối VT100. Tôi trang trí nó bằng những bức tranh về không gian. Các lập trình viên phần mềm của chúng tôi sẽ sử dụng căn phòng này để viết và biên dịch mã.

Khi chiếc máy đó đến nơi, tôi đã mất vài ngày để thiết lập nó, nối dây tất cả các thiết bị đầu cuối và đưa mọi thứ vào hoạt động. Đó là một niềm vui – một tình yêu lao động.

Chúng tôi đã mua các trình biên dịch hợp ngữ tiêu chuẩn cho 8085 từ Boston Systems Office và chúng tôi đã dịch mã 4-Tel Micro sang cú pháp đó. Chúng tôi đã xây dựng một hệ thống biên dịch chéo cho phép chúng tôi nạp các file nhị phân đã biên dịch từ PDP-11 xuống môi trường phát triển 8085 và trình ghi ROM của chúng tôi. Và tất cả đều hoạt động muột mà.

C

Nhưng chúng tôi vẫn gặp vấn đề về việc sử dụng trình biên dịch hợp ngữ 8085. Đó không phải là thứ khiến tôi hài lòng. Tôi đã nghe nói rằng có ngôn ngữ “mới” được sử dụng nhiều tại Bell Labs. Họ gọi nó là “C.” Vì vậy, tôi đã mua một bản sao Ngôn ngữ lập trình C của Kernighan và Ritchie. Giống như các hướng dẫn sử dụng PDP-11 vài tháng trước, tôi đã đọc ngấu nghiến cuốn sách.

Tôi đã rất ngạc nhiên bởi sự thanh lịch đơn giản của ngôn ngữ này. Nó không hy sinh sức mạnh của hợp ngữ, mà vẫn cung cấp quyền truy cập vào sức mạnh đó với một cú pháp thuận tiện hơn nhiều. Tôi đã bị thuyết phục.

Tôi đã mua một trình biên dịch C từ Whitesmiths và để nó chạy trên PDP-11. Đầu ra của trình biên dịch là cú pháp trình hợp dịch tương thích với trình biên dịch Boston Systems Office 8085. Như vậy, chúng tôi đã có một con đường để đi từ C đến phần cứng 8085!

Bây giờ vấn đề duy nhất là thuyết phục một nhóm lập trình viên hợp ngữ nhúng rằng họ nên sử dụng C. Nhưng đó là một câu chuyện ác mộng cho một khoảng thời gian khác …


[1] Vâng, tôi hiểu đó là oxymoron.

[2] Chúng có một cửa sổ bằng nhựa trong suốt nhỏ cho phép bạn nhìn thấy chip silicon bên trong và cho phép tia cực tím xóa dữ liệu.

[3] Có, tôi biết rằng khi phần mềm được ghi vào ROM, nó được gọi là firmware — nhưng thậm chí firmware thực sự vẫn mềm.

[4] RKO7

Clean Architecture – Phụ Lục A. Khảo Cổ Kiến Trúc (Phần 1)

Để khám phá các nguyên tắc của một kiến trúc tốt, chúng ta hãy thực hiện hành trình 45 năm qua một số dự án mà tôi đã thực hiện kể từ năm 1970. Một số dự án này rất thú vị theo quan điểm kiến trúc. Một số khác thú vị vì những bài học kinh nghiệm và vì cách mà chúng có thể áp dụng vào các dự án tiếp theo.

Phụ lục này mang hơi hướng tự truyện. Tôi đã cố gắng giữ cho cuộc thảo luận có liên quan đến chủ đề kiến trúc; nhưng như trong bất kỳ cuốn tự truyện nào, các yếu tố khác đôi khi vẫn xâm nhập vào. 😉

Hệ thống kế toán hợp nhất

Vào cuối những năm 1960, một công ty tên là ASC Tabulating đã ký một hợp đồng với Local 705 của Liên Hiệp Vận Tải để cung cấp một hệ thống kế toán. Máy tính ASC được chọn để triển khai hệ thống này là một máy GE Datanet 30, như thấy ở Hình A.1.

Hình A.1 GE Datanet 30

Như bạn có thể thấy trong bức ảnh, đây là một cỗ máy khổng lồ[1]. Nó chiếm nguyên một căn phòng, và căn phòng này cần được kiểm soát thông số môi trường một cách nghiêm ngặt.

Chiếc máy tính này được tạo ra từ những ngày trước khi mạch tích hợp xuất hiện. Nó được dựng nên bởi các transistor rời. Nó thậm chí có cả một số ống chân không (mặc dù chỉ trong bộ khuếch đại cảm biến của ổ đĩa băng từ).

So với các tiêu chuẩn ngày nay thì cỗ máy này quá cồng kềnh, chậm chạp. Nó có 16K x 18 bits của lõi xử lý, với thời gian một chu kỳ khoảng 7 micro-giây[2]. Nó chiếm đầy một căn phòng lớn được kiểm soát môi trường. Nó có các ổ đĩa băng từ 7 rãnh và một ổ đĩa cứng với dung lượng 20 mega-byte.

Chiếc đĩa cứng đó trông như một con quái vật. Bạn có thể thấy nó trong bức ảnh ở Hình A.2 – nhưng có lẽ nó sẽ không mô tả đủ kích cỡ của con thú đó. Đỉnh của cái tủ đó cao hơn cả đầu tôi. Các đĩa có đường kính 36 inch (khoảng 914mm), và dày 3/8 inch (khoảng 9.5mm). Một trong những chiếc đĩa được chụp trong Hình A.3.

Bây giờ hãy đếm số đĩa trong bức ảnh đầu tiên. Có tới hơn một tá. Mỗi một chiếc có cánh tay tìm kiếm riêng và được điều khiển bởi các bộ truyền động khí nén. Bạn có thể nhìn thấy những đầu tìm kiếm này di chuyển qua các đĩa. Thời gian tìm kiếm có lẽ khoảng nửa giây tới một giây.

Khi con quái vật này bật lên, nó phát ra âm thanh như động cơ phản lực. Sàn nhà sẽ rung và lắc cho đến khi nó đạt được tốc độ[3].

Hình A.2 Bộ lưu trữ dữ liệu bằng các đĩa

Điểm mạnh của Datanet 30 là khả năng điều khiển một lượng lớn các thiết bị đầu cuối không đồng bộ ở tốc độ tương đối cao. Đó chính xác là những gì ASC cần.

ASC nằm ở Lake Bluff, Illinois, cách phía bắc Chicago 30 dặm. Văn phòng Local 705 nằm ở trung tâm Chicago. Hiệp hội này muốn hàng chục các thư ký nhập liệu của họ dùng các thiết bị đầu cuối CRT[4] (Hình A.4) để nhập dữ liệu vào hệ thống. Họ sẽ in các báo cáo lên máy teletype ASR35 (Hình A.5).

Hình A.3 Một tấm của ổ đĩa đó: dày 3/8 inch, đường kính 36 inch

Các thiết bị đầu cuối CRT chạy ở tốc độ 30 ký tự mỗi giây. Đó là tốc độ tương đối khá vào cuối những năm 1960 bởi vì thiết bị modem tương đối đơn giản vào những ngày đó.

ASC đã thuê cả tá các đường dây điện thoại riêng và số lượng modem 300 baund gấp đôi số đó từ công ty điện thoại kết nối tới Datanet 30 tới các thiết bị đầu cuối này.

Những chiếc máy tính này không đi kèm với hệ điều hành. Chúng thậm chí còn không có cả hệ thống file. Thứ mà bạn có là một trình biên dịch hợp ngữ.

Nếu bạn cần lưu dữ liệu trên đĩa cứng, thì bạn sẽ lưu dữ liệu lên đĩa cứng đó. Không phải trong một file. Không phải trong một thư mục. Bạn xác định xem đĩa nào, rãnh nào, và sector nào để đặt dữ liệu vào và sau đó bạn vận hành đĩa để đặt dữ liệu vào đó. Vâng, điều đó có nghĩa là chúng ta sẽ phải viết driver đĩa cứng của chính chúng ta.

Hình A.4 Thiết bị đầu cuối CRT

Hệ thống kế toán hợp nhất có ba loại bản ghi: Nhân Viên, Chủ, và Hội Viên. Hệ thống này là một hệ thống CRUD đối với những bản ghi này, nhưng cũng bao gồm các thao tác để đăng phí, tính toán thay đổi trong sổ cái chung.v.v.

Hệ thống gốc được viết bằng trình biên dịch hợp ngữ bởi một nhà tư vấn có khả năng nhồi tất cả những thứ đó trong 16K.

Như bạn có thể tưởng tượng, cỗ máy Datanet 30 to lớn đó rất đắt đỏ để vận hành và bảo dưỡng. Chi phí để thuê nhà tư vấn phần mềm để duy trì phần mềm chạy được cũng đắt không kém. Hơn nữa, máy tính mini đang trở nên phổ biến và rẻ hơn nhiều.

Hình A.5 Máy teletype ASR35

Vào năm 1971, khi tôi 18 tuổi, ASC đã thuê tôi và hai người bạn có cùng đam mê của tôi tới để thay thế toàn bộ hệ thống kế toán hợp nhất với một cái dựa trên máy tính mini Varian 620/f (Hình A.6). Chiếc máy tính này có giá rẻ. Tiền thuê chúng tôi cũng rẻ. Vì vậy có vẻ như ASC có một thương vụ hời.

Chiếc máy Varian có bus 16-bit và bộ nhớ lõi 32K * 16. Nó có chu kỳ khoảng 1 micro-giây. Nó mạnh mẽ hơn nhiều so với Datanet 30. Nó sử dụng công nghệ đĩa cứng 2314 rất thành công của IBM, cho phép chúng tôi lưu trữ 30-megabyte trên các đĩa chỉ có đường kính 14-inch và không thể phát nổ xuyên qua các bức tường khối bê tông!

Dĩ nhiên, chúng tôi vẫn không có hệ điều hành. Không có hệ thống file. Không có ngôn ngữ bậc cao. Tất cả thứ mà chúng tôi có là một trình biên dịch hợp ngữ. Nhưng chúng tôi đã làm.

Hình A.6 Máy tính mini Varian 620/f

Thay vì cố gắng nhồi nhét toàn bộ hệ thống vào 32K, chúng tôi đã tạo ra một hệ thống phủ chồng. Các ứng dụng sẽ được nạp từ ổ đĩa vào một khối bộ nhớ dành riêng để phủ chồng. Chúng sẽ được thực thi trong bộ nhớ đó, và được hoán đổi trước vào ổ đĩa với RAM cục bộ của chúng, để cho phép các chương trình khác thực thi.

Dĩ nhiên, khi UI của bạn chạy với tốc độ 30 ký tự một giây, thì các chương trình của bạn sẽ có nhiều thời gian để đợi. Chúng tôi có nhiều thời gian để hoán đổi các chương trình vào và ra khỏi ổ đĩa để giữ cho tất cả các thiết bị đầu cuối chạy nhanh hết mức có thể. Không một ai phàn nàn về vấn đề thời gian phản hồi hết.

Chúng tôi đã viết một trình giám sát để quản lý các ngắt và IO. Chúng tôi đã viết các ứng dụng; chúng đã viết các driver ổ đĩa, các driver thiết bị đầu cuối, các driver băng từ, và mọi thứ khác trong hệ thống đó. Không có gì trong hệ thống đó mà chúng tôi không viết cả. Mặc dù đó là một cuộc vận lộn kéo dài với quá nhiều tuần làm 80 tiếng, nhưng chúng tôi cũng đã đưa con quái vật lên và chạy được trong vòng 8 hoặc 9 tháng.

Kiến trúc của hệ thống này rất đơn giản (Hình A.7). Khi một ứng dụng bắt đầu, nó sẽ tạo ra đầu ra đến khi bộ nhớ đệm thiết bị đầu cuối của nó đầy. Sau đó bộ giám sát sẽ hoán đổi ứng dụng ra, và đổi một ứng dụng mới vào. Bộ giám sát sẽ tiếp tục trích xuất nội dung của bộ đệm đầu cuối ở tốc độ 30-cps cho đến khi nó gần hết. Sau đó, nó sẽ hoán đổi ứng dụng trở lại để lấp đầy bộ nhớ đệm lần nữa.

Hình A.7 Kiến trúc hệ thống

Có hai ranh giới trong hệ thống này. Đầu tiên là ranh giới đầu ra ký tự. Các ứng dụng này không hề biết rằng đầu ra của chúng sẽ tới một thiết bị đầu cuối 30-cps. Thực vậy, đầu ra ký tự hoàn toàn trừu tượng từ quan điểm của các ứng dụng. Các ứng dụng đơn giản truyền các chuỗi ký tự cho bộ giám sát, và bộ giám sát sẽ lo việc nạp bộ nhớ đệm, gửi các ký tự tới các thiết bị đầu cuối, và hoán đổi các ứng dụng vào và ra khỏi bộ nhớ.

Ranh giới này là phụ thuộc thông thường – nghĩa là sự phụ thuộc được trỏ theo hướng của luồng điều khiển. Các ứng dụng có các phụ thuộc vào bộ giám sát lúc biên dịch, và luồng điều khiển chuyển từ các ứng dụng tới bộ giám sát. Ranh giới này ngăn cho các ứng dụng biết về loại thiết bị mà đầu ra sẽ đi tới.

Ranh giới thứ hai là phụ thuộc được đảo ngược. Bộ giám sát có thể bắt đầu các ứng dụng, nhưng không có phụ thuộc lúc biên dịch nào vào chúng. Luồng điều khiển chuyển từ bộ giám sát tới các ứng dụng. Interface đa hình đảo ngược phụ thuộc đơn giản là thế này: Mọi ứng dụng được bắt đầu bằng cách nhảy tới chính xác cùng một địa chỉ bộ nhớ trong khu vực phủ chồng. Ranh giới này ngăn bộ giám sát khỏi biết bất cứ thứ gì về các ứng dụng ngoài điểm bắt đầu.

Cắt laser

Vào năm 1973, tôi gia nhập một công ty ở Chicago tên Teradyne Applied System (TAS). Đây là một chi nhánh của tập đoàn Teradyne, có trụ sở tại Boston. Sản phẩm của chúng tôi là một hệ thống sử dụng các tia laser công suất tương đối cao để cắt các bộ phận điện tử với độ chính xác rất cao.

Những ngày đó, các nhà sản xuất sẽ in lụa các linh kiện điện tử lên chất nền gốm. Các chất nền đó có kích thước1 inch vuông. Các bộ phận thường là điện trở – thiết bị để hạn chế dòng điện.

Trở kháng của điện trở phụ thuộc vào một số yếu tố, bao gồm thành phần và kích thước hình học của nó. Kích thước càng rộng thì điện trở của nó càng nhỏ.

Hệ thống của chúng tôi sẽ định vị chất nền gốm trong một dụng cụ có các đầu dò tiếp xúc với các điện trở. Hệ thống này sẽ đo đạc trở kháng của các điện trở, và sau đó dùng tia laser để đốt các phần của điện trở, làm cho nó ngày càng mỏng hơn cho đến khi nó đạt được giá trị điện trở mong muốn với dung sai khoảng 10%.

Chúng tôi đã bán hệ thống này cho các nhà sản xuất. Chúng tôi cũng sử dụng một số hệ thống nội bộ để cắt một số lô hàng tương đối nhỏ cho các nhà sản xuất nhỏ.

Chiếc máy tính này là một chiếc M365. Đây là thời kỳ nhiều công ty tự chế tạo máy tính: Teradyne đã chế tạo M365 và cung cấp nó cho tất cả các bộ phận của mình. M365 là một phiên bản nâng cao của PDP-8 – một máy tính mini phổ biến thời đó.

M365 điều khiển bảng định vị, nó di chuyển các tấm nền gốm dưới các đầu dò. Nó điều khiển hệ thống đo đạc và tia laser. Tia laser được định vị bằng các tấm gương X-Y có thể quay dưới sự điều khiển của chương trình. Chiếc máy tính này cũng có thể điều khiển cường độ của laser.

Môi trường phát triển của M365 tương đối thô sơ. Nó không có ổ đĩa. Thiết bị lưu trữ là các cuộn băng từ trông giống như những chiếc casset băng âm thanh 8-track. Các cuộn băng và ổ đĩa được chế tạo bởi Tri-Data.

Cũng giống như băng casset âm thanh 8-track thời đó, cuộn băng này được dẫn hướng theo một vòng lặp. Ổ đĩa chỉ di chuyển cuộn băng theo một hướng – không có tua lại! Nếu bạn muốn xác định vị trí của cuộn băng ngay từ đầu, bạn phải chuyển nó về phía trước cho tới “điểm nạp” của nó.

Cuộn băng di chuyển với tốc độ xấp xỉ 1 foot (khoảng 30cm) mỗi giây. Do vậy, nếu vòng lặp cuộn băng dài 25 feet, thì nó sẽ mất 25 giây để chuyển nó tới điểm nạp. Vì lý do này Tri-Data đã làm các cuộn băng có chiều dài khác nhau, từ 10 feet cho tới 100 feet.

M365 có một nút bấm phía trước có thể nạp bộ nhớ bằng một chương trình khởi động nhỏ và thực thi nó. Chương trình này sẽ đọc khối dữ liệu đầu tiên của băng từ, và thực thi nó. Thông thường khối này sẽ lưu một bộ nạp để nạp hệ điều hành ở trên phần còn lại của cuộn băng.

Hệ điều hành này sẽ nhắc người dùng nhập tên của một chương trình để chạy. Những chương trình này được lưu trên cuộn băng, ngay sau hệ điều hành. Chúng tôi sẽ gõ tên của chương trình – lấy ví dụ, ED-402 Editor – và hệ điều hành sẽ tìm cuộn băng có chương trình đó, nạp, và thực thi nó.

Console là một màn hình ASCII CRT phốt-pho xanh lá, rộng 72 ký tự[5], 24 dòng. Tất cả các ký tự đều viết hoa.

Để sửa một chương trình, bạn sẽ phải nạp trình soạn thảo ED-402 Editor, và sau đó đưa cuộn băng chứa mã nguồn của bạn. Bạn có thể đọc một khối mã nguồn trên cuộn băng vào bộ nhớ, và nó sẽ được hiển thị lên màn hình. Mỗi khối có thể lưu giữ 50 dòng code. Bạn thực hiện chỉnh sửa bằng cách di chuyển con trỏ quanh màn hình và gõ theo cách tương tự như vi[6]. Khi bạn hoàn thành, bạn sẽ ghi khối đó lên một cuộn băng khác, và đọc khối tiếp theo từ cuộn băng nguồn. Bạn tiếp tục làm như vậy cho đến khi hoàn thành công việc.

Không có khả năng quay trở lại các khối trước đó. Bạn sẽ phải chỉnh sửa chương trình của mình theo một đường thẳng, từ đầu đến cuối. Việc quay lại từ đầu buộc bạn phải hoàn thành việc sao chép mã nguồn vào cuộn băng đầu ra và sau đó bắt đầu một phiên chỉnh sửa mới trên băng đó. Có lẽ không có gì đáng ngạc nhiên, với những ràng buộc này, chúng tôi đã phải in các chương trình của mình ra giấy, đánh dấu tất cả các chỉnh sửa bằng tay bằng mực đỏ, và sau đó chỉnh sửa chương trình của chúng tôi theo từng khối bằng cách tham khảo các đánh dấu của chúng tôi trong danh sách này.

Khi chương trình đã được sửa xong, chúng tôi trở về hệ điều hành và gọi trình biên dịch hợp ngữ. Trình biên dịch hợp ngữ đọc cuộn băng mã nguồn, và ghi một cuộn băng nhị phân, trong khi đó cũng tạo ra một danh sách trên máy in dòng sản phẩm dữ liệu của chúng tôi.

Các cuộn băng không đáng tin cậy 100%, vì vậy chúng tôi luôn viết hai cuộn băng cùng một lúc. Bằng cách đó, ít nhất một trong số chúng có xác suất cao là không có lỗi.

Chương trình của chúng tôi có khoảng 20.000 dòng mã và mất gần 30 phút để biên dịch. Tỷ lệ chúng tôi gặp lỗi đọc băng thời kỳ đó là khoảng 1 trên 10. Nếu trình biên dịch hợp ngữ gặp lỗi băng, nó sẽ rung chuông trên bảng điều khiển và sau đó bắt đầu in một loạt lỗi trên máy in. Bạn có thể nghe thấy tiếng chuông kinh hoàng này khắp nơi trong phòng thí nghiệm. Bạn cũng có thể nghe thấy tiếng chửi rủa của một lập trình viên đáng thương, người vừa mới biết rằng quá trình biên dịch kéo dài 30 phút cần phải bắt đầu lại.

Kiến trúc này của chương trình là điển hình vào thời kỳ đó. Nó có một Chương Trình Điều hành Tổng thể (Master Operating Program), được gọi là “MOP”. Công việc của nó là quản lý các chức năng IO cơ bản và cung cấp các tính năng thô sơ của một “shell” console. Nhiều bộ phận của Teradyne đã chia sẻ mã nguồn MOP, nhưng mỗi bộ phận lại tạo một nhánh của mã nguồn này để sử dụng với mục đích riêng. Do đó, chúng tôi sẽ gửi các bản cập nhật mã nguồn cho nhau dưới dạng danh sách được đánh dấu mà sau đó chúng tôi sẽ tích hợp chúng lại theo cách thủ công (và rất cẩn thận).

Một layer tiện ích có mục đích đặc biệt là kiểm soát phần cứng đo đạc, bảng định vị và tia laser. Trong khi lớp tiện ích được gọi là MOP, MOP đã được sửa đổi riêng cho lớp đó và thường được gọi trở lại trong đó. Quả thực, chúng tôi không thực sự nghĩ về hai lớp này là các lớp riêng biệt. Đối với chúng tôi, đó chỉ là một số code mà chúng tôi đã thêm vào MOP theo một cách rất gắn kết.

Tiếp theo là layer cách ly. Lớp này cung cấp một interface máy ảo cho các chương trình ứng dụng, được viết bằng một ngôn ngữ hướng dữ liệu của một lĩnh vực cụ thể hoàn toàn khác (DSL). Ngôn ngữ này có các hoạt động để di chuyển tia laser, di chuyển bàn, tạo vết cắt, thực hiện các phép đo, v.v. Khách hàng của chúng tôi sẽ viết các chương trình ứng dụng cắt laser của họ bằng ngôn ngữ này và layer cách ly sẽ thực thi chúng.

Cách tiếp cận này không nhằm mục đích tạo ra một ngôn ngữ cắt laser độc lập với máy. Thật vậy, ngôn ngữ này có nhiều đặc điểm riêng được gắn kết sâu với các layer bên dưới. Thay vào đó, cách tiếp cận này đã cung cấp cho các lập trình viên ứng dụng một ngôn ngữ “đơn giản hơn” so với trình hợp dịch M356 để lập trình các công việc cắt của họ.

Các công việc cắt có thể được nạp từ băng và được thực thi bởi hệ thống này. Về cơ bản, hệ thống của chúng tôi là một hệ điều hành dành cho các ứng dụng cắt.

Hệ thống này được viết bằng trình biên dịch hợp ngữ M365 và được biên dịch trong một đơn vị biên dịch duy nhất để tạo ra mã nhị phân hoàn toàn.

Các ranh giới trong ứng dụng này đều rất mềm. Ngay cả ranh giới giữa mã hệ thống và các ứng dụng được viết bằng DSL cũng không được tuân thủ tốt. Sự gắn kết xuất hiện ở khắp mọi nơi.

Nhưng đó là điều bình thường của phần mềm vào đầu những năm 1970.

Theo dõi máy đúc nhôm

Vào giữa những năm 1970, trong khi OPEC đặt lệnh cấm vận đối với dầu mỏ và tình trạng thiếu xăng khiến các tài xế giận dữ đánh nhau tại các trạm xăng, thì tôi bắt đầu làm việc tại Outboard Marine Corporation (OMC). Đây là công ty mẹ của Johnson Motors và máy cắt cỏ Lawnboy.

OMC đã duy trì một cơ sở lớn ở Waukegan, Illinois, để tạo ra các bộ phận bằng nhôm đúc cho tất cả các động cơ và sản phẩm của công ty. Nhôm được nấu chảy trong các lò nung lớn, và sau đó được chở trong các thùng lớn đến hàng tá máy đúc nhôm hoạt động riêng lẻ. Mỗi chiếc máy đều có một nhân viên vận hành chịu trách nhiệm đặt khuôn, xoay máy và tách các chi tiết mới đúc. Những người vận hành này được trả tiền dựa trên số lượng chi tiết mà họ sản xuất được.

Tôi được thuê để làm việc trong một dự án tự động hóa cấp phân xưởng. OMC đã mua một máy IBM System/7 – đó là câu trả lời của IBM đối với máy tính mini. Họ gắn máy tính này vào tất cả các máy đúc trên phân xưởng để chúng tôi có thể đếm và căn thời gian, chu kỳ của mỗi máy. Nhiệm vụ của chúng tôi là thu thập tất cả thông tin đó và trình bày nó trên các màn hình xanh 3279.

Ngôn ngữ được sử dụng là hợp ngữ. Và, một lần nữa, toàn bộ mã được thực thi trong chiếc máy tính này đều là mã mà chúng tôi viết. Không có hệ điều hành, không có thư viện chương trình con và không có framework. Nó chỉ hoàn toàn là mã thô.

Nó cũng là mã thời gian thực điều khiển bởi ngắt. Mỗi chu kỳ của một máy đúc, chúng tôi phải cập nhật một loạt các thống kê và gửi các gói tin tới một chiếc máy tính IBM 370 vĩ đại trên-trời, nó đang chạy một chương trình CICS-COBOL trình bày những số liệu thống kê đó trên các màn hình màu xanh lá cây.

Tôi ghét công việc này. Ồ, tôi đã vậy ư. Ồ, công việc thật thú vị! Nhưng văn hóa thì… Chỉ cần nói rằng tôi bị buộc phải đeo cà vạt là đủ hiểu.

Ồ, tôi đã cố gắng. Tôi thực sự đã làm vậy. Nhưng rõ ràng tôi không hài lòng khi làm việc ở đó, và các đồng nghiệp của tôi biết điều đó. Họ biết điều đó vì tôi không thể nhớ những ngày quan trọng hoặc không thể dậy đủ sớm để tham dự các cuộc họp quan trọng. Đây là công việc lập trình duy nhất mà tôi từng bị sa thải — và tôi xứng đáng bị như vậy.

Từ quan điểm kiến ​​trúc, không có nhiều thứ để học ở đây ngoại trừ một điều. System/7 có một lệnh rất thú vị được gọi là thiết lập chương trình ngắt (Set Program InterruptSPI). Điều này cho phép bạn kích hoạt một ngắt của bộ xử lý, cho phép nó xử lý bất cứ ngắt nào có mức độ ưu tiên thấp hơn đang đợi khác. Ngày nay, trong Java, chúng tôi gọi nó là Thread.yield().


[1] Một trong những câu chuyện mà chúng tôi đã nghe về chiếc máy này tại ASC là nó được vận chuyển trên một chiếc xe tải se-mi rơ moóc lớn cùng với một đống đồ đạc phụ kiện. Trên đường đi, chiếc xe tải tông vào thành cầu với tốc độ cao. Chiếc máy tính này thì không sao, nhưng nó trượt về phía trước và nghiền nát mọi đồ đạc thành mảnh vụn.

[2] Ngày nay chúng ta nói rằng nó có clock rate là 142 kHz.

[3] Hãy tưởng tượng khối lượng của chiếc đĩa đó. Hãy tưởng tượng về động năng của nó! Một hôm, chúng tôi bước vào và thấy những mảnh vụn kim loại nhỏ rơi ra từ tay nắm tủ. Chúng tôi đã gọi cho người bảo trì đến kiểm tra. Anh ta khuyên chúng tôi nên đóng thiết bị này lại. Khi đến sửa chữa, anh ta nói rằng một trong những vòng bi đã bị mòn. Sau đó, anh ấy kể cho chúng tôi nghe câu chuyện về việc những chiếc đĩa này, nếu không được sửa chữa, nó có thể bị bung ra khỏi dây neo, cày xuyên tường bê tông và cắm thẳng vào ô tô trong bãi đậu xe.

[4] Cathode Ray Tube: Ống tia âm cực, đơn sắc, màn hình xanh lục, hiển thị ASCII.

[5] Con số 72 kỳ diệu đến từ các thẻ đục lỗ Hollerith, mỗi thẻ chứa 80 ký tự. 8 ký tự cuối cùng được “dành riêng” cho số thứ tự trong trường hợp bạn bỏ bộ thẻ.

[6] Chương trình soạn thảo văn bản phổ biến trên nền tảng Linux (người dịch)

Clean Architecture – Chương 34. Chương Bỏ Sót

Tất cả lời khuyên mà bạn đọc được cho tới nay chắc chắn sẽ giúp bạn thiết kế phần mềm tốt hơn, bao gồm các lớp và các component có ranh giới được xác định rõ, trách nhiệm rõ ràng, và các phụ thuộc được quản lý. Nhưng hóa ra điều khủng khiếp lại nằm trong các chi tiết lúc triển khai, và bạn thực sự sẽ dễ dàng rơi vào rào cản cuối cùng đó nếu bạn không suy nghĩ kỹ.

Chúng ta hãy tưởng tượng rằng chúng ta đang xây dựng một cửa hàng sách online, và một use case mà chúng ta được đề nghị để triển khai là về các khách hàng có thể xem tình trạng đơn hàng của họ. Mặc dù đây là một ví dụ Java, nhưng các nguyên lý cũng có thể áp dụng tương tự cho các ngôn ngữ lập trình khác. Lúc này, chúng ta hãy đặt Kiến Trúc Tinh Gọn sang một bên và nhìn vào số lượng phương pháp để thiết kế và tổ chức code.

Đóng gói bởi layer

Đầu tiên, và có lẽ là phương pháp thiết kế đơn giản nhất là kiến trúc phân lớp theo chiều ngang truyền thống, chúng ta tách biệt code dựa theo nó làm cái gì từ quan điểm kỹ thuật. Đây thường được gọi là “đóng gói bởi layer”. Hình 34.1 chỉ ra nó trông như thế nào dưới dạng biểu đồ lớp UML.

Trong kiến trúc phân lớp thông thường này, chúng ta có một layer cho code web, một layer cho “quy tắc nghiệp vụ” của chúng ta, và một layer để lưu trữ. Nói cách khác, code được chia theo chiều ngang thành những layer để nhóm những thứ tương tự nhau. Trong một “kiến trúc phân lớp chặt chẽ”, các layer chỉ được phụ thuộc vào layer thấp hơn kế tiếp. Trong Java, các layer thường được triển khai thành các gói (package). Như bạn có thể thấy trong Hình 34.1, tất cả phụ thuộc giữa các layer (các gói) đều hướng xuống. Trong ví dụ này, chúng ta có những loại Java sau:

  • OrdersController: Một web controller, thứ gì đó giống như một Spring MVC controller, dùng để xử lý các yêu cầu từ web.
  • OrdersService: Một interface định nghĩa “quy tắc nghiệp vụ” liên quan tới các đơn hàng.
  • OrdersServiceImpl: Triển khai của dịch vụ đặt hàng.[1]
  • OrdersRepository: Một interface định nghĩa cách chúng ta truy cập tới thông tin đơn hàng được lưu trữ.
  • JbdcOrdersRepository: Triển khai của interface OrdersRepository.
Hình 34‑1 Đóng gói bởi layer

Trong bài viết “Presentation Domain Data Layering[2], Martin Fowler nói rằng áp dụng một kiến trúc phân lớp là một phương pháp hay để khởi đầu. Không phải riêng ông ấy nghĩ vậy. Nhiều cuốn sách, hướng dẫn, các khóa đào tạo, và code mẫu mà bạn tìm thấy cũng sẽ chỉ cho bạn theo con đường tạo ra một kiến trúc phân lớp. Đó là một cách rất nhanh để giúp thứ bạn làm ra chạy được mà không quá phức tạp. Vấn đề như Martin đã chỉ ra là một khi phần mềm của bạn phát triển về độ lớn và độ phức tạp, thì bạn sẽ nhanh chóng thấy rằng ba component đó là không đủ, và bạn sẽ cần nghĩ cách module hóa nó hơn nữa.

Một vấn đề khác như tôi đã nói, kiến trúc phân lớp không hề đề cấp bất cứ thứ gì về lĩnh vực nghiệp vụ (business domain). Hãy đặt code cho hai kiến trúc phân lớp, từ hai lĩnh vực nghiệp vụ rất khác nhau, cạnh nhau và chúng có thể trông giống nhau một cách kỳ lạ: web, các dịch vụ, và các kho lưu trữ (repository). Ngoài ra còn có một vấn đề lớn khác với kiến trúc phân lớp, nhưng chúng ta sẽ bàn tới vấn đề đó sau.

Đóng gói bởi chức năng

Một lựa chọn khác để tổ chức code của bạn là sử dụng kiểu “đóng gói bởi chức năng”. Đây là một phương pháp cắt dọc, dựa trên các chức năng liên quan, các khái niệm lĩnh vực, hoặc các gốc tổng hợp (để sử dụng thuật ngữ thiết kế hướng lĩnh vực). Trong các triển khai thông thường mà tôi đã thấy, tất cả các kiểu được đặt trong một gói Java đơn, được đặt tên để phản ánh khái niệm đang được nhóm lại.

Với cách này, như thấy ở Hình 34.2, chúng ta có các interface và các lớp tương tự như trước, nhưng tất cả chúng được đặt trong một gói Java đơn thay vì chia thành ba gói. Đây là một cách refactor đơn giản từ kiểu “đóng gói bởi layer”, nhưng bây giờ cách tổ chức cấp cao nhất của code lại có thể nói lên được về lĩnh vực nghiệp vụ. Bây giờ chúng ta có thể biết rằng code base này có gì đó để làm với đơn hàng thay vì web, các dịch vụ, và các kho lưu trữ. Một lợi ích khác đó là có thể dễ dàng tìm thấy tất cả code mà bạn cần sửa đổi trong trường hợp cần thay đổi use case “xem đơn hàng”. Tất cả nằm trong một gói Java duy nhất thay vì dàn trải nhiều nơi[3].

Tôi thường thấy các đội phát triển phần mềm nhận ra rằng họ có các vấn đề với cách phân lớp ngang (“đóng gói bởi layer”) và thay vào đó chuyển sang cách phân lớp dọc (“đóng gói bởi chức năng”). Theo ý kiến của tôi, cả hai đều chưa tối ưu. Nếu bạn đã đọc cuốn sách này tới đây, bạn có thể đang nghĩ rằng chúng ta có thể làm tốt hơn nữa – và bạn đã đúng.

Hình 34‑2 Đóng gói bởi chức năng

Ports and adapters

Như tôi đã nói, các phương pháp như “ports và adapters”, “hexagonal architecture”, “boundaries, controllers, entities”.v.v. hướng tới việc tạo ra một kiến trúc có phần code tập trung vào nghiệp vụ/lĩnh vực được độc lập và tách biệt khỏi những chi tiết triển khai kỹ thuật như framework và cơ sở dữ liệu. Để tổng kết, bạn thường nhìn code base như vậy được cấu thành bởi một “bên trong” (lĩnh vực) và một “bên ngoài” (hạ tầng), như được đề nghị trong Hình 34.3.

Hình 34‑3 Một code base với một bên trong và một bên ngoài

Vùng “bên trong” bao gồm tất cả các khái niệm lĩnh vực, trong khi đó vùng “bên ngoài” bao gồm các tương tác với thế giới bên ngoài (ví dụ các UI, cơ sở dữ liệu, tích hợp với hãng thứ ba). Quy tắc chính ở đây đó là “bên ngoài” phụ thuộc vào “bên trong” – không bao giờ ngược lại. Hình 34.4 cho thấy một phiên bản về cách use case “xem đơn hàng” có thể được triển khai.

Gói com.mycompany.myapp.doamin ở đây là “bên trong”, và các gói khác là “bên ngoài”. Lưu ý về cách luồng phụ thuộc hướng vào “bên trong”. Người đọc để ý kỹ sẽ thấy OrdersRepository từ các biểu đồ trước đã được đổi tên thành Orders. Việc này là do phương pháp thiết kế hướng lĩnh vực, nó khuyến khích việc đặt tên mọi thứ ở “bên trong” theo “ngôn ngữ lĩnh vực thường gặp”. Nói cách khác, chúng ta nói về “đơn hàng” khi chúng ta có một cuộc thảo luận về lĩnh vực đó, chứ không phải là “kho chứa đơn hàng”.

Hình 34‑4 Use case Xem đơn hàng

Cũng cần chỉ ra rằng đây là phiên bản đơn giản hóa của biểu đồ lớp UML trông thế nào, bởi vì nó thiếu nhiều thứ như interactor và đối tượng để sắp xếp dữ liệu qua các ranh giới phụ thuộc.

Đóng gói bởi component

Mặc dù tôi hoàn toàn đồng ý với các cuộc thảo luận về SOLID, REP, CCP, và CRP và phần lớn các lời khuyên trong cuốn sách này, tôi đi tới một kết luận hơi khác về cách tổ chức code. Vì vậy tôi sẽ biểu diễn lựa chọn khác ở đây, tôi gọi nó là “đóng gói bởi component”. Để bạn được biết về một số kinh nghiệm của tôi, tôi đã dành phần lớn sự nghiệp của mình để xây dựng phần mềm doanh nghiệp, chủ yếu bằng Java, qua nhiều lĩnh vực nghiệp vụ khác nhau.

Những hệ thống phần mềm này cũng thay đổi rất đa dạng. Một lượng lớn là nền web, nhưng một số khác là dạng client-server (máy khách-máy chủ)[4], phân tán, message-based (nền gói tin), hoặc một số loại khác. Mặc dù các công nghệ khác nhau, nhưng nhìn chung kiến trúc của hầu hết các hệ thống phần mềm đó đều dựa trên kiến trúc phân lớp truyền thống.

Tôi đã từng đề cập tới tới các nguyên nhân tại sao kiến trúc phân lớp lại bị xem như tệ, nhưng đó không phải là toàn bộ câu truyện. Mục đích của một kiến trúc phân lớp là để tách biệt code có cùng chức năng. Các thứ về web được tách biệt khỏi phần quy tắc nghiệp vụ, còn quy tắc nghiệp vụ thì lại được tách biệt khỏi việc truy cập dữ liệu. Như chúng ta đã thấy từ biểu đồ lớp UML, từ góc độ triển khai, một layer thường tương đương với một Java package. Từ góc độ về khả năng truy cập code, để OrdersController có thể có một phụ thuộc vào interface OrdersService, interface OrdersService cần được đánh dấu là public, bởi vì chúng ở trong các gói khác nhau. Tương tự như vậy, interface OrdersRepository cần được đánh dấu là public để nó có thể được nhìn thấy từ bên ngoài của gói repository, bởi lớp OrdersServiceImpl.

Trong một kiến trúc phân lớp chặt chẽ, các mũi tên phụ thuộc luôn phải hướng xuống, với các layer chỉ phụ thuộc vào layer thấp hơn liền kề. Điều này tạo ra một đồ thị phụ thuộc không vòng lặp, sạch đẹp, đạt được bằng cách đưa ra một số quy tắc về cách các thành phần trong một code base phải phụ thuộc vào nhau. Vấn đề lớn ở đây là chúng ta có thể ăn gian bằng cách đưa vào một số phụ thuộc không mong muốn, để vẫn tạo ra được một biểu đồ phụ thuộc không vòng lặp đẹp đẽ.

Hãy xem như có ai đó mới gia nhập đội của bạn, và bạn đưa cho người mới đó một use case khác liên quan tới Orders để triển khai. Do là người mới, anh ta muốn tạo ấn tượng mạnh và triển khai use case này càng nhanh càng tốt. Sau khi ngồi xuống với một tách cà phê được vài phút, anh ta phát hiện ra lớp OrdersController đang có, vì vậy anh ta quyết định đó là nơi bắt đầu trang web liên quan tới Orders mới. Nhưng nó cần một số dữ liệu Orders từ cơ sở dữ liệu. Anh chàng mới nảy ra ý tưởng: “Ồ, có một interface OrdersRepository cũng đã có sẵn đây rồi. Ta có thể đơn giản dependency-inject triển khai này vào controller của ta. Hoàn hảo!”. Sau một vài phút nữa hacking, trang web đã có thể hoạt động. Nhưng biểu đồ UML kết quả sẽ trông như ở Hình 34.5.

Các mũi tên phụ thuộc vẫn trỏ xuống, nhưng OrdersController bây giờ thêm vào đó lại bỏ qua OrdersService ở một số use case. Cách tổ chức này thường được gọi là một kiến trúc phân lớp thoải mái (relaxed layered architecture), tại đó các layer được phép bỏ qua các lớp liền kề. Trong một số tình huống, đây là kết quả mong muốn – lấy ví dụ, nếu bạn đang có tuân theo mẫu thiết kế CQRS[5]. Trong nhiều trường hợp khác, việc nhảy qua layer quy tắc nghiệp vụ là điều không mong muốn, đặc biệt nếu quy tắc nghiệp vụ đó chịu trách nhiệm để đảm bảo việc truy xuất được ủy quyền tới từng bản ghi dữ liệu, lấy ví dụ.

Mặc dù use case mới hoạt động, nhưng có lẽ nó không được triển khai theo cách mà chúng ta mong muốn. Tôi thấy điều này xảy ra nhiều với các đội mà tôi đã từng tư vấn, và nó thường được tiết lộ khi các đội này bắt đầu hình dung code base của họ sẽ thực sự trông như thế nào, thường là lần đầu tiên.

Hình 34‑5 Kiến trúc phân lớp thoải mái

Điều chúng ta cần ở đây là một hướng dẫn – một nguyên lý kiến trúc – nói thứ gì đó đại loại như “Web controller không bao giờ được truy cập repository trực tiếp.” Dĩ nhiên, câu hỏi ở đây là sự tuân thủ. Nhiều đội tôi đã từng gặp đơn giản nói rằng “Chúng tôi tuân thủ nguyên lý này thông qua kỷ luật tốt và các buổi review code, bởi vì chúng tôi tin tưởng các lập trình viên của chúng tôi.” Sự tự tin này nghe thì rất hay, nhưng tất cả chúng ta đều biết điều gì sẽ xảy ra khi ngân sách và deadline bắt đầu lờ mờ đến gần.

Một số lượng nhỏ hơn nhiều các đội phát triển đã nói với tôi rằng họ dùng các công cụ phân tích thống kê (như Ndpend, Structure101, Checkstyle) để kiểm tra và tự động ép buộc các vi phạm kiến trúc trong khi build. Bản thân bạn có thể nhìn thấy những quy tắc này; chúng thường được biểu diễn dưới dạng regular expression hoặc các chuỗi ký tự wildcard cho biết “các loại trong package **/web không được chấp nhận các loại trong **/data; và chúng được thực thi sau bước biên dịch.

Cách tiếp cận này hơi thô nhưng nó có thể thực hiện được công việc này, báo cáo các vi phạm của các nguyên lý kiến trúc mà bạn đã định nghĩa khi đội phát triển và bạn build không thành công. Vấn đề với cả hai cách tiếp cận này là chúng đều có thể sai, và vòng lặp phản hồi dài hơn bình thường. Nếu không được dùng cẩn thận, phương pháp này có thể biến code base thành một “quả bóng bùn lớn[6]”. Cá nhân tôi thích dùng trình biên dịch để ép buộc kiến trúc nếu có thể.

Điều này mang tới cho chúng ta lựa chọn “đóng gói bởi component”. Nó là một cách tiếp cận lai mọi thứ mà chúng ta đã thấy cho đến nay, với mục tiêu là gộp tất cả các trách nhiệm liên quan thành một component thô duy nhất trong một package Java. Đó là về việc lấy cái nhìn dịch vụ làm trung tâm của một hệ thống phần mềm, đó cũng là điều mà chúng ta đang thấy với kiến trúc micro service. Cũng giống như cách mà kiến trúc “ports and apdaters” coi web chỉ như một cơ chế truyền thông tin khác, “đóng gói bởi component” giúp cho giao diện người dùng tách biệt khỏi các component thô này. Hình 34.6 chỉ ra use case “xem đơn hàng” có thể trông như thế nào.

Về bản chất, phương pháp này gộp “quy tắc nghiệp vụ” và code bền vững thành một thứ duy nhất, mà tôi gọi đó là một “component”. Tôi đã trình bày định nghĩa về “component” trước đó trong cuốn sách, nói rằng:

Component là một đơn vị triển khai. Chúng là thực thể nhỏ nhất mà có thể được triển khai như là một phần của hệ thống. Trong Java, chúng là các file jar.

Hình 34‑6 Use case Xem đơn hàng

Định nghĩa của tôi về một component có khác một chút: “Đó là một nhóm các chức năng có quan hệ với nhau phía sau một interface sạch đẹp, được nằm bên trong một môi trường thực thi như là một ứng dụng.” Định nghĩa này đến từ “mô hình kiến trúc phần mềm C4[7]” của tôi, đó là một cách phân cấp đơn giản để nghĩ về các cấu trúc tĩnh của một hệ thống phần mềm dưới dạng các container, component, và class (hoặc code). Nó nói rằng một hệ thống phần mềm được làm từ một hoặc nhiều container (ví dụ như các ứng dụng web, ứng dụng di động, ứng dụng độc lập, cơ sở dữ liệu, hệ thống file), mỗi hệ thống chứa một hoặc nhiều component, đến lượt những component lại được triển khai bởi một hoặc nhiều class (hoặc code). Liệu mỗi component nằm trong một file jar riêng biệt là một vấn đề trực giao không?

Một lợi ích chính của phương pháp “đóng gói bởi component” là nếu bạn đang viết code mà cần làm điều gì đó với Orders, thì chỉ có một nơi duy nhất để tới đó là OrdersComponent. Bên trong component này, việc tách biệt các vấn đề vẫn được duy trì, vì vậy quy tắc nghiệp vụ được tách biệt khỏi dữ liệu lưu trữ, nhưng đó là một chi tiết triển khai component mà người sử dụng không cần thiết biết tới. Điều này na ná với thứ bạn có thể gặp nếu bạn sử dụng các micro-service hoặc Kiến Trúc Hướng Dịch Vụ – một OrdersService tách biệt mà đóng gói mọi thứ liên quan tới việc xử lý các đơn hàng. Điểm khác biệt chính là chế độ tách rời. Bạn có thể nghĩ component được định nghĩa tốt trong một ứng dụng nguyên khối là một bước đi vững chắc sang kiến trúc micro-service.

Điều khủng khiếp là ở các chi tiết triển khai

Bốn cách tiếp cận đều trông giống như những cách khác nhau để tổ chức code và do đó, có thể được coi là những phong cách kiến trúc khác nhau. Tuy nhiên, cảm nhận này bắt đầu mất đi rất nhanh nếu bạn có các chi tiết triển khai sai.

Một điều tôi thường thấy là việc sử dụng quá tự do câu lệnh public trong các ngôn ngữ như Java. Hầu như chúng ta, các lập trình viên, theo bản năng sử dụng từ khóa public mà không cần suy nghĩ nhiều. Nó nằm trong bộ nhớ cơ (muscle memory) của chúng ta. Nếu bạn không tin tôi, hãy nhìn vào các mẫu code của các cuốn sách, hướng dẫn, và các framework mã nguồn mở trên GitHub. Xu hướng này là rất rõ ràng, bất kể phong cách kiến trúc nào mà một code base hướng tới áp dụng – các layer ngang, các layer dọc, ports and adapters, hoặc thứ gì đó khác. Việc đánh dấu tất cả các loại của bạn là public nghĩa là bạn không tận dụng ưu điểm của các tiện ích mà ngôn ngữ lập trình của bạn cung cấp cho khả năng đóng gói. Trong một số trường hợp, không có gì ngăn cản ai đó viết code để khởi tạo một lớp triển khai cụ thể một cách trực tiếp, vi phạm phong cách kiến trúc dự định.

Tổ chức Vs Đóng gói

Nhìn vào vấn đề này theo cách khác, nếu bạn để tất cả các loại trong ứng dụng Java của bạn là public, thì các package đơn giản chỉ là một cơ chế tổ chức (một nhóm, giống như các thư mục), hơn là được dùng để đóng gói. Do các loại public có thể sử dụng ở bất cứ nơi nào trong code base, nên bạn có thể loại bỏ các package cho gọn bởi vì chúng cung cấp rất ít giá trị thực tế. Kết quả là nếu bạn bỏ qua các package (bởi vì chúng không cung cấp bất cứ ý nghĩa gì về mặt đóng gói và ẩn giấu), thì loại kiến trúc bạn muốn tạo ra không còn thực sự quan trọng. Nếu bạn nhìn lại vào các biều đồ UML ví dụ, các package Java trở thành một chi tiết không liên quan nếu tất các loại đều được đánh dấu là public. Về bản chất, cả bốn phương pháp kiến trúc được trình bày trước đó trong chương này chính xác là giống nhau khi chúng ta lạm dụng từ khóa public (Hình 34.7).

Hãy xem kỹ những mũi tên nằm giữa mỗi loại trong Hình 34.7: Tất cả chúng đều giống nhau bất kể bạn đang cố gắng áp dụng phương pháp kiến trúc nào. Về lý thuyết các phương pháp này rất khác nhau, nhưng về mặt cú pháp thì chúng giống hệt nhau. Hơn nữa, bạn có thể tranh luận rằng khi bạn để cho tất cả các loại là public, thì thứ mà bạn thực sự có chỉ là bốn cách để mô tả một kiến trúc phân lớp theo chiều ngang truyền thống. Đây là một thủ thuật khéo léo, và dĩ nhiên không ai lại để tất cả các kiểu Java của họ là public. Trừ khi họ làm vậy. Và tôi đã từng thấy điều đó.

Các từ khóa truy cập (access modifier) trong Java không phải hoàn hảo[8], nhưng bỏ qua chúng chỉ khiến chúng ta gặp rắc rối. Cách mà các loại Java được đặt trong các package thực tế có thể tạo ra một sự khác biệt lớn tới cách các loại có thể truy cập được (hoặc không thể truy cập được) khi các từ khóa truy cập của Java được áp dụng một cách thích hợp. Nếu tôi mang các package này trở lại và đánh dấu (bằng cách làm mờ đồ họa) những kiểu mà từ khóa truy cập có thể làm cho nó trở nên hạn chế hơn, thì bức tranh sẽ trở nên khá thú vị (Hình 34.8).

Hình 34‑7 Cả bốn phương pháp kiến trúc đều giống nhau

Di chuyển từ trái sang phải, trong phương pháp “đóng gói bởi layer”, các interface OrdersService OrdersRepository cần đặt là public, bởi vì chúng truyền các phụ thuộc từ các lớp bên ngoài của package xác định của chúng. Ngược lại, các lớp triển khai (OrdersServiceImpl JdbcOrdersRepository) có thể được làm hạn chế hơn (package protected). Không ai cần biết về chúng; chúng là một chi tiết triển khai.

Trong phương pháp “đóng gói bởi chức năng”, OrdersController cung cấp một điểm vào duy nhất bên trong package đó, vì vậy những thứ khác có thể được đặt là package protected. Một cảnh báo lớn ở đây đó là không gì khác trong code base, bên ngoài package này, có thể truy cập thông tin liên quan tới Orders trừ khi chúng đi qua controller đó. Điều này có thể là điều mong muốn hoặc không.

Trong phương pháp “ports and adpaters”, các interface OrdersService Orders truyền các phụ thuộc từ các package khác, vì vậy chúng cần được đặt là public. Một lần nữa, các lớp triển khai có thể được đạt là package protected và được chèn phụ thuộc lúc runtime.

Hình 34‑8 Các loại màu xám là nơi mà từ khóa truy cập có thể làm cho nó trở nên hạn chế hơn

Cuối cùng, trong phương pháp “đóng gói bởi component”, interface OrdersComponent có một phụ thuộc được truyền từ controller, còn mọi thứ khác có thể được đặt là package protected. Bạn càng có ít loại public, thì bạn càng có ít số lượng phụ thuộc. Bây giờ không có cách nào[9] mà code bên ngoài package đó có thể dùng interface hoặc triển khai OrdersRepository một cách trực tiếp, vì vậy chúng ta có thể dựa vào trình biên dịch để cưỡng ép nguyên lý kiến trúc này. Bạn có thể làm điều tương tự trong .NET với từ khóa internal, mặc dù bạn sẽ cần tạo ra một cụm riêng đối với mỗi component.

Để cho rõ ràng hoàn toàn, những gì mà tôi mô tả ở đây liên quan tới một ứng dụng nguyên khối, nơi tất cả code đều nằm trong một cây mã nguồn duy nhất. Nếu bạn đang xây dựng một ứng dụng như vậy (và nhiều người khác cũng vậy), thì tôi chắc chắn sẽ khuyến khích bạn nghiêng về phía sử dụng trình biên dịch để ép buộc các nguyên lý kiến trúc của bạn, hơn là dựa vào kỷ luật tự giác và các công cụ hậu biên dịch.

Các chế độ tách rời khác

Ngoài ngôn ngữ lập trình mà bạn đang sử dụng, thường có những cách khác để bạn có thể tách rời sự phụ thuộc mã nguồn của bạn. Với Java, bạn có các framework như OSGi và hệ thống module Java 9 mới. Với các hệ thống module, nếu được dùng đúng, thì bạn có thể tạo ra một sự phân biệt giữa các loại là public và các loại là published. Lấy ví dụ, bạn có thể tạo ra một module Orders có tất cả các loại được đánh dấu là public, nhưng lại chỉ phát hành một tập nhỏ trong những loại này để sử dụng bên ngoài. Còn lâu mới đến lúc đó, nhưng tôi rất háo hức về việc hệ thống module Java 9 sẽ cung cấp cho chúng ta một công cụ khác để xây dựng phần mềm tốt hơn, và một lần nữa khơi dậy sự quan tâm của mọi người về tư duy thiết kế.

Một tùy chọn khác là tách rời các phụ thuộc của bạn ở cấp độ mã nguồn, bằng cách phân chia code trên các cây mã nguồn khác nhau. Nếu chúng ta lấy ví dụ “ports and adapters”, chúng ta có thể có ba cây mã nguồn:

  • Mã nguồn dành cho nghiệp vụ và lĩnh vực (mọi thứ độc lập với việc lựa chọn framework và công nghệ): OrdersService, OrdersServiceImpl, Orders.
  • Mã nguồn dành cho web: OrdersControllers.
  • Mã nguồn dành cho lưu trữ dữ liệu: JdbcOrdersRepository.

Hai cây mã nguồn sau có phụ thuộc lúc biên dịch vào code nghiệp vụ và lĩnh vục, thứ mà bản thân không biết điều gì về code web hoặc lưu trữ dữ liệu. Từ quan điểm triển khai, bạn có thể làm việc này bằng cách cấu hình các module riêng biệt hoặc các dự án trong công cụ build của bạn (như Maven, Gradle, MSBuild). Tốt nhất là bạn nên lặp lại mẫu thiết kế này, có một cây mã nguồn tách riêng cho mỗi loại và mỗi component trong ứng dụng của bạn. Tuy nhiên, đấy là một giải pháp lý tưởng hóa, bởi vì có những vấn đề về hiệu suất, độ phức tạp, và các vấn đề bảo trì liên quan tới việc chia nhỏ mã nguồn của bạn theo cách này.

Một phương pháp đơn giản hơn mà một số người sử dụng cho code “ports and adapters” của họ là chỉ có hai cây mã nguồn:

  • Code lĩnh vực (phần “bên trong”)
  • Code hạ tầng (phần “bên ngoài”)

Điều này phản ánh một cách đẹp đẽ trên biểu đồ (Hình 34.9) mà nhiều người sử dụng để tổng kết kiến trúc “ports and adapters”, và có một phụ thuộc lúc biên dịch từ phần hạ tầng tới phần lĩnh vực.

Hình 34‑9 Code lĩnh vực và hạ tầng

Phương pháp để tổ chức mã nguồn này cũng hiệu quả, nhưng hãy cẩn thận đến những đánh đổi có thể xảy ra. Đó là thứ mà tôi gọi là “mẫu thiết kế xấu Périphérique của ports and adapters.” Thành phố Paris, nước Pháp, có một con đường vành đai được gọi là Đại lộ Périphérique, nó cho phép bạn đi vòng quanh Paris mà không cần đi vào những khu phức hợp của thành phố. Việc có tất cả code hạ tầng của bạn trong một cây mã nguồn duy nhất nghĩa là có thể xảy ra khả năng code hạ tầng trong một khu vực của ứng dụng của bạn (ví dụ như web controller) có thể gọi trực tiếp code trong vùng khác của ứng dụng của bạn (ví dụ như kho cơ sở dữ liệu), mà không cần đi qua phần lĩnh vực. Điều này đặc biệt đúng nếu bạn quên áp dụng các từ khóa truy cập thích hợp cho phần code đó.

Kết luận: Lời khuyên bỏ sót

Điểm chính của chương này là để nhấn mạnh rằng những ý định thiết kế tốt nhất của bạn có thể bị hủy hoại trong nháy máy nếu bạn không xem xét đến tính phức tạp của chiến thuật triển khai. Hãy nghĩ về việc chuyển thiết kế mong muốn của bạn thành các cấu trúc code như thế nào, cách để tổ chức code, và chế độ tách rời nào sẽ được áp dụng trong thời gian chạy runtime và trong thời gian biên dịch. Hãy để các lựa chọn mở nếu có thể nhưng hãy thực dụng, và tính đến cả kích thước đội phát triển, trình độ kỹ năng của họ, và độ phức tạp của giải pháp đi kèm với những giới hạn về thời gian và ngân sách của bạn. Hãy nghĩ về việc sử dụng trình biên dịch của bạn để giúp bạn ép buộc kiểu kiến trúc được lựa chọn, và đề phòng sự gắn kết xảy ra trong các khu vực khác, ví dụ như các mô hình dữ liệu. Điều khủng khiếp thật sự nằm trong các chi tiết lúc triển khai.


[1] Người ta có thể cho rằng đây là một cách kinh khủng để đặt tên một lớp, nhưng như chúng ta sẽ thấy sau này, có lẽ nó cũng không quan trọng lắm.

[2] https://martinfowler.com/bliki/PresentationDomainDataLayering.html

[3] Lợi ích này ít liên quan hơn nhiều đến các công cụ điều hướng của IDE hiện đại, nhưng có vẻ như đã có một thời kỳ phục hưng quay trở lại các trình soạn thảo văn bản gọn nhẹ, vì những lý do mà tôi rõ ràng là quá già để có thể hiểu được.

[4] Công việc đầu tiên của tôi sau khi tốt nghiệp đại học năm 1996 là xây dựng các ứng dụng máy tính để bàn máy khách-máy chủ với công nghệ gọi là PowerBuilder, một 4GL siêu hiệu quả, vượt trội trong việc xây dựng các ứng dụng dựa trên cơ sở dữ liệu. Vài năm sau, tôi xây dựng các ứng dụng máy khách-máy chủ bằng Java, ở đó chúng tôi phải tự xây dựng bộ kết nối cơ sở dữ liệu của riêng mình (đây là tiền thân của JDBC) và bộ công cụ GUI của riêng chúng tôi trên AWT. Đó là “tiến bộ” đối với bạn!

[5] Trong mẫu thiết kế Command Query Responsibiltiy Segregation, bạn có các mẫu riêng dành cho việc cập nhật và đọc dữ liệu.

[6] http://www.laputan.org/mud/

[7] Xem https://www.structurizr.com/help/c4 để biết thêm chi tiết.

[8] Lấy ví dụ trong Java, mặc dù chúng ta có xu hướng coi các package là dạng phân cấp, nhưng chúng ta không thể tạo ra các hạn chế truy cập dự trên mối quan hệ giữa package và subpackage. Bất cứ cấu trúc phân cấp nào mà bạn tạo ra chỉ nằm trên tên của những package đó, và cấu trúc thư mục trên ổ đĩa.

[9] Trừ khi bạn ăn gian và dùng cơ chế reflection của Java, nhưng xin hãy đừng làm như vậy!