天天看點

設計模式例子

設計模式

為什麼學習設計模式

應對面試中的設計模式相關問題;告别寫被人吐槽的爛代碼;提高複雜代碼的設計和開發能力;讓讀源碼、學架構事半功倍;為你的職場發展做鋪墊。

設計模式作用

解耦

建立型設計模式

将建立和使用代碼解耦

單例模式

一個類隻允許建立一個執行個體,這個類就是單例類

為什麼使用單例(why,when)

控制對于共享資源的順序通路

降低記憶體、檔案句柄等資源的開銷

有些資料在系統中隻應儲存一份

如何實作單例(hwo)

懶漢(延遲加載)

餓漢(預先加載,早失敗,早發現)

雙重檢查機制(減少鎖的沖突)

靜态類(懶加載 Holder裡執行個體化)

枚舉(最簡單優雅)

單例有什麼問題

如何了解單例唯一性

指程序内的唯一執行個體,程序間是不唯一的。

實作線程唯一執行個體

思路是用ConcurrentHashMap,key:線程ID,value為要建立的執行個體

java中有ThreadLoca實作線程唯一執行個體

實作叢集唯一執行個體

程序間唯一,其實就是分布式唯一

多例模式

一個類可以建立多個對象,但是個數是有限制的

使用靜态塊+hashMap定義

使用concurrentHashMap定義

工廠模式

分類

簡單工廠

建立一個工廠類,根據類型來傳回不同對象

每次建立一個新對象

每次建立

類比:某手機店1個櫥櫃,擺放各式手機

工廠方法

建立多個工廠類,負責建立對應的對象

類比:某手機店n個櫥櫃,每個櫥櫃擺放對應款式手機

抽象工廠

建立一個綜合工廠,建立多種類型的對象

類比:大型商超,裡面有手機店,有電腦店,有家電店。

何時使用

當建立邏輯比較複雜,考慮使用工廠模式,封裝對象的建立過程,将對象的建立和使用相分離。

如規則配置解析,存在多個if-else,根據不同的類型建立不同對象,将if-else建立對象的邏輯放到工廠類中

如果建立對象的邏輯不複雜,考慮簡單工廠

如果建立對象的邏輯複雜,考慮工廠方法

單個對象本身建立較為複雜,需要組合其他類對象,做各種初始化操作

是否要使用工廠模式

封裝變化:建立邏輯有可能變化,封裝成工廠類之後,建立邏輯的變更對調用者透明。

代碼複用:建立代碼抽離到獨立的工廠類之後可以複用。

隔離複雜性:封裝複雜的建立邏輯,調用者無需了解如何建立對象。

控制複雜度:将建立代碼抽離出來,讓原本的函數或類職責更單一,代碼更簡潔。

建造者模式builder

可以把校驗邏輯放置到 Builder 類中,先建立建造者,并且通過 set() 方法設定建造者的變量值,然後在使用 build() 方法真正建立對象之前,做集中的校驗,校驗通過之後才會建立對象

為什麼使用建造者模式 why

避免過度參數導緻的可讀性和易用性

容易執行限制條件,校驗邏輯

實作類對象不可變

避免對象的無效狀态

工廠模式與建造者模式差別

工廠模式是用來建立不同但是相關類型的對象(繼承同一父類或者接口的一組子類),由給定的參數來決定建立哪種類型的對象。建造者模式是用來建立一種類型的複雜對象,可以通過設定不同的可選參數,“定制化”地建立不同的對象。

原型模式

如果對象的建立成本比較大,而同一個類的不同對象之間差别不大(大部分字段都相同),在這種情況下,我們可以利用對已有對象(原型)進行複制(或者叫拷貝)的方式來建立新對象,以達到節省建立時間的目的

基于原型來建立對象的方式就叫作原型設計模式(Prototype Design Pattern),簡稱原型模式。

何為“對象的建立成本比較大”?

對象中的資料需要經過複雜的計算才能得到(比如排序、計算哈希值),或者需要從 RPC、網絡、資料庫、檔案系統等非常慢速的 IO 中讀取

結構型設計模式

将不同功能代碼解耦

行為型設計模式

将不同行為代碼解耦

行為型設計模式解決的就是“類或對象之間的互動”問題。

觀察者模式

模闆模式

作用

複用

所有子類可以複用父類中提供的模闆方法的代碼

擴充

架構通過模闆模式提供功能擴充點,讓架構使用者可以在不修改架構源碼情況下,基于擴充點定制化架構功能

回調 vs 模闆模式

回調的優勢

像 Java 這種隻支援單繼承的語言,基于模闆模式編寫的子類,已經繼承了一個父類,不再具有繼承的能力。

回調可以使用匿名類來建立回調對象,可以不用事先定義類;而模闆模式針對不同的實作都要定義不同的子類。

如果某個類中定義了多個模闆方法,每個方法都有對應的抽象方法,那即便我們隻用到其中的一個模闆方法,子類也必須實作所有的抽象方法。而回調就更加靈活,我們隻需要往用到的模闆方法中注入回調對象即可。

政策模式

職責鍊模式

在職責鍊模式中,多個處理器依次處理同一個請求。一個請求先經過 A 處理器處理,然後再把請求傳遞給 B 處理器,B 處理器處理完後再傳遞給 C 處理器,以此類推,形成一個鍊條。鍊條上的每個處理器各自承擔各自的處理職責,是以叫作職責鍊模式。

職責鍊模式常用在架構開發中,用來實作架構的過濾器、攔截器功能

三種職責鍊常用的應用場景:過濾器(Servlet Filter)、攔截器(Spring Interceptor)、插件(MyBatis Plugin)。

Servlet Filter 采用遞歸來實作攔截方法前後添加邏輯

Spring Interceptor 的實作比較簡單,把攔截方法前後要添加的邏輯放到兩個方法中實作

MyBatis Plugin 采用嵌套動态代理的方法來實作,實作思路很有技巧。

狀态模式

疊代器模式

通路者模式

備忘錄模式

指令模式

解釋器模式

中介模式

JDK中的設計模式

Calendar建造者模式和工廠模式

既然已經有了 getInstance() 工廠方法來建立 Calendar 類對象,為什麼還要用 Builder 來建立 Calendar 類對象呢

建造者模式用于定制化建立複雜的對象,而工廠模式用于建立不同但相關類型的對象。本身不沖突。我們點了特級牛排,依然可以随心搭配喜歡的醬。

從 Calendar 這個例子,我們也能學到,不要過于死闆地套用各種模式的原理和實作,不要不敢做絲毫的改動。模式是死的,用的人是活的。在實際上的項目開發中,不僅各種模式可以混合在一起使用,而且具體的代碼實作,也可以根據具體的功能需求做靈活的調整。

Collections裝飾器模式和擴充卡模式

為什麼說 UnmodifiableCollection 類是 Collection 類的裝飾器類呢?

UnmodifiableCollection 類可以算是對 Collection 類的一種功能增強,但這點還不具備足夠的說服力來斷定 UnmodifiableCollection

UnmodifiableCollection 的構造函數接收一個 Collection 類對象,然後對其所有的函數進行了包裹(Wrap):重新實作(比如 add() 函數)或者簡單封裝(比如 stream() 函數)。而簡單的接口實作或者繼承,并不會如此來實作 UnmodifiableCollection 類。是以,從代碼實作的角度來說,UnmodifiableCollection 類是典型的裝飾器類。

