Code Organization and Conventions
This section provides naming conventions and project code organization guidelines, with the goals of:
- Reducing the burden for engineers who take over existing projects, since we may also be maintainers one day.
- Helping new team members quickly understand the overall project structure.
Routing
A project usually consists of multiple modules, and each module has multiple business methods. Define the main routes of all modules in one interface, uniformly named CmdModule.
Each module should have its own file, and each module file manages its corresponding business methods.
Use file names ending with xxxCmd, and name the module-level main route as cmd.
If a legacy project follows these conventions, you can check CmdModule.java to see how many modules exist.
From each module file, you can also see how many business methods the module contains.
For naming, you can use standard interface-constant naming (UPPER_CASE with underscores), or camelCase. For routing, camelCase is generally preferred.
public interface CmdModule {
int userCmd = 1;
int emailCmd = 2;
}
public interface UserCmd {
int cmd = CmdModule.userCmd;
int loginVerify = 1;
}
public interface EmailCmd {
int cmd = CmdModule.emailCmd;
int openEmail = 1;
int listEmail = 2;
}
Broadcast Route
Use an incrementing broadcast start marker to specify broadcast routes, so you do not need to care about specific route values.
public interface BroadcastCmd {
int cmd = CmdModule.broadcastCmd;
// ---------- broadcastUser ----------
int triggerBroadcastUser = 1;
...
AtomicInteger inc = new AtomicInteger(10);
private static CmdInfo ofBroadcastCmd() {
return CmdInfo.of(cmd, inc.getAndIncrement());
}
// ---------- broadcastUser ----------
CmdInfo broadcastUserEmpty = ofBroadcastCmd();
CmdInfo broadcastUserInt = ofBroadcastCmd();
CmdInfo broadcastUserBool = ofBroadcastCmd();
CmdInfo broadcastUserLong = ofBroadcastCmd();
}
// test
@ActionController(BroadcastCmd.cmd)
public class BroadcastAction {
AtomicInteger inc = new AtomicInteger();
@ActionMethod(triggerBroadcastUser)
private void triggerBroadcastUser(long userId) {
var communication = CommunicationKit.getCommunication();
// ---------- empty ----------
communication.broadcastUser(userId, BroadcastCmd.broadcastUserEmpty);
// ---------- int ----------
int dataInt = inc.getAndIncrement();
communication.broadcastUser(userId, BroadcastCmd.broadcastUserInt, dataInt);
// ---------- boolean ----------
boolean dataBool = inc.getAndIncrement() % 2 == 0;
communication.broadcastUser(userId, BroadcastCmd.broadcastUserBool, dataBool);
}
}
Action
For Action classes, it is recommended to use names ending with xxxAction.
@ActionController(UserCmd.cmd)
public class UserAction {
@ActionMethod(UserCmd.loginVerify)
private void loginVerify(String jwt) {
}
}
Error Enum
Assertion + Exception Mechanism = Clear and Concise Code
It is recommended to use enums that implement interfaces, which makes error messages easy to manage. Use a unified name like ErrorCode.
Two error-code styles are shown below: one manually specifies error codes,
and the other uses enum ordinal, which avoids manually setting specific codes.
@Getter
public enum ErrorCode implements ErrorInformation {
emailChecked(100, "Email error.");
final int code;
final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
}
// or
@Getter
public enum ErrorCode implements ErrorInformation {
emailChecked("Email error.");
final int code;
final String message;
ErrorCode(String message) {
this.code = this.ordinal();
this.message = message;
}
}
Usage Example
@ActionController(EmailCmd.cmd)
public class EmailAction {
@ActionMethod(EmailCmd.openEmail)
private void openEmail(String email) {
Pattern pattern = Pattern.compile("[a-zA-Z0-9_-]+@\\w+\\.[a-z]+(\\.[a-z]+)?");
var checkedResult = pattern.matcher(email).find();
ErrorCode.emailChecked.assertTrue(checkedResult);
}
}
Data Protocol
Recommended data protocol definition style:
- code 1: Mark with
Protobuf. - code 2: Use
publicuniformly for all member fields. - code 3: Name data protocol classes with the suffix xxMessage.
@ProtobufClass
@FieldDefaults(level = AccessLevel.PUBLIC)
public class HelloMessage {
String name;
int level;
}
Use public uniformly for all member fields
This makes it clear that the data protocol class does not use setter/getter logic transformations.
Another reason is that native proto also does not perform logic transformations in setters/getters.
When a data protocol class uses setters/getters, we can quickly tell that extra logic is being processed inside.
Logic Server
For logic servers, names ending with xxxLogicServer are recommended.
public class HallLogicServer implements LogicServer {
@Override
public void settingBarSkeletonBuilder(BarSkeletonBuilder builder) {
...
}
@Override
public void settingServerBuilder(ServerBuilder builder) {
builder.setName("HallLogicServer");
}
}
Project Structure
- code 4: External server and related extensions.
- code 7: Directory for all logic server modules.
- code 10: Single-process startup entry for daily development.
- code 13: Simulation client for daily simulation tests.
- code 16: Directory storing data exposed by each logic server module, such as routes, data protocols, and module-access interfaces.
- code 17: Shared module package for all modules, such as
CmdModule, common data protocols, utility classes, etc.
├── external
│ ├── pom.xml
│ └── src
├── logic
│ ├── email-logic
│ └── user-logic
├── one-application
│ ├── pom.xml
│ └── src
├── one-client
│ ├── pom.xml
│ └── src
└── provide
├── common-provide
├── email-provide
└── user-provide