Hồn ma coder - Design Pattern

Đón đọc các bài viết kĩ thuật đặc sắc tại

http://www.giaosucan.com/

F-Complex, ngày 30 tết
Đã 2 giờ sáng, đội dự án XXX vẫn ngồi miệt mài làm việc. Theo yêu cầu của PM, toàn đội phải ON để hoàn thành sản phẩm cho đợt deliver ngày mai.
Ngày cuối năm, toàn bộ khu nhà không một bóng người, chỉ có tiếng gió rít và tiếng cửa sổ kẽo kẹt nghe đến rợn người.
Chàng coder K gục đầu bên bàn phím, source code đã chạy UT và CT tuy nhiên tester report quá nhiều bug. Dự án áp dụng đủ các process Waterfall, Scrum, Agile rồi cả CMMI nhưng bug vẫn hoàn bug. Chàng cũng không biết vì nguyên nhân từ đâu.
Bỗng trời chuyển mưa , bên ngoài không gian tối sầm lại và gió nổi vùn vụt rờn rợn trong những khóm mây. Lẫn trong tiếng gió rít , chàng bổng nghe văng vẳng có tiếng nói âm vang

Fix … bug cho ta…

Dưới ánh trăng mờ chàng nhìn thấy một bóng trắng vật vờ tiến lại gần, hai con mắt trắng dã, mặt dính đầy đất đá.

Anh vốn là PM của dự án này, các chú thiết kế phần mềm không theo design pattern, dẫn tới sản phẩm khó maintaince, quá nhiều bug. Deliver xong anh phải hứng cả rổ gạch đá từ khách hàng. Tại khoang làm việc này đây, anh đã chết vì OT kiệt sức, hôm nay về báo oán

Chàng coder hoảng sợ quỳ lạy rối rít

Design Pattern là gì anh, xin anh chỉ cho mấy chiêu design pattern để em thoát cảnh OT

Bóng ma há miệng đỏ như máu, cười the thé

Developer mà không biết design pattern thì OT là đúng roài

Software quan trọng là phải có tính bảo trì. Hãy tưởng tượng, software có 100 module có liên kết tới nhau, nếu sai sót hay cần thay đổi ở 1 module, không lẽ phải sửa toàn bộ module còn lại ?? Design Pattern ra đời để tránh điều đó
Design Pattern là một kỹ thuật trong lập trình hướng đối tượng, được các nhà thiết kế nghiên cứu thử nghiệm và đúc kết lại thành những mẫu hình chuẩn, giúp chú có thể giải quyết bài toán phần mềm một cách tối ưu nhất
Trong Design Pattern có 3 nhóm bao gồm:

  • Creational Pattern (nhóm khởi tạo) gồm: Abstract Factory, Factory Method, Singleton, Builder, Prototype. Dùng trong việc khởi tạo đối tượng, chú hay sử dụng từ khóa new để tạo đối tượng, nhóm Creational Pattern sẽ sử dụng một số thủ thuật để khởi tạo đối tượng mà không cần dùng đến từ khóa này
  • Structural Pattern (nhóm cấu trúc) gồm: Adapter, Bridge, Composite, Decorator, Facade, Proxy và Flyweight… Nó dùng để thiết lập, định nghĩa quan hệ giữa các đối tượng.
  • Behavioral Pattern gồm: Interpreter, Template Method, Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy và Visitor. Nhóm này dùng trong thực hiện các hành vi của đối tượng.
    Dưới đây là một số mẫu hình thiết kế thông dụng. Những mẫu hình này, nếu tra Google thì không thiếu, nhưng anh muốn mô tả bằng phong cách độc đáo để chú nắm được dễ dàng
    Singleton Pattern
    Đây là pattern thuộc nhóm Creational Pattern được sử dụng để khởi tạo đối tượng, khá phổ biến
    Hãy phân tích đoạn code mà chú đang code
  private void callGhost()
        {
            Ghost maTroi = new Ghost ("Ma trơi");
            Ghost maXo = new Ghost ("Ma xó");
            Ghost maMatThoi = new Ghost ("Ma men");
            maTroi.doXXX();
            ///..
        }