java.util.Collections#enumeration方法展現了擴充卡模式。Enumeration是擴充卡接口,Iterator是其适配實作。Enumeration類和enumeration方法存在的意義在于相容舊版本的jdk代碼更新到新版本時的相容。

public static <T> Enumeration<T> enumeration(final Collection<T> c) {
    return new Enumeration<T>() {
        private final Iterator<T> i = c.iterator();

        public boolean hasMoreElements() {
            return i.hasNext();
        }

        public T nextElement() {
            return i.next();
        }
    };
}
           

Collections.sort模闆模式,政策模式

sort方法傳入comparator可以認為是模闆方法的實作,同時也可以認為是一種算法,政策

List<Student> students = new ArrayList<>();
    students.add(new Student("Alice", 19, 89.0f));
    students.add(new Student("Peter", 20, 78.0f));
    students.add(new Student("Leo", 18, 99.0f));

    Collections.sort(students, new AgeAscComparator());
           

最終調用模闆方法compare的地方

private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi,
                                                Comparator<? super T> c) {
    assert lo < hi;
    int runHi = lo + 1;
    if (runHi == hi)
        return 1;

    // Find end of run, and reverse range if descending
    if (c.compare(a[runHi++], a[lo]) < 0) { // Descending
        while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
            runHi++;
        reverseRange(a, lo, runHi);
    } else {                              // Ascending
        while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
            runHi++;
    }

    return runHi - lo;
}
           

觀察者模式

jdk中的觀察者模式實作。有可能新加入的觀察者會miss通知,參看notifyObservers方法

Observer/Observable

調用notifyObservers前先調用setChanged,否則不會通知,why?

public interface Observer {

void update(Observable o, Object arg);

}

private boolean changed = false;

protected synchronized void setChanged() {

changed = true;

}

public void notifyObservers(Object arg) {

Object[] arrLocal;

synchronized (this) {
        /* We don't want the Observer doing callbacks into
         * arbitrary code while holding its own Monitor.
         * The code where we extract each Observable from
         * the Vector and store the state of the Observer
         * needs synchronization, but notifying observers
         * does not (should not).  The worst result of any
         * potential race-condition here is that:
         * 1) a newly-added Observer will miss a
         *   notification in progress
         * 2) a recently unregistered Observer will be
         *   wrongly notified when it doesn't care
         */
        if (!changed)
            return;
        arrLocal = obs.toArray();
        clearChanged();
    }

    for (int i = arrLocal.length-1; i>=0; i--)
        ((Observer)arrLocal[i]).update(this, arg);
}
           

單例模式

Runtime.getRuntime

  • Every Java application has a single instance of class
  • Runtime

    that allows the application to interface with
  • the environment in which the application is running. The current
  • runtime can be obtained from the

    getRuntime

    method.
  • An application cannot create its own instance of this class.

    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {

    return currentRuntime;

    }

    private Runtime() {}

模闆模式

Java Servlet

Junit TestCase

Java InputStream

Java AbstractList

享元模式

Integer -128~127

String常量池

職責鍊模式

Servlet Filter

Spring interceptor

疊代器模式

Iterator

Guava 中的設計模式

建造者模式CacheBuilder

Wrapper模式(代理模式、裝飾器、擴充卡模式)

預設的Wrapper實作,如com.google.common.collect.ForwardingCollection等,這樣使用者類繼承這些預設類後,隻需實作接口中的少量方法。

Immutable模式

一個對象的狀态在對象建立之後就不再改變,這就是所謂的不變模式

對應的是不變類,不變對象

java中String是不變類

普通不變模式,内部引用對象可變

深度不變模式,内部引用對象也不可變

建構

所有成員變量通過構造函數一次性設定好,不暴露set等修改成員變量的方法

不變模式常用在多線程環境下,因為不存在并發安全問題,是以避免加鎖

Guava不變集合類ImmutableCollection、ImmutableList、ImmutableSet、ImmutableMap

與Jdk的不變類差別是jdk中對原集合修改會展現在早先建立的不變集合中,而guava的則會拷貝出新的集合,拷貝後對原集合的改動不會展現在新的集合中

Spring架構中的設計模式

經典設計思想或原則

架構的作用:利用架構的好處有:解耦業務和非業務開發、讓程式員聚焦在業務開發上;隐藏複雜實作細節、降低開發難度、減少代碼 bug;實作代碼複用、節省開發時間;規範化标準化項目開發、降低學習和維護成本等等。簡言之:簡化開發

約定優于配置

使用預設配置,對于偏離預設配置的項目顯式配置

低侵入松耦合

IOC容器替換,利用AOP技術來做非業務功能

子產品化,輕量級

子產品劃分做得好,低耦合,可單獨引入其中的子產品,非常輕量級。

再封裝、再抽象

Spring Data , Spring JDBC,Spring Cache DataAccessException

支援可擴充的2種設計模式

觀察者模式 使用IOC容器的publishEvent特性

在監聽者程式中使用異步,以防阻塞事件釋出者

public class DemoEvent extends ApplicationEvent {

private String message;

public DemoEvent(Object source, String message) {
    super(source);
    this.message = message;
}

public String getMessage() {
    return this.message;
}
           

}

@Component

public class DemoPublisher {

@Autowired

private ApplicationContext applicationContext;

public void publishEvent(DemoEvent demoEvent) {
    this.applicationContext.publishEvent(demoEvent);
}
           

}

@Component

public class DemoListener implements ApplicationListener {

@Override

public void onApplicationEvent(DemoEvent demoEvent) {

String message = demoEvent.getMessage();

System.out.println(message);

}

}

模闆模式, 回調式的模闆模式

将要執行的函數封裝成對象(比如,初始化方法封裝成 InitializingBean 對象),傳遞給模闆(BeanFactory)來執行。

Spring bean生命周期圖

Spring架構用到的11種設計模式

擴充卡模式 Spring web mvc中對于多種Controller實作,采用擴充卡模式,統一處理接口

protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
	if (this.handlerAdapters != null) {
		for (HandlerAdapter adapter : this.handlerAdapters) {
			if (adapter.supports(handler)) {
				return adapter;
			}
		}
	}
	throw new ServletException("No adapter for handler [" + handler +
			"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}

public class SimpleServletHandlerAdapter implements HandlerAdapter {

@Override
public boolean supports(Object handler) {
	return (handler instanceof Servlet);
}

@Override
@Nullable
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
		throws Exception {

	((Servlet) handler).service(request, response);
	return null;
}

@Override
public long getLastModified(HttpServletRequest request, Object handler) {
	return -1;
}
           

}

政策模式

建立AOP代理的政策

public interface AopProxyFactory {

/**
 * Create an {@link AopProxy} for the given AOP configuration.
 * @param config the AOP configuration in the form of an
 * AdvisedSupport object
 * @return the corresponding AOP proxy
 * @throws AopConfigException if the configuration is invalid
 */
AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException;
           

}

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
	if (!NativeDetector.inNativeImage() &&
			(config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {
		Class<?> targetClass = config.getTargetClass();
		if (targetClass == null) {
			throw new AopConfigException("TargetSource cannot determine target class: " +
					"Either an interface or a target is required for proxy creation.");
		}
		if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
			return new JdkDynamicAopProxy(config);
		}
		return new ObjenesisCglibAopProxy(config);
	}
	else {
		return new JdkDynamicAopProxy(config);
	}
}
           

}

