Thực hiện trạng thái đối tượng trong một ngôn ngữ OO?


11

Tôi đã được cung cấp một số mã Java để xem xét, mô phỏng một cuộc đua xe hơi, trong đó bao gồm việc triển khai một máy trạng thái cơ bản. Đây không phải là một máy trạng thái khoa học máy tính cổ điển, mà chỉ là một đối tượng có thể có nhiều trạng thái và có thể chuyển đổi giữa các trạng thái của nó dựa trên một loạt các tính toán.

Để chỉ mô tả vấn đề, tôi đã có một lớp Xe hơi, với một lớp enum lồng nhau xác định một số hằng số cho trạng thái của Xe (như TẮT, IDLE, DRIVE, REVERSE, v.v.). Trong cùng một lớp Xe này, tôi có một chức năng cập nhật, về cơ bản bao gồm một câu lệnh chuyển đổi lớn, chuyển sang trạng thái hiện tại của ô tô, thực hiện một số tính toán và sau đó thay đổi trạng thái của ô tô.

Theo như tôi có thể thấy, trạng thái Cars chỉ được sử dụng trong lớp riêng của nó.

Câu hỏi của tôi là, đây có phải là cách tốt nhất để đối phó với việc thực hiện một bộ máy nhà nước có bản chất được mô tả ở trên? Nghe có vẻ như là giải pháp rõ ràng nhất, nhưng trong quá khứ tôi luôn nghe rằng "các câu lệnh chuyển đổi là xấu".

Vấn đề chính tôi có thể thấy ở đây là câu lệnh chuyển đổi có thể trở nên rất lớn khi chúng ta thêm nhiều trạng thái (nếu thấy cần thiết) và mã có thể trở nên khó sử dụng và khó bảo trì.

Điều gì sẽ là một giải pháp tốt hơn cho vấn đề này?


3
Mô tả của bạn không giống như một máy trạng thái đối với tôi; nó chỉ đơn thuần là một loạt các đối tượng xe hơi, mỗi đối tượng có trạng thái bên trong riêng. Xem xét việc đăng mã thực tế, làm việc của bạn lên codereview.stackexchange.com ; những người này rất giỏi trong việc cung cấp phản hồi về mã làm việc.
Robert Harvey

Có lẽ "máy trạng thái" là một lựa chọn không tốt về từ ngữ, nhưng vâng, về cơ bản, chúng ta có một loạt các đối tượng xe hơi tự bật trạng thái bên trong của chúng. Hệ thống có thể được mô tả hùng hồn bằng sơ đồ trạng thái UML, đó là lý do tại sao tôi đặt tiêu đề cho bài viết của mình như vậy. Nhìn nhận lại, đây không phải là cách tốt nhất để mô tả vấn đề, tôi sẽ chỉnh sửa bài viết của mình.
PythonNewb

1
Tôi vẫn nghĩ rằng bạn nên xem xét việc đăng mã của mình lên codereview.
Robert Harvey

1
Nghe có vẻ như một cỗ máy nhà nước đối với tôi. object.state = object.function(object.state);
robert bristow-johnson

Tất cả các câu trả lời được đưa ra cho đến nay, bao gồm cả câu trả lời được chấp nhận đều bỏ lỡ lý do chính khiến các câu lệnh chuyển đổi bị coi là xấu. Họ không cho phép tuân thủ nguyên tắc mở / đóng.
Dunk

Câu trả lời:


13
  • Tôi đã biến chiếc xe thành một cỗ máy trạng thái bằng cách sử dụng Mẫu trạng thái . Thông báo không có switchhoặc if-then-elsebáo cáo được sử dụng để lựa chọn nhà nước.

  • Trong trường hợp này, tất cả các trạng thái là các lớp bên trong nhưng nó có thể được thực hiện theo cách khác.

  • Mỗi trạng thái chứa các trạng thái hợp lệ nó có thể thay đổi thành.

  • Người dùng được nhắc về trạng thái tiếp theo trong trường hợp có thể có nhiều hơn một hoặc chỉ đơn giản là xác nhận trong trường hợp chỉ có một trạng thái có thể.

  • Bạn có thể biên dịch nó và chạy nó để kiểm tra nó.

  • Tôi đã sử dụng một hộp thoại đồ họa bởi vì cách đó dễ dàng hơn để chạy nó một cách tương tác trong Eclipse.

