摘要:模板方法模式的更多應(yīng)用事實上很多有關(guān)生命周期的類都用到了模板方法模式,最典
把大象裝進冰箱將大象裝進冰箱需要三步,那么老虎了?如何優(yōu)雅的將大象裝進冰箱?
Step | 大象 | 老虎 | ... |
---|---|---|---|
First | 打開冰箱門 | 打開冰箱門 | 打開冰箱門 |
Second | 把大象放進去 | 把老虎放進去 | ... |
Third | 關(guān)閉冰箱門 | 關(guān)閉冰箱門 | 關(guān)閉冰箱門 |
大象類
public class Elephant {
public void putRefrigerator() {
openDoor();
putElephant();
closeDoor();
}
public void openDoor() {
System.out.println("open the door");
}
public void putElephant() {
System.out.println("put in the Elephant");
}
public void closeDoor() {
System.out.println("close the door");
}
}
老虎類
public class Tiger {
public void putRefrigerator() {
openDoor();
putTiger();
closeDoor();
}
public void openDoor() {
System.out.println("open the door");
}
public void putTiger() {
System.out.println("put in the Tiger");
}
public void closeDoor() {
System.out.println("close the door");
}
}
可以看出我們將大象和老虎放進冰箱的過程中出現(xiàn)了大量的重復(fù)代碼,這顯然不是一個好的設(shè)計,如果我們在以后的系統(tǒng)升級過程中需要再放入長頸鹿怎么辦,我們應(yīng)該如何從我們的設(shè)計中刪除這些重復(fù)代碼?通過觀察我們發(fā)現(xiàn)放大象和放老虎之間有很多共同點,都需要進行開關(guān)門的操作,只是放的過程不盡相同,我們是否可以將共同點抽離?我們一起試試看
抽象超類
public abstract class AbstractPutAnyAnimal {
//這是一個模板方法,它是一個算法的模板,描述我們將動物放進冰箱的步驟,每一個方法代表了一個步驟
public void putRefrigerator() {
openDoor();
putAnyAnimal();
closeDoor();
}
//在超類中實現(xiàn)共同的方法,由超類來處理
public void openDoor() {
System.out.println("open the door");
}
public void closeDoor() {
System.out.println("close the door");
}
//每個子類可能有不同的方法,我們定義成抽象方法讓子類去實現(xiàn)
abstract void putAnyAnimal();
}
大象類
public class Elephant extends AbstractPutAnyAnimal {
//子類實現(xiàn)自己的業(yè)務(wù)邏輯
@Override
void putAnyAnimal() {
System.out.println("put in the Elephant");
}
}
老虎類
public class Tiger extends AbstractPutAnyAnimal {
//子類實現(xiàn)自己的業(yè)務(wù)邏輯
@Override
void putAnyAnimal() {
System.out.println("put in the Tiger");
}
}
通過將相同的方法抽離到超類中,并定義一個抽象方法供子類提供不同的實現(xiàn),事實上我們剛剛實現(xiàn)了一個模板方法模式。
模板方法模式定義?模板方法模式定義了一個算法的步驟,并允許子類為一個或多個步驟提供實現(xiàn),putRefrigerator 方法定義了我們將大象裝進冰箱的步驟它就是一個模板方法。模板方法模式在一個方法中定義一個算法的骨架,而將一些步驟延遲到子類中,模板方法使得子類可以不在改變算法結(jié)構(gòu)的情況下,重新定義算法的某些步驟(子類提供自己的實現(xiàn))
模板方法模式中的鉤子我們可以在超類中定義一個空方法,我們稱這種方法為鉤子(hook)。子類可以依據(jù)情況選擇覆蓋,鉤子的存在可以讓子類有能力對算法的不同點進行掛載;鉤子可以讓子類實現(xiàn)算法中的可選部分,鉤子也可以讓子類為抽象類做一些決定我們將大象裝進冰箱后可能會想調(diào)整冰箱溫度,也可能什么都不做使用默認(rèn)溫度,我們可以通過定義一個鉤子,讓子類來選擇是否調(diào)整溫度,如下:
抽象父類
public abstract class AbstractPutAnyAnimal {
public void putRefrigerator() {
openDoor();
putAnyAnimal();
closeDoor();
//默認(rèn)為false,重新這個方法決定是否執(zhí)行addTemperature();方法
if (isAdd()) {
addTemperature();
}
}
public void openDoor() {
System.out.println("open the door");
}
public void closeDoor() {
System.out.println("close the door");
}
abstract void putAnyAnimal();
void addTemperature(){
System.out.println("plus one");
};
//定義一個空實現(xiàn),由子類決定是否對其進行實現(xiàn)
boolean isAdd(){
return false;
}
}
大象類
public class Elephant extends AbstractPutAnyAnimal {
@Override
void putAnyAnimal() {
System.out.println("put in the Elephant");
}
//子類實現(xiàn)鉤子方法
@Override
boolean isAdd() {
return true;
}
}
我們通過定義一個鉤子方法,子類選擇是否實現(xiàn)這個鉤子方法,來決定是否調(diào)整溫度;當(dāng)然鉤子方法的用途不止如此,它還能讓子類有機會對模板中即將發(fā)生或剛剛發(fā)生的步驟做出反應(yīng),這在JDK中有很多的例子,甚至在前端開發(fā)領(lǐng)域也有很多例子,我就不具體展開代碼演示了,后面在模板方法模式的更多應(yīng)用中展開。
傳統(tǒng)JDBC編程JDK以及Spring中使用了很多的設(shè)計模式,下面我們通過比較傳統(tǒng)JDBC編程和JDBCTemplate來看看模板方法模式是如何幫我們消除樣板代碼的
JDBC編程之新增
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/test");;
String username = "root";
String password = "1234";
Connection connection = null;
Statement statement = null;
try {
Class.forName(driver);
connection = DriverManager.getConnection(url, username, password);
String sql = "insert into users(nickname,comment,age) values("小小譚","I love three thousand times", "21")";
statement = connection.createStatement();
int i = statement.executeUpdate(sql);
return i;
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (null != statement) {
statement.close();
}
if (null != connection) {
connection.close();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
return 0;
JDBC編程之查詢
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/test");;
String username = "root";
String password = "1234";
Connection connection = null;
Statement statement = null;
try{
Class.forName(driver);
connection = DriverManager.getConnection(url, username, password);
String sql = "select nickname,comment,age from users";
statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
List usersList = new ArrayList<>();
while (resultSet.next()) {
Users users = new Users();
users.setNickname(resultSet.getString(1));
users.setComment(resultSet.getString(2));
users.setAge(resultSet.getInt(3));
usersList.add(users);
}
return usersList;
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (null != statement) {
statement.close();
}
if (null != connection) {
connection.close();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
return null;
上面給出了我們在傳統(tǒng)JDBC編程中的兩個案例,可以看到傳統(tǒng)JDBC的很多缺點,當(dāng)然在實際項目中我們可能不會這么原始的進行數(shù)據(jù)庫開發(fā),可能會對JDBC進行一定的封裝,方便我們的使用。Spring 官方為了簡化JDBC的開發(fā)也發(fā)布了JDBCTemplate,下面我們就看一下它是如何簡化開發(fā)的,以及模板方法模式在其中的應(yīng)用
JDBCTemplate是個啥,它到底簡化了什么?從JDBCTemplate的名字我們就不難看出,它簡化了我們JDBC的開發(fā),而且很可能大量應(yīng)用了模板方法模式,它到底為我們提供了什么?它提供了與平臺無光的異常處理機制。使用過原生JDBC開發(fā)的同學(xué)可能有經(jīng)歷,幾乎所有的操作代碼都需要我們強制捕獲異常,但是在出現(xiàn)異常時我們往往無法通過異常讀懂錯誤。Spring解決了我們的問題它提供了多個數(shù)據(jù)訪問異常,并且分別描述了他們拋出時對應(yīng)的問題,同時對異常進行了包裝不強制要求我們進行捕獲,同時它為我們提供了數(shù)據(jù)訪問的模板化,從上面的傳統(tǒng)JDBC編程我們可以發(fā)現(xiàn),很多操作其實是重復(fù)的不變得比如事務(wù)控制、資源的獲取關(guān)閉以及異常處理等,同時結(jié)果集的處理實體的綁定,參數(shù)的綁定這些東西都是特有的。因此Spring將數(shù)據(jù)訪問過程中固定部分和可變部分劃分為了兩個不同的類(Template)和回調(diào)(Callback),模板處理過程中不變得部分,回調(diào)處理自定義的訪問代碼;下面我們具體通過源碼來學(xué)學(xué)習(xí)一下
模板方法模式在JDBCTemplate中的應(yīng)用我所使用的版本是5.1.5.RELEASE
打開JdbcTemplate類(我這里就不截圖了,截圖可能不清晰我直接將代碼copy出來):
JdbcTemplate
public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
//查詢前綴
private static final String RETURN_RESULT_SET_PREFIX = "#result-set-";
//計數(shù)前綴
private static final String RETURN_UPDATE_COUNT_PREFIX = "#update-count-";
//是否跳過警告
private boolean ignoreWarnings = true;
//查詢大小
private int fetchSize = -1;
//最大行
private int maxRows = -1;
//查詢超時
private int queryTimeout = -1;
//是否跳過結(jié)果集處理
private boolean skipResultsProcessing = false;
//是否跳過非公共結(jié)果集處理
private boolean skipUndeclaredResults = false;
//map結(jié)果集是否大小寫敏感
private boolean resultsMapCaseInsensitive = false;
public JdbcTemplate() {
}
//調(diào)用父類方法設(shè)置數(shù)據(jù)源和其他參數(shù)
public JdbcTemplate(DataSource dataSource) {
this.setDataSource(dataSource);
this.afterPropertiesSet();
}
//調(diào)用父類方法設(shè)置數(shù)據(jù)源,懶加載策略和其他參數(shù)
public JdbcTemplate(DataSource dataSource, boolean lazyInit) {
this.setDataSource(dataSource);
this.setLazyInit(lazyInit);
this.afterPropertiesSet();
}
}
JdbcTemplate 繼承了JdbcAccessor實現(xiàn)了JdbcOperations,JdbcAccessor主要封裝了數(shù)據(jù)源的操作,JdbcOperations主要定義了一些操作接口。我們一起看一下JdbcOperations類;
public abstract class JdbcAccessor implements InitializingBean {
protected final Log logger = LogFactory.getLog(this.getClass());
//數(shù)據(jù)源
@Nullable
private DataSource dataSource;
//異常翻譯
@Nullable
private volatile SQLExceptionTranslator exceptionTranslator;
//懶加載策略
private boolean lazyInit = true;
public JdbcAccessor() {
}
public void setDataSource(@Nullable DataSource dataSource) {
this.dataSource = dataSource;
}
@Nullable
public DataSource getDataSource() {
return this.dataSource;
}
protected DataSource obtainDataSource() {
DataSource dataSource = this.getDataSource();
Assert.state(dataSource != null, "No DataSource set");
return dataSource;
}
public void setDatabaseProductName(String dbName) {
this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dbName);
}
public void setExceptionTranslator(SQLExceptionTranslator exceptionTranslator) {
this.exceptionTranslator = exceptionTranslator;
}
}
之所以前面提到spring讓我們更方便的處理異常就是這里他包裝了一個SQLExceptionTranslator,其他的代碼都是做數(shù)據(jù)源的檢查之類的設(shè)置數(shù)據(jù)源,我們看一下其中g(shù)etExceptionTranslator()方法
public SQLExceptionTranslator getExceptionTranslator() {
SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator;
if (exceptionTranslator != null) {
return exceptionTranslator;
} else {
synchronized(this) {
SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator;
if (exceptionTranslator == null) {
DataSource dataSource = this.getDataSource();
if (dataSource != null) {
exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
} else {
exceptionTranslator = new SQLStateSQLExceptionTranslator();
}
this.exceptionTranslator = (SQLExceptionTranslator)exceptionTranslator;
}
return (SQLExceptionTranslator)exceptionTranslator;
}
}
}
這是一個標(biāo)準(zhǔn)的單例模式,我們在學(xué)習(xí)模板方法模式的路途中有捕獲了一個野生的單例;我們繼續(xù)看JdbcOperations接口我們調(diào)其中一個接口進行解析;
@Nullable
T execute(StatementCallback var1) throws DataAccessException;
StatementCallback 接口
@FunctionalInterface
public interface StatementCallback<T> {
@Nullable
T doInStatement(Statement var1) throws SQLException, DataAccessException;
}
execute實現(xiàn)
@Nullable
public T execute(StatementCallback action) throws DataAccessException {
//參數(shù)檢查
Assert.notNull(action, "Callback object must not be null");
//獲取連接
Connection con = DataSourceUtils.getConnection(this.obtainDataSource());
Statement stmt = null;
Object var11;
try {
//創(chuàng)建一個Statement
stmt = con.createStatement();
//設(shè)置查詢超時時間,最大行等參數(shù)(就是一開始那些成員變量)
this.applyStatementSettings(stmt);
//執(zhí)行回調(diào)方法獲取結(jié)果集
T result = action.doInStatement(stmt);
//處理警告
this.handleWarnings(stmt);
var11 = result;
} catch (SQLException var9) {
//出現(xiàn)錯誤優(yōu)雅退出
String sql = getSql(action);
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, this.getDataSource());
con = null;
throw this.translateException("StatementCallback", sql, var9);
} finally {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, this.getDataSource());
}
return var11;
}
這一個方法可謂是展現(xiàn)的淋漓盡致,這是一個典型的模板方法+回調(diào)模式,我們不需要再寫過多的重復(fù)代碼只需要實現(xiàn)自己獲取result的方法就好(StatementCallback)事實上我們自己也不需要實現(xiàn)這個方法,繼續(xù)向上看,我們是如何調(diào)用execute方法的,以查詢?yōu)槔?我們看他是如何一步步調(diào)用的:
查詢方法
public List findAll() {
JdbcTemplate jdbcTemplate = DataSourceConfig.getTemplate();
String sql = "select nickname,comment,age from users";
return jdbcTemplate.query(sql, new BeanPropertyRowMapper(Users.class));
}
query實現(xiàn)
public List query(String sql, RowMapper rowMapper) throws DataAccessException {
return (List)result(this.query((String)sql, (ResultSetExtractor)(new RowMapperResultSetExtractor(rowMapper))));
}
這里的RowMapper是負(fù)責(zé)將結(jié)果集中一行的數(shù)據(jù)映射成實體返回,用到了反射技術(shù),這里就不展開了,有興趣的同學(xué)可以自己打開源碼閱讀,繼續(xù)向下:
query實現(xiàn)
@Nullable
public T query(final String sql, final ResultSetExtractor rse) throws DataAccessException {
Assert.notNull(sql, "SQL must not be null");
Assert.notNull(rse, "ResultSetExtractor must not be null");
if (this.logger.isDebugEnabled()) {
this.logger.debug("Executing SQL query [" + sql + "]");
}
//實現(xiàn)回調(diào)接口
class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
QueryStatementCallback() {
}
@Nullable
public T doInStatement(Statement stmt) throws SQLException {
ResultSet rs = null;
Object var3;
try {
//這里真正的執(zhí)行我們的sql語句
rs = stmt.executeQuery(sql);
//處理對象映射
var3 = rse.extractData(rs);
} finally {
JdbcUtils.closeResultSet(rs);
}
return var3;
}
public String getSql() {
return sql;
}
}
//調(diào)用execute接口
return this.execute((StatementCallback)(new QueryStatementCallback()));
}
看到這里相信你也不得拍手稱奇,Spring處理的非常巧妙,請繼續(xù)向下看:
update詳解
protected int update(PreparedStatementCreator psc, @Nullable PreparedStatementSetter pss) throws DataAccessException {
this.logger.debug("Executing prepared SQL update");
return updateCount((Integer)this.execute(psc, (ps) -> {
Integer var4;
try {
if (pss != null) {
pss.setValues(ps);
}
int rows = ps.executeUpdate();
if (this.logger.isTraceEnabled()) {
this.logger.trace("SQL update affected " + rows + " rows");
}
var4 = rows;
} finally {
if (pss instanceof ParameterDisposer) {
((ParameterDisposer)pss).cleanupParameters();
}
}
return var4;
}));
}
為什么我要把update函數(shù)拎出來講了,因為update這里使用了lambda函數(shù),回想我們StatementCallback定義只有一個方法的接口,他就是一個函數(shù)是接口,所以他是一個函數(shù)式接口,所以這里直接使用lambda語法,lambda函數(shù)允許你直接內(nèi)連,為函數(shù)接口的抽象方法提供實現(xiàn),并且整個表達(dá)式作為函數(shù)接口的一個實例。我們在平時學(xué)習(xí)中可能知道了lambda語法但是可能使用的較少,或者不知道如何用于實戰(zhàn),那么多閱讀源碼一定可以提升你的實戰(zhàn)能力。 我們可以看到JDBCTemplate使用了很多回調(diào)。為什么要用回調(diào)(Callback)");如果父類有多個抽象方法,子類需要全部實現(xiàn)這樣特別麻煩,而有時候某個子類只需要定制父類中的某一個方法該怎么辦呢?這個時候就要用到Callback回調(diào)了就可以完美解決這個問題,可以發(fā)現(xiàn)JDBCTemplate并沒有完全拘泥于模板方法,非常靈活。我們在實際開發(fā)中也可以借鑒這種方法。
模板方法模式的更多應(yīng)用事實上很多有關(guān)生命周期的類都用到了模板方法模式,最典型的也是可能我們最熟悉的莫過于Servlet了,廢話不多說上源碼
public abstract class HttpServlet extends GenericServlet
{
}
HttpServlet的所有方法,我們看到HttpServlet繼承了GenericServlet,我們繼續(xù)看:
public abstract class GenericServlet
implements Servlet, ServletConfig, java.io.Serializable
{
private static final String LSTRING_FILE = "javax.servlet.LocalStrings";
private static ResourceBundle lStrings =
ResourceBundle.getBundle(LSTRING_FILE);
private transient ServletConfig config;
public GenericServlet() { }
//沒有實現(xiàn)鉤子
public void destroy() {
}
public String getInitParameter(String name) {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}
return sc.getInitParameter(name);
}
public Enumeration getInitParameterNames() {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}
return sc.getInitParameterNames();
}
public ServletConfig getServletConfig() {
return config;
}
public ServletContext getServletContext() {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}
return sc.getServletContext();
}
public String getServletInfo() {
return "";
}
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public void init() throws ServletException {
}
public void log(String msg) {
getServletContext().log(getServletName() + ": "+ msg);
}
public void log(String message, Throwable t) {
getServletContext().log(getServletName() + ": " + message, t);
}
public abstract void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
public String getServletName() {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}
return sc.getServletName();
}
}
可以看到這就是個典型的模板方法類蠻,而且鉤子函數(shù)也在這里展現(xiàn)的淋漓盡致,如init、destroy方法等,JDK中很多類都是用了模板方法等著你發(fā)現(xiàn)哦。
模板方法模式在Vue.js中的應(yīng)用模板方法模式在其他語言中也有實現(xiàn)比如Vue.js、React中;比如Vue生命周期肯定使用了模板方法,我就不對源碼展開分析了。
總結(jié)
設(shè)計模式在Spring中得到了大量的應(yīng)用,感興趣的同學(xué)可以看看Spring源碼加以學(xué)習(xí),如果你覺得我寫的還不錯的話點個贊吧,如果你發(fā)現(xiàn)了錯誤,或者不好的地方也可以及時告訴我加以改正,謝謝!您的贊賞和批評是進步路上的好伙伴。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/6899.html
摘要:模板方法模式的更多應(yīng)用事實上很多有關(guān)生命周期的類都用到了模板方法模式,最典 將大象裝進冰箱需要三步,那么老虎了?如何優(yōu)雅的將大象裝進冰箱? 把大象裝進冰箱 Step 大象 老虎 ... First 打開冰箱門 打開冰箱門 打開冰箱門 Second 把大象放進去 把老虎放進去 ... Third 關(guān)閉冰箱門 關(guān)閉冰箱門 關(guān)閉冰箱門 大象類 public ...
摘要:模板方法模式的更多應(yīng)用事實上很多有關(guān)生命周期的類都用到了模板方法模式,最典 將大象裝進冰箱需要三步,那么老虎了?如何優(yōu)雅的將大象裝進冰箱? 把大象裝進冰箱 Step 大象 老虎 ... First 打開冰箱門 打開冰箱門 打開冰箱門 Second 把大象放進去 把老虎放進去 ... Third 關(guān)閉冰箱門 關(guān)閉冰箱門 關(guān)閉冰箱門 大象類 public ...
摘要:目錄建造者模式應(yīng)用。其實不用也可以,因為不是很復(fù)雜,只是為了復(fù)習(xí)一下所學(xué)過的設(shè)計模式知識目錄工廠模式應(yīng)用。 為了提高開發(fā)效率,通常會想辦法把一些模式固定的重復(fù)性的勞動抽取出來,以后再使用的時候,拿來主義就可以了。這樣既可以提高開發(fā)效率,又降低了出錯的風(fēng)險。 這一思想在我們的日常工作中可以說隨處可見,我們完成一項復(fù)雜的工程,并不需要面面俱到什么都自己寫,我們完全可以利用第三方的jar包讓...
摘要:簡單工廠模式的實質(zhì)是由一個工廠類根據(jù)傳入的參數(shù),動態(tài)決定應(yīng)該創(chuàng)建哪一個產(chǎn)品類。中的就是簡單工廠模式的體現(xiàn),根據(jù)傳入一個唯一的標(biāo)識來獲得對象,但是否是在傳入?yún)?shù)后創(chuàng)建還是傳入?yún)?shù)前創(chuàng)建這個要根據(jù)具體情況來定。中的就是典型的工廠方法模式。 showImg(https://segmentfault.com/img/bVbwbd9?w=640&h=492); 一. 簡單工廠又叫做靜態(tài)工廠方法(...
摘要:簡單工廠模式的實質(zhì)是由一個工廠類根據(jù)傳入的參數(shù),動態(tài)決定應(yīng)該創(chuàng)建哪一個產(chǎn)品類。中的就是簡單工廠模式的體現(xiàn),根據(jù)傳入一個唯一的標(biāo)識來獲得對象,但是否是在傳入?yún)?shù)后創(chuàng)建還是傳入?yún)?shù)前創(chuàng)建這個要根據(jù)具體情況來定。 設(shè)計模式作為工作學(xué)習(xí)中的枕邊書,卻時常處于勤說不用的尷尬境地,也不是我們時常忘記,只是一直沒有記憶。 Spring作為業(yè)界的經(jīng)典框架,無論是在架構(gòu)設(shè)計方面,還是在代碼編寫方面,都堪...
閱讀 3571·2021-09-22 10:52
閱讀 1594·2021-09-09 09:34
閱讀 1996·2021-09-09 09:33
閱讀 763·2019-08-30 15:54
閱讀 2610·2019-08-29 11:15
閱讀 721·2019-08-26 13:37
閱讀 1674·2019-08-26 12:11
閱讀 2980·2019-08-26 12:00