組合模式

CacheManager的實作有中間CacheManager,如CompositeCacheManager,也有葉子CacheManager,如SimpleCacheManager

如下getCache,getCacheNames的實作使用了組合模式

public interface CacheManager {

/**
 * Get the cache associated with the given name.
 * <p>Note that the cache may be lazily created at runtime if the
 * native provider supports it.
 * @param name the cache identifier (must not be {@code null})
 * @return the associated cache, or {@code null} if such a cache
 * does not exist or could be not created
 */
@Nullable
Cache getCache(String name);

/**
 * Get a collection of the cache names known by this manager.
 * @return the names of all caches known by the cache manager
 */
Collection<String> getCacheNames();
           

}

public class CompositeCacheManager implements CacheManager, InitializingBean {

@Override

@Nullable

public Cache getCache(String name) {

for (CacheManager cacheManager : this.cacheManagers) {

Cache cache = cacheManager.getCache(name);

if (cache != null) {

return cache;

}

}

return null;

}

@Override
public Collection<String> getCacheNames() {
	Set<String> names = new LinkedHashSet<>();
	for (CacheManager manager : this.cacheManagers) {
		names.addAll(manager.getCacheNames());
	}
	return Collections.unmodifiableSet(names);
}
           

}

裝飾器模式

org.springframework.cache.transaction.TransactionAwareCacheDecorator實作了Cache接口

持有Cache接口引用targetCache,将所有Cache操作委托給targetCache。在TransactionAwareCacheDecorator中根據事務的執行情況對targetCache進行相關操作。

public class TransactionAwareCacheDecorator implements Cache {

@Override

public void put(final Object key, @Nullable final Object value) {

if (TransactionSynchronizationManager.isSynchronizationActive()) {

TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {

@Override

public void afterCommit() {

TransactionAwareCacheDecorator.this.targetCache.put(key, value);

}

});

}

else {

this.targetCache.put(key, value);

}

}

}

工廠模式

靜态工廠(類的靜态方法),動态工廠(執行個體工廠方法)

解釋器模式

SpEL Spring Expression Language ,spring-expresssion子產品

單例模式

IOC容器作用域範圍内的單例

觀察者模式

模闆模式

RedisTemplate,JdbcTemplate等Template。大部分通過Callback回調實作

職責鍊模式

Interceptor

代理模式

AOP

Mybatis架構中的設計模式

Mybatis為簡化資料庫開發而生

MyBatis 如何權衡代碼的易用性、性能和靈活性

與jdbc的比較

JdbcTemplate比Mybatis更加輕量,性能更好,缺點是代碼和sql耦合,不具備orm功能,需要自己編寫代碼解析對象和資料關系。易用性不及Mybatis和Hibernate

與hibernate比較

hibernate更加重量級,mybatis是半自動化的orm架構,而hibernate是全自動的。hibernate可以生成sql。生成的sql對于優化sql的場景支援較差

是以mybatis較hibernate性能較好。程式員自己寫sql,靈活性高。

一般來說架構易用性和性能成對立關系

JdbcTemplate 提供的功能最簡單,易用性最差,性能損耗最少,用它程式設計性能最好。Hibernate 提供的功能最完善,易用性最好,但相對來說性能損耗就最高了。MyBatis 介于兩者中間,在易用性、性能、靈活性三個方面做到了權衡。它支撐程式員自己編寫 SQL,能夠延續程式員對 SQL 知識的積累。相對于完全黑盒子的 Hibernate,很多程式員反倒是更加喜歡 MyBatis 這種半透明的架構。這也提醒我們,過度封裝,提供過于簡化的開發方式,也會喪失開發的靈活性。

如何利用職責鍊與代理模式實作 MyBatis Plugin

MyBatis Plugin 跟 Servlet Filter、Spring Interceptor 的功能是類似的,都是在不需要修改原有流程代碼的情況下,攔截某些方法調用,在攔截的方法調用的前後,執行一些額外的代碼邏輯。它們的唯一差別在于攔截的位置是不同的。Servlet Filter 主要攔截 Servlet 請求,Spring Interceptor 主要攔截 Spring 管理的 Bean 方法(比如 Controller 類的方法等),而 MyBatis Plugin 主要攔截的是 MyBatis 在執行 SQL 的過程中涉及的一些方法。

預設情況下,MyBatis Plugin 允許攔截的方法有下面這樣幾個

MyBatis 底層是通過 Executor 類來執行 SQL 的。Executor 類會建立 StatementHandler、ParameterHandler、ResultSetHandler 三個對象,并且,首先使用 ParameterHandler 設定 SQL 中的占位符參數,然後使用 StatementHandler 執行 SQL 語句,最後使用 ResultSetHandler 封裝執行結果。是以,我們隻需要攔截 Executor、ParameterHandler、ResultSetHandler、StatementHandler 這幾個類的方法,基本上就能滿足我們對整個 SQL 執行流程的攔截了。

利用 MyBatis Plugin,我們還可以做很多事情,比如分庫分表、自動分頁、資料脫敏、加密解密等等

MyBatis 中的職責鍊模式的實作方式比較特殊。它對同一個目标對象嵌套多次代理(也就是 InteceptorChain 中的 pluginAll() 函數要執行的任務)。每個代理對象(Plugin 對象)代理一個攔截器(Interceptor 對象)功能

當執行 Executor、StatementHandler、ParameterHandler、ResultSetHandler 這四個類上的某個方法的時候,MyBatis 會嵌套執行每層代理對象(Plugin 對象)上的 invoke() 方法。而 invoke() 方法會先執行代理對象中的 interceptor 的 intecept() 函數,然後再執行被代理對象上的方法。就這樣,一層一層地把代理對象上的 intercept() 函數執行完之後,MyBatis 才最終執行那 4 個原始類對象上的方法。

MyBatis 架構中用到的十幾種設計模式。

建造者模式:org.apache.ibatis.session.SqlSessionFactoryBuilder,不是标準的嚴格意義的建造者,隻是為了封裝複雜的Configuration的構造,它裡面定義了n多build方法,具有不同的參數,與使用多構造函數重載的方式沒有本質差別

工廠模式:org.apache.ibatis.session.SqlSessionFactory 也非标準工廠模式,更像建造者模式

舉的這2個例子他們的名稱具有很大的迷惑性。

模闆模式:org.apache.ibatis.executor.BaseExecutor

模闆模式基于繼承來實作代碼複用。如果抽象類中包含模闆方法,模闆方法調用有待子類實作的抽象方法,那這一般就是模闆模式的代碼實作。而且,在命名上,模闆方法與抽象方法一般是一一對應的,抽象方法在模闆方法前面多一個“do”,比如,在 BaseExecutor 類中,其中一個模闆方法叫 update(),那對應的抽象方法就叫 doUpdate()

public abstract class BaseExecutor implements Executor {

@Override

public int update(MappedStatement ms, Object parameter) throws SQLException {

ErrorContext.instance().resource(ms.getResource()).activity(“executing an update”).object(ms.getId());

if (closed) {

throw new ExecutorException(“Executor was closed.”);

}

clearLocalCache();

return doUpdate(ms, parameter);

}

protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;

}

解釋器模式:SqlNode

整個解釋器的調用入口在 DynamicSqlSource.getBoundSql 方法中,它調用了 rootSqlNode.apply(context) 方法

public interface SqlNode {

boolean apply(DynamicContext context);

}