Mỗi khi hàm callGhost được gọi, các instance của Ghost lại được khởi tạo, nhiều người sẽ lầm tưởng rằng sau khi hàm kết thúc thì các object trên sẽ được xóa ngay (vì đó là local variable)
Tuy nhiên vấn đề không đơn giản như vậy, các ngôn ngữ như Java hay C# dùng Garbage Collection (GC) để thu dọn các vùng nhớ đã được cấp phát. Các instance object trên được tạo ra sẽ lưu giữ trên vùng nhớ Head (khác với loại primitive variable lưu trên vùng Stack). Sau khi hàm chạy xong, chúng sẽ được eligible to be collected . Còn khi nào thực sự được xóa thì do GC xử lý. Developer không thể can thiệp. Điều này khác với ngôn ngữ C/C++ khi developer phải tự quản lý bộ nhớ
Như vậy, việc tạo nhiều instance như trên sẽ dẫn tới chiếm dụng quá nhiều bộ nhớ → tiềm ẩn lỗi memory leak, đặc biệt trong ứng dụng đa luồng, khi có nhiều thread cùng call tới hàm callGhost.
Mẫu hình Singleton đảm bảo tạo duy nhất một đối tượng cho một lớp cụ thể

 public class Ghost(){        
            private static Ghost singleGhost;
            private String name;
      Ghost(String name)
  {
    this.name = name;
  }
            
            public static Ghost getInstance(String name) {  
  if (singleGhost == null) {
    singleGhost = new Ghost(name);
   }
  return singleGhost;
          }
}

Như vậy duy nhất một đối tượng Ghost được tạo ra ở cùng 1 thời điểm
Và hàm CallGhost chỉ cần gọi
Ghost maTroi = Ghost.getInstance(“Matroi”);
Tuy nhiên, trong trường hợp ứng dụng có xử lý multi-thread, nếu có nhiều thread cùng call tới hàm getInstance cùng 1 lúc và thỏa mãn điều kiện if thì dẫn tới việc nhiều thread cùng khởi tạo instance Ghost
Để giải quyết điều này cần thêm vào từ khóa synchronized ( dùng trong Java)
public static synchronized Ghost getInstance(String name)
Từ khóa synchronized sẽ khóa việc truy cập vào hàm getInstance, trong khi hàm getInstance được chạy. Bất cứ luồng nào muốn gọi hàm getInstance, đều phải đợi hàm này hoạt động xong. Sử dụng kỹ thuật đồng bộ hóa synchronized là cách dễ nhất để thực thi việc đơn luồng trong gọi hàm, và kỹ thuật này đã giải quyết được vấn đề đa luồng
Tương tự trong C# thì sử dụng từ khóa lock ( tính năng tương tự như synchronized trong Java)
Strategy Pattern
Đây là pattern thuộc nhóm Behavioral Pattern được sử dụng để lựa chọn hành vi đối với đối tượng
Mẫu hình này được sử dụng nhiều nhất, yêu cầu mọi developer cần phải nắm vững.
Hãy xem xét bài toán sau

Object NgocTrinh, ThuyTop và LinhMuu đều có khả năng showHang, nhưng behavior khác nhau

public class NgocTrinh {
 public void showHang() {
 // Show da trắng
       }
}
public class ThuyTop() {
 public void showHang() {
 // Show ngực khủng
       }
}
public class LinhMuu () {
 public void showHang () {
 // Show chân dài
        }
}

Object HoangKieu call method showHang của object NgocTrinh để thư giãn

 class HoangKieu {
 void checkHang(){
 (new NgocTrinh()).showHang();
   }
}

Object HoangKieu sau 2 tháng call method của NgocTrinh chán muốn chuyển sang method showHang của ThuyTop và LinhMuu để thay đổi khẩu vị

Cách thiết kế trên sẽ gặp vấn đề trong trường hợp này, HoangKieu buộc phải sửa đổi lại method checkHang để chuyển sang ThuyTop và LinhMuu, dẫn tới phải test lại method → thiếu tính maintaince

Mẫu hình Strategy Pattern sẽ giải quyết được bài toán trên

Code như sau

public interface IShow {
  public void showHang();
}


public class NgocTrinh implements IShow {
  public void showHang() {
    //show Da trắng
  }
}
public class ThuyTop implements IShow {
  public void showHang() {
    //show Ngực Khủng
}
}
public class LinhMuu implements IShow {
  public void showHang() {
    //show Chan dai
}
}

Class DaiLyMoiGioi sẽ cung cấp cho HoangKieu phương thức lựa chọn kiểu ShowHang theo ý muốn

public class DaiLyMoiGioi {
  private IShow strategy;
  //this can be set at runtime by the application preferences
  public void setShowHangStrategy(IShow strategy) {
    this.strategy = strategy;
  }
  
  //use the strategy
  public void checkHang() {
    strategy.showHang();
  }
}

Như vậy HoangKieu chỉ việc lựa chọn thông qua DaiLyMoiGioi chứ không cần trực tiếp liên lạc tới NgocTrinh hay ThuyTop

public class HoangKieu {
  public static void main(String[] args) {
    DaiLyMoiGioi daily = new DaiLyMoiGioi ();
    //we could assume context is already set by preferences
    daily.setShowHangStrategy (new ThuyTop());
    daily.checkHang();
  }
}