nhập mô tả hình ảnh ở đây

Sơ đồ UML được lấy từ đây .

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.JOptionPane;

public class Car {

    private State state;
    public static final int ST_OFF=0;
    public static final int ST_IDDLE=1;
    public static final int ST_DRIVE=2;
    public static final int ST_REVERSE=3;

    Map<Integer,State> states=new HashMap<Integer,State>();

    public Car(){
        this.states.put(Car.ST_OFF, new Off());
        this.states.put(Car.ST_IDDLE, new Idle());
        this.states.put(Car.ST_DRIVE, new Drive());
        this.states.put(Car.ST_REVERSE, new Reverse()); 
        this.state=this.states.get(Car.ST_OFF);
    }

    private abstract class State{

        protected List<Integer> nextStates = new ArrayList<Integer>();

        public abstract void handle();
        public abstract void change();

        protected State promptForState(String prompt){
            State s = state;
            String word = JOptionPane.showInputDialog(prompt);
            int ch = -1;
            try {
                ch = Integer.parseInt(word);
            }catch (NumberFormatException e) {
            }   

            if (this.nextStates.contains(ch)){
                s=states.get(ch);
            } else {
                System.out.println("Invalid option");
            }
            return s;               
        }       

    }

    private class Off extends State{

        public Off(){ 
            super.nextStates.add(Car.ST_IDDLE);             
        }

        public void handle() { System.out.println("Stopped");}

        public void change() {
            state = this.promptForState("Stopped, iddle="+Car.ST_IDDLE+": ");
        }

    }

    private class Idle extends State{
        private List<Integer> nextStates = new ArrayList<Integer>();
        public Idle(){
            super.nextStates.add(Car.ST_DRIVE);
            super.nextStates.add(Car.ST_REVERSE);
            super.nextStates.add(Car.ST_OFF);       
        }

        public void handle() {  System.out.println("Idling");}

        public void change() { 
            state=this.promptForState("Idling, enter 0=off 2=drive 3=reverse: ");
        }

    }

    private class Drive extends State{

        private List<Integer> nextStates = new ArrayList<Integer>();
        public Drive(){
            super.nextStates.add(Car.ST_IDDLE);
        }       
        public void handle() {System.out.println("Driving");}

        public void change() {
            state=this.promptForState("Idling, enter 1=iddle: ");
        }       
    }

    private class Reverse extends State{
        private List<Integer> nextStates = new ArrayList<Integer>();
        public Reverse(){ 
            super.nextStates.add(Car.ST_IDDLE);
        }           
        public void handle() {System.out.println("Reversing");} 

        public void change() {
            state = this.promptForState("Reversing, enter 1=iddle: ");
        }       
    }

    public void request(){
        this.state.handle();
    }

    public void changeState(){
        this.state.change();
    }

    public static void main (String args[]){
        Car c = new Car();
        c.request(); //car is stopped
        c.changeState();
        c.request(); // car is iddling
        c.changeState(); // prompts for next state
        c.request(); 
        c.changeState();
        c.request();    
        c.changeState();
        c.request();        
    }

}

1
Tôi thực sự thích điều này. Mặc dù tôi đánh giá cao câu trả lời hàng đầu và nó bảo vệ các câu lệnh chuyển đổi (tôi sẽ mãi mãi nhớ điều đó bây giờ), tôi thực sự thực sự thích ý tưởng của mẫu này. Cảm ơn bạn
PythonNewb

@PythonNewb Bạn đã chạy nó?
Tulains Córdova

Vâng, nó hoạt động hoàn hảo. Việc triển khai sẽ hơi khác với mã tôi có, nhưng ý tưởng chung là tuyệt vời. Tôi nghĩ rằng tôi có thể xem xét chuyển các lớp trạng thái ra khỏi lớp kèm theo.
PythonNewb

