node seneca
處理新需求而無需重構 (Handling new requirements without refactoring)
Part 1 of this series talked about defining and calling microservices using Seneca. A handful of services were created to return all legal moves of a lone chess piece on a chessboard. The series continues in Part 3.
本系列的第1部分討論了使用Seneca定義和調用微服務。 創建了一些服務,以返回棋盤上一個單獨的棋子的所有合法舉動。 該系列在第3部分中繼續。
快速回顧: (A quick review:)
Seneca services are identified by a pattern consisting of
role
andcmd
properties. Additional properties can be added to the pattern as well.Seneca服務由包含
role
和cmd
屬性的模式標識。 附加屬性也可以添加到模式中。
this.add({role: "movement",cmd: "legalMoves" //, otherProp: value, ...}, (msg, reply) => {...}
Services also have an implementation that takes a
msg
object and a reply callback. Themsg
object contains the pattern properties in addition to all other data sent to the service.服務還具有一個接受
msg
對象和回復回調的實現。msg
對象除了發送到該服務的所有其他數據之外,還包含模式屬性。Seneca.act()
is used to indirectly invoke a service. Theact
method takes an object and a callback function. The object contains therole
,cmd
, and other properties that comprise the message to the service.Seneca.act()
用于間接調用服務。act
方法帶有一個對象和一個回調函數。 該對象包含role
,cmd
和其他屬性,這些屬性構成向服務發送的消息。
seneca.act({role: "movement",cmd: "legalMoves",piece: p,board: board}, (err, msg) => {
When an action could be handled by more than one service that matches the pattern, the service with the most specific pattern match will be invoked.
當一個動作可以由多個與模式匹配的服務來處理時,具有最特定模式匹配的服務將被調用。
There were a handful of services defined in the first part of this series. One of threerawMoves
services took a piece and its position as parameters and returned 15 x 15 movement mask. These were truncated to an 8 x 8 board using alegalSquares
service. The result was that the services together can return all the legal moves of any piece on any legal square of the otherwise empty chessboard.
在本系列的第一部分中定義了一些服務。 三個rawMoves
服務之一將一塊及其位置作為參數,并返回15 x 15移動蒙版。 使用legalSquares
服務將它們截斷成8 x 8的木板。 結果是這些服務可以一起返回原本空的棋盤的任何合法廣場上的任何棋子的所有合法移動。
微服務和技術債務 (Microservices and Technical Debt)
One of the motivations for microservices is to reduce technical debt. Every project has deadlines and, as they loom larger, expediency often trumps quality. FIXME and TODO comments litter the source code after a while. Those comments identify technical debt that “someday” will be taken care of.
微服務的動機之一是減少技術債務 。 每個項目都有最后期限,并且隨著期限的增加,權宜之計往往比質量重要。 FIXME和TODO注釋過了一會兒會亂碼源代碼。 這些評論指出了“某天”將得到解決的技術債務。
總有一天永遠不會來 (Someday never comes)
Microservices focus on functional decomposition and loose coupling. Neither of those are new ideas, but it is a rethinking about how to implement those concepts. A microservice should be small, single-purposed, and extensible. Extending a service can happen with few or no side-effects. A new service can extend an existing service, and neither the old service nor the client that once called it will know the service implementation changed. Less refactoring of classes, methods, method signatures, process flow… all this makes it easier to deal with dreaded TD.
微服務專注于功能分解和松散耦合。 這些都不是新想法,但這是對如何實現這些概念的重新思??考。 微服務應小型,單一用途且可擴展。 擴展服務幾乎沒有副作用。 新服務可以擴展現有服務,并且舊服務或曾經調用它的客戶端都不會知道服務實現已更改。 更少的類,方法,方法簽名,過程流的重構……所有這些使得處理可怕的TD更容易。
回到游戲進行中… (Back to the game in progress…)
Moving a single chess piece around a lonely board is not really all that entertaining. In a real chess game the chessboard is shared with friendly and hostile pieces, and these impact each other’s movement.
在一個孤獨的棋盤上移動單個棋子并不是真正有趣的事情。 在真正的國際象棋游戲中,國際象棋棋盤與友善而敵對的棋子共享在一起,這些棋子相互影響著對方的動作。
Right now I have alegalSquares
service which can be the basis of a more completelegalMoves
service. If you recall, the legalSquares
service would invoke a rawMoves
service, then remove all the ‘bad’ squares that didn’t belong on a chessboard.
現在,我有一個legalSquares
服務,可以作為更完整的legalMoves
服務的基礎。 如果您還記得的話, legalSquares
服務將調用rawMoves
服務,然后刪除所有不屬于棋盤的“壞”正方形。
The new legalMoves
service will take into account other pieces, something that legalSquares
didn’t. This requires an extra parameter, one called board
. The board
is just going to be an array of ChessPiece instances, and will assume that the pieces on the board have already been checked for validity. For instance, two pieces don’t occupy the same square, pawns aren’t on the first rank, kings aren’t be next to each other, and so forth.
新的legalMoves
服務將考慮其他部分,而legalSquares
沒有。 這需要一個額外的參數,稱為board
。 board
將只是一個ChessPiece實例的數組,并將假定棋盤上的棋子已經過有效性檢查。 例如,兩個棋子不在同一廣場上,棋子不在第一位,國王不在彼此之間,依此類推。
The following pattern will identify the service:
以下模式將標識服務:
'role: movement;cmd: legalMoves'
This pattern is a stringified version of JSON called jsonic; you can use a regular JSON object if you prefer. The message to the service will contain the pattern. It will also contain a ChessPiece instance that has a piece type such as ‘K’ing, ‘Q’ueen, ‘R’ook and board position (see algebraic notation). Later I’ll add to this class a piece color (White or Black) so that the service can tell friend from foe. But for now the service will assume all pieces are friendly.
此模式是JSON的字符串化版本,稱為jsonic ; 您可以根據需要使用常規JSON對象。 發送給服務的消息將包含該模式。 它還將包含一個ChessPiece實例,其實例類型為“ K”,“ Q”,“ R”和棋盤位置(參見代數表示法)。 稍后,我將在該類中添加一塊顏色(白色或黑色),以便該服務可以將敵人告訴朋友。 但就目前而言,該服務將假定所有部件都友好。
Since a friendly piece cannot be captured, it will restrict movement of other friendly pieces. Determining those restrictions is a bit of work. I made it harder for myself in the implementation of the rawMoves
service… which brings me to:
由于無法捕獲一個友善的棋子,它將限制其他友善棋子的移動。 確定這些限制需要一些工作。 在執行rawMoves
服務時,我自己變得更加困難……這使我從事以下工作:
微服務不是萬能藥 (Microservices are not a Panacea)
If you design a service that retrieves or calculates information and doesn’t pass that data on up the chain, some service upstream may have to redo that work later. In my example, rawMoves
returned an array of move objects (file and rank positions on the board). Let’s take the method that generates diagonal moves for a piece using the rawMoves
service:
如果您設計的服務可以檢索或計算信息, 但不會在鏈上傳遞該數據,則上游的某些服務可能必須稍后重做。 在我的示例中, rawMoves
返回了一組移動對象(板上的文件和排名位置)。 讓我們采用使用rawMoves
服務為一塊生成對角線移動的方法:
module.exports = function diagonal(position, range = 7) {var moves = [];const cFile = position.file.charCodeAt()const cRank = position.rank.charCodeAt();for (var i = 1; i < range + 1; i++) {moves.push({file: String.fromCharCode(cFile - i),rank: String.fromCharCode(cRank - i)});moves.push({file: String.fromCharCode(cFile + i),rank: String.fromCharCode(cRank + i)});moves.push({file: String.fromCharCode(cFile - i),rank: String.fromCharCode(cRank + i)});moves.push({file: String.fromCharCode(cFile + i),rank: String.fromCharCode(cRank - i)});}return moves;
}
At first glance, there’s nothing wrong with this. But, those fourmove.push
operations actually operate along movement vectors. I could have constructed four movement vectors, then returned a list of moves by concatenating them, like so:
乍看之下,這沒有什么錯。 但是,這四個move.push
操作實際上是沿著運動矢量進行操作的。 我本可以構造四個運動矢量,然后通過將它們連接起來返回一個運動列表,如下所示:
function diagonalMoves(position, range) {var vectors = [[], [], [], []];const cFile = position.file.charCodeAt()const cRank = position.rank.charCodeAt();for (var i = 1; i < range + 1; i++) {vectors[0].push({file: String.fromCharCode(cFile - i),rank: String.fromCharCode(cRank - i)});vectors[1].push({file: String.fromCharCode(cFile + i),rank: String.fromCharCode(cRank + i)});vectors[2].push({file: String.fromCharCode(cFile - i),rank: String.fromCharCode(cRank + i)});vectors[3].push({file: String.fromCharCode(cFile + i),rank: String.fromCharCode(cRank - i)});}const moves = Array.prototype.concat(...vectors)return moves;
}
As it stood, there was no point in doing this. But later on those vectors would have come in handy for truncating movements along diagonals (or ranks or files) when a friendly piece is in the way. Instead, I had to decompose the move list along vectors in services upstream — more work and inefficiency which you will see later.
就目前而言,這樣做是沒有意義的。 但是后來,當友好片段出現時,這些矢量將可以方便地沿對角線(或等級或文件)截斷運動。 取而代之的是,我不得不沿著上游服務中的向量分解移動列表,這會增加工作量和效率,這將在以后看到。
The real flaw, though, was that I returned an array, rather than a data object. Data objects have properties that are extendable, not so arrays. As a consequence, all my upstream services depend on receiving a movement array, and only a movement array. No flexibility. I can’t now add a list of movement vectors in addition to a move list. But I could if I had returned an object from this method and the service that called it instead.
但是,真正的缺陷是我返回了一個數組,而不是一個數據對象。 數據對象具有可擴展的屬性,而不是數組。 結果,我所有的上游服務都依賴于接收移動數組, 并且只有一個運動數組。 沒有靈活性。 我現在不能添加除了運動向量的列表 到移動列表。 但是我可以從該方法和調用它的服務中返回一個對象。
Lesson learned? Consider returning data objects from your services. Have your upstream services work on parts of the data, but pass all data they receive back upstream. Exceptions to this rule will abound, of course.
學過的知識? 考慮從服務中返回數據對象。 讓上游服務處理部分數據,但將它們接收的所有數據傳回上游。 當然,會有很多例外。
和像這樣的朋友一起... (With Friends like These…)
In Part 1, there was a service under the pattern:
在第1部分中,使用以下模式提供服務:
role:"movement",cmd:"legalSquares"
role:"movement",cmd:"legalSquares"
It returned all moves of an unimpeded piece. Since this will be the base service for determining legal moves on a populated chessboard, I’ll rename the cmd
to legalMoves
. Now I want to extend that to take into account friendly pieces that might be blocking a path of my chosen piece.
它返回了一塊暢通無阻的動作。 由于這將是確定填充棋盤上法律動作的基本服務,因此我將cmd
重命名為legalMoves
。 現在,我想擴展該范圍,以考慮到可能會阻礙我選擇的作品路徑的友好作品。
擴展服務 (The extended service)
The service that extends role:"movement",cmd:"legalMoves"
is… role:"movement",cmd:"legalMoves"
!
擴展role:"movement",cmd:"legalMoves"
是… role:"movement",cmd:"legalMoves"
!
Yep, it has the same service pattern as the service it calls. You may recall that services are identified by pattern, and so how it this going to work? When the program acts on role:"movement",cmd:"legalMoves"
, it will use the most recently defined service. But the new service has to call the formerlegalMoves
service. That can be solved easily:
是的,它具有與其調用的服務相同的服務模式。 您可能還記得,服務是通過模式來標識的,那么它如何工作? 當程序執行role:"movement",cmd:"legalMoves"
,它將使用最新定義的服務。 但是新服務必須調用以前的legalMoves
服務。 這很容易解決:
this.add({role: "movement",cmd: "legalMoves"}, (msg, reply) => {//returns unimpeded moves}this.add('role:movement,cmd:legalMoves', function (msg, reply) {this.
prior(msg, function (err, moves) {if (msg.board) {const boardMoves = legalMovesWithBoard(msg, moves);reply(err, boardMoves);return;}reply(err, moves);});});
This new service is able to call the former service by using the prior()
method in Seneca. If no board
parameter is supplied in the incoming msg
object, then this service will just act as a pass-thru to the former service. But what if there is a board?
通過使用Seneca中的Priority prior()
方法,此新服務可以調用以前的服務。 如果傳入的msg
對象中未提供board
參數,則此服務將msg
當前一個服務的直通。 但是,如果有董事會呢?
I’m not going to show a complete code listing here (see link below), but the gist of it is:
我不會在此處顯示完整的代碼清單(請參見下面的鏈接),但是要點是:
module.exports = function (msg, moves) {if (!msg.board) return moves;const blockers = moves.filter(m => {return (msg.board.pieceAt(m))})var newMoves = [];const pp = msg.piece.position;const rangeChecks = {B: diagonalChecks,R: rankAndFileChecks,K: panopticonChecks,Q: panopticonChecks,P: pawnChecks,N: knightChecks};var rangeCheck = rangeChecks[msg.piece.piece];// console.error(msg.piece.piece, rangeCheck.name)newMoves = moves.filter(m => {return rangeCheck(m, blockers, pp);})return newMoves;
}
Remember our old friend diagonalMoves
from the rawMoves
service? In order to do a range check on diagonals without handy vectors, the new legalMoves
service calls this:
還記得我們來自rawMoves
服務的老朋友diagonalMoves
rawMoves
嗎? 為了對不帶方便向量的對角線進行范圍檢查,新的legalMoves
服務將其稱為:
Ugly, no? I’d be happy if some algorithmically-inclined reader reduced this to two lines in the comments section. Three, even.
丑陋,不是嗎? 如果某些有算法傾向的讀者在評論部分將其減少到兩行,我將感到高興。 三,甚至。
So that takes care of friendly pieces. The next installment will deal with hostile pieces, which can be captured.
這樣就可以處理友好的片段。 下一部分將處理可捕獲的敵對碎片。
Full source code for this article can be found at GitHub.
可以在GitHub上找到本文的完整源代碼。
翻譯自: https://www.freecodecamp.org/news/follow-the-rules-with-seneca-ii-c22074debac/
node seneca