線程唯一式單例模式:

org.apache.ibatis.executor.ErrorContext

private static final ThreadLocal LOCAL = ThreadLocal.withInitial(ErrorContext::new);

裝飾器模式:Cache

org.apache.ibatis.cache.impl.PerpetualCache

之是以 MyBatis 采用裝飾器模式來實作緩存功能,是因為裝飾器模式采用了組合,而非繼承,更加靈活,能夠有效地避免繼承關系的組合爆炸

疊代器模式:org.apache.ibatis.reflection.property.PropertyTokenizer

擴充卡模式:org.apache.ibatis.logging.Log 重複造輪子

如何使用設計模式

在項目中應用設計模式,切不可生搬硬套,過于學院派,要學會結合實際情況做靈活調整,做到心中無劍勝有劍。

代碼品質

review

示例:https://time.geekbang.org/column/article/190979

總的從可讀性,可維護性(對于其他特性的總的概括),可擴充性,可複用性,可測試性,是否簡潔,靈活等角度把控

展開來說

高内聚、低耦合》目錄設定是否合理、子產品劃分是否清晰、代碼結構是否滿足“高内聚、松耦合

設計原則》是否遵循經典的設計原則和設計思想(SOLID、DRY、KISS、YAGNI、LOD 等)

設計模式》設計模式是否應用得當?是否有過度設計?

可擴充性》代碼是否容易擴充?如果要添加新功能,是否容易實作?

可複用性》代碼是否可以複用?是否可以複用已有的項目代碼或類庫?是否有重複造輪子?

可測試性》代碼是否容易測試?單元測試是否全面覆寫了各種正常和異常的情況?

可讀性》代碼是否易讀?是否符合編碼規範(比如命名和注釋是否恰當、代碼風格是否一緻等)?

功能性需求滿足與否

代碼是否實作了預期的業務需求?

邏輯是否正确?是否處理了各種異常情況?

代碼是否存在并發問題?是否線程安全?

非功能性需求滿足與否

可運維》日志列印是否得當?是否友善 debug 排查問題?

易用性》接口是否易用?是否支援幂等、事務等?

性能》性能是否有優化空間,比如,SQL、算法是否可以優化?

安全性》是否有安全漏洞?比如輸入輸出校驗是否全面?

是否簡潔靈活

可讀性

高層實作面向抽象而不是細節程式設計,寫了一大段代碼,不如封裝成一個意義明确的函數來得可讀性高。

可維護性

可擴充性

可複用性

減少代碼耦合

滿足單一職責原則

子產品化

不單單指一組類構成的子產品,還可以了解為單個類、函數。我們要善于将功能獨立的代碼,封裝成子產品。獨立的子產品就像一塊一塊的積木,更加容易複用,可以直接拿來搭建更加複雜的系統。

業務與非業務邏輯分離

通用代碼下沉

從分層的角度來看,越底層的代碼越通用、會被越多的子產品調用,越應該設計得足夠可複用。一般情況下,在代碼分層之後,為了避免交叉調用導緻調用關系混亂,我們隻允許上層代碼調用下層代碼及同層代碼之間的調用,杜絕下層代碼調用上層代碼。是以,通用的代碼我們盡量下沉到更下層。

繼承、多态、抽象、封裝

應用模闆等設計模式

程式設計語言的某些特性

泛型

可測試性

單元測試

what

代碼層面測試

以類或函數為單元

why

code review

重構

發現bug和設計問題

TDD落地的改進方案

how

利用測試架構

建立正确認知

編寫單元測試盡管繁瑣,但并不是太耗時;

我們可以稍微放低對單元測試代碼品質的要求;

覆寫率作為衡量單元測試品質的唯一标準是不合理的;

單元測試不要依賴被測代碼的具體實作邏輯;

單元測試架構無法測試,多半是因為代碼的可測試性不好

單元測試為何難落地

寫單元測試本身比較繁瑣,技術挑戰不大,很多程式員不願意去寫

國内研發比較偏向“快、糙、猛”,容易因為開發進度緊,導緻單元測試的執行虎頭蛇尾

團隊沒有建立對單元測試正确的認識,覺得可有可無,單靠督促很難執行得很好

什麼是代碼的可測試性

針對代碼編寫單元測試的難易程度

如何寫出可測試的代碼

依賴注入是編寫可測試性代碼的最有效手段。通過依賴注入,我們在編寫單元測試的時候,可以通過 mock 的方法解依賴外部服務,這也是我們在編寫單元測試的過程中最有技術挑戰的地方。

有哪些常見的不好測試的代碼?反模式

代碼中包含未決行為邏輯

濫用可變全局變量

濫用靜态方法

使用複雜的繼承關系

高度耦合的代碼

重構

對于review(自己或團隊)後發現問題的點進行小步快速重構,每輪可以按照一個次元重構,比如

第一輪重構:提高代碼的可讀性

第二輪重構:提高代碼的可測試性

第三輪重構:編寫完善的單元測試

第四輪重構:所有重構完成之後添加注釋

解耦

解耦重要性

高内聚,低耦合

代碼可讀性和維護性

如何判定代碼是否需要解耦

把子產品與子產品之間、類與類之間的依賴關系畫出來,根據依賴關系圖的複雜性來判斷是否需要解耦重構。

如何給代碼解耦

封裝與抽象,屏蔽底層實作細節依賴

引入中間層,簡化依賴

子產品化(分而治之)

其他設計思想和原則

單一職責

基于接口而非實作程式設計

依賴注入

多用組合少用繼承

迪米特法則

編碼規範

命名與注釋

命名

長度

小作用域,命名可以短些

大作用域,命名可以長些

熟悉的詞,可以使用簡寫

長度太短影響可讀性,太長導緻語句換行也影響可讀性

利用上下文簡化命名,如類裡的變量名,函數的參數

命名可讀,可搜尋

單詞可以讀出來

命名符合項目統一規約

接口和抽象類的命名

項目統一約定即可

接口

IUserService-UserService

UserService-UserServiceImpl

抽象類

帶Abstract

不帶Abstract

注釋

内容

做什麼,為什麼,怎麼做

類上注釋需要寫做什麼

quickstart how to use

總結性注釋

哪些地方寫注釋

類和函數上要寫,且要全面,詳細

函數内注釋少些

函數出錯傳回類型

傳回錯誤碼 C語言中用得多

傳回NULL值 表示要查找的資源不存在

傳回空對象,應對NPE問題,減少NULL值判斷

抛出異常對象

檢查異常

非檢查異常

函數異常處理的三種方法

直接吞掉

直接往上抛出

包裹成新的異常抛出

函數裡是否要對null值或空字元串判斷

對于私有函數,隻在類内部調用,自己確定不傳入null即可

對于公有函數,為了提高代碼健壯性,需要在public函數中作判斷

代碼風格

重要的是在團隊内保持一緻

類、函數的大小

函數不要超出一屏顯示器的高度

類很難了解了,臃腫了。職責過多時類就太大了

一行代碼多長

最長不能超過顯示器寬度

善于空行分隔單元塊

類内、函數内

四格縮進、兩格縮進

建議2格

IDEA中不要使用tab縮進

大括号另起一行

類中成員的排列順序

成員變量在函數前面

成員變量和函數之間都是按照先靜态後普通

成員變量和函數之間,都會按照作用域由大到小排列:public、protected、private

程式設計技巧

把代碼分割成更小的單元塊

