台州cms建站系统,资讯门户网站怎么做,服装网站建设费用预算,做网站需要什么人写代码设计原则是指在编写代码时#xff0c;遵循一些通用的指导原则#xff0c;以确保代码的可读性、可维护性、可扩展性和可重用性。这些原则有助于开发人员创建出高质量的软件系统。下面我将介绍几个常见的代码设计原则#xff0c;并通过C代码例子来说明它们的应用。 1. 单…写代码设计原则是指在编写代码时遵循一些通用的指导原则以确保代码的可读性、可维护性、可扩展性和可重用性。这些原则有助于开发人员创建出高质量的软件系统。下面我将介绍几个常见的代码设计原则并通过C代码例子来说明它们的应用。 1. 单一原则 Single Responsibility Principle, SRP
含义一个类、函数或模块只应负责一项职责即只有一个引起它变化的原因。 换句话说一个类应该只有一个职责。如果一个类有多个职责那么这些职责就可能会相互干扰导致类的设计变得脆弱和难以维护。 例子1假设我们要编写一个程序来管理学生的信息包括成绩和个人信息。如果不遵循单一职责原则我们可能会创建一个包含所有功能的庞大类。但是按照单一职责原则我们应该将不同的职责分离到不同的类中。
// 违反单一职责原则
class StudentManager {
public: void AddStudent(Student s) { /*...*/ } void UpdateGrade(Student s, Grade g) { /*...*/ } void PrintStudentInfo(Student s) { /*...*/ } // ... 其他与学生管理相关的功能
}; // 遵循单一职责原则
class StudentRepository {
public: void AddStudent(Student s) { /*...*/ } // ... 其他与学生存储相关的功能
}; class GradeManager {
public: void UpdateGrade(Student s, Grade g) { /*...*/ } // ... 其他与成绩管理相关的功能
}; class StudentPrinter {
public: void PrintStudentInfo(Student s) { /*...*/ } // ... 其他与学生信息打印相关的功能
};
在这个例子中我们将学生管理功能拆分为三个类StudentRepository 负责学生信息的存储GradeManager 负责成绩管理StudentPrinter 负责打印学生信息。这样每个类都只负责一个特定的职责。 例子2假设我们有一个类既负责处理用户认证又负责处理用户授权。
违反单一职责原则的代码可能是这样的
class AuthManager {
public: void AuthenticateUser(const std::string username, const std::string password) { // 实现用户认证逻辑 } void AuthorizeUser(const std::string username, const std::string resource) { // 实现用户授权逻辑 }
};
遵循单一职责原则的代码应该是这样的
class AuthenticationManager {
public: void AuthenticateUser(const std::string username, const std::string password) { // 实现用户认证逻辑 }
}; class AuthorizationManager {
public: void AuthorizeUser(const std::string username, const std::string resource) { // 实现用户授权逻辑 }
};
在这个改进的例子中我们将AuthManager类的职责拆分成了两个独立的类AuthenticationManager和AuthorizationManager。每个类都只有一个明确的职责这使得代码更加清晰、可维护并且降低了类之间职责的耦合性。 2. 开闭原则Open/Closed Principle, OCP
含义软件实体类、模块、函数等应该是可扩展的但不可修改的。也就是说当需要添加新功能时应该通过扩展现有代码来实现而不是修改现有代码。
例子假设我们有一个简单的日志系统它可以记录不同级别的日志信息。最初系统只支持记录“信息”和“警告”级别的日志。后来我们需要添加一个新的日志级别“错误”而不修改已有的代码。 违背开闭原则的C例子1
#include iostream
#include string // 日志级别枚举
enum class LogLevel { Info, Warning
}; // 日志记录器基类
class Logger {
public: virtual ~Logger() default; virtual void log(LogLevel level, const std::string message) const 0;
}; // 控制台日志记录器实现
class ConsoleLogger : public Logger {
public: void log(LogLevel level, const std::string message) const override { switch (level) { case LogLevel::Info: std::cout [Info] message std::endl; break; case LogLevel::Warning: std::cout [Warning] message std::endl; break; // 注意这里没有处理新的日志级别因为我们假设这是在添加新级别之前的代码 } }
}; // 客户端代码使用日志记录器
void useLogger(Logger logger, LogLevel level, const std::string message) { logger.log(level, message);
} int main() { ConsoleLogger consoleLogger; useLogger(consoleLogger, LogLevel::Info, This is an info message.); useLogger(consoleLogger, LogLevel::Warning, This is a warning message.); // 现在如果我们想添加一个新的日志级别“错误”我们应该怎么做呢 // 遵循开闭原则我们不应该修改已有的ConsoleLogger类。 // 取而代之的是我们可以扩展LogLevel枚举和ConsoleLogger类在实际应用中。 // 但由于这个例子是静态的我们不能在这里动态地添加新的枚举值或修改类。 // 因此这个例子实际上是不完整的它只是为了说明如果我们能够修改代码我们应该如何扩展它。 // 在真实场景中我们可能会使用其他设计模式如策略模式或编程技术如插件系统来实现真正的开闭原则。 return 0;
}
注意上面的例子实际上并没有完全遵守开闭原则因为我们没有展示如何不修改ConsoleLogger类来添加新的日志级别。在真实的项目中我们可能会使用如下方法
使用配置文件或数据库来定义日志级别而不是硬编码在枚举中。使用策略模式或插件架构来允许动态添加新的日志处理器。利用反射或类似的机制来在运行时动态识别和调用相应的日志处理方法。
由于C语言本身的静态特性完全实现开闭原则可能需要一些高级技巧和额外的设计考虑。
违背开闭原则的C例子2
假设我们没有使用上面的抽象基类Logger和虚函数而是直接在ConsoleLogger类中实现了所有日志级别的处理逻辑。当需要添加新的日志级别时我们就不得不修改ConsoleLogger类的代码。
#include iostream
#include string // 日志级别枚举同上
enum class LogLevel { /* ... */ }; // 控制台日志记录器实现没有使用抽象基类和虚函数
class ConsoleLogger {
public: void logInfo(const std::string message) const { std::cout [Info] message std::endl; } void logWarning(const std::string message) const { std::cout [Warning] message std::endl; } // 假设这里原本没有logError方法现在我们需要添加它。 // 这将违反开闭原则因为我们需要修改ConsoleLogger类的代码。 void logError(const std::string message) const { std::cout [Error] message std::endl; }
}; // 客户端代码直接使用具体的日志记录器类
int main() { ConsoleLogger consoleLogger; consoleLogger.logInfo(This is an info message.); consoleLogger.logWarning(This is a warning message.); // 现在我们需要添加对错误日志的支持所以我们必须修改ConsoleLogger类来添加logError方法。 consoleLogger.logError(This is an error message.); // 这将违反开闭原则。 return 0;
}
在这个例子中当我们需要添加新的日志级别时我们不得不修改ConsoleLogger类来添加新的logError方法。这就违反了开闭原则的要求。 为了完全遵守开闭原则我们需要设计一种扩展性更好的日志系统。在C中我们可以通过使用抽象基类、虚函数以及可能的工厂模式来实现这一目标。 遵守开闭原则的C例子
下面是一个改进的例子展示了如何遵守开闭原则
#include iostream
#include map
#include string
#include memory // 日志级别枚举
enum class LogLevel { Info, Warning, Error // 新添加的日志级别
}; // 日志记录器抽象基类
class Logger {
public: virtual ~Logger() default; virtual void log(LogLevel level, const std::string message) const 0; // 其他可能的共享功能...
}; // 控制台日志记录器
class ConsoleLogger : public Logger {
public: void log(LogLevel level, const std::string message) const override { switch (level) { case LogLevel::Info: std::cout [Info] message std::endl; break; case LogLevel::Warning: std::cout [Warning] message std::endl; break; case LogLevel::Error: // 处理新添加的日志级别 std::cout [Error] message std::endl; break; // 其他可能的日志级别... } }
}; // 日志记录器工厂类
class LoggerFactory {
public: static std::unique_ptrLogger createLogger(const std::string type) { if (type console) { return std::make_uniqueConsoleLogger(); } // 可以在这里扩展其他类型的日志记录器而不需要修改已有的代码 throw std::invalid_argument(Invalid logger type); }
}; // 客户端代码
int main() { // 通过工厂创建一个控制台日志记录器 auto consoleLogger LoggerFactory::createLogger(console); // 使用日志记录器记录不同级别的日志信息 consoleLogger-log(LogLevel::Info, This is an info message.); consoleLogger-log(LogLevel::Warning, This is a warning message.); consoleLogger-log(LogLevel::Error, This is an error message.); // 新添加的日志级别可以正常使用 return 0;
}
在这个例子中我们定义了一个抽象基类Logger和一个派生类ConsoleLogger它们通过虚函数log来实现多态。这样我们就可以在不修改ConsoleLogger类的情况下添加新的日志级别。我们还定义了一个LoggerFactory工厂类来创建不同类型的日志记录器这使得我们的系统更加灵活和可扩展。
现在如果我们想添加一个新的日志记录器类型比如文件日志记录器我们只需要创建一个新的类比如FileLogger继承自Logger并实现log函数。然后在LoggerFactory类中添加一个新的条件分支来创建FileLogger对象即可。整个过程不需要修改已有的代码完全符合开闭原则的要求。 3. 里氏替换原则Liskov Substitution Principle, LSP
含义在软件中如果 S 是 T 的子类型则程序中使用 T 类型的对象的地方都可以用 S 类型的对象来替换而不会改变程序的期望行为。这要求子类型必须能够完全替代其父类型。
例子假设我们有一个Vehicle基类和一个派生类Bicycle。Vehicle类有一个StartEngine()方法。但是自行车没有引擎所以这个方法在Bicycle类中没有意义。
违反里氏替换原则的代码可能是这样的
class Vehicle {
public: virtual void StartEngine() 0; // ... 其他通用方法
}; class Bicycle : public Vehicle {
public: void StartEngine() override { /* 什么也不做因为自行车没有引擎 */ } // ... Bicycle特有的方法
};
这个例子违反了里氏替换原则因为Bicycle不能正确地实现Vehicle的StartEngine()方法。正确的做法是不让Bicycle继承自Vehicle或者重新设计类层次结构以反映实际情况。例如可以创建一个没有StartEngine()方法的更通用的基类如Transport并让Vehicle和Bicycle都继承自这个基类。或者如果继承关系确实存在那么应该避免在基类中定义那些不能被子类正确实现的方法。 上面的例子展示了违反里氏替换原则的情况。为了遵循这个原则我们需要重新设计类层次结构确保子类型能够正确地替换父类型。
下面是一个遵循里氏替换原则的改进例子
// 定义一个更通用的基类不包含特定于有引擎车辆的方法
class Transport {
public: virtual void Move() 0; // 所有交通工具都可以移动 // ... 其他通用方法
}; // Vehicle类继承自Transport并添加与引擎相关的方法
class Vehicle : public Transport {
protected: Engine engine; // 假设有一个Engine类表示引擎
public: void StartEngine() { /* 启动引擎的代码 */ } void Move() override { /* 使用引擎移动的代码 */ } // ... 其他与车辆相关的方法
}; // Bicycle类也继承自Transport但不包含StartEngine方法
class Bicycle : public Transport {
public: void Move() override { /* 骑自行车移动的代码 */ } // ... 其他与自行车相关的方法
}; // 现在我们可以使用Transport指针或引用来操作Vehicle和Bicycle对象
// 而不需要担心调用不适合的方法比如StartEngine
void UseTransport(Transport transport) { transport.Move(); // 无论是Vehicle还是Bicycle这个方法都是安全的 // 注意我们不能在这里调用StartEngine()因为不是所有Transport都有引擎
}
在这个改进的例子中我们创建了一个更通用的基类Transport它只包含所有交通工具共有的方法比如Move()。然后Vehicle类继承自Transport并添加了与引擎相关的方法而Bicycle类也继承自Transport但没有引擎相关的方法。这样我们就可以安全地使用Transport类型的引用来操作任何交通工具而不用担心调用不适当的方法。 4. 接口隔离原则Interface Segregation Principle, ISP
含义客户端不应该依赖于它不需要的接口。一个类对另一个类的依赖应该是最小的。这通常意味着将大接口拆分成更小、更具体的接口这样客户端只需要知道和使用它们感兴趣的方法。
例子假设我们有一个打印机接口它包含了打印、扫描和传真等多种功能的方法。但是有些打印机只支持打印功能。
违反接口隔离原则的代码可能是这样的
// 一个包含多种功能的打印机接口
class MultiFunctionPrinter {
public: virtual void Print(const Document doc) 0; virtual void Scan(const Document doc) 0; virtual void Fax(const Document doc, const FaxInfo info) 0; // ... 其他方法
}; // 一个只支持打印功能的打印机类但不得不实现所有接口方法
class SimplePrinter : public MultiFunctionPrinter {
public: void Print(const Document doc) override { /* 实现打印 */ } void Scan(const Document doc) override { /* 无法实现可能抛出异常或什么都不做 */ } void Fax(const Document doc, const FaxInfo info) override { /* 无法实现 */ } // ... 其他方法的空实现或异常抛出
};
遵循接口隔离原则的代码应该是这样的
// 将功能拆分成不同的接口
class Printer {
public: virtual void Print(const Document doc) 0; // ... 其他与打印相关的方法
}; class Scanner {
public: virtual void Scan(const Document doc) 0; // ... 其他与扫描相关的方法
}; class FaxMachine {
public: virtual void Fax(const Document doc, const FaxInfo info) 0; // ... 其他与传真相关的方法
}; // 现在我们可以创建只支持打印功能的打印机类而不需要实现其他无关的方法
class SimplePrinter : public Printer {
public: void Print(const Document doc) override { /* 实现打印 */ } // ... 其他与打印相关的方法的实现
};
在这个改进的例子中我们将大接口MultiFunctionPrinter拆分成了三个小接口Printer、Scanner和FaxMachine。这样SimplePrinter类就只需要实现它真正支持的打印功能而不需要关心扫描和传真等其他无关的功能。这减少了类之间的不必要依赖提高了代码的可维护性和可扩展性。 5. 依赖倒置原则Dependency Inversion Principle, DIP
含义高层模块不应该依赖于低层模块它们都应该依赖于抽象。抽象不应该依赖于细节细节应该依赖于抽象。
例子假设我们有一个读取文件的类和一个处理文件内容的类。如果处理类直接依赖于具体的读取类那么它就违反了依赖倒置原则。 违反依赖倒置原则的代码可能是这样的
// 具体的读取类
class FileReader {
public: std::string ReadFile(const std::string path) { // 实现文件读取 return file content; }
}; // 处理类直接依赖于FileReader
class FileProcessor {
private: FileReader reader;
public: void ProcessFile(const std::string path) { std::string content reader.ReadFile(path); // 处理文件内容 }
};
遵循依赖倒置原则的代码应该是这样的
// 定义读取文件的抽象接口
class IReader {
public: virtual std::string Read(const std::string source) 0;
}; // 具体的读取类实现抽象接口
class FileReader : public IReader {
public: std::string Read(const std::string path) override { // 实现文件读取 return file content; }
}; // 处理类依赖于抽象接口而不是具体实现
class FileProcessor {
private: IReader* reader; // 使用指针或智能指针以便动态绑定
public: FileProcessor(IReader* rdr) : reader(rdr) {} // 通过构造函数注入依赖 void ProcessFile(const std::string path) { std::string content reader-Read(path); // 使用接口方法 // 处理文件内容 }
};
在这个改进的例子中我们创建了一个抽象接口IReader并让FileReader类实现这个接口。然后我们将FileProcessor类的依赖从具体的FileReader类改为IReader接口。这样我们就可以轻松地替换不同的读取实现而不需要修改FileProcessor类的代码。 6. 迪米特原则Law of Demeter
含义迪米特原则也称为最少知识原则Law of Demeter, LoD是面向对象设计中的一个重要原则它强调一个对象应当对其他对象保持最少的了解使得系统各部分之间的耦合度降低。具体来说一个类应该尽量减少与其他类的直接交互只与它的直接朋友即直接与之关联的对象通信而不是与“陌生人”通信。
例子假设我们有一个简单的图形绘制系统其中有一个Shape基类以及两个派生类Circle和Rectangle。我们还有一个Drawer类负责绘制这些形状。按照迪米特原则Drawer类应该只与Shape接口交互而不必关心具体是哪种形状。 遵守迪米特原则的例子C
#include iostream // Shape基类
class Shape {
public: virtual void draw() const 0;
}; // Circle类
class Circle : public Shape {
public: void draw() const override { std::cout Drawing a circle. std::endl; }
}; // Rectangle类
class Rectangle : public Shape {
public: void draw() const override { std::cout Drawing a rectangle. std::endl; }
}; // Drawer类
class Drawer {
public: void drawShape(const Shape shape) { shape.draw(); // 只调用Shape接口的方法 }
}; int main() { Circle circle; Rectangle rectangle; Drawer drawer; drawer.drawShape(circle); drawer.drawShape(rectangle); return 0;
} 在这个例子中Drawer类只与Shape接口交互它不需要知道具体是Circle还是Rectangle因此遵守了迪米特原则。 违背迪米特原则的例子C
#include iostream // Circle类
class Circle {
public: void draw() const { std::cout Drawing a circle. std::endl; }
}; // Rectangle类
class Rectangle {
public: void draw() const { std::cout Drawing a rectangle. std::endl; }
}; // Drawer类直接依赖于具体的Circle和Rectangle类
class Drawer {
public: void drawCircle(const Circle circle) { circle.draw(); } void drawRectangle(const Rectangle rectangle) { rectangle.draw(); }
}; int main() { Circle circle; Rectangle rectangle; Drawer drawer; drawer.drawCircle(circle); drawer.drawRectangle(rectangle); return 0;
}
在这个例子中Drawer类直接与Circle和Rectangle类交互这意味着它对这些具体类的实现有了过多的了解。如果未来需要添加新的形状或者修改现有形状的实现Drawer类可能也需要进行相应的修改这增加了系统的耦合度。通过引入Shape基类并使用多态我们可以避免这种情况从而遵守迪米特原则。