Java最佳实践和建议:设计模式


设计模式是软件开发过程中经常出现的问题的常见解决方案。这些解决方案提供了优雅且在大多数情况下解决与对象创建,资源分配,简化代码等相关的不同问题的最有效方法。需要维护它们的上下文,而解决方案本身需要定制,根据业务逻辑。

设计模式分为三类:

  • 创造性,提供解决方案来解决在对象创建过程中发生的不同问题
  • 结构化,通过找到如何在更大的结构中组合类的方法,为实例化问题提供解决方案
  • 行为,为代码的不同部分之间的通信中发生的问题提供解决方案。

DAO模式
在架构设计过程中,一些设计模式实际上可以用作指导,就像DAO设计模式的情况一样。软件体系结构通常有三层:应用程序的端点,服务层,即业务逻辑和数据层。
数据层是使用DAO设计模式(数据访问对象)实现的,该模式将与数据库通信的部分与应用程序的其余部分分开。DAO模式定义了所有实体的CRUD(创建,读取,更新,删除)操作。通过添加将经常用于实体本身的命名/本机查询,可以完全分离持久层。

public interface DAO<T,E extends Serializable>{
  public T save(T object);
  public Boolean delete(T object);
  public T update(T object);
  public T find(E id);
}

DAO的接口本身仅定义了需要在实现中指定的操作。实现本身使用提供的实体管理器的泛型类型。实体管理器是一个负责应用程序中所有持久性操作的类,可以使用应用程序上下文获取。

public abstract class GenericDAO<T,E> implements DAO<T,E>{
  @PersistenceContext  
  private EntityManager entityManager;  
public T save(T object){
    return entityManager.persist(object);
  }
public T find(E id){
    return entityManager.find(T.class,id);
  }
public Boolean delete(T object){
    return entityManager.remove(object);
  }
public T update(T object){
    return entityManager.merge(object);
  }
}

提供的示例需要基本了解Hibernate和Java的持久性。Hibernate是一个ORM工具(对象关系映射),它从java代码创建表,并使用HQL(休眠查询语言)进行查询输入和执行。

