程序设计艺术与方法课程设计报告 (2)
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
程序设计方法与艺术
课
程
报
告
班级:计算机科学与技术班
指导老师:徐本柱
组长:2013211685 黄俊祥
组员:2013211684 姜文鹏
2013211705 李东明
2013211707 袁清高
解题报告
题目A :First Blood
解题思路:
我的思路是首先取出俩个数,求出这俩个数的最大公约数,然后再用最大公约数求出这俩个数的最小公倍数。
将这俩个数的最小公倍数与第三个数求最大公约数,再求出最小公倍数即可。
具体解法:
首先任取俩个数,比较这俩个数的大小,用大的数除小的数看其是否为零,如果为零,则求出其最大公约数,如果不为零,取其余数继续。
求出最大公约数后用俩个数的乘积除以最大公约数既得最小公倍数。
接着按照这个方法就能求出三个数的最小公倍数。
代码实现:
#include<iostream>
using namespace std;
//最大公约数
int maxCommonDivisor(int i, int j){
int temp;
if (i < j){
temp = i;
i = j;
j = temp;
}
if (i%j == 0){
return j;
}
else{
return maxCommonDivisor(i%j, j);
}
}
//最小公倍数
int minCommonMultiplier(int i,int j,int k){
int mcd = maxCommonDivisor(i, j);
int mcm = i*j / mcd;
mcd = maxCommonDivisor(mcm, k);
return mcm*k / mcd;
}
//最大值
int maxValue(int val){
int mcm = 0;
int temp;
for (int i = 1; i <= val; i++){
for (int j = i; j <= val; j++){
for (int k = j; k <= val; k++){
temp = minCommonMultiplier(i, j, k);
if (mcm < temp)
mcm = temp;
}
}
}
return mcm;
}
int main(){
int a, b,c;
int n;
cin >> n;
for (int i = 0; i < n; i++){
cin >> a;
cout << maxValue(a) << endl;
}
system("pause");
return 0;
}
运行结果:
题目B 求和
解题思路:
首先定义一个函数,让函数满足题目中的条件,用bool来判断isMinus是否正确,定义整数i,j,通过条件循环来完成。
主函数则是输出函数值。
具体解法:
代码实现:
#include<iostream>
using namespace std;
int f(int n, int k){
bool isMinus = false;
int value=0;
for (int i = 1; i <= n;){
for (int j = 1; (j <= k)&&(i<=n); j++){
if (isMinus == false){
value -= i;
i++;
}
else{
value += i;
i++;
}
}
isMinus = !isMinus;
}
return value;
}
int main(){
int t, n, k;
cin>>t;
for (int i = 0; i < t; i++){
cin >> n >> k;
cout << f(n, k) << endl;
}
system("pause");
}
运行结果:
题目C LU的疑惑
解题思路:
斜率:(1)斜率不存在
(2)斜率存在不重复
(3)斜率存在重复
创建存放点的结构体
创建能够存储可变元素数量的容器
具体解法:
(1)创建vector容器,用以存放、插入数量可变的点集合以及斜率数目可变的数组(2)创建单点数据结构struct point
(3)根据输入创建多级for循环
代码实现:
#include<iostream>
#include<vector>
using namespace std;
struct Point{
double x, y;
};
vector<Point> p;
vector<double> gradient;
void check(double grad){
for (int i = 0; i < gradient.size(); i++){
if (grad == gradient[i])
return;
}
gradient.push_back(grad);
}
void out(){
for (int i = 0; i < p.size(); i++){
for (int j = i + 1; j < p.size(); j++){
if ((p[j].x - p[i].x) != 0){
double grad = (p[j].y - p[i].y) / (p[j].x - p[i].x);
check(grad);
}
}
}
}
int main(){
int t,n;
double x, y;
Point pTemp;
cin >> t;
for (int i = 0; i < t; i++){
cin >> n;
for (int j = 0; j < n; j++){
cin >> x >> y;
pTemp.x = x;
pTemp.y = y;
p.push_back(pTemp);
}
out();
cout << gradient.size() << endl;
p.clear();
gradient.clear();
}
system("pause");
return 0;
}
运行结果:
题目D 瑞文上单不给就送
解题思路:
重点:使用角色与概率数组之间的联系
当所求的次数为1时,结果应该是m
具体解法:
(1)创建二维数组用以盛放概率矩阵
(2)当n=1时,输入输出相等
(3)利用多层循环,完成次数n和概率排序比较
代码实现:
#include<iostream>
using namespace std;
double a[5][5];
void init(){
for (int i = 0; i < 5; i++){
for (int j = 0; j < 5; j++){
a[i][i] = 0;
}
}
}
void check(){
int val=0;
for (int i = 0; i < 5; i++){
for (int j = 0; j < 5; j++){
val += a[i][j];
}
if (val != 1){
cout << "ERROR:各行概率相加不为1" << endl;
exit(1);
}
}
}
void probolity(int m,int n){
int temp=0;
m = m - 1;
for (int i = 2; i <= n; i++){
for (int j = 0; j < 5; j++){
if (a[temp][m] < a[j][m])
temp = j;
}
m = temp;
temp = 0;
}
cout << m+1<<endl;
}
int main(){
int T, N,m;
double x;
cin >> T;
for (int t = 0; t < T; t++){
cin >> N;//第n次游戏
for (int i = 0; i < 5; i++){
for (int j = 0; j < 5; j++){
cin >> x;
a[i][j] = x;
}
}
cin >> m;//第一次游戏玩的角色
probolity(m, N);
init();
}
system("pause");
return 0;
}
运行结果:
题目F 多重部分和问题
解题思路
使用动态规划求解,dp[i][j]=用前i种数字是否能加成j
具体解法
利用此循环构建数组dp
如果dp[n][k]为true,表示能够从这些数字中选出若干使他们的和恰好为K。
代码实现
#include <iostream>
using namespace std;
#define maxn 100
#define maxk 100000
int a[maxn], m[maxn];
bool dp[maxn][maxk];
void init() {
for (int i = 0; i < maxn; i++) {
for (int j = 0; j < maxk; j++) {
dp[i][j] = false;
}
}
}
void select(int n,int K) {
dp[0][0] = true;
for (int i = 0; i < n; i++)
for (int j = 0; j <= K; j++)
for (int k = 0; k <= m[i] && k*a[i] <= j; k++) {
dp[i + 1][j] |= dp[i][j - k*a[i]];
}
}
int main()
{
int T,n, K;
cin >> T;
for (int temp = 0; temp < T; temp++) {
cin >> n;
for (int i = 0; i < n; i++)
cin >> a[i];
for (int i = 0; i < n; i++)
cin >> m[i];
cin >> K;
select(n,K);
if (dp[n][K])cout << "yes" << endl;
else cout << "no" << endl;
init();
}
system("pause");
return 0;
}
运行结果
题目G 你来擒孟获
解题思路
本题需要求出从s到每个点l[i].s、每个点到t的最短距离l[i].t,然后找出这两个数l[i].s、l[i].t之和的最大值就是最短时间。
具体解法
本题需要用Floyd算法求出各点至各点的最短距离:
然后计算两个数l[i].s、l[i].t之和的最大值。
代码实现
#include<iostream>
using namespace std;
#define MAX 10001
#define MAXN 1000
#define MAXM 10000
struct length {
int s;
int t;
};
length l[MAXN];
int road[MAXN][MAXN];
int T, n, m, s, t, i, j, k;
//t组测试数据n山寨数量m山寨间道路数量
//i,j,k表示山寨i和山寨j间有一条路,在这路上需要行军k天//s起点编号t终点编号
//floyd算法
void floyd() {
for (int k = 0; k < n; k++) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n;j++) {
if (road[i][k] + road[k][j] < road[i][j]) {
//找到更短路径
road[i][j] = road[i][k] + road[k][j];
}
}
}
}
}
void init() {
for (int i = 0; i < MAXN; i++) {
if (i == s) {
l[i].s = 0;
}
else if (i == t) {
l[i].t = 0;
}
else {
l[i].s = MAX;
l[i].t = MAX;
}
for (int j = 0; j < MAXN; j++) {
if (i == j)
road[i][j] = 0;
else
road[i][j] = MAX;
}
}
}
void minimum() {
floyd();
int min=0;
//每个点到s与t的最短距离
for (int i = 0; i < n; i++) {
l[i].s = road[s][i];
l[i].t = road[i][t];
}
//求l[i].s+l[i].t最大值
for (int i = 0; i < n; i++) {
if (i != s && i != t) {
if (min < (l[i].s + l[i].t))
min = l[i].s + l[i].t;
}
}
cout << min << endl;
}
int main() {
cin >> T;
for (int a = 0; a < T; a++) {
init();
cin >> n >> m;
for (int b = 0; b< m; b++) {
cin >> i >> j >> k;
if (road[i][j] > k) {
road[i][j] = k;
road[j][i] = k;
}
}
cin >> s >> t;
minimum();
}
system("pause");
return 0;
}
运行结果
题目H 数7
数7是一个简单的饭桌游戏,有很多人围成一桌,先从任意一人开始数数,1、2、3……那样数下去,逢到7的倍数(7、14、21……)和含有7的数字(17、27……)必须以敲桌子代替。
如果有谁逢7却数出来了,就要接受惩罚。
小明觉得这个游戏太简单了,于是对它做出了改进,那就是每逢到素数的时候就以敲桌子代替,并且数数的方向发生改变,而且最开始的那个人可以从1到10000中选一个合数,开始数数。
假设现在有10个人,第一个人的编号为1,他选择4开始数,由于4不是素数,那么就是1说:4,轮到下一个编号为2的人来数,由于5是素数,2敲桌子(duang),由于5是素数,顺序发生变化,现在又轮到1说:6,然后以此类推10敲桌子(duang),1说:8,2说:9…一直到某人出现错误为止。
小明想知道轮到自己的时候应该干什么,你能够帮小明解决这个问题吗?
解题思路:
抽象游戏过程,给定开始编号和数字,通过判断数字是否为素数决定编号增或减,数字则一直增加,直到编号等于小明的编号时,程序结束。
具体解法:
设置标记记录当前游戏方向,编号增大方向为unchanged=true;
数字递增,通过调用函数IsPrimeNumber()依次判断是否为素数,结合当前游戏方向,使编号加1或减1;
循环至编号值与小明编号相等时终止,再次判断是否为素数,然后做出动作。
代码实现:
#include<iostream>
#include<cmath>
#include<vector>
using namespace std;
bool IsPrimeNumber(int a)
{
for(int i=2;i<=sqrt(a);i++)
if(a%i==0)
return false;
return true;
}
void WhatToDo(int n,int m,int person,int number)
{
bool unchanged=true; //设置标记,改变报数方向时该值取反
person-=1; //计算时编号改为从0开始
while(person!=m-1)
{
if(IsPrimeNumber(number))
{
if(unchanged)
person=(person+n-1)%n;
else
person=(person+1)%n;
unchanged=!unchanged;
}
else
{
if(unchanged)
person=(person+1)%n;
else
person=(person+n-1)%n;
}
number+=1;
}
if(IsPrimeNumber(number))
cout<<"duang"<<endl;
else
cout<<number<<endl;
}
int main()
{
int t;
cin>>t; //输入整数t,表示有t组数据
int n,m,a,b;
vector<int> N,M,A,B;
for(int i=0;i<t;i++)
{
cin>>n>>m;
N.push_back(n);
M.push_back(m);
cin>>a>>b;
A.push_back(a);
B.push_back(b);
}
for(int i=0;i<t;i++)
WhatToDo(N[i],M[i],A[i],B[i]);
system("pause");
return 0;
}
运行结果:
题目I 梯田
土豪YZK在一块小岛上有着一大片n*m的梯田,每块1*1的田地都有它的高度。
奴隶们不甘被YZK剥削,他们联合起来决定发动一场海啸淹掉YZK的梯田,因为要留一部分给自己吃,所以他们决定至少淹掉p块田地,但是不能超过q块田地,否则会因为剩下的田地不够而把奴隶自己饿死。
现在给你一个n*m的矩阵,代表梯田中每块田地的高度,求能否发动一场高度为h的海啸,使得满足奴隶们的要求。
由于发动海啸代价很高,所以如果存在多个解,请输出最小的一个h,否则输出-1。
解题思路:
由外而内,逐层考虑,被淹没的数组元素值改为-1,扫描矩阵中-1的个数,判断是否处于p和q之间从而输出结果。
具体解法:
1)先考虑矩阵的四边,在当前高度值h(初始值为1)下,将被淹没的(高度值小于h 的)元素值置为-1;
2)从第二层开始循环,将满足高度值小于h且周围4块相邻的梯田中已有被淹没(值为-1)的矩阵元素值置为-1,循环至层次layer<m/2;
3)扫描矩阵中-1的个数number,判断是否介于p、q之间,输出结果;
4)每次循环结束h加1,再重复1)2)至number>q。
代码实现:
#include<iostream>
#include<vector>
using namespace std;
void Drowned(vector<vector<int> > field,int h)
{
int m=field.size(),n=field[0].size();
for(int j=0;j<m;j++)
{
if(field[0][j]<=h) field[0][j]=-1;
if(field[n-1][j]<=h) field[n-1][j]=-1;
}
for(int k=0;k<n;k++)
{
if(field[k][0]<=h) field[k][0]=-1;
if(field[k][m-1]<=h) field[k][m-1]=-1;
}
for(int layer=1;layer<m/2;layer++)
{
for(int a=layer;a<m-layer;a++)
{
if(field[layer][a]<=h&&(field[layer-1][a]==-1||field[layer+1][a]==-1||field[layer][a-1]==-1|| field[layer][a+1]==-1))
field[layer][a]=-1;
if(field[n-1-layer][a]<=h&&(field[n-1-layer-1][a]==-1||field[n-layer][a]==-1||field[n-1-laye r][a-1]==-1||field[n-1-layer][a+1]==-1))
field[n-1-layer][a]=-1;
}
for(int b=layer;b<n-layer;b++)
{
if(field[b][layer]<=h&&(field[b-1][layer]==-1||field[b+1][layer]==-1||field[b][layer-1]==-1|| field[b][layer-1]==-1))
field[b][layer]=-1;
if(field[b][m-1-layer]<=h&&(field[b-1][m-1-layer]==-1||field[b+1][m-1-layer]==-1||field[b] [m-1-layer-1]==-1||field[b][m-layer]==-1))
field[b][m-1-layer]=-1;
}
}
}
int DrownedNumber(vector<vector<int> > field)
{
int n=0;
for(int i=0;i<field.size();i++)
for(int j=0;j<field[0].size();j++)
if(field[i][j]==-1)
n++;
return n;
}
int main()
{
int t,n,m,p,q,h,_h;
cin>>t; //输入整数t,表示有t组数据
vector<int> result;
vector<vector<int> > field;
for(int i=0;i<t;i++)
{
h=0; //海啸高度初始化
cin>>n>>m>>p>>q;
for(int j=0;j<n;j++)
{
vector<int> temp;
for(int k=0;k<m;k++)
{
cin>>_h;
temp.push_back(_h);
}
field.push_back(temp);
temp.clear();
}
while(DrownedNumber(field)<=q)
{
h++;
Drowned(field,h);
}
if(DrownedNumber(field)>=p&&DrownedNumber(field)<=q) result.push_back(h);
else
result.push_back(-1);
field.clear();
}
for(int i=0;i<t;i++)
cout<<result[i]<<endl;
system("pause");
return 0;
}
运行结果:
题目J 镜像树
一棵二叉树,若其与自己的镜像完全相同,就称其为镜像树(即这棵二叉树关于根完全对称)。
例如
是一棵镜像树;
而
不是镜像树。
现给你一棵二叉树,请你判断其是不是镜像树。
解题思路:
镜像树的中序遍历序列是对称的,故依此进行判定。
具体解法:
1)根据输入数据构造二叉树create;
2)中序遍历二叉树,得到其中序遍历序列,存入数组inorder;
3)判断inorder是否对称,输出结果。
代码实现:
运行结果:
课程学习报告
黄俊祥:
本课程名为程序设计方法与艺术,故本次实训讲座着重介绍及演示了程序设计方法的一些内里,主要内容涉及三方面:面向对象程序设计(OOD)原则、重构技术(Refactorying)以及测试驱动开发(TDD)。
尽管只是初窥门径,但通过一周的学习,亦是受益良多。
面向对象的程序设计一直围绕抽象、封装、继承、多态这四个特性展开,其中抽象是设计的关键。
而面向对象设计原则说的是前人在实践中对程序设计提出的一些后辈们应当关注及遵循的一些准则,这之中包括如“共产主义”般极具指导性的“远大目标”式的开放-闭合原则(OCP)、关注程序行为功能的里氏替换原则(LSP)、阐释高层与底层及抽象和细节关系的依赖倒置原则(DIP)、接口隔离原则(ISP)、单一职责原则(SRP)等。
基于以上原则的程序设计解决了软件系统可维护性方面的问题,增强了程序的可拓展性、灵活性等。
重构(Refactoring)就是通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。
软件产品最初制造出来,是经过精心的设计,具有良好架构的。
但是随着时间的发展、需求的变化,必须不断的修改原有的功能、追加新的功能,还免不了有一些缺陷需要修改。
为了实现变更,不可避免的要违反最初的设计构架。
经过一段时间以后,软件的架构就千疮百孔了。
bug越来越多,越来越难维护,新的需求越来越难实现,软件的架构对新的需求渐渐的失去支持能力,而是成为一种制约。
最后新需求的开发成本会超过开发一个新的软件的成本,这就是这个软件系统的生命走到尽头的时候。
重构就能够最大限度的避免这样一种现象。
系统发展到一定阶段后,使用重构的方式,不改变系统的外部功能,只对内部的结构进行重新的整理。
通过重构,不断的调整系统的结构,使系统对于需求的变更始终具有较强的适应能力。
重构可以降低项目的藕合度,使项目更加模块化,有利于项目的开发效率和后期的维护。
让项目主框架突出鲜明,给人一种思路清晰,一目了然的感觉,其实重构是对框架的一种维护。
测试驱动开发是一种不同于传统软件开发流程的新型的开发方法。
它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。
这有助于编写简洁可用和高质量的代码(Clean code that works),并加速开发过程。
测试驱动开发的基本思想就是在开发功能代码之前,先编写测试代码,然后只编写使测试通过的功能代码,从而以测试来驱动整个开发过程的进行。
这有助于编写简洁可用和高质量的代码,有很高的灵活性和健壮性,能快速响应变化,并加速开发过程。
测试驱动开发的基本过程如下:
①快速新增一个测试
②运行所有的测试(有时候只需要运行一个或一部分),发现新增的测试不能通过
③做一些小小的改动,尽快地让测试程序可运行,为此可以在程序中使用一些不合情理的方法
④运行所有的测试,并且全部通过
⑤重构代码,以消除重复设计,优化设计结构
简单来说,就是不可运行/可运行/重构——这正是测试驱动开发的口号。
将测试驱动开发用于实践时,确有收获,感觉设计思路水到渠成般的形成了,而且由于对每步的操作结果都已进行过测试,所以代码也极有信心的一气呵成。
姜文鹏:
OO设计:通过五天的课程,让我对编程有了一些新的感悟。
首先老师给我们讲解了OO设计, OO设计原则,即面向对象的设计原则,OO(Object Oriented,面向对象)。
是当前计算机界关心的重点,它是90年代软件开发方法的主流。
面向对象的概念和应用已超越了程序设计和软件开发,扩展到很宽的范围。
如数据库系统、交互式界面、应用结构、应用平台、分布式系统、网络管理结构、CAD技术、人工智能等领域。
软件系统的可维护性:软件其实很难维护,因为软件系统一旦形成,是很难改动的。
很难加入新功能。
因为软件系统整体一旦形成,每个部分都是相关的,所以很难改动。
复用率较低,算法什么的很难分离。
黏度过高。
面向对象设计一共有六大原则:1) Open-Close Principle(OCP),开-闭原则,讲的是设计要对扩展有好的支持,而对修改要严格限制。
这是最重要也是最为抽象的原则,基本上我们所说的Reusable Software既是基于此原则而开发的。
其他的原则也是对它的实现提供了路径。
2) Liskov Substituition Principle(LSP),里氏代换原则,很严格的原则,规则是“子类必须能够替换基类,否则不应当设计为其子类。
”也就是说,子类只能去扩展基类,而不是隐藏或覆盖基类,如有这方面需要的设计就应当参考以下两种方法替换:
1.
2.
3) Dependence Inversion Principle(DIP),依赖倒换原则,“设计要依赖于抽象而不是具体化”。
换句话说就是设计的时候我们要用抽象来思考,而不是一上来就开始划分我需要哪些哪些类,因为这些是具体。
这样做有什么好处呢?人的思维本身实际上就是很抽象的,我们分析问题的时候不是一下子就考虑到细节,而是很抽象的将整个问题都构思出来,所以面向抽象设计是符合人的思维的。
另外这个原则会很好的支持OCP,面向抽象的设计使我们能够不必太多依赖于实现,这样扩展就成为了可能,这个原则也是另一篇文章《Design by Contract》的基石。
4) Interface Segregation Principle(ISP),“将大的接口打散成多个小接口”,这样做的好处很明显,我不知道有没有必要再继续描述了,为了节省篇幅,实际上我对这些原则只是做了一个小总结,如果有需要更深入了解的话推荐看《Java与模式》,MS MVP的一本巨作!^_^ 5) Composition/Aggregation Reuse Principle(CARP),设计者首先应当考虑复合/聚合,而不是继承(因为它很直观,第一印象就是“哦,这个就是OO啊”)。
这个就是所谓的“Favor Composition over Inheritance”,在实践中复合/聚合会带来比继承更大的利益,所以要优先考虑。
6) Law of Demeter or Least Knowlegde Principle(LoD or LKP),迪米特法则或最少知识原则,这个原则首次在Demeter系统中得到正式运用,所以定义为迪米特法则。
它讲的是“一个对象应当尽可能少的去了解其他对象”。
也就是又一个关于如何松耦合(Loosely-Coupled)的法则。
开放-封闭原则OCP:在上面的六种原则中,我们着重介绍了OCP原则,遵循开放-封闭(open-closed)原则设计出的模块具有两个主要的表征。
1.可扩展,即“对扩展是开放的”(Open For Extension)。
这意味着模块的行为功能可以被扩展,在应用需求改变或需要满足新的应用需求时,我们可以让模块以不同的方式工作。
2.不可更改,即“对更改是封闭的”(Closed for Modification)。
这些模块的源代码是不可改动的。
任何人都不许修改模块的源代码。
从多种意义上来讲,这个原则是面向对象设计的核心。
遵循这个原则带来的好处就是面向对象技术所声称的优点:可重用性和可维护性。
然而,并不是说仅仅使用一种面向对象编程语言就是遵循这个原则。
而是依赖于设计者对程序中他认为可能发生变化的部分做出合理的设计上的抽象。
Liskov 替换原则LSP:OCP原则背后的主要机制是抽象(abstraction)和多态(polymorphism)。
在静态类型语言中,比如C++,支持抽象和多态的关键机制是继承(inheritance)。
正是使用了继承,我们才可以构建出派生类并使之遵循抽象基类中纯虚函数所定义的抽象多行接口。
OCP原则是OOD中很多说法的核心,它可以让应用程序更易维护、更易重用及更健壮。
LSP原则(也即DBC)是符合OCP原则应用程序的一项重要特性。
仅当派生类能完全替换基类时,我们才能放心地重用那些使用基类的函数和修改派生类型。
依赖倒置原则DIP:该原则指出:如果一个函数可以对一个基类的指针或引用进行操作,那么它应该同样可以操作该基类的派生类并且不需要知道派生类的具体类型。
这意味着派生类中定义的虚函数不应该比基类的对应虚函数有更严格的前提条件(译者:虚函数执行前的输入条件,当然不仅仅是指虚函数的输入参数);也不应该减弱调用后的后续条件(译者:虚函数执行后的结果)。
这也意味着基类中定义的虚函数必须在派生类中出现;而且应该的确是完成某个有意义的工作。
当这个原则被破坏,操作基类指针或引用的函数就不得不判断具体对象的类型以确保这些函数可以正确地操作该对象。
需要进行类型判断就意味着违反了我们最开始讨论的开放-封闭OCP 原则(Open-Closed Principle)。
关联倒置原则是实现面向对象所声称的诸多优点的一个重要原则。
这个原则的正确应用对于创建可重用框架是必需的。
它对于构建具有高弹性的代码同样是至关重要的,因为,当抽象和具体细节被分离以后,代码的维护工作就变得容易多了。
Refactoring技术:
为什么要进行重构:通过重构可以达到以下的目标:持续纠偏和改进软件设计重构和设计是相辅相成的,它和设计彼此互补。
有了重构,你仍然必须做预先的设计,但是不必是最优的设计,只需要一个合理的解决方案就够了,如果没有重构、程序设计会逐渐腐败变质,愈来
愈像断线的风筝,脱缰的野马无法控制。
重构其实就是整理代码,让所有带着发散倾向的代码回归本位。
Martin Flower在《重构》中有一句经典的话:"任何一个傻瓜都能写出计算机可以理解的程序,只有写出人类容易理解的程序才是优秀的程序员。
"对此,笔者感触很深,有些程序员总是能够快速编写出可运行的代码,但代码中晦涩的命名使人晕眩得需要紧握坐椅扶手,试想一个新兵到来接手这样的代码他会不会想当逃兵呢?软件的生命周期往往需要多批程序员来维护,我们往往忽略了这些后来人。
为了使代码容易被他人理解,需要在实现软件功能时做许多额外的事件,如清晰的排版布局,简明扼要的注释,其中命名也是一个重要的方面。
一个很好的办法就是采用暗喻命名,即以对象实现的功能的依据,用形象化或拟人化的手法进行命名,一个很好的态度就是将每个代码元素像新生儿一样命名,也许笔者有点命名偏执狂的倾向,如能荣此雅号,将深以此为幸。
重构的难题:学习一种可以大幅提高生产力的新技术时,你总是难以察觉其不适用的场合。
通常你在一个特定场景中学习它,这个场景往往是个项目。
这种情况下你很难看出什么会造成这种新技术成效不彰或甚至形成危害。
十年前,对象技术(object tech.)的情况也是如此。
那时如果有人问我「何时不要使用对象」,我很难回答。
并非我认为对象十全十美、没有局限性—我最反对这种盲目态度,而是尽管我知道它的好处,但确实不知道其局限性在哪儿。
现在,重构的处境也是如此。
我们知道重构的好处,我们知道重构可以给我们的工作带来垂手可得的改变。
但是我们还没有获得足够的经验,我们还看不到它的局限性。
这一小节比我希望的要短。
暂且如此吧。
随着更多人学会重构技巧,我们也将对??你应该尝试一下重构,获得它所提供的利益,但在此同时,你也应该时时监控其过程,注意寻找重构可能引入的问题。
请让我们知道你所遭遇的问题。
随着对重构的了解日益增多,我们将找出更多解决办法,并清楚知道哪些问题是真正难以解决的。
·数据库(Databases)
「重构」经常出问题的一个领域就是数据库。
绝大多数商用程序都与它们背后的database schema(数据库表格结构)紧密耦合(coupled)在一起,这也是database schema如此难以修改的原因之一。
另一个原因是数据迁移(migration)。
就算你非常小心地将系统分层(layered),将database schema和对象模型(object model)间的依赖降至最低,但database schema的改变还是让你不得不迁移所有数据,这可能是件漫长而烦琐的工作。
在「非对象数据库」(nonobject databases)中,解决这个问题的办法之一就是:在对象模型(object model)和数据库模型(database model)之间插入一个分隔层(separate layer),这就可以隔离两个模型各自的变化。
升级某一模型时无需同时升级另一模型,只需升级上述的分隔层即可。
这样的分隔层会增加系统复杂度,但可以给你很大的灵活度。
如果你同时拥有多个数据库,或如果数据库模型较为复杂使你难以控制,那么即使不进行重构,这分隔层也是很重要的。
你无需一开始就插入分隔层,可以在发现对象模型变得不稳定时再产生它。
这样你就可以为你的改变找到最好的杠杆效应。
对开发者而言,对象数据库既有帮助也有妨碍。
某些面向对象数据库提供不同版本的对象之间的自动迁移功能,这减少了数据迁移时的工作量,但还是会损失一定时间。
如果各数据库之间的数据迁移并非自动进行,你就必须自行完成迁移工作,这个工作量可是很大的。
这种情况下你必须更加留神classes内的数据结构变化。
你仍然可以放心将classes的行为转移过去,但转移值域(field)时就必须格外小心。
数据尚未被转移前你就得先运用访问函数(accessors)造成「数据已经转移」的假象。
一旦你确定知道「数据应该在何处」时,就可
以一次性地将数据迁移过去。
这时惟一需要修改的只有访问函数(accessors),这也降低了错误风险。
李东明:
软件系统的可维护性
难维护:
(1)过于僵硬
(2)过于脆弱
(3)复用性低
(4)黏度过高
面向对象的设计原则
开放封闭原则(Open-closed principle)
(1)其核心思想是:软件实体应该是可扩展的,而不可修改的。
也就是,对扩展开放,对修改封闭的。
(2)设计目标:
1.可扩展性(反僵硬)
2.灵活性(反脆弱)
3.可插入性(反黏合)
(3)开放封闭原则主要体现在两个方面:
1、对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
2、2、对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对其进行任何尝试的修改。
实现开开放封闭原则的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。
让类依赖于固定的抽象,所以修改就是封闭的;而通过面向对象的继承和多态机制,又可以实现对抽象类的继承,通过覆写其方法来改变固有行为,实现新的拓展方法,所以就是开放的。
“需求总是变化”没有不变的软件,所以就需要用封闭开放原则来封闭变化满足需求,同时还能保持软件内部的封装体系稳定,不被需求的变化影响。
里氏代换原则
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。
里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。
LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。