注意邏輯複雜時才值得提煉為函數,否則2、3行的函數也要跳進跳出。

避免函數參數過多

4個還能接受

函數拆成多個函數減少參數

函數參數封裝成對象,對于API接口可提高相容性

勿用函數參數來控制邏輯

結合實際情況看bool參數控制的邏輯是否要拆分成2個函數。

根據null判斷來執行分支的也可以拆分為多個函數

函數設計要職責要單一

移除過深的嵌套層次

嵌套最好不要超過2層

去除多餘的if/else

使用程式設計語言提供的 continue、break、return 關鍵字,提前退出嵌套。

調整執行順序來減少嵌套

将部分嵌套邏輯封裝成函數調用,以此來減少嵌套。

通過使用多态來替代 if-else、switch-case 條件判斷的方法

學會使用解釋性變量

常量取代魔法數字。

使用解釋性變量來解釋複雜表達式

程式設計範式

範式之間并不是互相取代的關系,或者說并不完全互相取代,現代語言如Java中含有主流範式的影子,如lamda表達式是函數式程式設計的展現,java語言本身面向對象,而函數的實作也展現了過程式的影子

面向對象

面向對象的程式設計單元是類或對象

它以類或對象作為組織代碼的基本單元,并将封裝、抽象、繼承、多态四個特性,作為代碼設計和實作的基石。

面向對象程式設計語言是支援類或對象的文法機制,并有現成的文法機制,能友善地實作面向對象程式設計四大特性(封裝、抽象、繼承、多态)的程式設計語言。

vs 面向過程的優勢

面向對象程式設計是以類為思考對象。在進行面向對象程式設計的時候,我們并不是一上來就去思考,如何将複雜的流程拆解為一個一個方法,而是采用曲線救國的政策,先去思考如何給業務模組化,如何将需求翻譯為類,如何給類之間建立互動關系,而完成這些工作完全不需要考慮錯綜複雜的處理流程。當我們有了類的設計之後,然後再像搭積木一樣,按照處理流程,将類組裝起來形成整個程式。這種開發模式、思考問題的方式,能讓我們在應對複雜程式開發的時候,思路更加清晰。

除此之外,面向對象程式設計還提供了一種更加清晰的、更加子產品化的代碼組織方式。比如,我們開發一個電商交易系統,業務邏輯複雜,代碼量很大,可能要定義數百個函數、數百個資料結構,那如何分門别類地組織這些函數和資料結構,才能不至于看起來比較淩亂呢?類就是一種非常好的組織這些函數和資料結構的方式,是一種将代碼子產品化的有效手段。

主要從面向對象支援的四大特性和面向過程做比較

當軟體複雜度提升後,面向過程式的開發,思維模式已很難應對。而面向對象以類的組織,及類與類的互動來組織軟體的複雜協作關系。

面向過程

面向過程的程式設計單元是函數

面向過程程式設計也是一種程式設計範式或程式設計風格。它以過程(可以了解為方法、函數、操作)作為組織代碼的基本單元,以資料(可以了解為成員變量、屬性)與方法相分離為最主要的特點。面向過程風格是一種流程化的程式設計風格,通過拼接一組順序執行的方法來操作資料完成一項功能。

面向過程程式設計語言首先是一種程式設計語言。它最大的特點是不支援類和對象兩個文法概念,不支援豐富的面向對象程式設計特性(比如繼承、多态、封裝),僅支援面向過程程式設計。

函數式程式設計

函數式程式設計的程式設計單元是無狀态函數

程式可以用一系列數學函數或表達式的組合來表示。函數式程式設計是程式面向數學的更底層的抽象,将計算過程描述為表達式

函數内部涉及的變量都是局部變量,不會像面向對象程式設計那樣,共享類成員變量,也不會像面向過程程式設計那樣,共享全局變量。函數的執行結果隻與入參有關,跟其他任何外部變量無關。同樣的入參,不管怎麼執行,得到的結果都是一樣的

Java語言提供了三個文法機制支援函數式程式設計

Stream類

Lambda表達式

函數式接口

面向對象OOP

封裝

特性

資訊隐藏(資料通路保護)

提供有限接口通路類内部資料狀态

實作

java類和對象

java通路修飾符 private ,protected, public

意義

避免屬性被随意通路和修改,導緻代碼可維護性差,牽一發動全身。

提供少量接口,屏蔽實作細節,降低使用門檻和出錯率。

抽象

特性

隐藏方法的具體實作,讓調用者隻需要關心方法提供了哪些功能,并不需要知道這些功能是如何實作的。

實作

interface關鍵字

abstract關鍵字

即便沒有實作接口或繼承抽象類,類中的函數本身也是一種抽象,比如調用C函數,我們無需關心内部如何實作。

是以抽象有時不作為面向對象特性,因為隻要有函數存在就是一種抽象,這不是面向對象特有的。

意義

抽象展現一種宏觀把控,建立高層視角,避免過多細節占用了解帶寬。

抽象作為一個非常寬泛的設計思想,在代碼設計中,起到非常重要的指導作用。

定義方法名時要展現抽象思維,如getAliyunPictureUrl()與getPictureUrl(),明顯後者沒有暴露細節

繼承

特性

表示類之間的is-a關系

模式

單繼承,繼承一個父類

多繼承 ,繼承多個父類

實作

java-> A extends B

C+±> A:B

意義

代碼複用

繼承群組合都能做到

繼承關聯兩個類表達is-a的關系符合人類認知。

繼承大概是面向對象有争議的一個特性,過度使用繼承,導緻代碼結構複雜,可維護性差。

多态

特性

多态是指,子類可以替換父類,在實際的代碼運作過程中,調用子類的方法實作

實作

繼承+重寫

接口類文法

duck-typing文法,用在諸如python等動态語言中

If it looks like a duck and quacks like a duck, it’s a duck

注重實作的行為,而不管實際類型。

意義

提高代碼的擴充性

增加新的實作

複用性

隻需實作帶有接口參數的函數,而不需要實作2個具有不同類型參數的函數。

多态是設計原則、設計模式的代碼實作基礎。

設計思想

高内聚低耦合

用來指導不同粒度代碼的設計與開發,比如系統、子產品、類,甚至是函數,也可以應用到不同的開發場景中,比如微服務、架構、元件、類庫等

“高内聚”用來指導類本身的設計,“松耦合”用來指導類與類之間依賴關系的設計。

設計原則

不管是應用設計原則還是設計模式,最終的目的還是提高代碼的可讀性、可擴充性、複用性、可維護性等。我們在考慮應用某一個設計原則是否合理的時候,也可以以此作為最終的考量标準。

單一職責SRP(Single Responsibility Principle)

A class or module should have a single responsibility

這個原則描述的對象包含兩個,一個是類(class),一個是子產品(module)

把子產品看作比類更加抽象的概念,類也可以看作子產品

把子產品看作比類更加粗粒度的代碼塊,子產品中包含多個類,多個類組成一個子產品。

不同的應用場景、不同階段的需求背景下,對同一個類的職責是否單一的判定,可能都是不一樣的。在某種應用場景或者當下的需求背景下,一個類的設計可能已經滿足單一職責原則了,但如果換個應用場景或着在未來的某個需求背景下,可能就不滿足了,需要繼續拆分成粒度更細的類

不同的業務層面去看待同一個類的設計,對類是否職責單一,也會有不同的認識