1
@PythonNewb Tôi đã thay đổi mã thành phiên bản ngắn hơn sử dụng lại trạng thái thay đổi / dấu nhắc cho logic đầu vào bằng một lớp trừu tượng thay vì giao diện. Nó ngắn hơn 20 dòng nhưng tôi đã thử nghiệm và hoạt động như nhau. Bạn luôn có thể có được phiên bản cũ hơn, dài hơn nhìn vào lịch sử chỉnh sửa.
Tulains Córdova

1
@Caleth Như một vấn đề thực tế, tôi đã viết nó như thế này bởi vì tôi thường làm điều đó trong cuộc sống thực, tức là lưu trữ các phần có thể hoán đổi trong bản đồ và nhận chúng dựa trên ID được tải từ một tệp tham số. Thông thường những gì tôi lưu trữ trong bản đồ không phải là chính các đối tượng mà là người tạo ra chúng nếu các đối tượng đắt tiền hoặc có nhiều trạng thái không tĩnh.
Tulains Córdova

16

báo cáo chuyển đổi là xấu

Đây là loại đơn giản hóa quá mức khiến cho lập trình hướng đối tượng trở thành một tên xấu. Sử dụng ifcũng "xấu" như sử dụng câu lệnh chuyển đổi. Dù bằng cách nào bạn không gửi đa hình.

Nếu bạn phải có một quy tắc phù hợp với vết cắn âm thanh, hãy thử quy tắc này:

Báo cáo chuyển đổi trở nên rất xấu khi bạn có hai bản sao của chúng.

Một câu lệnh chuyển đổi không được sao chép ở bất kỳ nơi nào khác trong cơ sở mã đôi khi có thể quản lý để không trở nên xấu xa. Nếu các trường hợp không công khai, nhưng được gói gọn, thì đó thực sự không phải là việc của ai khác. Đặc biệt là nếu bạn biết làm thế nào và khi nào để cấu trúc lại nó thành các lớp. Chỉ vì bạn không thể có nghĩa là bạn phải. Đó là bởi vì bạn có thể làm nó ít quan trọng hơn để làm điều đó ngay bây giờ.

Nếu bạn thấy mình đang cố gắng đưa ngày càng nhiều thứ vào câu lệnh chuyển đổi, truyền bá kiến ​​thức về các trường hợp xung quanh hoặc mong muốn nó không quá tệ để tạo một bản sao của nó thì đã đến lúc cấu trúc lại các trường hợp thành các lớp riêng biệt.

Nếu bạn có thời gian để đọc nhiều hơn một vài âm thanh về việc tái cấu trúc các câu lệnh chuyển đổi c2 có một trang cân bằng rất tốt về mùi của câu lệnh chuyển đổi .

Ngay cả trong mã OOP, không phải mọi chuyển đổi đều xấu. Đó là cách bạn đang sử dụng nó, và tại sao.


2

Chiếc xe là một loại máy nhà nước. Câu lệnh chuyển đổi là cách đơn giản nhất để thực hiện một máy trạng thái thiếu siêu trạng thái và trạng thái phụ.


2

Báo cáo chuyển đổi không phải là xấu. Đừng nghe những người nói những điều như "chuyển đổi là xấu"! Một số cách sử dụng cụ thể của các câu lệnh chuyển đổi là một antipotype, như sử dụng chuyển đổi để mô phỏng phân lớp. (Nhưng bạn cũng có thể triển khai antipotype này với if, vì vậy tôi đoán nếu nó cũng tệ!).

Việc thực hiện của bạn có vẻ tốt. Bạn đúng là sẽ khó duy trì nếu bạn thêm nhiều trạng thái. Nhưng đây không chỉ là vấn đề thực hiện - có một đối tượng với nhiều trạng thái với hành vi khác nhau tự nó là một vấn đề. Hình ảnh chiếc xe của bạn có 25 tiểu bang, mỗi tiểu bang thể hiện hành vi khác nhau và các quy tắc khác nhau để chuyển trạng thái. Chỉ cần xác định và ghi lại hành vi này sẽ là một nhiệm vụ to lớn. Bạn sẽ có hàng ngàn quy tắc chuyển đổi nhà nước! Kích thước của switchsẽ chỉ là một triệu chứng của một vấn đề lớn hơn. Vì vậy, nếu có thể tránh đi xuống con đường này.