@Entity
@Table(name="person")
@NamedQueries
(
 {
  @NamedQuery(name=Person.GET_PERSON_BY_AGE,query=
"Select * from 
  User u where u.age>:age
")
 }
)
public class Person{
 
  public static final String GET_PERSON_BY_AGE =   
 
"Person.getPersonByAge";
  @Id
  @GeneratedValue( strategy = GenerationType.IDENTITY)
  @Column(name=
"id",unique="true")
  public int id;
  @Column(name=
"name")
  public String name;
  public Person(String name){
    this.name=name;
  }
 
//getters and setters...
}

将用于实体的DAO类扩展了通用DAO,其中实现了基本的CRUD操作,因此我们只需要添加将要使用的特定查询。

public PersonDAO extends GenericDAO<Person,Integer>{
public List<Person> getPersonByAge(int age){
    Query q=entityManager.createNamedQuery(Person.GET_PERSON_BY_AGE,
    Person.class);
    q.setParameter("age",5);
    return (List<Person>)q.getResultList();
  }
}

优点:
  • 提供代码与业务逻辑的逻辑和物理分离,易于实现;
  • 可以使用缓存策略轻松扩展DAO类,可以在方法中实现;
  • 如果将DAO类声明为EJB,则每个方法都可以指定事务属性,以便控制底层事务的范围;

缺点:
  • 它会在与数据库的连接中产生开销,因为DAO对象通常会处理整个对象。当涉及到保存操作时,这是一个优点,因为整个对象一次存储但是读取可能是昂贵的操作;
  • 为了避免这种情况,可以使用本机或命名查询,以便根据业务需要检索对象的较小部分;
  • DAO模式不应该在小型应用程序中使用,因为它的优点很小,而且代码会变得更复杂;

工厂模式
设计模式通常用于简化大块代码,甚至可以隐藏应用程序流中的特定实现。这类问题的完美示例是工厂设计模式,它是一种创造性设计模式,无需指定对象的确切类别即可提供对象创建。它建议使用从超类继承的超类和多个子类。在执行期间,仅使用超类,其值因工厂类而异。


public class Car{

  private String model;
  private int numberOfDoors;
  public Car(){
  }
  public String getModel(){
    return this.model;
  }
  public int getNumberOfDoors(){
    return this.numberOfDoors;
  }
  public void setModel(String model){ this.model = model; }
  public void setNumberOfDoors(int n){ this.numberOfDoors = n; }
}
public class Jeep extends Car{
  private boolean land;
  public Jeep(){
  }
 
  public void setLand(boolean land){
    this.land=land;
  }
  public boolean getLand(){
    return this.land;
  }
}

public class Truck extends Car{
  private float capacity;
  public Truck(){
  }
  public void setCapacity(float capacity){
    this.capacity=capacity;
  }
  public float getCapacity(){
    return this.capacity;
  }
}

为了使用这种模式,我们需要实现一个工厂类,它将为给定的输入返回正确的子类。上面的java类指定了一个超类(Car.java)和两个子类(Truck.java和Jeep.java)。在我们的实现中,我们实例化Car类的一个对象,并且根据参数,工厂类将决定它是Jeep还是Truck。

public class CarFactory{
   public Car getCarType(int numberOfDoors, String model,Float
   capacity, Boolean land){ 
      Car car=null;      
         if(capacity!=null){
            car=new Jeep();
            //implement setters
         }else{
            car=new Truck();
           
//implement setters
         }
     return car;
   }
}

在运行时,工厂类考虑输入实例化正确的子类。

public static void main(String [] args){ 
  Car c = null
  CarFactory carFactory = new CarFactory(); 
  c = carFactory.getCarType(2,“BMW”,nulltrue); 
}

抽象工厂
抽象工厂设计模式以相同的方式工作,但父类不是常规类,而是一个抽象类。抽象类通常更快,更容易实例化,因为它们基本上是空的。实现是相同的,只有父类被声明为抽象及其所有方法,并且子类需要实现抽象类中声明的方法的行为。
Abstract工厂的示例是使用接口创建的。通过简单地用抽象类替换接口可以完成同样的操作,而不是实现接口,子类将扩展抽象类。

public interface Car {
   
   public String getModel();
   public Integer getNumberOfDoors();
   public String getType();
}
public class Jeep implements Car{
   private String model;
   private Integer numberOfDoors;
   private Boolean isLand;
   public Jeep() {}
   public Jeep(String model, Integer numberOfDoors, Boolean isLand){
     this.model = model;
     this.numberOfDoors = numberOfDoors;
     this.isLand = isLand;
   }
   public String getModel(){
     return model;
   }
   public Integer getNumberOfDoors() {
     return numberOfDoors;
   }
   public Boolean isLand() {
     return isLand;
   }
   public void setLand(Boolean isLand) { this.isLand = isLand; }
  
   public void setModel(String model) { this.model = model; }
  
   public void setNumberOfDoors(Integer numberOfDoors){    
      this.numberOfDoors = numberOfDoors; 
   }
   public String getType(){
      return "jeep";
   }

public class Truck implements Car{
   private String model;
   private Integer numberOfDoors;
   private Integer numberOfWheels;
   public Truck(String model, Integer numberOfDoors, Integer numberOfWheels) {
      this.model = model;
      this.numberOfDoors = numberOfDoors;
      this.numberOfWheels = numberOfWheels;
   }
   public Truck() {}
   public String getModel() { return model; }
   public Integer getNumberOfDoors() { return numberOfDoors; }
   public Integer getNumberOfWheels() { return numberOfWheels; }
   public void setNumberOfWheels(Integer numberOfWheels) {
     this.numberOfWheels = numberOfWheels;
   }
   public void setModel(String model) { this.model = model; }
   public void setNumberOfDoors(Integer numberOfDoors) {
      this.numberOfDoors = numberOfDoors;
   }
   public String getType(){ return
"truck"; }
}
public class CarFactory {
   public CarFactory(){}
   public Car getCarType(String model,Integer numberOfDoors, Integer numberOfWheels, Boolean isLand){
     if(numberOfWheels==null){
        return new Jeep(model,numberOfDoors,isLand);
     }else{
        return new Truck(model,numberOfDoors,numberOfWheels);
     }
   }
}

唯一的区别是抽象类中声明的方法必须在每个子类中实现。在这两种情况下,工厂和主要方法都保持不变。

public class CarMain {
   public static void main(String[] args) {
      Car car=null;
      CarFactory carFactory=new CarFactory();
      car=carFactory.getCarType("Ford", new Integer(4), null, new Boolean(true));
     System.out.println(car.getType());
    }
}

优点:

  • 它允许松散耦合和更高级别的抽象;
  • 它是可扩展的,可用于将某些实现与应用程序分开;
  • 通过简单地添加适当的实例化逻辑,可以在层次结构中创建新类之后重用工厂类,并且代码仍然可以工作。
  • 单元测试,因为使用超类可以很容易地覆盖所有场景;

缺点:
  • 它往往太抽象,难以理解;
  • 了解何时实现工厂设计模式非常重要,因为在小型应用程序中,它只会在对象创建期间创建开销(更多代码);
  • 工厂设计模式必须保持其上下文,即只有从同一父类继承或实现相同接口的类才适用于工厂设计模式。

singleton单例模式
这个设计模式是最有名的和有争议的造物设计模式之一。单例类是一个类,它将在应用程序的生命周期中仅实例化一次,即只有一个对象共享所有资源。单例方法是线程安全的,并且可以由应用程序的多个部分同时使用,即使它们访问Singleton类中的共享资源也是如此。关于何时使用单例类的完美示例是记录器实现,其中所有资源都在同一日志文件中写入并且是线程安全的。其他示例包括数据库连接和共享网络资源。

此外,每当应用程序需要从服务器读取文件时,使用Singleton类就很方便,因为在这种情况下,只有应用程序的一个对象才能访问存储在服务器上的文件。除了记录器实现之外,配置文件是使用单例类有效的另一个示例。
在java中,singleton是一个带有私有构造函数的类。单例类使用类本身的实例保留一个字段。该对象是使用get方法创建的,如果尚未启动实例,则调用构造函数。早些时候,我们提到过这种模式最具争议性,因为实例生成的多个实现。它必须是线程安全的,但它也必须是高效的。在示例中,我们有两个解决方案。

import java.nio.file.Files;
import java.nio.file.Paths;
public class LoggerSingleton{
private static Logger logger;
    private String logFileLocation="log.txt";
    private PrintWriter pw;
    private FileWriter fw;
    private BufferedWriter bw;
    private Logger(){
       fw = new FileWriter(logFileLocation, true);
       bw = new BufferedWriter(fw)
       this.pw = new PrintWriter(bw);
    }
    
   public static synchronised Logger getLogger(){
       if(this.logger==null){
          logger=new Logger();
       }
       return this.logger;
    }
 
    public void write(String txt){
       pw.println(txt);
    }
}

因为将经常访问日志文件。使用缓冲写入器的打印编写器确保文件不会多次打开和关闭。
第二个实现包括一个私有类,它包含Singleton类实例的静态字段。私有类只能在单例类中访问,即只能从get方法访问。

public class Logger{
    
    private static class LoggerHolder(){
         public static Singleton instance=new Singleton();
    }
    
    private Logger(){
    // init
    }
   
    public static Logger getInstance(){
        return LoggerHolder.instance;
    }
}

然后可以从app中的任何其他类使用单例类:

Logger log=Logger.getInstance();
log.write("something");

优点:

  • 单例类只在应用程序的生命周期中实例化一次,并且可以多次使用;
  • singleton类允许线程安全访问共享资源;
  • 单例类不能扩展,如果正确实现,即get方法应该是同步和静态的,它是线程安全的;
  • 建议首先创建一个接口,然后设计单例类本身,因为它更容易测试接口;

缺点:
  • 测试期间的问题,当单例类访问共享资源并且测试的执行很重要时;
  • 单例类还隐藏了代码中的一些依赖项,即创建未明确创建的依赖项;
  • 使用没有工厂模式的单例的问题在于它打破了单一责任原则,因为类正在管理自己的生命周期;

Builder模式
生成器模式也是创建模式,它允许对复杂对象的增量创建。当字段设置需要复杂操作或仅仅字段列表太长时,建议使用此模式。该类的所有字段都保存在私有内部类中

public class Example{
   private String txt;
   private int num;
   public static class ExampleBuilder{
      private String txt;
      private int num;
      public ExampleBuilder(int num){
          this.num=num;
      }
        
      public ExampleBuilder withTxt(String txt){
          this.txt=txt;
          return this;
      }
      public Example build(){
          return new Example(num,txt);
      }
  }
  
  private Example(int num,String txt){
     this.num=num;
     this.txt=txt;
  }
}

在实际情况中,参数列表将更长,并且可以基于其他输入计算类的一些参数。构建器类与类本身具有相同的字段,并且必须将其声明为静态才能访问,而无需实例化持有者类的对象(在本例中为Example.java)。上面给出的实现是线程安全的,可以通过以下方式使用:

Example example=new ExampleBuilder(10).withTxt("yes").build();

优点:

  • 如果类中的参数数量大于6或7,则代码更加整洁和可重用;
  • 在设置所有需要的字段之后创建对象,并且只有完全创建的对象可用;
  • 构建器模式隐藏构建器类中的一些复杂计算,并将其与应用程序流分离;

缺点:
  • 构建器类必须包含原始类中的所有字段,因此与单独使用类相比,可能需要更多的时间来开发;

观察模式
观察 设计模式是一种行为设计模式,它通过将某些实体传播到应用程序的相关部分来观察某些实体并处理这些更改。每个容器可以为不同的设计模式提供不同的实现,并且观察者模式在java中使用接口Observer来实现,该接口将受到观察者类中的更改的影响。另一方面,观察到的类需要实现Observable接口。Observer接口只有update方法,但在Java 9中已弃用,因为它的简单性不建议使用它。它没有提供有关更改内容的详细信息,只是在较大的对象中查找更改可能是一项代价高昂的操作。