實作代碼高内聚、低耦合,提高代碼的複用性、可讀性、可維護性。

如何判斷類的職責是否足夠單一?(側面回答)

類中的代碼行數、函數或者屬性過多;

類依賴的其他類過多,或者依賴類的其他類過多;

私有方法過多;

比較難給類起一個合适的名字;

類中大量的方法都是集中操作類中的某幾個屬性。

類的職責是否設計得越單一越好?

單一職責原則通過避免設計大而全的類,避免将不相關的功能耦合在一起,來提高類的内聚性。同時,類職責單一,類依賴的和被依賴的其他類也會變少,減少了代碼的耦合性,以此來實作代碼的高内聚、低耦合。但是,如果拆分得過細,實際上會适得其反,反倒會降低内聚性,也會影響代碼的可維護性。

開閉原則OCP(Open Closed Principle)

software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification

如何了解“對擴充開放、對修改關閉”

添加一個新的功能,應該是通過在已有代碼基礎上擴充代碼(新增子產品、類、方法、屬性等),而非修改已有代碼(修改子產品、類、方法、屬性等)的方式來完成。關于定義,我們有兩點要注意。第一點是,開閉原則并不是說完全杜絕修改,而是以最小的修改代碼的代價來完成新功能的開發。第二點是,同樣的代碼改動,在粗代碼粒度下,可能被認定為“修改”;在細代碼粒度下,可能又被認定為“擴充”。

如何做到“對擴充開放、修改關閉”?

具備擴充意識、抽象意識、封裝意識。在寫代碼的時候,我們要多花點時間思考一下,這段代碼未來可能有哪些需求變更,如何設計代碼結構,事先留好擴充點,以便在未來需求變更的時候,在不改動代碼整體結構、做到最小代碼改動的情況下,将新的代碼靈活地插入到擴充點上。

很多設計原則、設計思想、設計模式,都是以提高代碼的擴充性為最終目的的。特别是 23 種經典設計模式,大部分都是為了解決代碼的擴充性問題而總結出來的,都是以開閉原則為指導原則的。最常用來提高代碼擴充性的方法有:多态、依賴注入、基于接口而非實作程式設計,以及大部分的設計模式(比如,裝飾、政策、模闆、職責鍊、狀态)。

裡氏替換原則LSP(Liskov Substitution Principle)

If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。

子類對象(object of subtype/derived class)能夠替換程式(program)中父類對象(object of base/parent class)出現的任何地方,并且保證原來程式的邏輯行為(behavior)不變及正确性不被破壞。

多态和裡氏替換原則:從定義描述和代碼實作上來看,多态和裡式替換有點類似,但它們關注的角度是不一樣的

多态是面向對象程式設計的一大特性,也是面向對象程式設計語言的一種文法。它是一種代碼實作的思路

而裡式替換是一種設計原則,是用來指導繼承關系中子類該如何設計的,子類的設計要保證在替換父類的時候,不改變原有程式的邏輯以及不破壞原有程式的正确性。

Design By Contract按照協定(父類的行為約定)來設計

子類遵循父類的行為約定,可以更改函數内部實作邏輯,但不能改變函數原有的行為約定

哪些是行為約定

函數聲明要實作的功能;

對輸入、輸出、異常的約定;

甚至包括注釋中所羅列的任何特殊說明

違反裡氏替換原則的例子

子類違背父類聲明要實作的功能

父類sortOrdersByAmount按訂單金額大小排序,子類sortOrdersByAmount按訂單建立日期排序

子類違背父類對輸入、輸出、異常的約定

在父類中,某個函數約定,輸入資料可以是任意整數,但子類實作的時候,隻允許輸入資料是正整數,負數就抛出,也就是說,子類對輸入的資料的校驗比父類更加嚴格,那子類的設計就違背了裡式替換原則。

在父類中,某個函數約定,隻會抛出 ArgumentNullException 異常,那子類的設計實作中隻允許抛出 ArgumentNullException 異常,任何其他異常的抛出,都會導緻子類違背裡式替換原則。

子類違背父類注釋中所羅列的任何特殊說明

父類中定義的 withdraw() 提現函數的注釋是這麼寫的:“使用者的提現金額不得超過賬戶餘額……”,而子類重寫 withdraw() 函數之後,針對 VIP 賬号實作了透支提現的功能,也就是提現金額可以大于賬戶餘額,那這個子類的設計也是不符合裡式替換原則的。

驗證有沒有違背裡式替換原則的方法

用父類的單元測試來運作子類實作

接口隔離原則ISP(Interface Segregation Principle)

依賴反轉原則DIP(Dependency Inversion Principle)

控制反轉

原先由程式員自己控制程式流程執行,現由架構控制

實際上,控制反轉是一個比較籠統的設計思想,并不是一種具體的實作方法,一般用來指導架構層面的設計。這裡所說的“控制”指的是對程式執行流程的控制,而“反轉”指的是在沒有使用架構之前,程式員自己控制整個程式的執行。在使用架構之後,整個程式的執行流程通過架構來控制。流程的控制權從程式員“反轉”給了架構。

比如使用Spring Web MVC架構,那麼處理Web請求你隻需在Controller類上使用對應的@Controller注解即可。架構會派發請求,程式員無需從頭編寫處理連接配接,解析請求,響應請求等流程,隻需實作對應的業務即可。

依賴注入

依賴注入是一種具體的編碼技巧。我們不通過 new 的方式在類内部建立依賴類的對象,而是将依賴的類對象在外部建立好之後,通過構造函數、函數參數等方式傳遞(或注入)給類來使用。

依賴注入架構

我們通過依賴注入架構提供的擴充點,簡單配置一下所有需要的類及其類與類之間依賴關系,就可以實作由架構來自動建立對象、管理對象的生命周期、依賴注入等原本需要程式員來做的事情。

依賴反轉原則

依賴反轉原則也叫作依賴倒置原則。這條原則跟控制反轉有點類似,主要用來指導架構層面的設計。高層子產品不依賴低層子產品,它們共同依賴同一個抽象。抽象不要依賴具體實作細節,具體實作細節依賴抽象。

還是拿Servlet舉例,tomcat架構可以認為是高層子產品(調用方),業務代碼可以認為是底層子產品(被調用方,下遊)。高層和低層沒有直接的依賴,他們都依賴同一個抽象,Servlet規範。Servlet 規範不依賴具體的 Tomcat 容器和應用程式的實作細節,而 Tomcat 容器和應用程式依賴 Servlet 規範。

KISS原則

Keep It Simple and Stupid.側重如何做

KISS 原則是保持代碼可讀和可維護的重要手段。KISS 原則中的“簡單”并不是以代碼行數來考量的。代碼行數越少并不代表代碼越簡單,我們還要考慮邏輯複雜度、實作難度、代碼的可讀性等。而且,本身就複雜的問題,用複雜的方法解決,并不違背 KISS 原則。除此之外,同樣的代碼,在某個業務場景下滿足 KISS 原則,換一個應用場景可能就不滿足了。

不要使用同僚可能不懂的技術來實作代碼;

不要重複造輪子,要善于使用已經有的工具類庫;

不要過度優化。

YAGNI

You Ain’t Gonna Need It你不會需要它側重要不要做

實作邏輯重複

實作邏輯重複,但功能語義不重複的代碼,并不違反 DRY 原則。

功能語義重複

實作邏輯不重複,但功能語義重複的代碼,也算是違反 DRY 原則。

