نقض MVC بازی تیک تاک او را خراب کرد!

1. بررسی اجمالی
دوست من در حال کار بر روی یک بازی جدید VR جالب است و از من خواست تا با نکات و ترفندهایی در مورد نحوه آزمایش آن کمک کنم.
PS: با قسمت VR شوخی کردم، این یک بازی tic-tac-toe کنسولی خواهد بود:
public class Player {
public static final int MAX_NUMBER = 100;
public static final int MIN_NUMBER = 10;
private final Scanner scanner = new Scanner(System.in);
public int getNextMove() {
System.out.println("please, type your next move and hit <enter>");
int input = scanner.nextInt();
while(input < MIN_NUMBER || input > MAX_NUMBER) {
System.out.println(String.format("the number must between %d and %d! try again...", MIN_NUMBER, MAX_NUMBER));
input = scanner.nextInt();
}
// other game-related logic here
return input;
}
}
حتی اگر امکان انجام برخی «هکها» و آزمایش این روش وجود دارد، آزمایشهای زشت ممکن است نشانههای طراحی بد باشند – بنابراین همیشه ایده خوبی است که ابتدا کد را اصلاح کنید.
2. نقض MVC
اولاً، هیچ جدایی بین مدل، نما و کنترلر وجود ندارد. این منجر به جفت شدن زیادی بین منطق بازی و ورودی ای می شود که از طریق ترمینال می آید.
بهترین راه برای حذف جفت مستقیم او استفاده از اصل وارونگی وابستگی است. در این مورد، اجازه دهید رابطی را اعلام کنیم که امکان ارتباط کاربر را فراهم می کند. ما آن را “PlayerView” می نامیم:
public interface PlayerView {
int readNextInt();
void write(String message);
}
شی Player فقط در مورد این رابط می داند. در این مرحله، ما فقط به کلاسهای خودمان وابسته هستیم (دیگر از System.out، System.in، Scanner و غیره استفاده نمیکنیم):
public class Player {
public static final int MAX_NUMBER = 100;
public static final int MIN_NUMBER = 10;
private final PlayerView view;
public Player(PlayerView console) {
this.view = console;
}
public int getNextMove() {
view.write("please, type your next move and hit <enter>");
int input = view.readNextInt();
while(input < MIN_NUMBER || input > MAX_NUMBER) {
view.write(String.format("the number must between %d and %d! try again...", MIN_NUMBER, MAX_NUMBER));
input = view.readNextInt();
}
return input;
}
}
3. وارونگی وابستگی
چگونه کاری کنیم که کار کند؟ خوب، برای اینکه آن را مانند قبل کنیم، اکنون باید یک پیاده سازی از رابط PlayerView ایجاد کنیم و عملکرد قدیمی را کپی کنیم:
public class ConsoleView implements PlayerView {
private static final Scanner scanner = new Scanner(System.in);
@Override
public int readNextInt() {
return scanner.nextInt();
}
@Override
public void write(String message) {
System.out.println(message);
}
}
در نتیجه، زمانی که بازی مقدار دهی اولیه می شود، باید بازیکنان را بر اساس ConsoleView ایجاد کنیم، مانند این:
Player player = new Player(new ConsoleView());
4. نوشتن یک موک
چه دستاوردهایی دارد؟ پس از این وارونگی وابستگی، کلاس Player را می توان به راحتی آزمایش کرد. به عنوان مثال، ما می توانیم از یک کتابخانه تمسخر آمیز برای تمسخر نمای استفاده کنیم و به ما اجازه می دهیم بقیه عملکردها را به صورت مجزا آزمایش کنیم. در جاوا، mockito یک ابزار محبوب و قدرتمند برای این کار است.
از سوی دیگر، استفاده کورکورانه از کتابخانه ها و چارچوب ها ممکن است باعث شود ما تصویر بزرگتر را از دست بدهیم. بنابراین، هر از گاهی، بهتر است به جای آوردن یک کتابخانه شخص ثالث، راه حل را خودمان کدنویسی کنیم.
بنابراین، اجازه دهید یک نسخه بسیار ساده از یک شی ساختگی ایجاد کنیم. ما می توانیم این کار را به سادگی با نوشتن یک پیاده سازی جدید از رابط PlayerView انجام دهیم:
public class MockView implements PlayerView {
private List<Integer> mockedUserInputs = new ArrayList<>();
private List<String> displayedMessages = new ArrayList<>();
@Override
public int readNextInt() {
return mockedUserInputs.remove(0);
}
@Override
public void write(String message) {
displayedMessages.add(message);
}
public List<String> getDisplayedMessages(){
return displayedMessages;
}
public void mockedUserInputs(Integer... values) {
mockedUserInputs.addAll(Arrays.asList(values));
}
}
همانطور که می بینیم، از دو لیست استفاده می کنیم
mockedUserInputs در تنظیمات تست مشخص خواهد شد. هر بار که کسی readNextInt() را فراخوانی می کند، mock مقدار بعدی را از لیست برمی گرداند.
displayedMessages فقط گیرنده دارد. این لیست برای ذخیره تمام پیام هایی که می خواهیم چاپ کنیم استفاده می شود. در صورتی که بخواهیم بررسی کنیم که پیام ها به درستی نمایش داده می شوند، می تواند مفید باشد.
5. تست واحد
در نهایت، بیایید از کلاس ساختگی خود استفاده کنیم و چند تست واحد بنویسیم:
@Test
void shouldAskUserToEnterNextMove() {
//given
MockView mockedView = new MockView();
mockedView.mockedUserInputs(11);
TestablePlayer player = new TestablePlayer(mockedView);
//when
player.getNextMove();
//then
List<String> displayedMessages = mockedView.getDisplayedMessages();
assertThat(displayedMessages)
.containsExactly("please, type your next move and hit <enter>");
}
@Test
void givenInvalidInput_shouldAskUserToReEnterTheMove() {
//given
MockView mockedView = new MockView();
mockedView.mockedUserInputs(5, 22);
TestablePlayer player = new TestablePlayer(mockedView);
//when
player.getNextMove();
//then
assertThat(mockedView.getDisplayedMessages())
.containsExactly(
"please, type your next move and hit <enter>",
"the number must between 10 and 100! try again...");
}
@Test
void shouldReturnUsersMove() {
//given
MockView mockedView = new MockView();
mockedView.mockedUserInputs(44);
TestablePlayer player = new TestablePlayer(mockedView);
//when
int userMove = player.getNextMove();
//then
assertThat(userMove)
.isEqualTo(44);
}
6. نتیجه گیری
در این مقاله با الگوی طراحی MVC آشنا شدیم. ما از اصل وارونگی وابستگی (“D” در “SOLID”) برای جدا کردن کنترلر از نما استفاده کردیم.
در نهایت، ما توانستیم آن را آزمایش کنیم و یاد گرفتیم که چگونه یک شی ساختگی بسیار ساده برای شبیه سازی تعامل کاربر ایجاد کنیم.
متشکرم!
از خواندن مقاله متشکرم و لطفاً نظر خود را به من بگویید! هر گونه بازخورد استقبال می شود.
اگر میخواهید در مورد کد تمیز، طراحی، تست واحد، برنامهنویسی عملکردی و بسیاری موارد دیگر اطلاعات بیشتری کسب کنید، حتماً مقالات دیگر من را بررسی کنید.
اگر محتوای من را دوست دارید، فهرست ایمیل را دنبال کنید یا در آن مشترک شوید. در نهایت، اگر در نظر دارید از وبلاگ من حمایت کنید و برای من قهوه بخرید، سپاسگزار خواهم بود.
کد نویسی مبارک!