分别用MVC,MCP,MVVM实现一个井字棋游戏
1 mvc mvp mvvm他们到底是什么?
2 怎么演进的?
3 在什么时候用?
应用开发的原则
遵循面向对象的SOLID原则,在程序设计领域, SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由罗伯特·C·马丁在21世纪早期引入,指代了面向对象编程和面向对象设计的五个基本原则。当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软件维护和扩展的系统变得更加可能。
1 单一职责原则(SRP)
一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中,即又定义有且仅有一个原因使类变更。(甲类负责两个不同的职责:职责A,职责B。当由于职责A需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责B功能发生故障。也就是说职责A和B被耦合在了一起”)。
2 开放封闭原则(OCP)
实体应该对扩展是开放的,对修改是封闭的。即可扩展(extension),不可修改(modification)。
eg:原代码,不同用户类型进行不同服务,但是后续每新增不同的用户类型,只能在下面继续加判断代码。
public class VipCenter {
void serviceVip(T extedn User user) {
if (user instance SlumVip) {
// 小VIP
// do nothing
} else if (user instance RealVip) {
// do bid thing
}
}
}
修改后代码,用户实现统一的接口,后续新增用户类型,只需要新增对应实现类。
public class VIPCenter {
private Map<User.type, ServiceProvider> provider;
void serviceVip(T extend User user) {
provider.get(user.getType()).service(user);
}
}
interface ServiceProvider {
void service(T extend User user);
}
class SlumDogVIPServiceProvider implements ServiceProvider {
void service(T extend User user) {
// do something
}
}
class RealVIPServiceProvider implements ServiceProvider {
void service(T extend User user) {
// do something
}
}
3 里氏替换原则(LSP)
一个对象在其出现的任何地方,都可以用子类实例做替换,并且不会导致程序的错误。
经典的例子: 正方形不是长方形的子类。原因是正方形多了一个属性“长 == 宽”。这时,对正方形类设置不同的长和宽,计算面积的结果是最后设置那项的平方,而不是长*宽,从而发生了与长方形不一致的行为。如果程序依赖了长方形的面积计算方式,并使用正方形替换了长方形,实际表现与预期不符。
4 接口隔离原则(ISP)
接口隔离原则表明客户端不应该被强迫实现一些他们不会使用的接口,应该把胖接口中的方法分组,然后用多个接口替代它,每个接口服务于一个子模块。简单地说,就是使用多个专门的接口比使用单个接口要好很多。
ISP的主要观点如下:
1)一个类对另外一个类的依赖性应当是建立在最小的接口上的。
ISP可以达到不强迫客户(接口的使用方法)依赖于他们不用的方法,接口的实现类应该只呈现为单一职责的角色(遵循SRP原则)
ISP还可以降低客户之间的相互影响---当某个客户要求提供新的职责(需要变化)而迫使接口发生改变时,影响到其他客户程序的可能性最小。
2)客户端程序不应该依赖它不需要的接口方法(功能)。
客户端程序就应该依赖于它不需要的接口方法(功能),那依赖于什么?依赖它所需要的接口。客户端需要什么接口就是提供什么接口,把不需要的接口剔除,这就要求对接口进行细化,保证其纯洁性。
5 依赖倒置原则(DIP)
抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对抽象(接口)编程,而不是针对实现细节编程。
开闭原则(OCP)是面向对象设计原则的基础也是整个设计的一个终极目标,而依赖倒置原则(DIP )则是实现OCP原则的一个基础,换句话说开闭原则(OCP)是你盖一栋大楼的设计蓝图,那么依赖倒置原则就是盖这栋大楼的一个钢构框架。
来看一个例子假设我们在开发一个软件产品需要一个日志系统,要将系统产生的一些重要事情记录在记事本上。通常我们的实现如下:
但是随着时间的推移,产品做的好买了很多客户,产品变得越来越大,使用Logger 类的地方成千上万处,可怕的事情终于发生了:
A 客户提出来我想把日志存在数据库中便于做统计分析。
B 客户说我想把日志打印在一个控制台上便于我时时监测系统运行情况。
C 客户说我要把日志存到Windows Azure Storage上。
怎么办呢? 回过头来看看我们的这个日志系统的设计才恍然大悟:没有遵守面向对象设计原则的依赖倒置原则和开闭原则了。知道就好,找到法门了, 我们将日志这一块的设计重构一下让其符合OCP和DIP应该就可以了。 那么我们就要首先抽象写日志的接口ILog, 让实际调用的地方调用高层抽象(ILog),具体的实现类TextLogger,ConsoleLogger,DatabaseLogger,AzureStorageLogger都继承自ILog接口,然后我们在利用反射加配置,不同的用户配置不同的具体实现类,这样问题就迎任而解了。
常规模式实现井字棋
一个Activity打天下
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import static com.xiangxue.mvx.JinziqiActivity.Player.O;
import static com.xiangxue.mvx.JinziqiActivity.Player.X;
public class JinziqiActivity extends AppCompatActivity {
private static String TAG = JinziqiActivity.class.getName();
public class Cell {
private Player value;
public Player getValue() {
return value;
}
public void setValue(Player value) {
this.value = value;
}
}
private Cell[][] cells = new Cell[3][3];
public enum Player {X, O}
private Player winner;
private GameState state;
private Player currentTurn;
private enum GameState {IN_PROGRESS, FINISHED}
/* Views */
private ViewGroup buttonGrid;
private View winnerPlayerViewGroup;
private TextView winnerPlayerLabel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.jingziqi);
winnerPlayerLabel = (TextView) findViewById(R.id.winnerPlayerLabel);
winnerPlayerViewGroup = findViewById(R.id.winnerPlayerViewGroup);
buttonGrid = (ViewGroup) findViewById(R.id.buttonGrid);
restart();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_jingziqi, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_reset:
restart();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
public void onCellClicked(View v) {
Button button = (Button) v;
String tag = button.getTag().toString();
int row = Integer.valueOf(tag.substring(0, 1));
int col = Integer.valueOf(tag.substring(1, 2));
Log.i(TAG, "Click Row: [" + row + "," + col + "]");
Player playerThatMoved = mark(row, col);
if (playerThatMoved != null) {
button.setText(playerThatMoved.toString());
if (getWinner() != null) {
winnerPlayerLabel.setText(playerThatMoved.toString());
winnerPlayerViewGroup.setVisibility(View.VISIBLE);
}
}
}
/**
* 开始一个新游戏,清楚计分板和状态
*/
public void restart() {
clearCells();
winner = null;
currentTurn = X;
state = GameState.IN_PROGRESS;
/*Reset View*/
winnerPlayerViewGroup.setVisibility(View.GONE);
winnerPlayerLabel.setText("");
for (int i = 0; i < buttonGrid.getChildCount(); i++) {
((Button) buttonGrid.getChildAt(i)).setText("");
}
}
/**
* 标记当前的选手选择了哪行哪列
* 如果不是在没有选中的9个格子里面点击将视作无效;
* 另外,如果游戏已经结束,本次标记忽略
*
* @param row 0..2
* @param col 0..2
* @return 返回当前选手,如果点击无效发挥为null
*/
public Player mark(int row, int col) {
Player playerThatMoved = null;
if (isValid(row, col)) {
cells[row][col].setValue(currentTurn);
playerThatMoved = currentTurn;
if (isWinningMoveByPlayer(currentTurn, row, col)) {
state = GameState.FINISHED;
winner = currentTurn;
} else {
// 切换到另外一起棋手,继续
flipCurrentTurn();
}
}
return playerThatMoved;
}
public Player getWinner() {
return winner;
}
private void clearCells() {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
cells[i][j] = new Cell();
}
}
}
private boolean isValid(int row, int col) {
if (state == GameState.FINISHED) {
return false;
}else if (isCellValueAlreadySet(row, col)) {
return false;
} else {
return true;
}
}
private boolean isCellValueAlreadySet(int row, int col) {
return cells[row][col].getValue() != null;
}
/**
* @param player
* @param currentRow
* @param currentCol
* @return 如果当前行、当前列、或者两天对角线为同一位棋手下的棋子返回为真
*/
private boolean isWinningMoveByPlayer(Player player, int currentRow, int currentCol) {
return (cells[currentRow][0].getValue() == player // 3-行
&& cells[currentRow][1].getValue() == player
&& cells[currentRow][2].getValue() == player
|| cells[0][currentCol].getValue() == player // 3-列
&& cells[1][currentCol].getValue() == player
&& cells[2][currentCol].getValue() == player
|| currentRow == currentCol // 3-对角线
&& cells[0][0].getValue() == player
&& cells[1][1].getValue() == player
&& cells[2][2].getValue() == player
|| currentRow + currentCol == 2 // 3-反对角线
&& cells[0][2].getValue() == player
&& cells[1][1].getValue() == player
&& cells[2][0].getValue() == player);
}
private void flipCurrentTurn() {
currentTurn = currentTurn == X ? O : X;
}
}
MVC 模式实现井字棋
数据(Model) : 对数据进行的操作, 不依赖视图的操作
视图(View): 不同的模式有不同的定义 aml Activity fragment
逻辑(Controller): 对View 和 对 model进行的操作,View和model的通信和交互
Model 的部分
Board
import static com.xiangxue.mvx.model.Player.O;
import static com.xiangxue.mvx.model.Player.X;
public class Board {
private Cell[][] cells = new Cell[3][3];
private Player winner;
private GameState state;
private Player currentTurn;
public Board() {
restart();
}
/**
* 开始一个新游戏,清楚计分板和状态
*/
public void restart() {
clearCells();
winner = null;
currentTurn = Player.X;
state = GameState.IN_PROGRESS;
}
/**
* 标记当前的选手选择了哪行哪列
* 如果不是在没有选中的9个格子里面点击将视作无效;
* 另外,如果游戏已经结束,本次标记忽略
*
* @param row 0..2
* @param col 0..2
* @return 返回当前选手,如果点击无效发挥为null
*
*/
public Player mark( int row, int col ) {
Player playerThatMoved = null;
if(isValid(row, col)) {
cells[row][col].setValue(currentTurn);
playerThatMoved = currentTurn;
if(isWinningMoveByPlayer(currentTurn, row, col)) {
state = GameState.FINISHED;
winner = currentTurn;
} else {
// 切换到另外一起棋手,继续
flipCurrentTurn();
}
}
return playerThatMoved;
}
public Player getWinner() {
return winner;
}
private void clearCells() {
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 3; j++) {
cells[i][j] = new Cell();
}
}
}
private boolean isValid(int row, int col ) {
if( state == GameState.FINISHED ) {
return false;
} else if( isCellValueAlreadySet(row, col) ) {
return false;
} else {
return true;
}
}
private boolean isCellValueAlreadySet(int row, int col) {
return cells[row][col].getValue() != null;
}
/**
* @param player
* @param currentRow
* @param currentCol
* @return 如果当前行、当前列、或者两天对角线为同一位棋手下的棋子返回为真
*
*/
private boolean isWinningMoveByPlayer(Player player, int currentRow, int currentCol) {
return (cells[currentRow][0].getValue() == player // 3-行
&& cells[currentRow][1].getValue() == player
&& cells[currentRow][2].getValue() == player
|| cells[0][currentCol].getValue() == player // 3-列
&& cells[1][currentCol].getValue() == player
&& cells[2][currentCol].getValue() == player
|| currentRow == currentCol // 3-对角线
&& cells[0][0].getValue() == player
&& cells[1][1].getValue() == player
&& cells[2][2].getValue() == player
|| currentRow + currentCol == 2 // 3-反对角线
&& cells[0][2].getValue() == player
&& cells[1][1].getValue() == player
&& cells[2][0].getValue() == player);
}
private void flipCurrentTurn() {
currentTurn = currentTurn == X ? O : X;
}
}
Cell
public class Cell {
private Player value;
public Player getValue() {
return value;
}
public void setValue(Player value) {
this.value = value;
}
}
GameState
public enum GameState {
IN_PROGRESS,
FINISHED
}
Player
public enum Player {
X,
O
}
Controller
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.xiangxue.mvx.R;
import com.xiangxue.mvx.model.Board;
import com.xiangxue.mvx.model.Player;
public class JingziqiActivity extends AppCompatActivity {
private static String TAG = JingziqiActivity.class.getName();
private Board model;
private ViewGroup buttonGrid;
private View winnerPlayerViewGroup;
private TextView winnerPlayerLabel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.jingziqi);
winnerPlayerLabel = (TextView) findViewById(R.id.winnerPlayerLabel);
winnerPlayerViewGroup = findViewById(R.id.winnerPlayerViewGroup);
buttonGrid = (ViewGroup) findViewById(R.id.buttonGrid);
model = new Board();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_jingziqi, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_reset:
model.restart();
resetView();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
public void onCellClicked(View v) {
Button button = (Button) v;
String tag = button.getTag().toString();
int row = Integer.valueOf(tag.substring(0,1));
int col = Integer.valueOf(tag.substring(1,2));
Log.i(TAG, "Click Row: [" + row + "," + col + "]");
Player playerThatMoved = model.mark(row, col);
if(playerThatMoved != null) {
button.setText(playerThatMoved.toString());
if (model.getWinner() != null) {
winnerPlayerLabel.setText(playerThatMoved.toString());
winnerPlayerViewGroup.setVisibility(View.VISIBLE);
}
}
}
public void resetView() {
winnerPlayerViewGroup.setVisibility(View.GONE);
winnerPlayerLabel.setText("");
for( int i = 0; i < buttonGrid.getChildCount(); i++ ) {
((Button) buttonGrid.getChildAt(i)).setText("");
}
}
}
MVC 抽离了model , 清晰了代码逻辑, 缺陷, controller 的权利太大(activity) => 什么事情都能做,需求多变,Activity越改越大, 越来越臃肿。
MVP
presenter 通过接口去操作 View, 限制了权利
数据(Model) : 对数据进行的操作, 不依赖视图的操作
视图(View): 不同的模式有不同的定义 aml Activity fragment
逻辑(Controller): 对View 和 对 model进行的操作,View和model的通信和交互
接口 jingziqiView
public interface JingziqiView {
void showWinner(String winningPlayerDisplayLabel);
void clearWinnerDisplay();
void clearButtons();
void setButtonText(int row, int col, String text);
}
进步: activity 只剩下view, presenter 承担了View 和 model之间的交互,满足了单一职责原则
缺陷:引入了interface, 方法增多,增加一个方法要改的几个地方。
MVVM
双向数据绑定, 把数据自动刷新到 view, 使用的 module和app的module都加入dataBinding
viewBinding : 只能省略findViewById viewBinding {enable true} 不需要修改xml
dataBinding: 除了viewBinding 的功能还能绑定data, 需要修改 xml
gradle:
dataBinding {
enable true;
}
xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
## 和哪个类绑定
type="com.xiangxue.mvx.viewmodel.JingziqiViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context=".view.JingziqiActivity">
<GridLayout
android:id="@+id/buttonGrid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:columnCount="3"
android:rowCount="3">
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(0,0)}"
android:text='@{viewModel.cells["00"]}' />
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(0,1)}"
android:text='@{viewModel.cells["01"]}' />
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(0,2)}"
android:text='@{viewModel.cells["02"]}' />
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(1,0)}"
android:text='@{viewModel.cells["10"]}' />
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(1,1)}"
android:text='@{viewModel.cells["11"]}' />
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(1,2)}"
android:text='@{viewModel.cells["12"]}' />
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(2,0)}"
android:text='@{viewModel.cells["20"]}' />
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(2,1)}"
android:text='@{viewModel.cells["21"]}' />
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(2,2)}"
android:text='@{viewModel.cells["22"]}' />
</GridLayout>
<LinearLayout
android:id="@+id/winnerPlayerViewGroup"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="@{viewModel.winner != null ? View.VISIBLE : View.GONE}"
tools:visibility="visible">
<TextView
android:id="@+id/winnerPlayerLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:text="@{viewModel.winner}"
android:textSize="40sp"
tools:text="X" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/winner"
android:textSize="30sp" />
</LinearLayout>
</LinearLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="com.xiangxue.mvx.viewmodel.JingziqiViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context=".view.JingziqiActivity">
<GridLayout
android:id="@+id/buttonGrid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:columnCount="3"
android:rowCount="3">
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(0,0)}"
android:text='@{viewModel.cells["00"]}' />
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(0,1)}"
android:text='@{viewModel.cells["01"]}' />
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(0,2)}"
android:text='@{viewModel.cells["02"]}' />
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(1,0)}"
android:text='@{viewModel.cells["10"]}' />
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(1,1)}"
android:text='@{viewModel.cells["11"]}' />
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(1,2)}"
android:text='@{viewModel.cells["12"]}' />
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(2,0)}"
android:text='@{viewModel.cells["20"]}' />
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(2,1)}"
android:text='@{viewModel.cells["21"]}' />
<Button
style="@style/jingziqibutton"
android:onClick="@{() -> viewModel.onClickedCellAt(2,2)}"
android:text='@{viewModel.cells["22"]}' />
</GridLayout>
<LinearLayout
android:id="@+id/winnerPlayerViewGroup"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="@{viewModel.winner != null ? View.VISIBLE : View.GONE}"
tools:visibility="visible">
<TextView
android:id="@+id/winnerPlayerLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:text="@{viewModel.winner}"
android:textSize="40sp"
tools:text="X" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/winner"
android:textSize="30sp" />
</LinearLayout>
</LinearLayout>
</layout>
Viewmodel
public class JingziqiViewModel {
private Board model;
public final ObservableArrayMap<String, String> cells = new ObservableArrayMap<>();
public final ObservableField<String> winner = new ObservableField<>();
public JingziqiViewModel() {
model = new Board();
}
public void onResetSelected() {
model.restart();
winner.set(null);
cells.clear();
}
public void onClickedCellAt(int row, int col) {
Player playerThatMoved = model.mark(row, col);
if(playerThatMoved != null) {
cells.put("" + row + col, playerThatMoved == null ? null : playerThatMoved.toString());
winner.set(model.getWinner() == null ? null : model.getWinner().toString());
}
}
}
View
public class JingziqiActivity extends AppCompatActivity {
JingziqiViewModel viewModel = new JingziqiViewModel();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
JingziqiBinding binding = DataBindingUtil.setContentView(this, R.layout.jingziqi);
binding.setViewModel(viewModel);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_jingziqi, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_reset:
viewModel.onResetSelected();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}