代碼執行重複。

除此之外,代碼執行重複也算是違反 DRY 原則

DRY

Don’t Repeat Yourself不要寫重複的代碼

Rule Of Three

第一次編寫代碼的時候,我們不考慮複用性;第二次遇到複用場景的時候,再進行重構使其複用。需要注意的是,“Rule of Three”中的“Three”并不是真的就指确切的“三”,這裡就是指“二”。

編碼原則

基于接口而非實作程式設計

做到

函數的命名不能暴露任何實作細節。

封裝具體的實作細節。

為實作類定義抽象的接口。具體的實作類都依賴統一的接口定義,遵從一緻的上傳功能協定。使用者依賴接口,而不是具體的實作類來程式設計

原則初衷

将接口和實作相分離,封裝不穩定的實作,暴露穩定的接口。上遊系統面向接口而非實作程式設計,不依賴不穩定的實作細節,這樣當實作發生變化的時候,上遊系統的代碼基本上不需要做改動,以此來降低代碼間的耦合性,提高代碼的擴充性。

原則使用

如果在我們的業務場景中,某個功能隻有一種實作方式,未來也不可能被其他實作方式替換,那我們就沒有必要為其設計接口,也沒有必要基于接口程式設計,直接使用實作類就可以了

越是不穩定的系統,我們越是要在代碼的擴充性、維護性上下功夫。相反,如果某個系統特别穩定,在開發完之後,基本上不需要做維護,那我們就沒有必要為其擴充性,投入不必要的開發時間。

組合優于繼承

繼承作用:表示 is-a 關系,支援多态特性,代碼複用

繼承最大的問題就在于:繼承層次過深、繼承關系過于複雜會影響到代碼的可讀性和可維護性

組合缺點:繼承改寫成組合意味着要做更細粒度的類的拆分。這也就意味着,我們要定義更多的類和接口。類和接口的增多也就或多或少地增加代碼的複雜程度和維護成本

組合并不完美,繼承也不是一無是處

繼承使用場景

如果類之間的繼承結構穩定(不會輕易改變),繼承層次比較淺(比如,最多有兩層繼承關系),繼承關系不複雜,我們就可以大膽地使用繼承。反之,系統越不穩定,繼承層次很深,繼承關系複雜,我們就盡量使用組合來替代繼承。

模闆模式

一些第三方類,我們無法改動其API而又想實作多态時。

組合使用場景

遇到繼承的缺點

子類和複用的父類不具備is-a的關系

裝飾者模式(decorator pattern)、政策模式(strategy pattern)、組合模式(composite pattern)設計模式

組合替代繼承

使用組合和接口的has-a關系來

用接口實作多态

代碼複用通過組合和委托實作

原則使用

劃分出單一職責的接口

實作接口

如果實作類實作接口的代碼相同,則可能引發重複問題

組合使用實作接口的類,使用委托技術

定義實作接口的類A,原先實作接口的類轉換為組合使用A,而不再實作接口

産品是如何誕生的

需求分析

借鑒其他競品

搜尋

體驗

線框圖

使用者故事

系統設計

合理的子產品劃分

運用高内聚、低耦合的思想

設計子產品與子產品之間的互動關系

互動方式

同步接口調用

簡單直接

上下遊系統

消息中間件異步調用

解耦效果好

同層系統間調用

設計子產品的接口、資料庫、業務模型

通用架構設計

需求分析

需求分析可以用清晰的清單展示

功能性需求

非功能性需求

易用性

要有産品意識。架構是否易內建、易插拔、跟業務代碼是否松耦合、提供的接口是否夠靈活等等,都是我們應該花心思去思考和設計的。有的時候,文檔寫得好壞甚至都有可能決定一個架構是否受歡迎。

性能

對于需要內建到業務系統的架構來說,我們不希望架構本身的代碼執行效率,對業務系統有太多性能上的影響。對于性能計數器這個架構來說,一方面,我們希望它是低延遲的,也就是說,統計代碼不影響或很少影響接口本身的響應時間;另一方面,我們希望架構本身對記憶體的消耗不能太大。擴充性

擴充性

架構留給業務代碼的擴充點

容錯性

容錯性這一點也非常重要。對于性能計數器架構來說,不能因為架構本身的異常導緻接口請求出錯。是以,我們要對架構可能存在的各種異常情況都考慮全面,對外暴露的接口抛出的所有運作時、非運作時異常都進行捕獲處理。

通用性

架構在設計的時候,要盡可能通用。我們要多去思考一下,除了接口統計這樣一個需求,還可以适用到其他哪些場景中

架構設計

TDD或最小原型

聚焦簡單應用場景

實作簡單原型,作為疊代設計的基礎

最終設計

子產品劃分

軟體開發

應對複雜軟體開發

從設計原則和思想的角度來看,如何應對龐大而複雜的項目開發?

uninx是開源開發的,那麼它是如何應對複雜軟體開發的呢?

Everything is a file in Uninx/linux

封裝

抽象

基于接口而非實作程式設計

分層與子產品化

程序排程、程序通信、記憶體管理、虛拟檔案系統、網絡接口

Unix 系統也是基于分層開發的,它可以大緻上分為三層,分别是核心、系統調用、應用層。

面對複雜系統的開發,我們要善于應用分層技術,把容易複用、跟具體業務關系不大的代碼,盡量下沉到下層,把容易變動、跟具體業務強相關的代碼,盡量上移到上層。

基于接口通信

如open() 檔案操作函數

高内聚低耦合(和抽象、封裝、基于接口通信相輔相成)

為擴充而設計

開閉原則

識别出代碼可變部分和不可變部分,将可變部分封裝起來,隔離變化,提供抽象化的不可變接口,供上層系統使用

KISS原則(和為擴充而設計沖突)

可讀性和擴充性沖突時,首選可讀性

最小驚奇原則

遵守開發/設計規範

在大型項目開發中尤其重要

從研發管理和開發技巧的角度來看,如何應對龐大而複雜的項目開發?

導緻代碼品質不高的原因有很多,比如:代碼無注釋,無文檔,命名差,層次結構不清晰,調用關系混亂,到處 hardcode,充斥着各種臨時解決方案等等

面對大型複雜項目的開發,如何長期保證代碼品質,讓代碼長期可維護?

吹毛求疵般地執行編碼規範

代碼review并修正

細節決定成敗,代碼規範的嚴格執行極為關鍵。

主人翁心态

編寫高品質的單元測試

內建測試、黑盒測試都很難測試全面,因為組合爆炸,窮舉所有測試用例的成本很高,幾乎是不可能的。單元測試就是很好的補充。它可以在類、函數這些細粒度的代碼層面,保證代碼運作無誤。底層細粒度的代碼 bug 少了,組合起來建構而成的整個系統的 bug 也就相應的減少了。

高品質的單元測試不僅僅要求測試覆寫率要高,還要求測試的全面性,除了測試正常邏輯的執行之外,還要重點、全面地測試異常下的執行情況。畢竟代碼出問題的地方大部分都發生在異常、邊界條件下。

不流于形式的 Code Review

關鍵還是要執行到位,不能流于形式。

開發未動、文檔先行

對大部分工程師來說,編寫技術文檔是件挺讓人“反感”的事情。一般來講,在開發某個系統或者重要子產品或者功能之前,我們應該先寫技術文檔,然後,發送給同組或者相關同僚審查,在審查沒有問題的情況下再開發。這樣能夠保證事先達成共識,開發出來的東西不至于走樣。而且,當開發完成之後,進行 Code Review 的時候,代碼審查者通過閱讀開發文檔,也可以快速了解代碼

