技术提升的一个方法

最近看到知乎上的专栏https://zhuanlan.zhihu.com/c_183152541,感触颇多。

要突破自己首先的热爱编程,敬畏程序。毕竟有道无术,术尚可求,有术无道,止于术也。

你发现你的开发过程中总是需要重复复制-稍微改吧改吧-粘贴这个过程,你有没有去尝试阅读并实践《重构》?

你有没有尝试去自己发布一个公用的库?JCenter/Maven Central都是免费的。

你发现你每次修改完代码要抄起Postman点来点去

你有没有尝试去编写一个集成测试,代替手工的劳动?

你发现你碰到了很多奇奇怪怪你搞不明白的问题,只能一次次地尝试每个搜索结果中提到的解决方案,期望其中的某一个好使。
你有没有尝试过去阅读相关的书籍,查阅相关文档?

如何突破自己
如何成为顶级程序员
跳出弱鸡循环1
跳出弱鸡循环2

总结下来提升手段:
1.实践自动化测试

当你觉得技术上遇到瓶颈的时候,可以通过学习和实践自动化测试,来补全自己在相关工程领域的技术短板,提升自己的技术能力
从测试中学到的不仅是写测试用例的能力,而是一整套的工程化、自动化的能力。

实践流程
第一步,去看一下《Maven实战》,了解一下Maven的测试是怎么工作的。之所以让你去研究Maven,是因为你们这种系统,99%不会采用Gradle这种新技术的。
第二步,写第一个测试,代码如下:

public class MyTest {
    @Test
    public void 跳出弱鸡循环() {
    }
}

第三步,去搞一份你们线上数据库的表结构。各种数据库都有相应的命令dump表结构。有困难的的话,手写建表语句。
第四步,本地用Docker启动一个临时的数据库。
第五步,去研究一下flyway,用自动化方式把表结构灌到这个临时数据库里。
第六步,去了解一下你们的应用是怎么部署的,你们上线的应用不可能是通过在IDE里面点绿色三角来部署的。把部署的命令行要过来。
第七步,研究一下这个命令行,尝试在本地启动起来。碰到数据库没起来的问题,就把连接串改成刚刚那个Docker的临时数据库。
第八步,你平时怎么在网页上点点点测试的,把它翻译成Java。比如你平时会手工测试登录接口,那就用HttpClient写一段代码,模拟登录。
第九步,把上面这些整合起来:

public class MyTest {
    @Test
    public void 跳出弱鸡循环() {
        启动测试数据库();
        把表结构灌进去();
        本地启动应用();
        自动化方式测试接口();
    }
}

继续阅读“技术提升的一个方法”

Java CountDownLatch 和 CyclicBarrier 示例

复习了一下 JCIP 回顾一下同步工具类的使用

CountDownLatch


import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CountDownLatchExample1 {

	private final static int threadCount = 200;

	public static void main(String[] args) throws Exception {

		ExecutorService exec = Executors.newCachedThreadPool();

		final CountDownLatch countDownLatch = new CountDownLatch(threadCount);

		for (int i = 0; i < threadCount; i++) {
			final int threadNum = i;
			exec.execute(() -> {
				try {
					test(threadNum);
				} catch (Exception e) {
					e.printStackTrace();
				} finally {
					countDownLatch.countDown();
				}
			});
		}
		countDownLatch.await();
		System.out.println("finish");
		exec.shutdown();
	}

	private static void test(int threadNum) throws Exception {
		Thread.sleep(100);
		System.out.println("{" + threadNum + "}");
		Thread.sleep(100);
	}
}

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class CountDownLatchExample2 {
	private final static int threadCount = 200;

	public static void main(String[] args) throws Exception {

		ExecutorService exec = Executors.newCachedThreadPool();

		final CountDownLatch countDownLatch = new CountDownLatch(threadCount);

		for (int i = 0; i < threadCount; i++) {
			final int threadNum = i;
			exec.execute(() -> {
				try {
					test(threadNum);
				} catch (Exception e) {
					e.printStackTrace();
				} finally {
					countDownLatch.countDown();
				}
			});
		}
                //超时之后不阻塞,直接继续运行
		countDownLatch.await(10, TimeUnit.MILLISECONDS);
		System.out.println("finish");
		exec.shutdown();
	}

	private static void test(int threadNum) throws Exception {
		Thread.sleep(100);
		System.out.println("{" + threadNum + "}");
	}
}
import java.util.concurrent.CountDownLatch;