Hoàng Kiều chỉ cần thay thế tham số truyền vào ở setShowHangStrategy là hoàn toàn có thể lựa chọn phương thức showHang với behavior khác nhau
Số lượng mẫu hình design pattern rất nhiều, nhưng trời sắp sáng rồi, ta phải về chầu Diêm Vương đây. Ta khuyên ngươi nên bỏ công đọc cuốn Head First Design Pattern để hiểu sâu hơn về thiết kế phần mềm
Nói rồi hồn ma PM biến mất trong đám khói sương mờ ảo, để lại chàng coder với bản thiết kế lộn xộn đầy bug.
Thế rồi từ đó, hàng năm cứ đến đêm 30 tết, Fsofter lại thấy 2 hồn ma áo trắng đầu tóc bù xù, mắt trắng dã vật vờ đi lại trong khoang làm việc X của tòa nhà F-Complex.
Người ta đồn rằng đó chính là oan hồn của PM và developer dự án X đã chết vì OT quá nhiều khi fix bug.

2 Likes

Đã có Matroi làm sao để em sinh ra ma xó?

Vẫn sử dụng getIns thôi:

Ghost maTroi = Ghost.getInstance(“Matroi”);
Ghost maXo = Ghost.getInstance(“Maxo”);

Bạn thấy không, sẽ không cần tốn memory để new Obj nữa.
Còn nếu bạn chạy multi-thread, cả 2 đều chạy maTroimaXo thì thêm khóa synchronized trong hàm getIns như bài đã hướng dẫn thôi

1 Like

Mà singleton object có scope chẳng khác gì global (và chỉ có duy nhất một) nên đang cần “ma xó” thì lại gặp “ma trơi” thì làm thế nào đây.

1 Like

Đâu có được, singleton tạo ra rồi set đc mỗi 1 cái name ahihi.

Singleton rất vô dụng trong nhiều trường hợp.

2 Likes

Singleton thường tương tác với các thiết bị hardware như audio, camera, volume. Để điều khiển hardware dùng Command Pattern, nhận các phản hồi từ hardware dùng Observer Pattern.

Để điều khiển Singleton chỉ có 1 luồng (unidirectional), từ Command -> Singleton (global) -> Observer, giống với Flux, Redux bên họ hàng React.

2 Likes

Bậy bạ quá đi. Một khi đã có Matroi thông qua Ghost maTroi = Ghost.getInstance(“Matroi”); thì singleGhost != null vậy nên khi gọi Ghost maXo = Ghost.getInstance(“Maxo”); thì bạn đang lấy instance của Ghost maTroi. Nếu thực sự muốn giúp đỡ thì ít nhất hãy kiểm tra câu trả lời của bạn bằng IDE hoặc cái gì đó làm bạn thấy thoải mái trước khi trả lời giúp mình. Mình xin cảm ơn.

Bản chất của singleton của GOF được sinh ra để giải quyết bài toán synchronized đó là khi một operator có quyền write thì chỉ một và chỉ một instance được giao phó quyền điều khiển operator đó mà thôi. Vì lẽ đó mà ví dụ thường gặp nhất là singleton dùng trong việc gọi đến các DB drive. Về sau thì singleton còn có nhiều mục đích khác nữa tuy nhiên nếu không thực sự hiểu về singleton thì tốt nhất là không nên dùng sai mục đích đã ghi trong sách.

Lỗi được ví dụ trong bài không phải là Memory Leak thì bản chất mình không nhận thấy có gì bị Leak ở đây cả. Cùng lắm là việc tạo quá nhiều object sẽ gây áp lực lên GC mà thôi. Với quan điểm của mình thì Leak là concept về việc phần memory không thể giải phóng nhưng lại cũng không thể sử dụng. Như trong ví dụ của bài thì chỉ là chưa được giải phóng mà thôi. Nếu muốn giải quyết bài toán này theo hướng tác giả đang đề cập thì có thể sử dụng một kĩ thuật nâng cao hơn của singletonpooling. Tuy nhiên kĩ thuật này cũng không hẳn là hiệu quả trong ví dụ trên.

Ngoài ra người viết vô hình trung khẳng định là chỉ có một con ma :smiley: (chứ nếu không sao lại dùng singleton)

Vả lại singleton rất dở vì phải phụ thuộc vào logic của nó (tight coupling đấy) => phá hỏng abstraction. Singleton không phải là câu trả lời duy nhất cho yêu cầu duy nhất. Còn nếu dùng dependency injection thì lại là vác singleton đi muôn nơi, suy cho cùng cũng là mua dây buộc mình.

Thực ra có một cách đảm bảo singleton trong muti thread là dùng Initialization-on-demand holder idiom: dùng inner class để tạo instance,
dùng synchronized thì vẫn có vấn đề về performace.

public class Something {
    private Something() {}

    private static class LazyHolder {
        static final Something INSTANCE = new Something();
    }

    public static Something getInstance() {
        return LazyHolder.INSTANCE;
    }
}

https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom`

83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?