除此之外,對于團隊和公司來講,文檔是重要的财富。對新人熟悉代碼或任務的交接等,技術文檔很有幫助。而且,作為一個規範化的技術團隊,技術文檔是一種摒棄作坊式開發和個人英雄主義的有效方法,是保證團隊有效協作的途徑。

持續重構、重構、重構

不要等到問題堆得太多了再去解決,要時刻有人對代碼整體品質負責任,平時沒事就改改代碼。千萬不要覺得重構代碼就是浪費時間,不務正業!

特别是一些業務開發團隊,有時候為了快速完成一個業務需求,隻追求速度,到處 hard code,在完全不考慮非功能性需求、代碼品質的情況下,堆砌爛代碼。實際上,這種情況還是比較常見的。不過沒關系,等你有時間了,一定要記着重構,不然爛代碼越堆越多,總有一天代碼會變得無法維護。

對項目與團隊進行拆分

面對大型複雜項目,我們不僅僅需要對代碼進行拆分,還需要對研發團隊進行拆分

每個小團隊對應負責一個小的項目(子產品、微服務等),這樣每個團隊負責的項目包含的代碼都不至于很多,也不至于出現代碼品質太差無法維護的情況。

聚焦在 Code Review 上來看,如何通過 Code Reviwe 保持項目的代碼品質?

為什麼要進行 Code Review(代碼審查),Code Review 的價值在哪裡?樹立正确的code review認知

Code Review 踐行“三人行必有我師

永遠不要覺得自己很厲害,寫的代碼就不需要别人 Review 了;永遠不要覺得自己水準很一般,就沒有資格給别人 Review 了;更不要覺得技術大牛讓你 Review 代碼隻是缺少你的一個“approve”

中國有句老話,“三人行必有我師”,我覺得用在這裡非常合适。即便自己覺得寫得已經很好的代碼,隻要經過不停地推敲,都有持續改進的空間。

Code Review 能摒棄“個人英雄主義”

如果一個人默默地寫代碼送出,不經過團隊的 Review,這樣的代碼蘊含的是一個人的智慧。代碼的品質完全依賴于這個人的技術水準。這就會導緻代碼品質參差不齊。如果經過團隊多人 Review、打磨,代碼蘊含的是整個團隊的智慧,可以保證代碼按照團隊中的最高水準輸出。

Code Review 能有效提高代碼可讀性

Code Review 是技術傳幫帶的有效途徑

Code Review 保證代碼不止一個人熟悉

Code Review 能打造良好的技術氛圍

好的技術氛圍也能降低團隊的離職率。

Code Review 是一種技術溝通方式

Code Review 能提高團隊的自律性

在開發過程中,難免會有人不自律,存在僥幸心理:反正我寫的代碼也沒人看,随便寫寫就送出了。Code Review 相當于一次代碼直播,曝光 dirty code,有一定的威懾力

如何在團隊中落地執行Code Review?

主要是排除認知障礙,樹立正确認知

如何開發一個通用的功能子產品

既然有了 JDK,為什麼 Google 還要開發一套新的類庫 Google Guava?是否是重複早輪子?兩者的差異化在哪裡?

如何在業務開發中,發現通用的功能子產品,以及如何将它開發成類庫、架構或者功能元件。

首先發現這些可能的通用子產品

我們要有善于發現、善于抽象的能力,并且具有紮實的設計、開發能力,能夠發現這些非業務的、可複用的功能點,并且從業務邏輯中将其解耦抽象出來,設計并開發成獨立的功能子產品

在業務開發中,跟業務無關的通用功能子產品,常見的一般有三類(他們的特點是複用,和業務無關):

類庫(library)

API

架構(framework)

DI

功能元件(component)

類似類庫,但可能內建三方元件,更加重量級

如何将它設計開發成一個優秀的類庫、架構或功能元件呢

産品意識

對于這些類庫、架構、功能元件的開發,我們不能閉門造車,要把它們當作“産品”來開發。這個産品是一個“技術産品”,我們的目标使用者是“程式員”,解決的是他們的“開發痛點”

是否易用、易內建、易插拔、文檔是否全面、是否容易上手等,這些産品素質也非常重要

服務意識

從心态上,别的團隊使用我們開發出來的技術産品,我們要學會感謝。這點很重要。心态不同了,做起事來就會有微妙的不同。

們還要有抽出大量時間答疑、充當客服角色的心理準備。有了這個心理準備,别的團隊的人在問你問題的時候,你也就不會很煩了。

如果沒有單獨的機會

我建議初期先把這些通用的功能作為項目的一部分來開發。不過,在開發的時候,我們做好子產品化工作,将它們盡量跟其他子產品劃清界限,通過接口、擴充點等松耦合的方式跟其他模式互動。等到時機成熟了,我們再将它從項目中剝離出來。因為之前子產品化做的好,耦合程度低,剝離出來的成本也就不會很高。

現狀

對于“如何做需求分析,如何做職責劃分?需要定義哪些類?每個類應該具有哪些屬性、方法?類與類之間該如何互動?如何組裝類成一個可執行的程式?”等等諸多問題,都沒有清晰的思路,更别提利用成熟的設計原則、思想或者設計模式,開發出具有高内聚低耦合、易擴充、易讀等優秀特性的代碼了。

可能原因

需求不明确

對策

面向對象分析主要的分析對象是“需求”,是以,面向對象分析可以粗略地看成“需求分析”。實際上,不管是需求分析還是面向對象分析,我們首先要做的都是将籠統的需求細化到足夠清晰、可執行。我們需要通過溝通、挖掘、分析、假設、梳理,搞清楚具體的需求有哪些,哪些是現在要做的,哪些是未來可能要做的,哪些是不用考慮做的

缺少鍛煉

相比單純的業務 CRUD 開發,鑒權這個開發任務,要更有難度。鑒權作為一個跟具體業務無關的功能,我們完全可以把它開發成一個獨立的架構,內建到很多業務系統中。而作為被很多系統複用的通用架構,比起普通的業務代碼,我們對架構的代碼品質要求要更高。開發這樣通用的架構,對工程師的需求分析能力、設計能力、編碼能力,甚至邏輯思維能力的要求,都是比較高的。如果你平時做的都是簡單的 CRUD 業務開發,那這方面的鍛煉肯定不會很多,是以,一旦遇到這種開發需求,很容易因為缺少鍛煉,腦子放空,不知道從何入手,完全沒有思路。

對策

盡管針對架構、元件、類庫等非業務系統的開發,我們一定要有元件化意識、架構意識、抽象意識,開發出來的東西要足夠通用,不能局限于單一的某個業務需求,但這并不代表我們就可以脫離具體的應用場景,悶頭拍腦袋做需求分析。多跟業務團隊聊聊天,甚至自己去參與幾個業務系統的開發,隻有這樣,我們才能真正知道業務系統的痛點,才能分析出最有價值的需求。不過,針對鑒權這一功能的開發,最大的需求方還是我們自己,是以,我們也可以先從滿足我們自己系統的需求開始,然後再疊代優化。

由簡入難,不斷優化

劃分職責進而識别出有哪些類

定義類及其屬性和方法

定義類與類之間的互動關系

将類組裝起來并提供執行入口