public class TestCountDownLatch {

	public long timeTasks(int nThreads, final Runnable task) throws InterruptedException {

		final CountDownLatch startGate = new CountDownLatch(1);
		final CountDownLatch endGate = new CountDownLatch(nThreads);

		for (int i = 0; i < nThreads; i++) {
			Thread thread = new Thread() {
				@Override
				public void run() {
					try {
						startGate.await();
						try {
							task.run();
						} finally {
							endGate.countDown();
						}
					} catch (InterruptedException e) {

					}

				}
			};
			thread.start();
		}

		long start = System.nanoTime();
		startGate.countDown();
		endGate.await();
		long end = System.nanoTime();
		return end - start;
	}

	public static void main(String[] args) {
		int nTask = 10;
		TestCountDownLatch testCountDownLatch = new TestCountDownLatch();
		try {
			long timeUse = testCountDownLatch.timeTasks(nTask, new Task());
			System.out.println(nTask + " use time " + timeUse);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

	}

}

class Task implements Runnable {

	@Override
	public void run() {
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}

CyclicBarrier

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CyclicBarrierExample1 {

	private static CyclicBarrier barrier = new CyclicBarrier(5);

	public static void main(String[] args) throws Exception {

		ExecutorService executor = Executors.newCachedThreadPool();

		for (int i = 0; i < 10; i++) {
			final int threadNum = i;
			Thread.sleep(1000);
			executor.execute(() -> {
				try {
					race(threadNum);
				} catch (Exception e) {
					e.printStackTrace();
				}
			});
		}
		executor.shutdown();
	}

	private static void race(int threadNum) throws Exception {
		Thread.sleep(1000);
		System.out.println("{" + threadNum + "} is ready ");
		barrier.await();
		System.out.println("{" + threadNum + "} continue ");
	}
}
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class CyclicBarrierExample2 {
	private static CyclicBarrier barrier = new CyclicBarrier(5);

	public static void main(String[] args) throws Exception {

		ExecutorService executor = Executors.newCachedThreadPool();

		for (int i = 0; i < 10; i++) {
			final int threadNum = i;
			Thread.sleep(1000);
			executor.execute(() -> {
				try {
					race(threadNum);
				} catch (Exception e) {
					e.printStackTrace();
				}
			});
		}
		executor.shutdown();
	}

	private static void race(int threadNum) throws Exception {
		Thread.sleep(1000);
		System.out.println("{" + threadNum + "} is ready");
		try {
                        //等待超时之后直接放开栅栏,让线程执行,不需要等到规定线程数都就位
			barrier.await(2000, TimeUnit.MILLISECONDS);
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println("{" + threadNum + "} continue");
	}
}
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CyclicBarrierExample3 {
        //开始之前执行回调函数
	private static CyclicBarrier barrier = new CyclicBarrier(5, () -> {
		System.out.println("callback is running");
	});

	public static void main(String[] args) throws Exception {

		ExecutorService executor = Executors.newCachedThreadPool();

		for (int i = 0; i < 10; i++) {
			final int threadNum = i;
			Thread.sleep(1000);
			executor.execute(() -> {
				try {
					race(threadNum);
				} catch (Exception e) {
					e.printStackTrace();
				}
			});
		}
		executor.shutdown();
	}

	private static void race(int threadNum) throws Exception {
		Thread.sleep(1000);
		System.out.println("{" + threadNum + "} is ready");
		barrier.await();
		System.out.println("{" + threadNum + "} continue");
	}
}

缓存的使用和设计

原文地址 [本文整理发布]

缓存的一些基本常识

  • Cache(缓存): 从cpu的一级和二级缓存、Internet的DNS、到浏览器缓存都可以看做是一种缓存。(存贮数据(使用频繁的数据)的临时地方,因为取原始数据的代价太大了,所以我可以取得快一些)
  • Cache hit(缓存命中) Cahe miss(缓存未命中)

  • 缓存算法:缓存容量超过预设,如何踢掉“无用”的数据。例如:LRU(Least Recently Used) FIFO(First Input First Output)Least Frequently Used(LFU) 等等

  • System-of-Record(真实数据源): 例如关系型数据库、其他持久性系统等等。 也有英文书叫做authority data(权威数据)

  • serialization-and-deserialization(序列化与反序列化):可以参考:序列化与反序列化(美团工程师写的,非常棒的文章)
    serialization-and-deserialization

  • Scale Up (垂直扩容) 和 Scale out (水平扩容), 驴拉车,通常不是把一头驴养壮(有极限),而通常是一群驴去拉(当然每个个体也不能太差)。

    64258e75-ad29-32ef-ac55-45d95278ff0a

  • Write-through 和 write-behind
    154e49f4-f223-3b4f-9346-fa378dd2efd1

  • 阿姆而达定律:用于计算缓存加速比

  • LocalCache(独立式): 例如Ehcache、BigMemory Go
    (1) 缓存和应用在一个JVM中。
    (2) 缓存间是不通信的,独立的。
    (3) 弱一致性。

  • Standalone(单机):
    (1) 缓存和应用是独立部署的。
    (2) 缓存可以是单台。(例如memcache/redis单机等等)
    (3) 强一致性
    (4) 无高可用、无分布式。
    (5) 跨进程、跨网络

  • Distributed(分布式):例如Redis-Cluster, memcache集群等等
    (1) 缓存和应用是独立部署的。
    (2) 多个实例。(例如memcache/redis等等)
    (3) 强一致性或者最终一致性
    (4) 支持Scale Out、高可用。
    (5) 跨进程、跨网络

  • Replicated(复制式): 缓存数据时同时存放在多个应用节点的,数据复制和失效的事件以同步或者异步的形式在各个集群节点间传播。(也是弱一致性)这种用的不太多。

  • 数据层访问速度
    082dfcc7-1a06-3116-886f-055d4af98cf8
    继续阅读“缓存的使用和设计”

一次Memcached使用问题

收到群里反馈很多PHP请求执行超时,服务已经不可用。

查看服务器监控数据

发现 CPU 负载 在那段时间一直爬坡。 查询fpm slow 日志 发现请求都卡在了 memcache get 的方法上面。

发现出问题的请求有一个方法 频繁的读写一个key 这个 key 保存的内容 内存已经大于1MB, 一秒内有 500 多个请求,同时读写会导致memcache 的性能急剧下降。

自己写代码重现这个问题:
php 脚本 test-memcached.php:

<?php
$time_start = microtime(true);

$ac = new Memcached();
$ac->addServer('localhost', 11211);

#$ac->setOption(Memcached::OPT_SOCKET_SEND_SIZE, 1024*512);
#$ac->setOption(Memcached::OPT_SOCKET_RECV_SIZE, 1024*512);
#$ac->setOption(Memcached::OPT_RECV_TIMEOUT, 5);
#$ac->setOption(Memcached::OPT_SEND_TIMEOUT, 5);

$data = [];

for($i = 0; $i<30000; $i++){
  $data[] = md5($i);
}

$r = $ac->set('key', $data, 3600);
$set_time_end = microtime(true);
$set_time = $set_time_end - $time_start;
echo "Set Key Done in $set_time seconds \n";
$result = $ac->get('key');
$read_time_end = microtime(true);
$read_time = $read_time_end - $time_start;
echo "Read key Done in $read_time seconds \n";
$time_end = microtime(true);
$time = $time_end - $time_start;
echo "Did nothing in $time seconds\n";
?>

shell 测试脚本:

#!/bin/bash
date
for i in `seq 1 300`
do
{
 php test-memcached.php
 sleep 1
}&
done
wait #等待执行完成
date

分别修改 memcache key 保存内容的大小得到结果:

A:

for($i = 0; $i<30000; $i++){
  $data[] = md5($i);
}

B:

for($i = 0; $i<3; $i++){
  $data[] = md5($i);
}

测试结果:

A:

Did nothing in 18.769073009491 seconds
Did nothing in 14.950340032578 seconds
Did nothing in 19.823235034943 seconds
Did nothing in 20.014719009399 seconds
Did nothing in 20.894814014435 seconds
Did nothing in 20.827455997467 seconds
Did nothing in 15.345555067062 seconds
Did nothing in 18.984731197357 seconds
Did nothing in 21.08841586113 seconds

B:

Did nothing in 0.39238500595093 seconds
Did nothing in 0.42710494995117 seconds
Did nothing in 0.34245300292969 seconds
Did nothing in 0.084210872650146 seconds
Did nothing in 0.41426110267639 seconds
Did nothing in 0.12554693222046 seconds
Did nothing in 0.10487604141235 seconds
Did nothing in 0.60212993621826 seconds
Did nothing in 0.054439067840576 seconds
Did nothing in 0.11286902427673 seconds
Did nothing in 0.69583892822266 seconds
Did nothing in 0.14684200286865 seconds
Did nothing in 0.082473993301392 seconds
Did nothing in 0.34351587295532 seconds
Did nothing in 0.10698294639587 seconds


看结果可以得知 但 memcache 保存的内容变大的时候速度会非常慢,所以使用memcache 的时候要注意缓存值得大小。

memcache 缓存值最大大小是1M 引用 Oracle 网站的一个 FAQ

The default maximum object size is 1MB. In memcached 1.4.2 and later, you can change the maximum size of an object using the -I command line option.

For versions before this, to increase this size, you have to re-compile memcached. You can modify the value of the POWER_BLOCK within the slabs.c file within the source.

In memcached 1.4.2 and higher, you can configure the maximum supported object size by using the -I command-line option. For example, to increase the maximum object size to 5MB:

$ memcached -I 5m

If an object is larger than the maximum object size, you must manually split it. memcached is very simple: you give it a key and some data, it tries to cache it in RAM. If you try to store more than the default maximum size, the value is just truncated for speed reasons.

出现服务不可用的一个重要原因也有写缓存的次数和读缓存次数一样多。

最后修改测试脚本 依然保存很大的缓存值,但是只写入一次,测试并发的读取,基本都可以在1秒左右的读取到数据。

结论:
1.使用memcache的要注意缓存内容的大小,不要保存太大的内容在一个key上
2.缓存是读的次数远远大于写的次数,如果代码中发现写缓存的次数和读缓存的次数一样的时候要提高警惕
3.千万不要在数据库事务未提交的时候删除缓存

QA:
1.为啥使用MD5字符串?
因为memcache会自动压缩内容,如果用普通重复字符串测试效果不明显,线上出现问题的场景也是MD5串的缓存值,可以开启和关闭压缩选项?

测试代码下载

打印杨辉三角

构造杨辉三角逻辑并不难,主要打印格式的对齐比较困难.

public class Main {

	/**
	 * 计算行应该占得宽度
	 * 每个数字额外加了一个空格,数字的宽度加空格的宽度
	 * @param lastIntegers
	 * @return
	 */
	static int width_of(int[] lastIntegers) {
		int width = 0;
		int length = 0;
		for (int lastInteger : lastIntegers) {
			if (lastInteger > 0) {
				width += numWidth(lastInteger);
				length++;
			}
		}
		width += length;
		return width;
	}

	static int numWidth(int num) {
		int count = 0;
		while (num > 0) {
			num = num / 10;
			count++;
		}
		return count;
	}

	public static void main(String[] args) {
		int n = 10;
		int[][] result = new int[n][n];
		result[0][0] = 1;
		result[1][0] = 1;
		result[1][1] = 1;
		//构造杨辉三角
		for (int i = 2; i < n; i++) {
			result[i][0] = 1;
			result[i][i] = 1;
			for (int j = 1; j <= i - 1; j++) {
				result[i][j] = result[i - 1][j - 1] + result[i - 1][j];
			}
		}
		int maxWidth = width_of(result[n - 1]);
		for (int i = 0; i < n; i++) {
			int[] temp = result[i];
			int temp_width = width_of(temp);
			int wihteBlankLength = (maxWidth - temp_width) / 2;
			for (int k = 0; k < wihteBlankLength; k++) {
				System.out.print(" ");
			}
			for (int j = 0; j < temp.length; j++) {
				if (temp[j] > 0)
					System.out.print(String.format("%d", temp[j]) + " ");
			}
			System.out.println();
		}
	}

}

HTTPS数字证书与验证

数字证书作用
我们知道HTTPS比HTTP安全,它的安全在于通信过程被加密。然而加密算法是用对称加密,也就是说,客户端和服务端采用一个相同的密钥。为了让双方得到这个密钥,前期就有一个很重要的工作:协商密钥。
现在我们简单模拟一下通信过程:

客户端:hi,我准备跟你(xx.com)建立HTTPS通信。
服务端:好的,我就是xx.com,这是我的证书,你验证一下。
客户端:验证通过了,你的确是xx.com,我把密钥发给你,下面的通信咱们就加密了。
服务端:s&&(*3u247(
客户端:(&DY&#%%&#
上述只是简化后的过程, 我们可以看到协商密钥中有一个很重要的步骤:服务端要证明自己是xx.com。如何证明?证书+链式验证!
继续阅读“HTTPS数字证书与验证”

关于ThreadLocal的一些反思

ThreadLocal 是一个用于提供线程局部变量的工具类,它主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。例如Spring 中的单例Bean在多线程调用的时候,并行计算的时候各个线程缓存自己的中间计算结果等场景。

ThreadLocal 是解决线程安全问题一个很好的思路,它通过为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。在很多情况下,ThreadLocal 比直接使用 synchronized 同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

class PrintRunnable extends Runnable {
  val number = new ThreadLocal[Double]

  override def run(): Unit = {
    number.set(Math.random())
    println(number.get())
  }
}

object SimpleApp {
  def main(args: Array[String]): Unit = {
    val printRunnable = new PrintRunnable

    val thread1 = new Thread(printRunnable)
    val thread2 = new Thread(printRunnable)

    thread1.start()
    thread2.start()

    thread1.join()
    thread2.join()
  }
}
0.5157676349493098
0.37557496403907353

但是在父子线程要传递一些变量的时候会发现数据不能共享,需要 InheritableThreadLocal 来实现父子线程变量的传递。

class PrintRunnable extends Runnable {
  val number = new ThreadLocal[Double]

  override def run(): Unit = {
    number.set(Math.random())
    println(number.get())
    
    val childThread = new Thread(new Runnable {
      override def run(): Unit = {
        println(number.get())
      }
    })
    childThread.start()
    childThread.join()
  }
}

object SimpleApp {
  def main(args: Array[String]): Unit = {
    val printRunnable = new PrintRunnable

    val thread1 = new Thread(printRunnable)
    val thread2 = new Thread(printRunnable)

    thread1.start()
    thread2.start()

    thread1.join()
    thread2.join()
  }
}
0.5475226099407153
0.8376546404552231
null
null

正确的做法:

class PrintRunnable extends Runnable {
  val number = new InheritableThreadLocal[Double]

  override def run(): Unit = {
    number.set(Math.random())
    println(number.get())

    val childThread = new Thread(new Runnable {
      override def run(): Unit = {
        println(number.get())
      }
    })
    childThread.start()
    childThread.join()
  }
}

object SimpleApp {
  def main(args: Array[String]): Unit = {
    val printRunnable = new PrintRunnable

    val thread1 = new Thread(printRunnable)
    val thread2 = new Thread(printRunnable)

    thread1.start()
    thread2.start()

    thread1.join()
    thread2.join()
  }
}
0.006425375134899158
0.021932306310074368
0.006425375134899158
0.021932306310074368

在子线程中不能修改父线程的变量:

class PrintRunnable extends Runnable {
  val number = new InheritableThreadLocal[Double]

  override def run(): Unit = {
    number.set(Math.random())
    println(number.get())

    val childThread = new Thread(new Runnable {
      override def run(): Unit = {
        println(number.get())
        number.set(0.1)
      }
    })
    childThread.start()
    childThread.join()
    println(number.get())
  }
}

object SimpleApp {
  def main(args: Array[String]): Unit = {
    val printRunnable = new PrintRunnable
    val thread1 = new Thread(printRunnable)
    thread1.start()
    thread1.join()
  }
}
0.7413853012849937
0.7413853012849937
0.7413853012849937

但是现实开发中我们更多的是使用线程池管理线程,线程复用怎么解决ThreadLocal变量的传递或者跨线程池传递。如下面场景:分布式跟踪系统,日志收集记录系统上下文,Session级Cache,应用容器或上层框架跨应用代码给下层SDK传递信息。
可以看一下这个开源项目transmittable-thread-local

What does “LC_ALL=C” do?

$ LC_ALL=es_ES man
¿Qué página de manual desea?
$ LC_ALL=C man
What manual page do you want?
$ LC_ALL=en_US sort <<< $'a\nb\nA\nB'
a
A
b
B
$ LC_ALL=C sort <<< $'a\nb\nA\nB'
A
B
a
b

面向对象六大原则

单一职责原则(Single-Resposibility Principle)。

“对一个类而言,应该仅有一个引起它变化的原因。”本原则是我们非常熟悉地”高内聚性原则”的引申,但是通过将”职责”极具创意地定义为”变化的原因”,使得本原则极具操作性,尽显大师风范。同时,本原则还揭示了内聚性和耦合生,基本途径就是提高内聚性;如果一个类承担的职责过多,那么这些职责就会相互依赖,一个职责的变化可能会影响另一个职责的履行。其实OOD的实质,就是合理地进行类的职责分配。

开放封闭原则(Open-Closed principle)。

“软件实体应该是可以扩展的,但是不可修改。”本原则紧紧围绕变化展开,变化来临时,如果不必改动软件实体裁的源代码,就能扩充它的行为,那么这个软件实体设计就是满足开放封闭原则的。如果说我们预测到某种变化,或者某种变化发生了,我们应当创建抽象类来隔离以后发生的同类变化。在Java中,这种抽象是指抽象基类或接口;在C++中,这各抽象是指抽象基类或纯抽象基类。当然,没有对所有情况都贴切的模型,我们必须对软件实体应该面对的变化做出选择。

Liskov替换原则(Liskov-Substituion Principle)。

“子类型必须能够替换掉它们的基类型。”本原则和开放封闭原则关系密切,正是子类型的可替换性,才使得使用基类型模块无需修改就可扩充。Liskov替换原则从基于契约的设计演化而来,契约通过为每个方法声明”先验条件”和”后验条件”;定义子类时,必须遵守这些”先验条件”和”后验条件”。当前基于契的设计发展势头正劲,对实现”软件工厂”的”组装生产”梦想是一个有力的支持。

依赖倒置原则(Dependecy-Inversion Principle)。

“抽象不应依赖于细节,细节应该依赖于抽象。”本原则几乎就是软件设计的正本清源之道。因为人解决问题的思考过程是先抽象后具体,从笼统到细节,所以我们先生产出的势必是抽象程度比较高的实体,而后才是更加细节化的实体。于是,”细节依赖于抽象”就意味着后来的依赖于先前的,这是自然而然的重用之道。而且,抽象的实体代表着笼而统之的认识,人们总是比较容易正确认识它们,而且本身也是不易变的,依赖于它们是安全的。依赖倒置原则适应了人类认识过程的规律,是面向对象设计的标志所在。

接口隔离原则(Interface-Segregation Principle)。

“多个专用接口优于一个单一的通用接口。”本原则是单一职责原则用于接口设计的自然结果。一个接口应该保证,实现该接口的实例对象可以只呈现为单一的角色;这样,当某个客户程序的要求发生变化,而迫使接口发生改变时,影响到其他客户程序的可能生性小。

良性依赖原则。

“不会在实际中造成危害的依赖关系,都是良性依赖。”通过分析不难发现,本原则的核心思想是”务实”,很好地揭示了极限编程(Extreme Programming)中”简单设计”各”重构”的理论基础。本原则可以帮助我们抵御”面向对象设计五大原则”以及设计模式的诱惑,以免陷入过度设计(Over-engineering)的尴尬境地,带来不必要的复杂性。