Một biện pháp khắc phục có thể là phá vỡ nhà nước thành các trạm biến áp độc lập. Ví dụ, REVERSE có thực sự là một trạng thái khác biệt với DRIVE không? Có lẽ các trạng thái xe có thể được chia thành hai: Trạng thái động cơ (TẮT, IDLE, DRIVE) và hướng (FORWARD, REVERSE). Trạng thái và hướng động cơ có thể sẽ chủ yếu là độc lập, vì vậy bạn giảm sự trùng lặp logic và quy tắc chuyển đổi trạng thái. Nhiều đối tượng có ít trạng thái dễ quản lý hơn nhiều so với một đối tượng có nhiều trạng thái.


1

Trong ví dụ của bạn, ô tô chỉ đơn giản là những cỗ máy trạng thái theo nghĩa khoa học máy tính cổ điển. Chúng có một tập hợp nhỏ các trạng thái được xác định rõ và một số loại logic chuyển trạng thái.

Đề nghị đầu tiên của tôi là xem xét việc phá vỡ logic chuyển đổi thành chức năng của chính nó (hoặc lớp, nếu ngôn ngữ của bạn không hỗ trợ các chức năng hạng nhất).

Đề nghị thứ hai của tôi là xem xét việc phá vỡ logic chuyển đổi sang trạng thái, chính nó sẽ có chức năng riêng (hoặc lớp, nếu ngôn ngữ của bạn không hỗ trợ các chức năng hạng nhất).

Trong cả hai sơ đồ, quá trình chuyển đổi trạng thái sẽ trông giống như thế này:

mycar.transition()

hoặc là

mycar.state.transition()

Chiếc thứ hai dĩ nhiên có thể được bọc trong lớp xe hơi để trông giống chiếc thứ nhất.

Trong cả hai trường hợp, việc thêm một trạng thái mới (giả sử DRAFTING), sẽ chỉ liên quan đến việc thêm một loại đối tượng trạng thái mới và thay đổi các đối tượng cụ thể chuyển sang trạng thái mới.


0

Nó phụ thuộc vào mức độ lớn của switchnó.

Trong ví dụ của bạn, tôi nghĩ rằng điều đó switchổn vì thực sự không có bất kỳ trạng thái nào khác mà tôi có thể nghĩ về điều đó bạn Carcó thể có, vì vậy nó sẽ không trở nên lớn hơn theo thời gian.

Nếu vấn đề duy nhất là có một công tắc lớn trong đó mỗi công tắc casecó rất nhiều hướng dẫn, thì chỉ cần tạo các phương thức riêng biệt cho mỗi phương thức.

Đôi khi mọi người đề xuất mẫu thiết kế trạng thái , nhưng nó phù hợp hơn khi bạn xử lý logic phức tạp và các trạng thái đưa ra các quyết định kinh doanh khác nhau cho nhiều hoạt động khác nhau. Nếu không, các vấn đề đơn giản nên có giải pháp đơn giản.

Trong một số trường hợp, bạn có thể có các phương thức chỉ thực hiện các tác vụ khi trạng thái là A hoặc B, nhưng không có C hoặc D hoặc có nhiều phương thức với các thao tác rất đơn giản phụ thuộc vào trạng thái. Sau đó, một hoặc một số switchbáo cáo sẽ tốt hơn.


0

Điều này nghe có vẻ như một cỗ máy trạng thái trường học cũ được sử dụng trước khi bất kỳ ai thực hiện lập trình Hướng đối tượng, chứ đừng nói đến Thiết kế mẫu. Nó có thể được thực hiện trong bất kỳ ngôn ngữ nào có câu lệnh chuyển đổi, chẳng hạn như C.

Như những người khác đã nói, không có gì sai với các câu lệnh chuyển đổi. Các lựa chọn thay thế thường phức tạp hơn và khó hiểu hơn.

Trừ khi số lượng các trường hợp chuyển đổi trở nên lớn một cách lố bịch, điều này có thể vẫn khá dễ quản lý. Bước đầu tiên để giữ cho nó có thể đọc được là thay thế mã trong từng trường hợp bằng một lệnh gọi hàm để thực hiện hành vi của trạng thái.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.